The tab has said "Runway" next to a blank page-shaped placeholder since M1. Two files, both generated from the 1254px source: a multi-resolution `.ico` at 48, 32 and 16, and a 180px apple-touch-icon. The source has about 114px of transparent margin on each side, which at 16px is a pixel and a half of nothing on every edge -- so it is trimmed before being resized, and re-centred on a square canvas first, because the trimmed artwork is 1026x1036 and every downsample from it otherwise comes out a pixel narrower than it is tall. At 32px the plane, the swoosh and the calendar grid all still read. At 16px the grid flattens to a pale block; that is the artwork's level of detail rather than the resampling, and 32 is the one most displays ask for. They are copied rather than hashed. Trunk renames an icon asset the way it renames the stylesheet, and these two are the only files in the bundle whose URL is not ours to choose: Caddy serves the frontend with an `index.html` fallback, so anything that guesses at `/favicon.ico` rather than reading the tag -- a bookmark, a feed reader, a browser with no page loaded -- would get the page's HTML with a 200. That renders nothing while looking like it worked, which is the failure mode this repository keeps finding in other places and is no more welcome here. The apple-touch-icon is the one that is flattened onto the icon's own blue. iOS composites a home-screen icon onto black and then applies its own corner mask, so a rounded square that still has an alpha channel arrives with black sitting in the corners it had already rounded off. `assets` joins the `[watch]` list for the reason written above `[watch]` already: Trunk rebuilds on what it is told to watch, and without it changing an icon in development changes nothing and the browser keeps serving the previous one.
Runway
Passive infrastructure for life's coordination.
A CalDAV web client in Rust — Leptos/WASM frontend, Axum backend, speaking to any RFC-compliant CalDAV server (developed against Baikal).
This is a ground-up rewrite. Its predecessor lives in ../calendar; the reasons it was replaced,
and the feature-by-feature decisions that shaped this one, are recorded in
docs/legacy-audit.md — that document is the spec.
Layout
| Crate | Role |
|---|---|
runway-core |
RFC 5545 model, iCalendar round-trip, recurrence expansion. Pure, no I/O. |
runway-caldav |
CalDAV client: discovery, time-ranged queries, ETag-aware CRUD. |
runway-server |
Axum backend: CalDAV proxy, sessions, preferences, ICS feeds. |
runway-web |
Leptos CSR frontend. |
runway-cli |
Smoke tool for exercising a real CalDAV server from the terminal. |
runway-core is feature-gated (model / ical / recurrence) so the frontend gets the shared
types without pulling the parser and RRULE engine into the WASM bundle.
Development
The toolchain is pinned in rust-toolchain.toml and installs automatically on first use.
cargo check --workspace
cargo test --workspace
cargo clippy --workspace -- -D warnings
The CalDAV integration tests need a server and are skipped without one. This starts a throwaway Baikal in a container, installs it, runs them against it, and tears it down:
crates/runway-caldav/tests/baikal/run.sh
Running the backend needs an encryption key for stored CalDAV credentials. It must stay the same across restarts — a key invented at startup would silently make every saved credential unreadable — so the server refuses to start without one rather than generating a throwaway:
export RUNWAY_SECRET_KEY=$(cargo run -q -p runway-server -- genkey)
export RUNWAY_DATABASE_URL=sqlite:runway.db # default
export RUNWAY_BIND=0.0.0.0:3000 # default
export RUNWAY_INSECURE_COOKIES=1 # local HTTP only
cargo run -p runway-server
Frontend
npm install # Tailwind v4 and the browser tooling
cd crates/runway-web && trunk serve
Trunk serves on :8080 and proxies /api to the backend on :3000, so the
session cookie is same-origin in development exactly as in production — nothing
has to be weakened to make local work.
The whole stack, for looking at — a throwaway Baikal seeded with a week of events, the backend, and the frontend:
npx playwright install chromium # once
e2e/dev.sh up # → http://127.0.0.1:8080, testuser/testpassword
HOST=0.0.0.0 e2e/dev.sh up # ...and reachable from another machine
e2e/dev.sh shoot # screenshots + anything the console said
e2e/dev.sh down
Only the frontend binds to HOST; the backend stays on the loopback, because
Trunk proxies /api to it from the server side. HOST=0.0.0.0 is for looking
at the app from another machine on a network you trust — the dev stack runs with
RUNWAY_INSECURE_COOKIES=1 and a throwaway calendar server.
Driving a real browser is the loop v1 never had, and the reason its week grid
could only be checked by building, deploying and squinting. shoot.mjs also
asserts: that hiding a calendar removes its events, that a calendar made from
the sidebar reaches the CalDAV server and can be renamed and deleted there
again, and that every preference survives a reload.
Some tests can be pointed at a whole real calendar rather than the committed fixtures:
RUNWAY_TEST_FEED_FILE=/path/to/calendar.ics cargo test -p runway-server --test feeds
The CLI drives the same stack the app does, which makes it the quickest way to tell a display bug from a data one:
export RUNWAY_CALDAV_URL=https://example.com/dav.php/
export RUNWAY_CALDAV_USER=you
export RUNWAY_CALDAV_PASSWORD=... # from the environment, not a flag
cargo run -p runway-cli -- calendars
cargo run -p runway-cli -- list-events --from 2026-08-24 --to 2026-08-31
Deployment
One image holds both halves — the server binary and the built frontend — so
they cannot be deployed out of step with each other. CI builds and pushes it on
every merge to main; a timer on the server picks it up. Nothing goes up
from a laptop.
deploy/runway-update # what the timer runs, if you would rather not wait
The full arrangement, and the one-time setup it needs, is in
deploy/README.md.
Principles
Carried over from the audit, and enforced by lints and CI rather than by good intentions:
- One representation of an event.
VEventis the model and the wire format. No parallel DTOs. - Use the library. Recurrence is
rrule, iCalendar isicalendar, XML isquick-xml. - Fix the parse, not the output. Never post-process data to paper over a bad parse.
- State lives where it's owned. No localStorage-as-global, no prop-drilling megacomponents.
- Four token axes, none of which can see the others.
data-palettexdata-modexdata-shapexdata-densityon<html>. A palette is seven numbers and every colour is derived from them down a ladder all palettes share, so contrast is a property of the system rather than of each palette's luck. A test rejects any rule selected on two axes at once -- that is the mechanism by which v1's two axes became 36 hand-written stylesheets. - Test thoroughly. Real servers over mocks; the router under test is the router that ships.