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>
An aggregate with no default_sink already left events created in it alone, but
treated doing so as a failure: it reported a skip per event per cycle saying no
sink was configured, as though something had gone wrong. Nothing had. An
aggregate is also a calendar, and holding events of its own is a legitimate way
to use one.
Skipped::NoSink is replaced by a kept_local count, reported plainly. `@local`
joins the routing markers, so the mode also works per-event where a
default_sink is configured — which was not previously expressible. Unlike every
other marker it is deliberately not stripped: the others have done their job
once the event reaches its source, whereas this one never leaves, so it has to
stay legible for the next cycle to reach the same decision. `local` is
therefore a reserved endpoint id, and configuring one is refused.
This also fixes a real defect. `retarget` rebuilds the new target from the
recorded links, which cover derived events only, so an event belonging to the
aggregate itself did not follow the move — it stayed on the calendar being left
behind while everything around it moved on, quietly. It is now carried across,
since there is nothing to re-derive it from, and the new target's scheduling
rule is applied on the way: this is a write to an aggregate like any other, and
a guest list carried live onto a server that schedules would mail everyone on
it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SPECS.md says why the tool is built the way it is and TODO.md tracks what is
left; neither tells anyone how to use it. This covers install, the whole
configuration format, routing, the commands, and what the state directory
holds.
The Google section is walked through step by step rather than left to Google's
own documentation, because the parts that actually cost time are the ones it
does not cover: the client has to be a Desktop app, since calcalist redirects
to a loopback port it picks at run time and a Web application client demands a
redirect URI and a verified domain it cannot be given; you have to add yourself
under Test users or authorisation fails with access_denied against your own
account; and a consent screen left in Testing has its refresh tokens expired
after seven days, so a working installation stops with nothing having changed.
Checked against the binary rather than by reading: every documented command and
flag against `--help`, every field against config.rs, and the worked example
run through `doctor` and `status` to confirm it validates and prints the
routing markers the README says it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>