hypercal 2.1.3

A desktop app, an endpoint AI clients can drive the calendar through, and
calendars you can publish as a feed.

**A desktop app.** hypercal now ships as an Electron shell, in two modes. Local
mode runs the portable backend in the app's own process, against a real SQLite
file under your user-data directory, so the desktop build gets the whole
feature set the Android one gives up: filesystem backups, database snapshots,
and a /dav that another app on the same machine can actually reach. Remote mode
runs no backend at all. It points the window at your own instance and runs that
instance's frontend, which is the only arrangement in which passkeys can work,
because WebAuthn compares origins and a loopback proxy would present the wrong
one.

Closing the window hides it to a panel indicator instead of quitting, so the
backend, its pollers and the CalDAV listener stay up with the window put away.
The packages are an AppImage, a tar.gz, a deb, an rpm, a Flatpak and an AUR
PKGBUILD that installs the tarball with its launcher entry and icons.

There is no auto-updater, on purpose. Every one of those formats is owned by a
package manager, and a second thing replacing the binary would fight it. The
app asks the releases API once, and if something newer exists it raises the
same toast the web app uses, with a link to the notes. It fails silently:
offline, proxied, timed out and already-current all mean say nothing.

**An MCP endpoint.** /mcp lets an AI client read and edit the calendar over the
Model Context Protocol, with twelve tools covering search, create, update,
recurrence overrides, conflict checks and free-slot queries. It authenticates
with the app-specific passwords that already exist for CalDAV.

It is off by default, and it is an admin setting rather than an environment
variable, so an admin can see the feature exists and switch it on without shell
access to the deployment. The setting is read per request, so both directions
take effect at once, including for a client already connected. A disabled
endpoint answers 503 identically to a good token and a bad one, so it is not
somewhere to test app passwords. It stays off until someone decides otherwise,
because turning it on lets every app password already minted on the instance
reach /mcp.

**Published calendar feeds.** A calendar can be published at /feed/<token>.ics
for anything that subscribes to a URL. Encryption is checked when you publish
and again on every serve, so no future write path can leak a private calendar
by forgetting a hook, and making a calendar private auto-unpublishes it rather
than failing for a reason you would have to go fix elsewhere. The feed path is
redacted in the request log, since a subscribed client refetches forever and
would otherwise write a live credential into the log several times an hour.

Agents cannot publish. The MCP tools can see and revoke a feed, but making a
calendar world-readable is not a capability a bearer token handed to an AI
client should carry.

**Export one calendar.** /api/export.ics and /api/export.hypercalendar now take
an optional calendar id. Both also filter trashed events, which they did not
before, so exports and the daily backups built on them silently carried
deleted events and disagreed with what the app showed.

**Themes.** Three new palettes, Midnight, Nord and Paper, bringing the set to
six. Follow system is new as well, and is a preference rather than a seventh
palette: it picks the first palette matching your OS scheme, so the radius
scale and type stack still resolve from a real theme.

Underneath, four of the six are now derived from Radix Colors rather than
values somebody picked, and a theme is one object in one file instead of six
coordinated edits across an enum, a stylesheet, a picker, a label map and the
pre-paint script. Blacksite and Nord are deliberately not derived, one being a
port of a specific skin and the other a published palette in its own right.
Importing a professional scale still needed measuring against our own surfaces,
and the contrast test caught three places where Radix's guarantees do not carry
across; the generator handles each of them rather than a hand-applied patch.

**Dialogs, menus and tooltips.** The hand-rolled dialog, menu, select, popover
and tooltip variants are one primitive each now, which is what makes tooltips
work on touch.

**The calendar list is rebuilt.** Encrypted, read-only, shared, published and
CalDAV state are badges with visible text and an sr-only phrase, rather than
glyphs behind a native title. Sync health renders for CalDAV links too: the
server has always written lastFetchError for both mechanisms and only the UI
distinguished them, so a failing link showed nothing at all. Sections group and
collapse with counts.

**Drag an event between windows.** Holding Alt arms a native drag on an event
card, so it can be dropped into another window of the same browser, with a real
ghost drawn while it is in flight.

Fixes:

- A write from a locked window could send an encrypted event's title,
  description and location to the server in the clear, and without the
  encrypted flag. It now refuses. A times-only write still passes through, so
  moving or resizing an event from a locked window keeps working.
- Shutdown waited on the SSE stream the app holds open per client for change
  notifications, which is never idle, so server.close() hung. Not a
  desktop-only fix: under Docker the same hang meant waiting for SIGKILL on
  every deploy.
- The desktop app could not be quit, and its first-run mode chooser was dead
  code. Both were turned up by the new Electron test suite.

better-sqlite3 is on 13, which is N-API and so needs no rebuild against
Electron's ABI.

Android: versionCode 9. Feeds and /mcp are server features; the on-device
backend reports 501 and 503 for them rather than pretending.