pastilhasandClaude Opus 5 785e5f9eed edit and delete, on both screens
the screens could show a calendar and an address book and change neither. this
adds a pencil and a bin to every event and every card, with a confirm on the bin.

three things worth defending.

THE HREF IS THE ADDRESS, and the listing now carries it. it used to return only
the object's data, which is enough to show something and not enough to change
it. deriving `<uid>.ics` was the obvious shortcut and is wrong: a .ics filename
only conventionally matches the uid — radicale stores what the writing client
chose — so it works for everything officer wrote and misses everything a phone
did. proved immediately: this machine's own calendar holds
62970EC7-47B1-4214-858E-85C32C2476A5.ics, written by an iphone.

READ, PATCH, PUT — NEVER REBUILD. an event is a file with far more in it than
these screens show: alarms, attendees, RRULEs, every X- property. rebuilding it
from the fields on screen would silently drop all of that, and an edit that
quietly deletes half an event is worse than no editing. so a save reads the
original text, replaces only the lines it owns, and puts the whole file back.
verified on a real event: 22 lines before, 22 after, one changed.

THE EDITOR IS DELIBERATELY SMALL. title and place for an event, name/email/phone
for a card. times and recurrence are where an editor does damage, and a
half-editor that rewrites an RRULE it does not understand is the thing this
avoids. `setProperty` also escapes commas, semicolons and newlines, because a
raw comma silently splits an iCalendar TEXT value into parts.

the sidecar grew GET/PUT/DELETE on /_officer/item, above the `collection is
required` guard — they take an href rather than a collection, and sitting below
it they answered 400 forever. both check the href is inside this user's own tree
even though X-Remote-User already scopes radicale: an href from a request body
is attacker-shaped input either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-27 10:25:38 +00:00
2026-08-27 10:25:38 +00:00
2026-08-27 10:25:38 +00:00
2026-08-27 07:36:16 +00:00

Calendar

Calendars and contacts on your own machine, synced to your phone over CalDAV and CardDAV.

  • Radicale, not a container. A ~2MB Python package, spawned on loopback by this plugin's sidecar. It was a Docker service only because the old app store made everything one.
  • Per user. The sync door resolves an app password to one account and tells Radicale that account's id is the principal; [rights] type = owner_only does the rest. Every user gets their own calendars and their own contacts, in their own directory, invisible to everyone else.
  • Phones work. DAVx5, iOS and Thunderbird, including RFC 6764 autodiscovery from a bare domain.

Two screens, one plugin

/calendar and /contacts. They are two places you go rather than two sections of one page, which is why this is the first plugin to declare screens in its manifest — one sidecar, one permission, one install, two routes and two dock tiles.

What it claims from the platform, and why

Four paths outside /api, above the account gate:

Path Why it cannot sit behind the session
/dav/* DAVx5, iOS and Thunderbird send HTTP Basic on every request and have nowhere to put a JWT
/dav/provision/*.mobileconfig Safari is handed a URL by the phone app and has no credential; the one-shot token in the path IS the authentication
/.well-known/caldav RFC 6764 — probed with a bare domain before the client has any credential
/.well-known/carddav the same, for contacts

"Above the account gate" is not "open". The sync door demands a scoped DAV app password and resolves it to a user before anything reaches Radicale.

Every one of those is in the platform's RESERVED_ROOT, claimable only by a first-party plugin. A third-party plugin is confined to /p/<publisher>/ so it can never shadow a core route.

Extracted from the platform on 2026-08-27

It was core from 2026-08-19. What moved: the sidecar, the four routers, the panel apps, the app-password table, and the settings form — which became a modal on the Calendar screen, because a plugin ships panels and cannot contribute a settings section.

Two things deliberately did not change. The table is still dav_app_passwords rather than caldav_app_passwords: installs already hold real rows, and the platform's prefix convention is not worth a data migration on a live machine. And the panel names still resolve through the platform's translation catalogs, so the six locales they had as core screens survive.

S
Description
Calendars and contacts on your own machine, synced to your phone over CalDAV and CardDAV.
Readme
777 KiB
Languages
TypeScript 100%