Five crates: runway-core (pure domain), runway-caldav (protocol),
runway-server (axum), runway-web (leptos), runway-cli (smoke tool).
runway-core is feature-gated into model/ical/recurrence so the frontend
can depend on the shared types without pulling icalendar and rrule into
the WASM bundle. The previous iteration shipped reqwest, ical and regex
to the browser for a dead module; the feature split makes that mistake
structurally hard to repeat.
Guardrails are compiler- and CI-enforced rather than aspirational:
workspace lints deny unwrap_used/expect_used, dead_code and unsafe_code,
clippy.toml caps function length and arity, deny.toml pins licences.
Toolchain is pinned per-project so the machine-wide default is untouched.
Cargo.lock is committed this time.
docs/legacy-audit.md carries the marked-up feature decisions and is the
spec for the rewrite.
Purpose of this doc: an inventory you can mark up. Tick what matters, strike what doesn't,
and we'll turn the survivors into a build plan for the rewrite.
> **How to mark up.** Nothing is being ported: every feature you pick gets written fresh, so this
> list is about *what the app should do*, not which code survives.
>
> In Part 1 the first three columns are:
> - **B** — this feature matters; build it. A vote for the **capability**, not an endorsement of
> how v1 did it. Assume I'll design the execution fresh, and that anything marked *"Works,
> fragile"* gets a better approach by default rather than a faithful reproduction.
> - **S** — skip it, don't implement it at all.
> - **D** — it matters **and** you already have a specific change in mind. Append it to the Notes
> cell; that's the instruction I'll design against. As loose as "simpler" or as specific as you like.
>
> So **B vs D is not "as-is" vs "changed"** — both get written from scratch. The difference is
> whether you're handing me a constraint or leaving the execution to me. The **v1 status** column is
> there as history and as a warning about where the hard parts were; it is never a spec.
>
> Part 2 uses **Y**/**N**/**D** — make the same architectural call again, make a different one, or
> keep the intent but change the execution (again, say how in Notes).
>
> Each cell is a single space at a fixed character position — columns 2, 4 and 6 of every row — so
> you can move down a column in vim and hit `rx`. Leave all three blank if you're undecided.
---
## Part 1 — Feature inventory
Notes flag what each feature actually cost in v1.
### 1.1 Authentication & session
|B|S|D| # | Feature | v1 status | Notes |
|-|-|-|---|---------|-----------|-------|
|x| | | F1 | Log in with CalDAV server URL + username + password | Works | Auth = "can we PROPFIND your calendars?" Simple and correct. -- I'd like to add OpenID as well, to connect behind my Authelia instance |
|x| | | F2 | JWT token issued by backend, stored in localStorage | Works | |
| |x| | F3 | Second, parallel `session_token` (SQLite-backed) used *only* by the preferences API | Works | Two auth schemes for one app. Pick one. |
|x| | | F4 | CalDAV password re-sent on every request via `X-CalDAV-Password` header | Works | Password is kept in localStorage in cleartext and read from 8+ call sites. See §3.4. -- Keep, but if there's a better way I'm open to it |
|x| | | F5 | "Remember me" — persists server URL + username (not password) | Works | Cheap to build. |
|x| | | F6 | Token verification on app startup, auto-logout on invalid | Works | |
|x| | | F8 | Multi-user support (users table keyed on username+server_url) | Works | Was this ever used, or is it a single-user app? Big simplification if single-user. -- Currently not used, but I want to keep |
### 1.2 Calendar & event viewing
|B|S|D| # | Feature | v1 status | Notes |
|-|-|-|---|---------|-----------|-------|
|x| | | F9 | Month view | Works | 326 lines. The cheap one. |
|x| | | F10 | Week view with hour grid | Works | 1,431 lines — the single hardest file in the repo. |
|x| | | F11 | Day view | **Absent** | Never built. |
|x| | | F12 | Agenda / list view | **Absent** | Never built. |
|x| | | F13 | Year view | **Absent** | Never built. |
|x| | | F14 | Current-time indicator line in week view | Works | Re-renders every 5s. |
|x| | | F15 | Configurable time increment (grid granularity) | Works | Persisted per-user. |
|x| | | F16 | Overlapping-event column layout in week view | Works, fragile | Hand-rolled interval-packing inline in the view component. |
|x| | | F17 | All-day event lane at top of week view | Works | |
|x| | | F18 | Per-calendar colors | Works | |
|x| | | F19 | Calendar show/hide toggles in sidebar | Works | Filtering happens client-side *after* fetching everything. |
|x| | | F20 | "More events" overflow indicator in month cells | Works | |
|x| | | F21 | Reminder-bell icon on events that have alarms | Works | |
|x| | | F22 | Navigate prev/next period, jump to today | Works | |
|x| | | F23 | Selected-date persistence across reloads | Works | Stored in localStorage *and* the DB. |
### 1.3 Event editing
|B|S|D| # | Feature | v1 status | Notes |
|-|-|-|---|---------|-----------|-------|
|x| | | F24 | Create event (tabbed modal: Basic / Advanced / People / Categories / Location / Reminders) | Works | 6 tabs, ~1,400 lines across `event_form/`. |
|x| | | F25 | Edit existing event | Works | |
|x| | | F26 | Delete event | Works | |
|x| | | F27 | View event detail modal | Works | |
|x| | | F28 | Drag to move an event (week view) | Works, fragile | Hand-rolled mouse-event math with pixel offsets and snapping. |
|x| | | F30 | Drag on empty grid to create an event | Works | |
|x| | | F31 | Right-click context menu on an event | Works | |
|x| | | F32 | Right-click context menu on a day cell | Works | |
|x| | | F33 | Full RFC 5545 property support in the model (status, class, priority, organizer, attendees, categories, geo, url, resources, contact, related-to, sequence, transp, attachments) | Model complete, UI partial | The `VEvent` struct covers all of it; the form exposes maybe half; the wire format flattens most of it to comma-separated strings. |
|x| | | F34 | Attendees | Half-built | Backend has `// TODO: Parse attendees properly`. Round-trip is lossy. |
|x| | | F35 | Attachments | Model only | No UI, no CalDAV round-trip. |
### 1.4 Recurring events
This is where most of the complexity — and most of the bugs — lived.
|x| | | F37 | Weekly BYDAY (pick days of week) | Works | Sent as `Vec<bool>` of length 7, not as an RRULE. -- If RRULE is better, we should do that |
|x| | | F38 | COUNT limit / UNTIL date | Works | |
|x| | | F39 | Monthly BYMONTHDAY and BYDAY ("first Monday") | **Dead-ended** | Fields exist in the form model (`monthly_by_day`, `monthly_by_monthday`, `yearly_by_month`) but are dropped on the way to the API. |
|x| | | F40 | Client-side expansion of RRULE into visible occurrences | Works, ~650 lines hand-rolled | See §3.1 — the single biggest reinvention in the repo. -- To be clear, I want to see re-occurences visually, but we don't need to re-invent anything |
| | | | F47 | Dedicated `/api/calendar/events/series/*` endpoints | Works | 1,165 lines of handler. Mostly duplicating the non-series handlers. -- I'm ambivalent on this unless it serves the UI in some useful way |
### 1.5 Calendar management
|B|S|D| # | Feature | v1 status | Notes |
|-|-|-|---|---------|-----------|-------|
|x| | | F48 | Discover calendars on the CalDAV server (PROPFIND) | Works | |
|x| | | F49 | Create a new calendar collection (MKCALENDAR) | Works | |
|x| | | F50 | Delete a calendar collection | Works | |
| |x| | F57 | Heuristic de-duplication / consolidation of feed events | **Actively harmful** | ~500 lines that fuzzy-match events by normalized title and *merge or discard* them. See §3.2. |
|x| | | F60 | VALARM support in the model, add/edit/remove alarms in the form | Works | |
|x| | | F61 | Browser notification when an alarm fires | Works | |
|x| | | F62 | Alarm scheduler polling every 30s from the main app | Works | State in localStorage. |
| |x| | F63 | Service worker for "background" alarm processing | **Ceremony, no value** | The SW can't read localStorage, so it just pings the main tab, which is already polling. Deletable today with zero behavior change. |
| |x| | F64 | IndexedDB for persistent alarm storage | **Never built** | `web-sys` IndexedDB features + `indexed_db_futures` dep are enabled and shipped in the WASM bundle; zero code uses them. |
| |x| | F65 | Email/audio alarm actions | Model only | Only `Display` is handled. |
### 1.8 Presentation & printing
|B|S|D| # | Feature | v1 status | Notes |
|-|-|-|---|---------|-----------|-------|
|x| | | F66 | 12 named color themes (Ocean, Forest, Sunset, Purple, Dark, Rose, Mint, Midnight, Charcoal, Nord, Dracula, Default) | Works | -- I want the concept of "color themes", but it doesn't need to be these 12. Some are good, some bad |
|x| | | F67 | 3 layout "styles" (Default, Google, Apple), each a separate stylesheet | Works | 12 × 3 = 36 combinations to hand-maintain across ~8,300 lines of CSS. -- Again, I want the concept of "display styles", but it doesn't need to be implemented as it is currently, and we could do better than the ones we have |
|x| | | F68 | System dark-mode detection (`prefers-color-scheme`) | Works | |
|x| | | F69 | Print preview modal with live zoom, start/end hour clipping, per-element font-size and padding sliders | Works | 617 lines of Rust + 1,320 lines of print CSS, and it drives the *interactive* view components via extra `print_*` props. See §3.5. -- Implementation is bad here, I know, but this feature is important. I want to be able to print the calendars we're displaying |
|x| | | F70 | Actual printing | Works | Clones the DOM into a hidden node and measures it imperatively. |
|x| | | F71 | Mobile warning modal ("this app isn't for phones") | Works | Stands in for actually being responsive. |
| |x| | F72 | Responsive/mobile layout | **Not really** | F71 exists because F72 doesn't. -- We'll do it later, perhaps. CalDAV clients exist on Android already |
### 1.9 Infrastructure
|B|S|D| # | Feature | v1 status | Notes |
|-|-|-|---|---------|-----------|-------|
|x| | | F73 | Docker Compose stack (Axum backend + Caddy serving the WASM bundle) | Works | |
|x| | | F74 | SQLite + sqlx migrations, auto-run on container start | Works | |
|x| | | F75 | Gitea Actions workflow building & pushing a backend image | Works | |
| | |x| F76 | `deploy_frontend.sh` — rsync the `dist/` to the server over SSH | Works | Frontend deploy is manual and separate from the backend's CI. -- I'm ok with making this automated instead |
|x| | | F77 | Backend integration test suite (18 tests) | **Does not compile** | Stale: calls `AuthService::new(secret)` (now takes 2 args) and builds `AppState` without its `db` field. Also duplicates the whole router instead of importing it. |
|x| | | F78 | Playwright E2E suite | **Source is gone** | 7.2 MB of stale HTML reports and trace zips remain on disk; not a single `.spec.ts` survives. |
|x| | | F79 | Unit tests | 11 total | 4 in dead code, 3 in config, 4 in the CalDAV client. -- Broadly speaking, I want this code being tested very thoroughly. That's my hedge against AI writing bad code |
---
## Part 2 — Stack & architecture decisions to re-litigate
Each of these worked. The question is whether you'd choose it again — **Y**es, same call; **N**o,
drop the approach entirely; or **D** for same intent, different execution (several of these are
squarely in that third bucket — A3, A6 and A10 especially).
|Y|N|D| # | Decision | Verdict to consider |
|-|-|-|---|----------|---------------------|
|x| | | A1 | **Rust + Yew + WASM frontend** | The 2.5 MB bundle, the 869 `.clone()` calls, and the lack of a mature component/CSS ecosystem are all downstream of this. It's a legitimate choice if you want one language end-to-end — but it is the reason the calendar viz was a headache. Worth an explicit yes/no. -- I'm a big proponent of Rust/WASM/functional programming. I prefer to do it this way|
|x| | | A2 | **Axum backend as a CalDAV proxy** | Right call. Browsers can't do CalDAV directly (CORS, custom methods, credentials). Keep. |
|x| | | A3 | **Cargo workspace with a shared `calendar-models` crate** | Right instinct, under-used — see §3.3. Keep the crate; actually route everything through it. |
|x| | | A4 | **`VEvent` as an RFC 5545-faithful struct** | Genuinely good. The best asset in the repo. Carry it forward nearly as-is. |
| | |x| A6 | **Password forwarded per-request rather than stored server-side** | Defensible (server never persists the secret), but it forced the password into localStorage. Consider an httpOnly cookie holding an encrypted credential blob instead. -- I'm a bit ambivalent. Open to whichever approach is best |
|x| | | A7 | **Trunk for the frontend build** | Fine if A1 stays. |
| |x| | A8 | **Client-side recurrence expansion** | Reconsider. Doing it server-side means one implementation, easier tests, smaller bundle, and the client just renders what it's given. -- Agree, just want to clarify that I do want the UI displaying recurring events as many events, like most calendars |
| | | | A9 | **Fetch the whole calendar, filter client-side** | The CalDAV `calendar-query` REPORT supports a `time-range` filter and v1 never used it. Every view change refetches everything. -- ambivalent. whatever is best for performance |
| | | | A10 | **Hand-written CSS with 553 custom properties** | The 116 `!important` declarations are the tell. Needs a real strategy (design tokens + a small utility layer, or a component library). -- open to whatever as long as we keep the "themes" and "styles" concepts |
---
## Part 3 — What actually went wrong
Concrete, with evidence. This is the part worth internalizing before starting over.
| Offset-as-timezone | `chrono-tz` with IANA TZIDs |
| 8,300 lines of bespoke CSS | a small token set + one utility layer; ship **one** theme with a light/dark pair |
| Manual visual verification | headless-browser screenshots in the loop (see below) |
### Verifying the calendar viz this time
The thing that made week view painful — pixel math you can't see until you build, deploy, and
squint — is now directly addressable. I can drive a real browser, take screenshots of the running
app, and compare them as I go. That means for v2 we can:
- screenshot the week grid at each step and check the layout by eye before moving on;
- build a small fixture set (overlapping events, all-day spans, DST-boundary days, midnight-crossing
events, a 6-week month) and snapshot each one;
- catch layout regressions the moment they appear rather than three commits later;
- read the browser console for panics and warnings without you having to reproduce them.
This is the single biggest change in what's possible between v1 and v2, and it should shape the
plan: build the view layer against real rendered output from day one.
---
## Part 5 — Open questions for you
Answer inline underneath each — free text, no columns.
1.**Rust/WASM again, or a JS/TS frontend?** (A1) Everything about the CSS pain, the bundle size,
and the viz difficulty traces back here. Keeping Rust is defensible; it should just be a decision
rather than an inheritance.
> Keep it as Rust/WASM. This is important to me. I'm a big fan.
2.**Single-user or multi-user?** (F8) If it's only ever you, the users table, JWT, and half the
session machinery evaporate.
> Multi-user. I'd like this to eventually be a usable tool by households where more than one person might want to use it.
3.**Which views do you actually use?** Month and week both? Would a day or agenda view earn its keep?
> The other views are also nice to have. But I mostly use week and month. I do want them all though, I suppose.
4.**Themes: how many?** One good light/dark pair, or is the 12-theme picker something you use?
> It doesn't have to be 12, but I like having themes. I'm sure we can come up with a clever way to make this not **too** complex.
5.**Print preview: keep?** It was ~2,000 lines and the last 15 commits. Is printing a real workflow
or a weekend detour?
> Yes, printing is important. I want to be able to print one of these calendars with the currently visible items in a way that is pretty, consistent with the current theme, takes up the whole page and nothing more, etc so I can laminate it and put it on my fridge.
6.**Alarms: keep?** Browser notifications only fire when the tab is open. Your phone already does
this from the CalDAV server. Is the web client the right place for it?
> The alarms are important to make it to the server (my phone client will use them and notify me). Does the browser need to notify you when they go off? I mean, it's not critical, but I don't think it's that difficult and it's nice to have.
7.**External ICS feeds: keep?** Worth it, but only if we do the parse correctly (§3.2) instead of
the dedup heuristics.
> Yes, these are important. In particular outlook, so I can put my work schedule on there. But you're right, we should handle them correctly.
8.**Drag-and-drop editing: how important?** It's the highest-risk UI feature per unit of value.
> It's important. I really want a snazzy UX for this. Drag and drop for simple moves AND the context menu for "full editing"
9.**Anything in v1 you actively miss or that never worked right** that isn't listed above?
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.