Files
caldav/manifest.ts
pastilhasandClaude Opus 5 b578c2aa3b 2026.08.27.03 — a filter on both screens
a calendar of a few hundred events and an address book of a few hundred cards are
both lists you scroll until you give up. one box on each header narrows them.

CLIENT SIDE, because the data is already here: both screens fetch a whole
collection in one request and hold it, so filtering is a string comparison over
what is in hand. a round trip per keystroke would be slower, would need a search
endpoint on the sidecar, and would put a spinner where a person expects a list to
narrow as they type.

ACCENT-INSENSITIVE, and that is the half that decides whether this is usable
here: these contacts are portuguese, and a box where "jose" does not find "José"
is a box nobody uses twice. NFD splits the letter from its accent and the range
strips what is left, so it matches in both directions — "josé" finds "Jose" too.

every term must match somewhere, not all in one field, so "ana lisboa" finds the
ana whose address is in lisbon. events match on their start as well, which is an
ISO string, so "2026-08-29" narrows to a day without anything having to parse it.

the count in the header becomes "12 of 340" while filtering, and a filter that
matches nothing says so by name — otherwise it reads as a calendar that lost its
events.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-27 14:20:50 +00:00

92 lines
4.2 KiB
TypeScript

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: '2026.08.27.03',
platform: '>=1.0.0',
/**
* What it is called in the store, in words that assume nothing.
*
* Not "Calendar", which is one of its two screens and undersells it; not "CalDAV" or "caldav", which
* are protocol names a person browsing has no reason to know. The convention across the store is that
* a plugin is named for what you get, not for how it works.
*/
label: 'Contacts and Calendar',
summary: 'Calendars and contacts on your own machine, synced to your phone over CalDAV and CardDAV.',
color: '#ef4444',
/**
* Personal because that is what it holds; Degoogling because replacing Google Calendar and Google
* Contacts is the reason most people would install it.
*/
tags: ['Personal', 'Degoogling'],
/**
* 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' },
},
],
};