connor 3dcd8f76ad Add subscribed ICS feeds
The parse is the whole story. v1's importer had two open TODOs -- no
RECURRENCE-ID and no VTIMEZONE -- so a correct feed came back looking full
of duplicates, because a series master and its modified occurrences share
a UID and a SUMMARY and read as a flat list they look like one event
repeated. Rather than fix that, v1 added ~500 lines that stripped
punctuation from titles, grouped by the result, scored the collisions for
"completeness" and threw the losers away. Nothing here compares titles,
because nothing looks like a duplicate in the first place.

The regression test is two genuinely different meetings called "Stand-up"
and "Stand up" at the same moment: both survive. Against the real 1 MB
Outlook feed, 103 UIDs expand to 644 occurrences over twenty years with
every UID accounted for and no series producing two occurrences at one
instant.

Freshness has three answers, cheapest first: If-None-Match, then
If-Modified-Since, then a hash of the body -- and the hash is not academic,
because a published Outlook calendar sends neither validator, so without
it every poll would reparse a megabyte to discover nothing changed. v1 had
`let etag = None; // TODO`.

An unreachable feed serves yesterday's copy rather than blanking the week,
and says so. A URL that returns a login page with a 200 is refused at
subscribe time, when the person still has the link to hand, and the failed
subscription is removed rather than left as a broken row. webcal:// is
rewritten, because that is the scheme Outlook and Google hand people.
2026-08-26 18:16:44 -04:00
2026-08-26 18:16:44 -04:00
2026-08-26 12:07:44 -04:00
2026-08-26 12:07:44 -04:00
2026-08-26 17:07:38 -04:00
2026-08-26 17:07:38 -04:00
2026-08-26 12:07:44 -04:00
2026-08-26 12:07:44 -04:00
2026-08-26 18:16: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

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%