calendar and contacts are core again, with radicale as a plain sidecar

Reverses the 2026-08-13 decision to make caldav an app-store entry. Three
reasons it does not belong there.

It is not an integration. Photos-via-Immich or torrents-via-Transmission assume
you already run the thing; a calendar assumes nothing, and a personal server
without one is missing a baseline rather than an optional app.

It needs TWO top-level routes, /calendar and /contacts, and a plugin gets one —
derived from its appName, which is what makes it structurally impossible for a
plugin to shadow a core route. Nobody thinks "I'll open my dav", so collapsing
them under /dav was the wrong trade, and weakening the namespace guarantee to
allow two was a worse one. As core routes the question does not arise.

And it never actually needed Docker. Radicale is a ~2MB Python package;
sidecar/caldav/radicale.ts already spawns it directly on loopback with
http_x_remote_user. The container came from the app store making everything a
container, not from anything Radicale requires.

So: the routers are mounted again (/caldav for the UI's JSON door, /dav for app
passwords, the top-level /dav sync door and the two .well-known redirects),
officer-caldav joins CORE_PROCESSES, radicale joins the machine-setup package
list for all four package managers, and the catalogue entry is gone.

members.test.ts re-anchored a fifth time — caldav was one of two `accounts`
services and memos is now the last. Noted in the test that the sixth time
should be a literal fixture instead.

Verified: assertPermissionTotality accepts /caldav and /dav, tsgo clean,
1085 pass. NOT yet verified against a real client — radicale is not installed
on this machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 04:19:50 +00:00
co-authored by Claude Opus 5
parent f566fda0b2
commit 9c309c5ab6
7 changed files with 32 additions and 41 deletions
+8 -5
View File
@@ -18,11 +18,14 @@ describe('what adding a member would do', () => {
describe('which installed services have work to do', () => {
it('skips single-tenant daemons entirely', () => {
// caldav and memos provision accounts; vnc has no user concept. Both survivors are services with no
// extraction planned — this assertion has been re-pointed four times by services leaving for
// `plugins/`, so it now names ones that are staying.
const ids = servicesNeedingMemberProvision(['caldav', 'vnc', 'memos']).map((e) => e.id);
expect(ids.sort()).toEqual(['caldav', 'memos']);
// memos provisions accounts; vnc has no user concept and must be skipped.
//
// Down to ONE `accounts` service in the catalogue, which is why this names a single id rather than
// two. It has been re-pointed five times as services left — jellyfin, invoiceshelf and slskd to
// `plugins/`, caldav to CORE — and memos is the last one standing. If memos ever leaves too, this
// wants a literal fixture like `installer.test.ts` has, not a sixth re-point.
const ids = servicesNeedingMemberProvision(['memos', 'vnc']).map((e) => e.id);
expect(ids.sort()).toEqual(['memos']);
});
it('ignores services that are not installed', () => {