calendar and contacts, extracted from the officer platform

one sidecar, one permission, one install — and two screens, because Calendar and
Contacts are two places you go rather than two sections of one page. the first
plugin to declare `screens`, which the platform gained for this.

what moved: the radicale sidecar, the four routers, the panel apps, the
app-password table and the settings form. the form became a modal on the
calendar screen — a plugin ships panels and cannot contribute a settings
section, which is the same wall the email plugin hit and the same answer.

the routes are /calendar and /contacts, not /caldav/calendar. they were core
routes and keeping them is not cosmetic: a bookmark, a dock tile someone
arranged, and a persisted layout all address them. only a first-party plugin may
choose its own segment; a third-party one is confined to /p/<publisher>/.

four paths outside /api come with it, above the account gate, because phones are
not browsers: DAVx5, iOS and thunderbird send HTTP basic on every request and
have nowhere to put a JWT. that is not "open" — the sync door demands a scoped
app password and resolves it to one user before radicale sees anything.

two things deliberately unchanged. the table is still dav_app_passwords, not
caldav_app_passwords: installs hold real rows and a naming convention is not
worth a data migration on a live machine. and the panel names still resolve
through the platform's catalogs, so the six locales they had as core screens
survive rather than dropping to english to make the refactor tidier.

per-user was already true and is worth writing down because the config said
otherwise: the sync door resolves an app password to an account, tells radicale
that id is the principal, and owner_only does the rest. the comment claiming
"the owner is the only principal that exists" had been stale since multi-user
landed on 2026-08-07.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-27 04:53:45 +00:00
co-authored by Claude Opus 5
commit dcbc9b102c
30 changed files with 2362 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
import type { PluginManifest } from '@@/plugins/manifest';
/**
* Calendar and contacts, over CalDAV and CardDAV.
*
* One sidecar (Radicale on loopback), one permission, one install — and TWO screens, because Calendar and
* Contacts are two places you go rather than two sections of one page. That is what `screens` below is
* for, and caldav is the first plugin to use it.
*/
export const manifest: PluginManifest = {
publisher: 'officerdev',
version: '1.0.0',
platform: '>=1.0.0',
label: 'Calendar',
summary: 'Calendars and contacts on your own machine, synced to your phone over CalDAV and CardDAV.',
color: '#ef4444',
/**
* Two screens at URLs this plugin chose — `/calendar` and `/contacts`, not `/caldav/calendar`.
*
* They were core routes until 2026-08-27 and keeping them is not cosmetic: a bookmark, a dock tile a
* user has arranged, and the layout persisted under `screens/plugin/caldav/...` all address them.
*
* Only a first-party plugin may choose its own segment — a third-party one is confined to
* `/p/<publisher>/` so it cannot shadow a core route. Each ships its own artwork as
* `assets/<segment>.png`, because two identical tiles side by side say nothing about which is which.
*/
screens: [
{ segment: 'calendar', label: 'Calendar', icon: 'CalendarDays' },
{ segment: 'contacts', label: 'Contacts', icon: 'Contact' },
],
permissions: [
{
key: 'calendar',
label: 'Calendar and contacts',
description: 'Calendars, contacts, and the app passwords that sync them to a phone',
// App passwords are minted for and revoked by their own owner, and are how a member's own phone
// reaches their own calendar. Nothing under here is shared, so a read grant may still write them.
personal: ['/passwords', '/provision'],
},
],
/**
* The top-level doors, above the account gate — and the reason is that phones are not browsers.
*
* DAVx5, iOS and Thunderbird send HTTP Basic on every request and have nowhere to put a platform JWT,
* so the account gate would 401 every real client. `sync-router` demands its own credential instead: a
* scoped DAV app password, resolved to one user, whose id is then what Radicale is told the principal
* is. "Unauthenticated" here means "not authenticated by the PLATFORM's session", not "open".
*
* The two `.well-known` names are RFC 6764 autodiscovery: the client is given a bare domain and probes
* them before it has any credential at all. Without them iOS degrades to demanding a full collection
* URL, which is exactly the sort of thing that makes self-hosting feel worse than the product it
* replaces.
*
* Every one of these was the platform's until this plugin existed, and each is in `RESERVED_ROOT` for
* that reason — claimable by a first-party plugin, refused to everyone else.
*/
unauthenticatedRootPaths: ['/dav', '/.well-known/caldav', '/.well-known/carddav'],
/**
* Radicale — a ~2MB Python package, spawned on loopback by this plugin's sidecar.
*
* Not a container: it was one only because the old app store made everything one. No Homebrew formula
* exists (confirmed live: "No available formula with the name radicale"), so a macOS dev install has
* nowhere for the sidecar to find it — which is why `resolveRadicale()` failing is handled rather than
* fatal.
*/
osDependencies: [
{
binary: 'radicale',
reason: 'Serves the CalDAV and CardDAV collections. Without it there are no calendars and no contacts.',
packages: { apt: 'radicale', pacman: 'radicale', dnf: 'radicale' },
},
],
};