diff --git a/frontend/src/config/gameProfiles.ts b/frontend/src/config/gameProfiles.ts index 80b9fc6..da19256 100644 --- a/frontend/src/config/gameProfiles.ts +++ b/frontend/src/config/gameProfiles.ts @@ -102,6 +102,12 @@ export interface GameProfile { terminology: GameTerminology /** Notable game-specific mechanics that affect server administration. */ special?: string[] + /** + * Primary editable config file, relative to the instance root — prefilled in + * the Server-page config editor as a hint (operator can change it). null if + * the game has no single primary config file. + */ + primaryConfigFile?: string | null /** * Stat field labels shown on server cards and the dashboard. * First entry is always Players; subsequent entries are game-specific. @@ -185,6 +191,7 @@ export const GAME_PROFILES: Record = { group: 'Team', }, statFields: ['Players', 'uMod', 'Wipe'], + primaryConfigFile: 'server/cfg/server.cfg', nav: RUST_NAV, }, @@ -207,6 +214,7 @@ export const GAME_PROFILES: Record = { }, special: ['Clans', 'Thralls', 'Avatars', 'Purge', 'PvP windows'], statFields: ['Players', 'Clans', 'Purge'], + primaryConfigFile: 'ConanSandbox/Saved/Config/LinuxServer/ServerSettings.ini', nav: [ { label: '', items: [NAV_DASHBOARD] }, { @@ -252,6 +260,7 @@ export const GAME_PROFILES: Record = { }, special: ['Cluster', 'Tribes'], statFields: ['Players', 'Tribe', 'Mask'], + primaryConfigFile: 'WS/Saved/GameplaySettings/GameXishu.json', nav: [ { label: '', items: [NAV_DASHBOARD] }, { @@ -294,6 +303,7 @@ export const GAME_PROFILES: Record = { }, special: ['Sietches', 'Deep Desert', 'Bases', 'Landsraad'], statFields: ['Players', 'Sietches', 'Control'], + primaryConfigFile: null, nav: [ { label: '', items: [NAV_DASHBOARD] }, { diff --git a/frontend/src/stores/instances.ts b/frontend/src/stores/instances.ts index 93aa2fc..8c187ff 100644 --- a/frontend/src/stores/instances.ts +++ b/frontend/src/stores/instances.ts @@ -106,6 +106,23 @@ export const useInstancesStore = defineStore('instances', () => { return api.post>(`/instances/${id}/rcon`, { command }) } + /** Read a config/text file from the current instance (jailed to its root). */ + async function readFile(path: string): Promise { + const id = currentId.value + if (!id) throw new Error('No instance selected') + const res = await api.get<{ content?: string }>( + `/instances/${id}/file?path=${encodeURIComponent(path)}`, + ) + return res?.content ?? '' + } + + /** Write a config/text file to the current instance. */ + async function writeFile(path: string, content: string): Promise { + const id = currentId.value + if (!id) throw new Error('No instance selected') + await api.put(`/instances/${id}/file`, { path, content }) + } + return { instances, currentId, @@ -117,5 +134,7 @@ export const useInstancesStore = defineStore('instances', () => { select, lifecycle, rcon, + readFile, + writeFile, } }) diff --git a/frontend/src/views/admin/ServerView.vue b/frontend/src/views/admin/ServerView.vue index b4e12c4..c23c062 100644 --- a/frontend/src/views/admin/ServerView.vue +++ b/frontend/src/views/admin/ServerView.vue @@ -357,6 +357,51 @@ async function refreshInstanceStatus() { } } +// ---- Config file editor (reads/writes via the jailed agent file manager) ---- +const cfgPath = ref('') +const cfgContent = ref('') +const cfgLoaded = ref(false) +const cfgLoading = ref(false) +const cfgSaving = ref(false) +const cfgError = ref(null) + +// A reasonable default config-file hint per game (operator can change it). +const cfgHint = computed(() => profile.value.primaryConfigFile ?? '') + +async function loadConfigFile() { + const path = (cfgPath.value || cfgHint.value).trim() + if (!path || !currentInstance.value) return + cfgPath.value = path + cfgLoading.value = true + cfgError.value = null + try { + cfgContent.value = await instancesStore.readFile(path) + cfgLoaded.value = true + } catch (e) { + // Not-found is fine — present an empty editor to create it. + cfgContent.value = '' + cfgLoaded.value = true + cfgError.value = e instanceof Error ? e.message : 'File not found — saving will create it' + } finally { + cfgLoading.value = false + } +} + +async function saveConfigFile() { + const path = cfgPath.value.trim() + if (!path || !currentInstance.value) return + cfgSaving.value = true + try { + await instancesStore.writeFile(path, cfgContent.value) + cfgError.value = null + toast.success(`Saved ${path}`) + } catch (e) { + toast.error(e instanceof Error ? e.message : 'Failed to save file') + } finally { + cfgSaving.value = false + } +} + async function toggleAutomation(field: 'crash_recovery_enabled' | 'auto_update_on_force_wipe' | 'force_wipe_eligible') { if (!server.config) return const newValue = !server.config[field] @@ -1040,6 +1085,44 @@ onMounted(async () => { + + +
+ + + +
+ {{ cfgError }} + +

+ Load {{ cfgHint || 'a config file' }} to view and edit it. Changes are written + straight to the host through the agent — jailed to this instance's directory. +

+
+
@@ -1127,6 +1210,23 @@ onMounted(async () => { .sv__controls { display: flex; flex-wrap: wrap; gap: 10px; } .sv__mt-sm { margin-top: 12px; } .sv__instance-pick { display: flex; align-items: center; gap: 12px; } +.sv__cfg-row { display: flex; gap: 10px; align-items: center; } +.sv__cfg-path { flex: 1; } +.sv__cfg-editor { + width: 100%; + min-height: 320px; + background: var(--surface-base); + color: var(--text-primary); + border: none; + box-shadow: var(--ring-subtle); + border-radius: var(--radius-md); + padding: 14px 16px; + font-family: var(--font-mono); + font-size: var(--text-xs); + line-height: 1.6; + resize: vertical; +} +.sv__cfg-hint { margin: 0; font-size: var(--text-sm); color: var(--text-tertiary); line-height: 1.55; } .sv__select { background: var(--surface-base); color: var(--text-primary);