feat(server): config file editor — read/edit/save a host config file per instance
All checks were successful
CI / backend-types (push) Successful in 10s
CI / frontend-build (push) Successful in 16s
CI / agent-tests (push) Successful in 44s
CI / integration (push) Successful in 21s

The Server page's config-honesty note now leads somewhere real: a
Configuration file panel that loads a config file from the instance
(prefilled with the game's primaryConfigFile hint — server.cfg,
ServerSettings.ini, GameXishu.json), edits it in a mono textarea, and
saves it straight to the host through the jailed agent file bridge.
Not-found is handled gracefully (empty editor to create). Works across
games; gameProfiles gains primaryConfigFile per game.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 19:07:59 -04:00
parent 877fadcb6c
commit f60e6abd33
3 changed files with 129 additions and 0 deletions

View File

@@ -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<string | null>(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 () => {
</form>
</Panel>
<!-- Config file editor — reads/writes via the jailed agent file manager -->
<Panel v-if="currentInstance" title="Configuration file" subtitle="Edit a config file directly on the host (jailed to the instance)">
<div class="sv__cfg-row sv__mb-sm">
<Input
v-model="cfgPath"
:placeholder="cfgHint || 'path/relative/to/instance/root'"
class="sv__cfg-path"
:mono="true"
/>
<Button
variant="secondary"
icon="folder-open"
:loading="cfgLoading"
:disabled="(!cfgPath && !cfgHint) || cfgLoading"
@click="loadConfigFile"
>Load</Button>
<Button
v-if="cfgLoaded"
icon="check"
:loading="cfgSaving"
:disabled="!cfgPath || cfgSaving"
@click="saveConfigFile"
>Save</Button>
</div>
<Alert v-if="cfgError" tone="info" class="sv__mb-sm">{{ cfgError }}</Alert>
<textarea
v-if="cfgLoaded"
v-model="cfgContent"
class="sv__cfg-editor"
spellcheck="false"
rows="16"
></textarea>
<p v-else class="sv__cfg-hint">
Load <code>{{ cfgHint || 'a config file' }}</code> to view and edit it. Changes are written
straight to the host through the agent — jailed to this instance's directory.
</p>
</Panel>
<!-- Automation -->
<Panel title="Automation">
<div class="sv__toggles">
@@ -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);