Repository navigation
feat(config): timestamped config backups with a configurable folder and retention - #661
Conversation
|
Thank you @opticon454, this is a really useful feature: on-by-default config backups with change detection, retention that only touches its own folders, a checksum-verified restore with a pre-restore copy, and a key-level compare, all with thorough tests and docs. Typecheck, lint, format and your 38 tests pass here, and CI is green. A few things need changing before it can go in. Must fix
Should fix in the same round
Smaller things (I can fix these at merge time if you prefer)
Once 1 to 4 are in, this is ready to merge. Thanks again for the care that went into the docs and tests. |
… per-instance origin, reload intents on restore Review round on Ark0N#661: webhook.json and intents.json are redacted in compare and URL values lose userinfo/query; a custom folder is probed (bounded, pastCap) before each pass and in each route and only created when its parent exists; manifests record an origin and change check, prune, list, restore and diff are scoped to it; a restore reloads IntentStore, lists untouched files and treats settings.json as restart-required; remote-cases.json is tracked; stale .tmp-cfg-* folders are swept and a failed restore unlinks its temp; the group is hidden from non-admins; an empty days field saves 30.
2bc5d65 to
ff1d5ae
Compare
|
Thanks @Ark0N, all four required items and the small ones are in Must fix
Should fix
Smaller things
Typecheck, lint and the config-backup and route tests pass locally. |
|
Thank you @opticon454 for the second round. This PR adds on-by-default, change-detected config backups with retention, a checksum-verified restore and a redacted compare, and every item from my first review is in Two things still need changing, and the first one is my fault. Must fix
Smaller things (I can do these at merge time if you prefer)
Once 1 and 2 are in, this is ready to merge. Thanks again for the care in the docs and tests. |
…ter a settings restore, clis.json reloads Review round 2 on Ark0N#661: the hostname in the origin orphaned every snapshot when a Docker container was recreated, so the origin is a random id kept in <data dir>/config-backup-id; after restoring settings.json the Settings form is re-read from /api/settings so Save cannot write the old values back; the restore confirm names the files and the credential ones; clis.json is reloaded (reloadCliRegistry) and users.json needs no restart; API reference and route docs corrected.
|
Thanks @Ark0N, and no harm done on the hostname: the Docker recreate case is a good catch. Both must-fix items and the smaller ones are in the new commit on top of Must fix
Smaller things
Typecheck, lint and the config-backup, route and settings-structure tests pass locally. |
|
Thank you @opticon454 for the third round. This PR adds on-by-default config backups with change detection, retention, a checksum-verified restore and a redacted compare, and both items from my last review are in Running the retention against a realistic timeline turned up two problems that my earlier reviews missed. Must fix
Should fix in the same round
Smaller things (I can fix these at merge time if you prefer)
Once 1 and 2 are in, this is ready to merge. Thanks again for the care in the docs and tests, and for turning each round around so quickly. |
…y-only rewrites, keep at least 2 Review round 3 on Ark0N#661: retention aged each snapshot by its own createdAt, so the first change after a quiet month deleted the last good copy; it now ages from when the next snapshot was made. users.json lastLoginAt, settings.json lastUsedCase and intents.json recentPrompts/updatedAt are volatile keys for the change check. The keep count has a minimum of 2. Wiki restore paragraph aligned with RESTART_REQUIRED.
|
Thanks @Ark0N, both items and the smaller ones are in the new commit on top of Must fix
Should fix
Smaller things
Typecheck, lint and the config-backup, route, settings-structure and schema tests pass locally. |
|
Thank you @opticon454 for the fourth round. This PR adds on-by-default config backups with change detection, retention, a checksum-verified restore and a redacted compare, and every item from my last review is in One more file of the same class turned up, plus some doc lines this round left behind. Should fix
Doc lines
These are small enough that I will apply them on top at merge, together with the architecture-invariants section and the State Files entries I mentioned before, unless you would rather push them yourself first. Thanks again for the care in the docs and tests, and for turning every round around so quickly. |
|
Thanks @Ark0N, the Docker case file and the doc lines are in the new commit (rebased on current master first).
Typecheck, and the config-backup and route tests pass locally. The architecture-invariants section and State Files entries stay with you, as offered. |
… per-instance origin, reload intents on restore Review round on Ark0N#661: webhook.json and intents.json are redacted in compare and URL values lose userinfo/query; a custom folder is probed (bounded, pastCap) before each pass and in each route and only created when its parent exists; manifests record an origin and change check, prune, list, restore and diff are scoped to it; a restore reloads IntentStore, lists untouched files and treats settings.json as restart-required; remote-cases.json is tracked; stale .tmp-cfg-* folders are swept and a failed restore unlinks its temp; the group is hidden from non-admins; an empty days field saves 30.
…ter a settings restore, clis.json reloads Review round 2 on Ark0N#661: the hostname in the origin orphaned every snapshot when a Docker container was recreated, so the origin is a random id kept in <data dir>/config-backup-id; after restoring settings.json the Settings form is re-read from /api/settings so Save cannot write the old values back; the restore confirm names the files and the credential ones; clis.json is reloaded (reloadCliRegistry) and users.json needs no restart; API reference and route docs corrected.
…y-only rewrites, keep at least 2 Review round 3 on Ark0N#661: retention aged each snapshot by its own createdAt, so the first change after a quiet month deleted the last good copy; it now ages from when the next snapshot was made. users.json lastLoginAt, settings.json lastUsedCase and intents.json recentPrompts/updatedAt are volatile keys for the change check. The keep count has a minimum of 2. Wiki restore paragraph aligned with RESTART_REQUIRED.
acb2fea to
d4de3de
Compare
|
Thank you @opticon454 for the fifth round. This PR adds on-by-default config backups with change detection, retention, a checksum-verified restore and a redacted compare, and every item from my last review is in One more file of the same class turned up, plus a few smaller things. Should fix
Smaller things
I will fold these into the merge together with the architecture-invariants section and the State Files entries ( |
|
Thanks @Ark0N, all four items are in the new commit (on top of current master).
The architecture-invariants section and State Files entries stay with you, as offered. Typecheck, lint and the config-backup and route tests pass locally (58). |
|
Thank you @opticon454 for the sixth round. This PR adds on-by-default config backups with change detection, retention, a checksum-verified restore and a redacted compare, and every item from my last review is in Two things still need changing. Must fix
Smaller things (I can fix these at merge time if you prefer)
Once 1 and 2 are in, this is ready to merge, and I will add the architecture-invariants section and the State Files entries ( |
…nd retention The files that hold a user's customization (settings.json, clis.json, custom-model-hosts.json, webhook.json, intents.json, linked-cases.json, push-keys.json, users.json, .env) are plain JSON in the data dir, rewritten by the server, the Settings UI and the CLI, with no copy to go back to. A snapshot is taken at startup and whenever the tracked files' content changes (checked every five minutes, and only written when a digest of the file set differs from the latest snapshot's), into `<data dir>/backups/config` by default or any absolute folder. Retention keeps the newest configBackupKeepCount (1-500) and, when configBackupKeepDays > 0, drops older ones; it only ever deletes folders it made (name pattern and a parseable manifest) and never the newest. GET/POST /api/config-backups list and back up now; POST .../:id/restore verifies every file against the manifest's SHA-256, takes a pre-restore snapshot, then replaces files atomically with mode 0600, writing only allowlisted names. Snapshots are 0700 folders of 0600 files and all routes are admin only in multi-user mode. Settings -> System gets the controls and a Restore list.
…remote/docker/webview files
- New wiki page Config-Backups: what is backed up (and what is not, and why), when a backup is made,
where and under what name (cfg-YYYYMMDD-HHMMSS, manifest.json, reasons), retention and its
guarantees, the Settings controls, restore (verify, pre-restore copy, atomic write, which files
need a restart), security, the HTTP API and troubleshooting.
- Linked from the Home index and sidebar; Settings Reference, Troubleshooting ("my settings were
reset"), Security and Core Concepts updated; README and CLAUDE.md entries; the api-reference
section now states the naming convention and the full tracked list.
- Found while documenting: remote-hosts.json, docker-hosts.json, docker-cases.json and
webviews.json are user configuration too and were not tracked; they are now. state.json stays
out on purpose (rewritten per session event, also holds cron jobs and respawn presets) and the
docs say so.
custom-model-hosts.json carries a lastDiscoveredAt per host that the 5-minute model discovery poll rewrites, so the content hash never matched the latest snapshot and a snapshot was written every tick. At the default keep count of 20 that left about 100 minutes of history and evicted every useful older copy. The change check now looks through volatile keys per tracked file. The snapshot still stores the file as written and restore still verifies against its own digest.
…ning config Settings -> System -> Config backups gains a Compare view: a backup against the running config, a backup against the one before it, or any two backups. Each differing file lists its changed keys as before/after, plus files that appeared or vanished. GET /api/config-backups/diff?from=&to= serves it (`to` defaults to `current`; admin only in multi-user mode). Credential values are never returned: .env, users.json and push-keys.json report names only, and any other key that looks like a token, secret, password, credential or key is reported as changed without its value. A non-JSON file reports only that it differs, a file is cut at 200 changes, and a self-refreshed discovery timestamp does not count as a difference.
… per-instance origin, reload intents on restore Review round on Ark0N#661: webhook.json and intents.json are redacted in compare and URL values lose userinfo/query; a custom folder is probed (bounded, pastCap) before each pass and in each route and only created when its parent exists; manifests record an origin and change check, prune, list, restore and diff are scoped to it; a restore reloads IntentStore, lists untouched files and treats settings.json as restart-required; remote-cases.json is tracked; stale .tmp-cfg-* folders are swept and a failed restore unlinks its temp; the group is hidden from non-admins; an empty days field saves 30.
…ter a settings restore, clis.json reloads Review round 2 on Ark0N#661: the hostname in the origin orphaned every snapshot when a Docker container was recreated, so the origin is a random id kept in <data dir>/config-backup-id; after restoring settings.json the Settings form is re-read from /api/settings so Save cannot write the old values back; the restore confirm names the files and the credential ones; clis.json is reloaded (reloadCliRegistry) and users.json needs no restart; API reference and route docs corrected.
…y-only rewrites, keep at least 2 Review round 3 on Ark0N#661: retention aged each snapshot by its own createdAt, so the first change after a quiet month deleted the last good copy; it now ages from when the next snapshot was made. users.json lastLoginAt, settings.json lastUsedCase and intents.json recentPrompts/updatedAt are volatile keys for the change check. The keep count has a minimum of 2. Wiki restore paragraph aligned with RESTART_REQUIRED.
…y ignored key, correct the retention and restart docs
… restores, refuse symlinked or loosened snapshots, keep the folder settings on restore
…s.json, restore works on mode-less filesystems, compare checks snapshots too
8d4c42a to
c982fa1
Compare
|
Thanks @Ark0N, both must-fix items and the smaller ones are in the new commit (rebased on current master first). Must fix
Smaller things
Typecheck, lint and the config-backup and route tests (64) pass locally. |
|
Thank you @opticon454 for the seventh round. This PR adds on-by-default config backups with change detection, retention, a checksum-verified restore and a redacted compare, and every item from my last review is in Two things still need changing. Must fix
Smaller things (I can fix these at merge time if you prefer)
Once 1 and 2 are in, this is ready to merge, and I will add the architecture-invariants section and the State Files entries ( |
…mode probe cannot write through a planted symlink, the form shows the retention in use
|
Thanks @Ark0N, both must-fix items and the smaller ones are in the new commit. Must fix
Smaller things
Typecheck, lint and the config-backup and route tests (71) pass locally. |
|
Thank you @opticon454 for the eighth round. This PR adds on-by-default config backups with change detection, retention, a checksum-verified restore and a redacted compare, and every item from my last review is in Two edges still cost the backup history. Should fix
Smaller things (I can fix these at merge time if you prefer)
Once 1 and 2 are in, this is ready to merge, and I will add the architecture-invariants section and the State Files entries ( |
…k-ahead snapshot cannot make every pass write a copy
|
Thanks @Ark0N, both items and the manifest/API doc point are in the new commit.
The smaller point: the manifest field lists in Typecheck, lint and the config-backup and route tests (74) pass locally. |
|
Thank you @opticon454 for the ninth round. This PR adds on-by-default config backups with change detection, retention, a checksum-verified restore and a redacted compare, and both items from my last review are in One thing still needs changing, and it comes from the last round's fix. Must fix
Smaller things (I can fix these at merge time if you prefer)
Once 1 is in, this is ready to merge, and I will add the architecture-invariants section and the State Files entries ( |
…ck reads it, test that Save omits a displayed-only retention
|
Thanks @Ark0N, the must-fix is in the new commit, along with most of the smaller things. Must fix. Smaller things
Typecheck, lint and the config-backup, route and settings tests (91) pass locally. |
|
Thank you @opticon454 for the tenth round. This PR adds on-by-default config backups with change detection, retention that only touches its own folders, a checksum-verified restore with a pre-restore copy, and a redacted compare. The must-fix from my last review is in Nothing blocks the merge. A few small things, which I will fold in on top at merge unless you would rather push them yourself:
I will merge with those on top. Thanks again for the care in the docs and tests, and for turning every round around so quickly. |
…ed and what failed Merge-time fix for #661. A restore that could not write one file (a full disk, a directory where the file should be) used to stop there and answer a bare 500, naming neither the files it had already put back nor the pre-restore copy, and the Settings form was not re-read, so the next Save wrote the old settings back over a restored settings.json. restoreConfigBackup now writes every verified file it can, then throws ConfigBackupPartialRestoreError with restored, failed (name and error), untouched and safetySnapshot. The route runs the same reload hooks and audit for the files written as a full restore does, and answers 500 with the error envelope plus those fields (and restartRequired) in data, as the tab-layout 409 carries its layout. Settings refills the form when settings.json is among the restored files, also on that path. Tests: a unit case where a later file is still restored, the clis.json-as-a-directory route case, and two vm cases for the Settings handler. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s not answer Merge-time fix for #661. The unreachable-folder table covered GET, diff and restore; POST /api/config-backups (Back up now) now has its row too. runConfigBackup already probes before it touches the folder, and the row fails when the temp sweep is moved ahead of the probe. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #661. A pass listed the folder and parsed every manifest three times: for the recorded-retention fallback, for the change check and for the prune (and restore listed it twice more). On a network folder that is every manifest read three times every five minutes. runConfigBackupPass (config-backup.ts) now reads the folder once, shares the names with the temp sweep and the manifests with withRecordedRetention, the snapshot and the prune, and prepends the snapshot it just made (always the newest, by the date floor) to the listing retention counts. runConfigBackup in the route keeps the settings read and the bounded probe and calls it. restoreConfigBackup lists once for the recorded retention and the pre-restore snapshot. The exported createConfigBackup, latestBackupRetention, withRecordedRetention and pruneConfigBackups keep their signatures (latestBackupRetention and withRecordedRetention take an optional pre-read listing). A recording node:fs test pins one readdir and one read per manifest per pass, and that the new snapshot counts toward retention. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hing changed Merge-time fix for #661. The pre-restore copy was forced, so a restore made while the live files equalled the newest backup spent a retention slot on a duplicate. It now goes through the normal change check: when nothing changed since the newest backup, no copy is made and safetySnapshot names that backup. Two tests that relied on the forced copy now change a file before the restore; a new one pins the reuse. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…inter and State Files Merge-time fix for #661, as promised in the reviews. The long Config backups paragraph in CLAUDE.md moves into a new architecture-invariants section that gives each rule with the reason behind it (allowlist, volatile keys, retention by successor and the date floor, the origin id, retention after a bad or reset settings.json, the bounded probe, one walk per pass, verify before write, the undo copy and the partial-restore answer, the reload hooks, redaction, admin only). CLAUDE.md keeps the short rules and a pointer, and the State Files list names config-backup-id and backups/config/ with the retention each manifest records. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict in CLAUDE.md's State Files line resolved by keeping #661's new config-backup entries and this PR's "case default groups" in state.json. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Merged and shipped in 1.44.0. Thank you @opticon454, ten rounds is a lot, and every one came back fast and better tested. Config backups with change detection, retention that only touches its own folders, a verified restore and a redacted compare are a real safety net for everyone's setup. What I added at merge time, as separate commits:
|
What
Config backups: timestamped snapshots of the files that hold a user's customization, in a default but configurable folder, with configurable retention. Settings → System gains a "Config backups" group (on/off, folder, keep N backups, delete older than N days, Back up now, a list with Restore).
Why
Those files (
settings.json,clis.json, custom endpoints, webhook, linked cases, accounts) are plain JSON in the data dir, rewritten by the server, the Settings UI and the CLI, and there was no copy to go back to. A single bad write (a stray test run, a hand edit, a bad restart) silently resets everything, and nothing says what it used to be.How
src/config-backup.ts(pure, takes its directories): a fixed allowlist of tracked files (settings, CLI overrides, custom endpoints, webhook, intents, linked cases, remote and Docker hosts and cases, saved dashboards, push keys, accounts,.env), never "everything in the data dir";state.jsonis deliberately not tracked (it is rewritten with every session event and also holds cron jobs and respawn presets); one snapshot per actual content change (a digest of the file set is compared with the latest snapshot's, so an idle server writes nothing); snapshots are built in a temp folder and renamed into place.keepCount(1-500) and, whenkeepDays> 0, drop older ones. It only ever deletes folders it made (name pattern and a parseable manifest), so pointing the folder at a directory holding other things cannot delete them, and the newest snapshot always survives.pre-restoresnapshot of the live files is taken first, and each file is replaced atomically with mode 0600. Only allowlisted names are ever written, whatever a manifest lists..env,users.json,custom-model-hosts.jsonandpush-keys.jsoncarry credentials. In multi-user mode all three routes are admin only.configBackupEnabled(absent = on),configBackupDir,configBackupKeepCount(20),configBackupKeepDays(30). The server reads them fresh, so a change applies on the next pass.GET/POST /api/config-backups,POST /api/config-backups/:id/restore,GET /api/config-backups/diff?from=&to=. A 5-minute server timer plus a snapshot at startup; skipped in test mode..env,users.jsonandpush-keys.jsonshow names only, and any other key that looks like a token, secret, password, credential or key is shown as changed without its value. Admin only in multi-user mode, like the other routes.Tests
test/config-backup.test.ts(settings clamping and defaults, tracked set, no snapshot while unchanged, private modes, both retention limits and the never-delete-foreign-folders and keep-newest rules, restore ordering, checksum refusal, traversal and allowlist, and the diff: key-level changes, backup vs running config, credential redaction, volatile timestamps, non-JSON, the cap, unknown ids and a tampered snapshot) andtest/routes/config-backup-routes.test.ts(list, back up now, custom folder and retention, restore with restart hints, diff with its 400/404, 404s, multi-user admin gate). The settings, i18n and docs-parity suites pass. Changeset included (minor: a new feature).Docs
New wiki page
Config-Backups(what is backed up, when, naming, retention, restore, security, troubleshooting), linked from the Home index and sidebar; Settings Reference, Troubleshooting ("my settings were reset"), Security and Core Concepts; README and CLAUDE.md; endpoint reference indocs/api-reference.mdandHTTP-API.