feat(config-backup): compare backups with each other and with the running 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.
This commit is contained in:
Devvyn committed 2026-10-20 18:01:42 +08:00
1 parent 06182c6fce
commit a2e49eb26d
10 files changed
+426 -16

No files matched your search

+6 -5
View File
@@ -1160,11 +1160,12 @@ Tracked files (a fixed list, relative to the data dir): `settings.json`, `clis.j
Settings (synced, in `settings.json`): `configBackupEnabled` (absent = on), `configBackupDir` (absolute path, `~` expanded; empty = `<data dir>/backups/config`), `configBackupKeepCount` (1–500, default 20), `configBackupKeepDays` (0–3650, default 30; `0` = no age limit). Retention only ever deletes snapshot folders Codeman made there, and never the newest one. Each snapshot is a `0700` folder named `cfg-YYYYMMDD-HHMMSS` (server local time; `-2`, `-3`… on a same-second collision) holding the tracked files under their own names plus a `manifest.json` (`id`, `createdAt`, `reason` of `startup`/`auto`/`manual`/`pre-restore`, `appVersion`, and a SHA-256 and size per file), all `0600`. Only folders with that name and a valid manifest are listed, restored or pruned. Full guide: `docs/wiki/Config-Backups.md`.
| Method | Path | Body | Notes |
| ------ | ----------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/api/config-backups` | none | `settings` in effect, `dir`, `defaultDir`, `tracked[]` and `backups[]` (`id`, `createdAt`, `reason`, `appVersion`, `files[]`, `bytes`), newest first. |
| `POST` | `/api/config-backups` | none | Snapshot now, even if nothing changed (`reason: manual`), then apply retention. `status` is `created` or `empty` (nothing tracked exists yet). |
| `POST` | `/api/config-backups/:id/restore` | none | Put a snapshot's files back, atomically, after a `pre-restore` snapshot of the current ones. `404` unknown id; `409 CONFLICT` if a file fails its checksum (nothing changed). `restartRequired[]` names files the running server only reloads on restart. |
| Method | Path | Body | Notes |
| ------ | ------------------------------------ | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/api/config-backups` | none | `settings` in effect, `dir`, `defaultDir`, `tracked[]` and `backups[]` (`id`, `createdAt`, `reason`, `appVersion`, `files[]`, `bytes`), newest first. |
| `POST` | `/api/config-backups` | none | Snapshot now, even if nothing changed (`reason: manual`), then apply retention. `status` is `created` or `empty` (nothing tracked exists yet). |
| `GET` | `/api/config-backups/diff?from=&to=` | none | What differs between two states: `from`/`to` are a backup id or `current` (the running config; the default for `to`). `files[]` (`name`, `status` `added`/`removed`/`changed`/`unchanged`, `changes[]` of `path`, `kind`, `before`/`after`, `truncated`, `detail`) and `changed`. Credential values are never returned (`redacted: true`). `400` without `from`; `404` unknown id; `409` if a file fails its checksum. |
| `POST` | `/api/config-backups/:id/restore` | none | Put a snapshot's files back, atomically, after a `pre-restore` snapshot of the current ones. `404` unknown id; `409 CONFLICT` if a file fails its checksum (nothing changed). `restartRequired[]` names files the running server only reloads on restart. |
## Webhook notifications
+28 -4
View File
@@ -102,7 +102,8 @@ Both apply together. Three guarantees:
| Keep this many backups | `configBackupKeepCount` | 20 |
| Delete backups older than (days) | `configBackupKeepDays` | 30 |
| **Back up now** | | |
| The list, with **Restore** per entry | | |
| The list, with **Restore**, **Compare with current** and **Changes in this backup** per entry | | |
| **Compare** … with … **Show changes** | | |
The settings are saved server-side and read fresh, so a change applies on the next pass. The
list and **Back up now** use the **saved** folder and retention: apply your edits first.
@@ -127,6 +128,26 @@ You can also restore by hand: the files in a backup folder are the originals, so
over the file in the data directory is equivalent (stop Codeman first if you are replacing
`clis.json` or `users.json`).
## Comparing backups
Before you restore, or to find out what changed and when, use **Compare** in the list:
- **Compare with current** on a backup shows what differs between that backup and the config
running now, which is also what a restore of it would undo.
- **Changes in this backup** shows what differs between the backup before it and this one.
- The **Compare … with … Show changes** pickers compare any two backups, or a backup with the
current config.
Each file that differs is listed with its changed keys (`theme.mode`, `tags[1]`, a variable name
in `.env`) as before → after, plus files that appeared or vanished between the two states. Files
that are the same are listed on one line. A file that is not JSON shows only that it differs, and
a file with more than 200 changes is cut short, so use the files themselves for a full diff. A
self-refreshed discovery timestamp in `custom-model-hosts.json` is not counted as a change.
**Credential values are never shown.** `.env`, `users.json` and `push-keys.json` list only the
names that changed, and in every other file a key that looks like a token, secret, password,
credential or key is shown as "changed" without its value.
## Security
- Some tracked files carry credentials (`.env`, `users.json`, `custom-model-hosts.json`,
@@ -135,9 +156,10 @@ over the file in the data directory is equivalent (stop Codeman first if you are
- **A custom folder is your decision.** Putting it on a shared drive, a synced folder or a
network share copies those credentials there, with whatever permissions that location has.
Choose a private location, or turn the feature off if you cannot.
- In [multi-user mode](Multi-User-Mode) all of it is admin only: the list, Back up now and
Restore.
- Listing returns file names and sizes, never contents.
- In [multi-user mode](Multi-User-Mode) all of it is admin only: the list, Back up now,
Restore and Compare.
- Listing returns file names and sizes, never contents. A comparison returns changed values but
never a credential's (see [Comparing backups](#comparing-backups)).
## HTTP API
@@ -146,6 +168,8 @@ API=http://localhost:3000
curl -s "$API/api/config-backups" | jq # settings, folder, list
curl -s -X POST "$API/api/config-backups" | jq # back up now
curl -s -X POST "$API/api/config-backups/cfg-20261011-120305/restore" | jq
curl -s "$API/api/config-backups/diff?from=cfg-20261011-120305" | jq # that backup vs the running config
curl -s "$API/api/config-backups/diff?from=cfg-20261011-120305&to=cfg-20261012-090000" | jq
```
See [HTTP API](HTTP-API) and the endpoint reference in `docs/api-reference.md`.
+1
View File
@@ -176,6 +176,7 @@ curl -s -X POST "$API/api/mcp-sync" | jq # apply it: add missing ser
curl -s "$API/api/config-backups" | jq # config snapshots: folder, retention, list (admin only in multi-user mode)
curl -s -X POST "$API/api/config-backups" | jq # back up the customized config files now
curl -s -X POST "$API/api/config-backups/cfg-20261011-120000/restore" | jq # put a snapshot back (after a pre-restore copy)
curl -s "$API/api/config-backups/diff?from=cfg-20261011-120000" | jq # what differs from the running config (`to=` another snapshot id to compare two)
# with ID set to a session id:
curl -s "$API/api/sessions/$ID/last-response" | jq -r '.data.text' # last answer, from the transcript (claude, codex, deepseek)