Finish M2: run lock, systemd units, discover; drop the web UI

Designing the configuration UI in full made the case against building it. Its
audience would be people who find TOML hard, but with bring-your-own OAuth
client settled, every user must first create a Google Cloud project, configure
a consent screen and put a secret in a keyring — a far higher bar than editing
thirty lines of config. Anyone who clears it can edit the file; anyone who
cannot never reaches the file. Against that stood three dependencies, five
modules, an auth.rs refactor and a security surface guarding something that
reads the config and touches the keyring. `doctor` and `status` had already
absorbed most of what it was for. The reasoning is recorded in TODO.md and
SPECS.md rather than left as an apparent oversight.

The run lock is not a UI feature and closes a gap that already existed: nothing
stopped a timer firing into a hand-run cycle, and two cycles interleaving
writes over the same vdirs is what the design otherwise avoids. flock is used
rather than a pid file because the kernel releases it however the process ends,
so a crash cannot leave a lock to clear by hand — which also means a lock we
failed to take is held by a live process, so the pid in it is worth reporting.

The one idea worth keeping from the UI design was collection discovery, which
needed no web layer. `calcalist discover` prints a ready-to-paste endpoint
block per calendar a server offers, removing the most error-prone field in the
config. pimsync's discovery output is undocumented, so the format was
established against a real server first. Two things it teaches: everything
arrives on stdout including failures, and a pair has two storages, so pimsync
reports the scratch vdir's contents too — parsing anchors on the heading naming
the server, or a probe directory's leftovers would be offered as the user's
calendars.

Verified against Posteo as well as Radicale: all four calendars found, the
first matching the URL already configured.

Also fixes a real defect in the test harness rather than its symptom. Ports were
chosen by binding one and letting go, so two tests could pick the same number —
and the loser's readiness check then succeeded against the winner's server,
silently sharing it. Startup now confirms the child we spawned is the one alive,
retries on another port if not, and waits for a real HTTP response rather than
an open socket.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
randogoth 2026-09-10 16:17:34 +03:00
parent 47ad8b4c47
commit ef6482ed62
13 changed files with 808 additions and 92 deletions

44
TODO.md
View file

@ -131,15 +131,45 @@ calendar:
- [x] A retired endpoint's mirror is reported by `prune` and removed under
`--force`.
## M2 — packaging (done)
- [x] Run lock (`lock.rs`) — `flock` on the state directory, held by `sync`,
`aggregate retarget` and `prune --force`. Not a UI feature: nothing
previously stopped a timer firing into a hand-run cycle, and two cycles
interleaving writes over the same vdirs is what the design otherwise
avoids. The kernel releases it however the process ends, so a crash
cannot leave a lock to clear by hand; the refusal names the holding pid.
- [x] systemd user units in `systemd/``calcalist.service` (oneshot) and
`calcalist.timer`, with `RandomizedDelaySec` so installations do not all
call Google on the quarter hour, and no filesystem or IPC sandboxing
because the secret commands need the session keyring over D-Bus.
- [x] `calcalist discover` — asks a CalDAV server which calendars it has and
prints a ready-to-paste `[[endpoint]]` block for each. The `url` field is
the most error-prone thing in the config, and providers rarely show it.
- [x] `serve` removed from the CLI, and with it the last `unimplemented`
command, so every command the binary advertises now does something.
### The web interface, dropped
Specified from the start and designed in full before being dropped. The
reasoning, so it is not rediscovered as an oversight:
- Its audience would be people who find TOML hard. But with bring-your-own
OAuth client settled, every user must first create a Google Cloud project,
configure a consent screen and put a secret in a keyring — a far higher bar
than editing thirty lines of config. Anyone who clears it can edit the file;
anyone who cannot never reaches the file.
- The cost was three dependencies, five modules, an `auth.rs` refactor and a
security surface (token, `Host` validation, CSRF, secrets through subprocess
stdin) guarding something that reads the config and touches the keyring —
against SPECS.md's own "no speculative features or dependencies".
- `doctor`, `status` and `discover` had already absorbed what it was for.
The one idea worth keeping from the design was collection discovery, which
needed no web layer at all.
## Known gaps
- [ ] A `pimsync sync` that fails takes the whole CalDAV leg with it. Unlike the
Google side this cannot be narrowed: pimsync is one process covering every
pair, so a failure does not say which pair it belongs to.
## M2 — interface and packaging
- [ ] axum configuration UI, bound to 127.0.0.1
- [ ] Trigger `google login` from the UI (the loopback handler itself already exists
in `google/auth.rs`)
- [ ] systemd user units: `calcalist.service` (oneshot) and `calcalist.timer`