connor 4cf22bbe4d Let the dev frontend bind to a chosen address
HOST=0.0.0.0 makes the app reachable from another machine. Only the
frontend moves: Trunk proxies /api from the server side, so the backend
stays on the loopback and never needs exposing.

Prints the reachable addresses from the routing table rather than
`hostname -I`, which is empty on some systems, and skips the container
bridges, which are not useful to anyone.
2026-08-26 20:56:46 -04:00
2026-08-26 12:07:44 -04:00
2026-08-26 12:07:44 -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, 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

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.
  • Test thoroughly. Real servers over mocks; the router under test is the router that ships.
S
Description
No description provided
Readme
832 KiB
Languages
Rust 85.6%
JavaScript 9.1%
CSS 2.9%
Shell 1%
Python 0.9%
Other 0.5%