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.
29 KiB
Runway v1 audit — what to carry forward, what to leave behind
Source: /home/connor/docs/projects/calendar (Rust/WASM CalDAV web client, "Runway").
221 commits · 62 Rust files · ~22,056 lines of Rust · ~8,300 lines of hand-written CSS · 2.5 MB WASM bundle.
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 | F7 | Session expiry (24h) + expired-session cleanup | 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 | F29 | Drag top/bottom edge to resize (week view) | Works, fragile | Same. | ||
| 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.
| B | S | D | # | Feature | v1 status | Notes |
|---|---|---|---|---|---|---|
| x | F36 | Create recurring event (daily / weekly / monthly / yearly + interval) | Works | |||
| 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 | ||
| x | F41 | Edit scope: "this event only" (EXDATE + RECURRENCE-ID exception) | Works | Correct RFC approach. Documented well in-code. | ||
| x | F42 | Edit scope: "this and future" (UNTIL split + new series) | Works | Correct RFC approach. | ||
| x | F43 | Edit scope: "entire series" | Works | |||
| x | F44 | Delete scope: this / following / series | Works | |||
| x | F45 | EXDATE handling | Works | |||
| x | F46 | RECURRENCE-ID exception events | Works | |||
| 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 | F51 | Calendar management modal (rename/recolor/reorder) | Works | 448 lines. | ||
| x | F52 | Auto-assign a color per calendar from a hash of its path | Works | Nice touch. | ||
| x | F53 | Editable 16-color event palette + custom palette editor | Works | Stored as a JSON blob inside the calendar_colors preferences column. |
1.6 External calendar subscriptions
| B | S | D | # | Feature | v1 status | Notes |
|---|---|---|---|---|---|---|
| x | F54 | Subscribe to read-only ICS feed by URL | Works | |||
| x | F55 | Server-side caching of fetched ICS with staleness check | Works | Good idea, worth building. | ||
| x | F56 | Per-feed name, color, visibility | 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 | F58 | ETag support for external feeds | Not implemented | let etag = None; // TODO. |
||
| x | F59 | Timezone parsing for external feeds | Not implemented | dtstart_tzid: None, // TODO: Parse timezone from ICS. |
1.7 Alarms & notifications
| B | S | D | # | Feature | v1 status | Notes |
|---|---|---|---|---|---|---|
| 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 — Yes, same call; No, 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 | A5 | SQLite for users/sessions/prefs/feed cache | Fine. Keep. | ||
| 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.
3.1 Reinventing libraries that exist
-
RRULE expansion, ~650 lines, hand-rolled.
frontend/src/services/calendar_service.rs:331-1006—expand_recurring_events,generate_occurrences,generate_weekly_byday_occurrences,generate_monthly_bymonthday_occurrences,generate_monthly_byday_occurrences,generate_yearly_bymonth_occurrences,parse_byday,add_months,add_years,days_in_month,is_leap_year. Therrulecrate does all of this, RFC-compliant, with a test suite.days_in_monthandis_leap_yearare also already inchrono. -
CalDAV XML parsing by regex.
backend/src/calendar.rs:265—extract_xml_contenttries six regex patterns in sequence to cope with namespace prefixes, recompiling eachRegexon every call inside a loop.quick-xmlorroxmltreesolves this properly and faster. -
iCalendar serialization by string concatenation.
backend/src/calendar.rs:1303-1546— 240 lines ofpush_str(&format!(...)). No 75-octet line folding (an RFC violation that bites on long descriptions), noVTIMEZONEemission. Theicalendarcrate builds and folds correctly. -
HTTP client, three times. 17 raw
web_sys::RequestInitcall sites acrossauth.rs,services/preferences.rs, andservices/calendar_service.rs, each with its own header/error/JSON handling.gloo-netis one line per call. -
Two near-identical datetime parsers (
parse_datetimeandparse_datetime_with_tz,backend/src/calendar.rs:715and:807), plus a third copy inbackend/src/handlers/ics_fetcher.rs:333. One of them loops over aformatsarray whose loop variable most branches never use — the same work runs three times per call.
3.2 Solving symptoms instead of causes
backend/src/handlers/ics_fetcher.rs:485-880 is ~500 lines named deduplicate_events,
consolidate_same_title_events, deduplicate_exact_recurring_events, consolidate_weekly_patterns,
would_event_be_generated_by_rrule, normalize_title, event_completeness_score.
It strips punctuation from event titles, lowercases them, groups by the result, guesses which of the collisions is "most complete", and throws the rest away. It will also try to merge two weekly RRULEs into one multi-day rule.
Almost certainly the real bug was upstream: the ICS parser doesn't handle RECURRENCE-ID overrides
or VTIMEZONE (both are open TODOs in the same file), so a correct feed looked like it had
duplicates. Rather than fix the parser, v1 added a lossy heuristic on top. Any two genuinely
distinct events with similar titles and the same start time get silently merged.
Rule for v2: when output looks wrong, fix the parse, never post-process the output.
3.3 Four parallel representations of "an event"
calendar_models::VEvent— the good, RFC-faithful one.backend::calendar::OldCalendarEvent— a deprecated flattened copy, still compiled (calendar.rs:18).CreateEventRequest/UpdateEventRequest/CreateEventSeriesRequest/UpdateEventSeriesRequest(backend/src/models.rs:100-200) — four near-identical stringly-typed wire structs. Dates and times as separate"YYYY-MM-DD"/"HH:MM"strings; attendees and categories as comma-separated strings; recurrence decomposed intorecurrence: String+recurrence_days: Vec<bool>interval+count+end_dateinstead of just… the RRULE thatVEventalready holds.
EventCreationData(frontend/src/components/event_form/types.rs:64) — the form model, with its ownEventStatusandEventClassenums duplicating the ones incalendar-models, converted to the wire format via a 22-element tuple (to_create_event_params) that serializes enums withformat!("{:?}", …).to_uppercase().
Plus a fifth, frontend::calendar::CalendarEvent, in dead code (see §3.6).
Every conversion between these is a place to lose data, and several of them do — that's how
monthly_by_day (F39) ends up in the form but never reaches the server.
Rule for v2: VEvent is the wire format. POST /events takes a VEvent. No parallel DTOs.
3.4 localStorage as a global mutable variable
caldav_credentials (the plaintext CalDAV password) is read directly from localStorage in
8 separate places inside app.rs alone, plus components/calendar.rs. Preferences live in
localStorage and in the SQLite user_preferences table with no defined source of truth — some
keys are written to both, some to one. calendar_view_mode, calendar_theme, calendar_style,
calendar_selected_date, calendar_time_increment, calendar_colors, last_used_calendar,
user_preferences (a JSON blob of the others) all coexist.
This is the classic "one hidden global" pattern: any component can read or write app state without
declaring that it does, so nothing is traceable and cache-invalidation bugs are inevitable
(commit 933d7a8 Fix calendar visibility preservation during event updates is one of these).
Rule for v2: one auth context, one preferences store, one writer. Secrets never in localStorage.
3.5 The 2,008-line App component
frontend/src/app.rs is a single #[function_component] holding 35 use_state hooks and
265 .clone() calls. Every modal's open/closed flag, every context menu's x/y position, the
color palette, the alarm scheduler, the refresh interval — all in one function, all prop-drilled
down through components that only pass them along.
Symptoms this produced:
CalendarProps.on_event_update_requestis a callback taking a 7-element tuple.- A
timestampfield onUserInfowhose only job is// Add timestamp to force re-render. print_mode,print_pixels_per_hour,print_start_hourprops threaded intoWeekViewso the print-preview modal can reuse it — print concerns leaking into the interactive view.- The print modal doing imperative DOM surgery (
set_inner_html, cloning nodes, measuringclient_height, mutating inline styles,query_selector_all(".week-event")) inside a declarative framework —print_preview_modal.rs:244-303.
Rule for v2: state lives at the level that owns it. Use a reducer + context for genuinely global state. If a prop is a tuple of more than two things, it wants to be a struct.
3.6 Dead code shipped to users
frontend/src/calendar.rs— 638 lines: a complete second CalDAV client (its ownCalendarEvent,CalDAVClient, XML extraction, iCal parsing, 4 unit tests) that is not declared inmain.rsand has never been reachable.- Because that file looks used,
reqwest,ical, andregexstay in the frontend's dependency list and get compiled into the WASM bundle. - Also fully unused frontend deps:
chrono-tz,indexed_db_futures,base64,dotenvy,serde-wasm-bindgen, plus the 8 IndexedDBweb-sysfeatures. OldCalendarEvent,EventReminder,ReminderActioninbackend/src/calendar.rs— superseded, still compiled.backend/src/debug_caldav.rs— a debug scratch module.- Duplicate top-level
migrations/directory shadowing the realbackend/migrations/. - Three orphaned
_recurring_edit_*state hooks inapp.rs:140-142.
A meaningful share of that 2.5 MB bundle is code nobody can execute.
3.7 Observability and error handling
- 157
println!calls in the backend with emoji prefixes (🔄,✅,🗑️); zero uses oflogortracing. No levels, no filtering, no structure — and a login handler that printsPassword length: {}. - 74
web_sys::consolecalls in the frontend, same story. - 65
.unwrap()calls across both crates. - Errors are
Stringat nearly every frontend boundary (Result<T, String>), so nothing can be matched on — a 401 and a JSON parse failure are indistinguishable to the caller. - Handlers dispatch on stringly-typed actions (
"delete_this","this_and_future","all_in_series","update_series") — 53 occurrences — instead of enums, so a typo is a runtime fallthrough rather than a compile error.
3.8 Timezone modelling
The client computes its current UTC offset (format!("{:+03}:{:02}", …) from
js_sys::Date::get_timezone_offset()) and sends that as timezone: String on every request.
An offset is not a timezone: -06:00 in January and -06:00 in July are different zones, and a
recurring 9am meeting created in winter drifts an hour after the DST boundary. The VEvent model
already has proper *_tzid fields; the wire format throws them away in favour of an offset.
chrono-tz is in the frontend's dependency list and never used.
Rule for v2: IANA TZID everywhere (America/Denver), never a raw offset. Store the zone, derive the offset.
3.9 CSS at scale, without a system
styles.css: 5,610 lines.print-preview.css: 1,320 lines.styles/google.css: 19 KB.styles/apple.css: 15 KB.- 553 CSS custom property definitions, 116
!importantdeclarations. - Selectors are redefined many times over —
.external-calendar-modalappears 23 times,.login-form18,.calendar-day13,.week-event12. - Font Awesome pulled from a CDN in
index.html(an external dependency on every page load). - The last ~15 commits in the repo are almost entirely CSS-tweak commits for the print preview.
The 36 theme × style combinations were never worth their maintenance cost on a personal app.
3.10 Testing and correctness feedback
Effectively no safety net. 11 unit tests total (4 of them in dead code). The integration suite doesn't compile against current signatures. The Playwright specs were deleted but 7.2 MB of their reports remain. Every regression was found by using the app.
The recurring-event edit semantics (§F41-F44) are genuinely subtle — EXDATE + RECURRENCE-ID + UNTIL splitting — and they were shipped with zero automated coverage.
Part 4 — What v2 should reach for instead
No columns here — these follow from the decisions above rather than being decisions themselves.
| v1 hand-rolled | v2 candidate |
|---|---|
| 650-line RRULE expander | rrule crate |
| Regex XML extraction | quick-xml or roxmltree |
| String-concat iCal writer | icalendar crate (handles line folding) |
| 3 copies of a datetime parser | one function in calendar-models, tested |
17 raw RequestInit fetch sites |
gloo-net (or a single typed API client module) |
println! with emoji |
tracing + tracing-subscriber |
Result<T, String> |
thiserror enums, both crates |
| Stringly-typed actions | #[derive(Serialize, Deserialize)] enum EditScope { ThisOnly, ThisAndFuture, EntireSeries } |
| 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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"
- Anything in v1 you actively miss or that never worked right that isn't listed above?