Work produced by the /kb driver on 2026-07-30. Each change is recorded on its Kanboard task; all remain Done-unverified or parked pending alvis's decisions. #183 agap-mcp/src/gitea.js askpassScript() and giteaWikiWrite()'s wiki checkout both used /tmp/agap-mcp-wiki, so writing the askpass helper made the dir non-empty and git clone always failed. gitea_wiki_write had likely never succeeded in production. Askpass moved to its own dir. #181 agap-mcp/src/server.js Initialise registeredToolCount at module load so /health reports the real count immediately instead of 0 until the first MCP request. #189 kanboard/backup.sh, seafile/backup.sh, vaultwarden/backup.sh, users-backup.sh, openai/backup-{hindsight-adolf,llm-dbs}.sh Remove the dead *.ts Zabbix trapper pushes (never landed). users-backup.sh also pointed at localhost:81 instead of 192.168.1.4:81 and pushed a date string into a numeric item. Freshness monitoring now rides the .age items. #192 RESTORE-RUNBOOK.md, {kanboard,seafile,vaultwarden}/restore.sh Restore path for the three services, verified in throwaway containers. Note: this work found Seafile backups have carried an empty ccnet_db.sql since 2026-07-07 -- filed as kb#222, not fixed here. #164 openai/litellm-config.yaml Metered `judge` (anthropic/claude-haiku-4-5) entry removed per alvis's 2026-07-30 decision. ANTHROPIC_API_KEY was never wired, so it could not spend. #128 openai/agent_registry.py litellm_key_spec() now also grants the routing-mode aliases, gated by the same _reachable_tiers() check as raw grants, so a small-tier agent cannot acquire automatic routing that resolves to tier-large. #219 openai/migrate-adolf-state.sh Migration script only; inert until run. Copies (never moves) the openai_adolf-state volume to /mnt/ssd/dbs/adolf, verifying a full sha256 manifest before declaring success. Tested against a throwaway volume. Deliberately NOT included, both awaiting alvis: agap-mcp/docker-compose.yml -- kb#174's contested BW_EMAIL revert (parked). openai/docker-compose.yml -- kb#219's bind-mount switch; the target dirs under /mnt/ssd/dbs/adolf do not exist yet, so committing it would let a later `compose up` recreate Adolf against empty paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Y5QPagv4iun1ghpwM96Ff
8.6 KiB
Restore Runbook — Kanboard, Vaultwarden, Seafile
Companion to each service's backup.sh. Covers kb#192.
Each service now has a restore.sh next to its backup.sh:
kanboard/restore.shvaultwarden/restore.shseafile/restore.sh
All three follow the same convention: they default to the live container names/paths, but every target is overridable via env vars, so the exact same script restores into a real disaster or into a disposable/throwaway container for a dry-run test. Never invoke a restore.sh without env overrides unless you are doing a real, intentional disaster recovery — the defaults point at production.
Before you restore anything
/mnt/backups/<service>/is read-only in practice — copy the snapshot you want out to a scratch dir first, don't operate on it in place.- Confirm you have the right snapshot:
ls /mnt/backups/<service>/and pick the newest dir, but check its contents aren't empty (see the Seafile gotcha below — an empty dump looks like a valid directory). - Restoring into the live container is destructive and briefly stops the service. Only do this for a real incident, and say so out loud before running it.
Kanboard
cp -r /mnt/backups/kanboard/<snapshot> /tmp/kb-restore
# Real disaster recovery (overwrites the live "kanboard" container):
cd /home/alvis/agap_git/kanboard
./restore.sh /tmp/kb-restore
What it does: stops the kanboard container, replaces db.sqlite via
docker cp, restores plugins.tar.gz if present, restarts, then verifies
by querying SELECT COUNT(*) FROM tasks through the container's PHP PDO
sqlite driver (kanboard's image has no sqlite3 CLI).
Throwaway test (no live impact — no ports published, isolated volumes):
docker volume create kb_restore_test_data
docker volume create kb_restore_test_plugins
docker run -d --name kanboard-restore-test \
-v kb_restore_test_data:/var/www/app/data \
-v kb_restore_test_plugins:/var/www/app/plugins \
-e PLUGIN_INSTALLER=true kanboard/kanboard:latest
CONTAINER=kanboard-restore-test ./restore.sh /tmp/kb-restore
# Teardown
docker rm -f kanboard-restore-test
docker volume rm kb_restore_test_data kb_restore_test_plugins
Verified 2026-07-30: restored the 2026-07-28 03:00 snapshot into a
throwaway container this way. tasks table came back with 180 rows;
container started healthy. Throwaway container and volumes torn down after.
Vaultwarden
cp -r /mnt/backups/vaultwarden/<snapshot> /tmp/vw-restore
# Real disaster recovery (overwrites /mnt/ssd/dbs/vw-data and the live
# "vaultwarden" container — needs root; the data dir is root-owned):
cd /home/alvis/agap_git/vaultwarden
sudo ./restore.sh /tmp/vw-restore
What it does: stops the vaultwarden container, copies the db_*.sqlite3
snapshot to db.sqlite3, restores config.json, rsa_key*,
attachments/, sends/, restarts, then checks the DB file is in place
(the vaultwarden image has no sqlite3 CLI either — verification falls
back to a file-presence/size check and reading the startup log for a
clean launch with no re-keying).
Throwaway test (isolated data dir, isolated container, no ports published):
mkdir -p /tmp/vw-data-test
docker run -d --name vaultwarden-restore-test --user 1000:1000 \
-v /tmp/vw-data-test:/data vaultwarden/server:latest
CONTAINER=vaultwarden-restore-test DATA_DIR=/tmp/vw-data-test \
./restore.sh /tmp/vw-restore
# Teardown
docker rm -f vaultwarden-restore-test
rm -rf /tmp/vw-data-test
Note: --user 1000:1000 is only needed for the throwaway test so the bind
mount is writable by a non-root operator; the live container runs as root
and the live data dir is root-owned, so a real restore needs sudo.
Verified 2026-07-30: restored the 2026-07-28 02:00 snapshot into a
throwaway container this way. db.sqlite3 landed at the correct size,
config.json was picked up ("Using saved config from data/config.json"
in the startup log), and the RSA key was reused rather than regenerated
(no "Private key created" line on the post-restore boot) — i.e. structural
restore confirmed. Row-level vault content was not inspected (per
Vaultwarden-handling rules — never surface real vault contents). Throwaway
container and scratch dir removed after.
Seafile
cp -r /mnt/backups/seafile/<snapshot> /tmp/sf-restore
# Real disaster recovery (drops+reloads ccnet_db/seafile_db/seahub_db in
# the live "seafile-mysql" container, and rsyncs the data dir back into
# /mnt/misc/seafile, stopping/starting the "seafile" container around it —
# needs root; data dir is root-owned):
cd /home/alvis/agap_git/seafile
sudo ./restore.sh /tmp/sf-restore
What it does: refuses to run if any of the three *.sql dumps in the
snapshot is missing/empty (see gotcha below), then for each of
ccnet_db/seafile_db/seahub_db: DROP DATABASE IF EXISTS +
CREATE DATABASE + reload from the dump, and reports table counts as a
sanity check. If the snapshot has a data/ dir and SEAFILE_CONTAINER is
set (default), it also stops the seafile app container, rsyncs data/
into DATA_DIR, and restarts it.
Env-var gotcha fixed during this task: SEAFILE_CONTAINER="" used to
fall back to the live container name, because ${VAR:-default} treats an
empty string as unset. It's now ${VAR-default} so an explicit empty
string really means "skip the data-dir step." If you want a DB-only
restore, pass SEAFILE_CONTAINER="" explicitly.
Throwaway test (DB-only, fully isolated MariaDB container + scratch data dir):
docker volume create sf_restore_test_db
docker run -d --name seafile-mysql-restore-test \
-e MYSQL_ROOT_PASSWORD=<test-only-pw> \
-e MYSQL_USER=seafile -e MYSQL_PASSWORD=<matches SEAFILE_MYSQL_DB_PASSWORD> \
-e MYSQL_DATABASE=placeholder \
-v sf_restore_test_db:/var/lib/mysql mariadb:10.11
# grant seafile broad perms on this throwaway instance only:
docker exec seafile-mysql-restore-test mysql -u root -p<test-only-pw> \
-e "GRANT ALL PRIVILEGES ON *.* TO 'seafile'@'%'; FLUSH PRIVILEGES;"
mkdir -p /tmp/sf-data-test
MYSQL_CONTAINER=seafile-mysql-restore-test \
SEAFILE_CONTAINER=seafile-app-does-not-exist-test \
DATA_DIR=/tmp/sf-data-test \
./restore.sh /tmp/sf-restore
# Teardown
docker rm -f seafile-mysql-restore-test
docker volume rm sf_restore_test_db
rm -rf /tmp/sf-data-test
Verified 2026-07-30 — partially: DB restore was verified end-to-end
into a throwaway MariaDB container using the last known-good snapshot
(2026-07-04 02:00 — see the critical finding below): ccnet_db (13
tables), seafile_db (46 tables), seahub_db (127 tables) all restored
and importable. The data/ directory step ran cleanly against an isolated
scratch dir (/tmp/sf-data-test, not /mnt/misc/seafile) — the on-disk
tree (seafile-data/, seadoc-data/, onlyoffice-data/) landed as
expected. A full running Seafile app-stack test (seafile+redis+caddy
actually serving content from the restored data) was not attempted — that
would need the full compose stack, matching JWT_PRIVATE_KEY/hostname
config from .env, and meaningfully more setup than the box's read-only
/mnt/backups + non-root constraints support cleanly in one pass. If a
full app-level restore drill is wanted, treat it as separate follow-up
work with root access.
⚠️ Critical finding: Seafile backups have been broken since 2026-07-07
Every /mnt/backups/seafile/<snapshot> from 2026-07-07 through the
latest, 2026-07-28, contains only an empty ccnet_db.sql (0 bytes)
and nothing else — no seafile_db.sql, seahub_db.sql, or data/. The
last known-good snapshot is 2026-07-04 02:00. restore.sh now refuses
to run against a snapshot with a missing/empty dump file rather than
silently "restoring" an empty database, but the backup cron job itself
is still broken and needs its own fix (likely the mysqldump credentials
in seafile/backup.sh no longer matching the live seafile MySQL user, or
similar — not diagnosed further here since reproducing it requires running
mysqldump against the live seafile-mysql container, which is out of
scope for a restore-focused task and was blocked by the sandbox's
action classifier during this session). Recommend filing a new,
separate task to fix seafile/backup.sh — this is a live gap: three
weeks of Seafile backups are currently useless.
Files touched
/home/alvis/agap_git/kanboard/restore.sh(new)/home/alvis/agap_git/vaultwarden/restore.sh(new)/home/alvis/agap_git/seafile/restore.sh(new)/home/alvis/agap_git/RESTORE-RUNBOOK.md(this file, new)
Per the standing rule for this repo, nothing above was committed — it's left in the working tree for a human to review and commit.