connor 5d897a5567
Check / check (push) Successful in 2m8s
Check / guardrails (push) Failing after 1m13s
Check / bundle (push) Successful in 1m2s
Image / image (push) Successful in 2m17s
Release 0.1.2
Events can be imported from an .ics file (#2): POST /api/events/import, an import button in the sidebar, and dropping a file on the calendar. Nothing existing changes shape.
2026-10-20 14:38:42 -04:00
2026-10-20 14:32:23 -04:00
2026-08-26 12:07:44 -04:00
2026-10-20 14:32:23 -04:00
2026-10-20 14:38:42 -04:00
2026-10-20 14:38:42 -04:00
2026-08-26 12:07:44 -04:00
2026-08-26 12:07:44 -04:00

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. VEvent is the model and the wire format. No parallel DTOs.
  • Use the library. Recurrence is rrule, iCalendar is icalendar, XML is quick-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-palette x data-mode x data-shape x data-density on <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.
S
Description
No description provided
Readme
1.2 MiB
0 Stars 1 Watchers 0 Forks
Languages
Rust 85.9%
JavaScript 9.1%
CSS 2.7%
Shell 0.9%
Python 0.9%
Other 0.5%