diff --git a/docs/decisions/0024-off-host-snapshot-copy-pi-pull.md b/docs/decisions/0024-off-host-snapshot-copy-pi-pull.md index ad275c5..f69b065 100644 --- a/docs/decisions/0024-off-host-snapshot-copy-pi-pull.md +++ b/docs/decisions/0024-off-host-snapshot-copy-pi-pull.md @@ -1,7 +1,7 @@ # ADR 0024 - The off-host copy is a Pi-side pull, and it cannot be an rsync of the volume **Date:** 2026-10-12 -**Status:** Accepted +**Status:** Accepted, except the schedule - see **Not scheduled** under Decision **Deciders:** Prospero (CEO, risk model and mechanism), Daedalus (CTO, transport and implementation) --- @@ -96,12 +96,20 @@ container of the image cotel's own runtime is built on, which is already on that **A Pi-side pull on a systemd timer, hourly, using option 3's transport.** +> **Not scheduled (2026-10-13).** The timer was never installed. It would have had to +> live in the owner's own `~/ops` tree rather than in a company repository, that write +> is sanctioned per case, and the request was declined. So nothing on this machine runs +> the pull on its own: the mirror is refreshed only when someone runs +> `scripts/pull-snapshots.sh`, and its freshness is unknown until `--status` is read. +> Everything else below shipped and is live - the transport, the mirror, the +> verification and the pruning rule. + - **The script lives in this repo** (`scripts/pull-snapshots.sh`), and the timer in `~/ops` materializes it from `origin/main` at each tick. That is [ADR-0022](./0022-health-probe-scheduler-outside-github)'s pattern, unchanged and for the same reasons: the code path stays reviewed here and reaches the schedule with no sync step. -- **Hourly, not every 6 h.** A tick that finds nothing new costs one ssh round trip, so +- **Hourly was the intended cadence**, not every 6 h. A tick that finds nothing new costs one ssh round trip, so the cadence is a free variable; matching it to the snapshot interval would put the mirror's worst case at nearly two intervals behind for no saving. - **The mirror lives at `~/cotel-snapshots/`, mode 700** - outside `~/projects`, @@ -139,14 +147,15 @@ container of the image cotel's own runtime is built on, which is already on that itself is established at the source, with the drill in ADR-0023. Publishing an arm64 manifest would make the mirror independently verifiable on the Pi; that is a packaging decision with its own ticket, not a prerequisite for having the copy. -- **One more script and timer in `~/ops` that this company maintains.** Named as the - honest cost when the option was chosen, and accepted. It is also a write into the - owner's own maintenance tree rather than a company repo, which is sanctioned per - case - the same gate `cotel-healthz.timer` went through. -- **A failing pull is loud; an absent timer is not.** The unit's non-zero exit reaches - `~/ops/ALERT` and a desktop notification through the `OnFailure=` path the other ops - units already use, so a pull that breaks is announced. A timer that was never - installed, or was disabled, raises nothing - the same residual +- **No footprint in `~/ops` after all.** The extra script and timer this company would + have maintained there was named as the honest cost of the option and accepted by the + CEO, then declined at the gate that actually owns it: `~/ops` is the owner's + maintenance tree, not a company repo, and that write is sanctioned per case - the same + gate `cotel-healthz.timer` passed and this one did not. +- **Nothing is loud, because there is no unit.** The design put a failing pull on the + `OnFailure=` path the other ops units use, so a broken tick would have announced + itself; with the timer declined, that path does not exist and the only signal is + `--status` read by a human. The weaker half of this was already the accepted residual [ADR-0022](./0022-health-probe-scheduler-outside-github) accepted for the probe's own schedule, and the reason the mirror's newest instant is worth reading in the runbook rather than assumed. diff --git a/docs/operations/duckdb-snapshots.md b/docs/operations/duckdb-snapshots.md index 40346bc..3d701bd 100644 --- a/docs/operations/duckdb-snapshots.md +++ b/docs/operations/duckdb-snapshots.md @@ -253,8 +253,9 @@ of the credentials. Snapshots land on the same disk as production, so a second copy lives on another machine: the Pi pulls the complete snapshots it does not have yet and -keeps them. The decision, the transport it was chosen on and the risk that is -knowingly *not* covered are +keeps them. **Nothing schedules that pull**, so the mirror is exactly as fresh +as the last time someone ran the command below. The decision, the transport it +was chosen on and the risk that is knowingly *not* covered are [ADR-0024](../decisions/0024-off-host-snapshot-copy-pi-pull). ```bash @@ -273,9 +274,9 @@ to read - it is inside Docker Desktop's VM. Production is never touched. **not** under `~/projects`: that checkout is shared, and a copy inside a repository is a copy that can be committed. Deliberately **not** under `~/ops/reports/`, which is swept on a 31-day retention rule that has nothing to -do with this data. The schedule itself is a user systemd timer in the owner's -`~/ops` tree, which materializes the script from `main` at each tick - the same -arrangement the health probe's schedule uses, and for the same reason. +do with this data. There is **no** schedule: the systemd timer that would have +run this in the owner's `~/ops` tree was declined, so the pull happens only when +someone runs the command above. The mirror carries the same secrets as the data volume: `users.token` is plaintext by design, so every snapshot in it holds a copy of every agent's @@ -297,15 +298,15 @@ pull-snapshots: status ``` `lag` is the gap between the newest snapshot here and the newest one at the -source. At the shipped `6h` interval and an hourly tick, a lag under an hour is -a mirror that is working; a lag that keeps growing means the tick is no longer -running, and nothing else will tell you that - a timer that was never installed -or was disabled raises no alert. +source. With no schedule it grows by one snapshot every `6h` until someone runs +a tick, so treat it as the mirror's age rather than as a health signal, and read +it before relying on the copy. Nothing else will tell you: there is no unit, so +no alert. ### Exit codes -A tick's non-zero exit is what the `~/ops` unit turns into `~/ops/ALERT` plus a -desktop notification, so the codes are meant to be read: +A tick's non-zero exit reaches only whoever ran it - nothing watches this pull - +so the codes are meant to be read: | | | |---|---| @@ -367,10 +368,11 @@ source, with the drill in failures cotel has actually had - and not against the disk or the machine going away. The off-host copy is decided - [ADR-0024](../decisions/0024-off-host-snapshot-copy-pi-pull) - and the pull - that implements it is [the off-host mirror](#the-off-host-mirror): an hourly - pull to the Pi over the existing LAN ssh path, with premises loss accepted as - uncovered because it is another machine in the same building. A mirror is only - as current as its last tick, so read `--status` instead of assuming. + that implements it is [the off-host mirror](#the-off-host-mirror): a pull to + the Pi over the existing LAN ssh path, with premises loss accepted as + uncovered because it is another machine in the same building. That pull is + **unscheduled** - a mirror is only as current as its last tick, and here the + last tick was a person, so read `--status` instead of assuming. - **`docker volume prune`.** The snapshots volume is only "in use" while the container exists, so a prune on a stopped deploy takes the backups with it. This is the one new way the volume layout can bite; no code can prevent it.