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:
parent
47ad8b4c47
commit
ef6482ed62
13 changed files with 808 additions and 92 deletions
62
README.md
62
README.md
|
|
@ -323,6 +323,7 @@ calcalist sync [--dry-run] [--force]
|
|||
calcalist status
|
||||
calcalist doctor
|
||||
calcalist prune [--force]
|
||||
calcalist discover <url> --username <name> [--secret-command <cmd>]
|
||||
calcalist google login <endpoint>
|
||||
calcalist aggregate retarget <id> --to <endpoint> [--keep-old | --purge-old]
|
||||
```
|
||||
|
|
@ -334,6 +335,10 @@ calcalist aggregate retarget <id> --to <endpoint> [--keep-old | --purge-old]
|
|||
- **`status`** lists endpoints and aggregates, how many events each aggregate is
|
||||
mirroring, and the routing markers that would work.
|
||||
- **`doctor`** checks the environment and configuration. Run it first.
|
||||
- **`discover`** asks a CalDAV server which calendars it has and prints an
|
||||
`[[endpoint]]` block for each, ready to paste. Give it the account or
|
||||
principal URL rather than a single calendar. This is the easy way to get the
|
||||
`url` field right.
|
||||
- **`prune`** reports local mirrors belonging to endpoints your configuration no
|
||||
longer names. It only lists them until you pass `--force`, since an endpoint
|
||||
may just have been renamed.
|
||||
|
|
@ -346,7 +351,7 @@ calcalist aggregate retarget <id> --to <endpoint> [--keep-old | --purge-old]
|
|||
events calcalist put there.
|
||||
|
||||
Exit codes: `0` all well, `1` something failed or an endpoint could not be
|
||||
reached, `2` not implemented.
|
||||
reached.
|
||||
|
||||
If a Google endpoint cannot be reached — a lapsed token, no network — the cycle
|
||||
does not stop. The endpoint is named, only the aggregates that depend on it
|
||||
|
|
@ -372,48 +377,37 @@ configuration between machines; leave this behind.
|
|||
|
||||
## Running it regularly
|
||||
|
||||
There are no packaged units yet. A systemd user timer does the job:
|
||||
|
||||
`~/.config/systemd/user/calcalist.service`
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Synchronise calendars with calcalist
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=%h/.local/bin/calcalist sync
|
||||
```
|
||||
|
||||
`~/.config/systemd/user/calcalist.timer`
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Synchronise calendars every 15 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2m
|
||||
OnUnitActiveSec=15m
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
```
|
||||
Units ship in [systemd/](systemd/):
|
||||
|
||||
```sh
|
||||
install -Dm644 systemd/calcalist.service ~/.config/systemd/user/calcalist.service
|
||||
install -Dm644 systemd/calcalist.timer ~/.config/systemd/user/calcalist.timer
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now calcalist.timer
|
||||
```
|
||||
|
||||
A cycle is one-shot, and systemd will not start the service again while a run is
|
||||
still going, so a timer is all the scheduling needed. calcalist takes no lock of
|
||||
its own, though, so avoid running `calcalist sync` by hand while the timer might
|
||||
fire. If your secrets come from a keyring that needs an unlocked session, the
|
||||
The service expects the binary at `~/.local/bin/calcalist`; edit `ExecStart` if
|
||||
yours is elsewhere. The timer runs every 15 minutes with a randomised delay of
|
||||
up to two minutes, which spreads requests instead of every installation calling
|
||||
Google on the quarter hour — its quota is enforced per minute, per project.
|
||||
|
||||
A cycle that could not reach an endpoint exits non-zero deliberately, so a
|
||||
lapsed token surfaces as a failed unit in `systemctl --user status calcalist`
|
||||
rather than passing unnoticed. `journalctl --user -u calcalist` has the report.
|
||||
|
||||
Only one cycle runs at a time: calcalist takes a lock on its state directory, so
|
||||
a timer firing while you are running `calcalist sync` by hand is refused with a
|
||||
message naming the process that holds it, rather than the two interleaving their
|
||||
writes. If your secrets come from a keyring that needs an unlocked session, the
|
||||
timer will only work while you are logged in.
|
||||
|
||||
## Not yet
|
||||
|
||||
- **The web configuration interface.** `calcalist serve` exits 2.
|
||||
- **Packaged systemd units**, as above.
|
||||
- **A failing `pimsync sync` stops the whole CalDAV leg.** Google endpoints are
|
||||
isolated from each other, but pimsync is a single process covering every
|
||||
CalDAV and feed pair, and a failure does not say which pair it belongs to.
|
||||
|
||||
There is deliberately no web interface. It was designed and then dropped: the
|
||||
Google OAuth setup, which no interface can remove, is a far higher bar than
|
||||
editing this file, so a UI would have served an audience that never gets as far
|
||||
as the config. `doctor`, `status` and `discover` cover what it was for.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue