docs: v1.1 research (stack/features/architecture/pitfalls/summary)
This commit is contained in:
+238
-149
@@ -1,8 +1,14 @@
|
||||
# Feature Research
|
||||
|
||||
**Domain:** Self-hosted family organization hub — shared calendar + shared collaborative lists
|
||||
**Researched:** 2026-06-03
|
||||
**Confidence:** HIGH (table stakes and pitfalls well-evidenced across multiple products; differentiators MEDIUM — scoped to 2-person self-hosted context)
|
||||
**Domain:** Self-hosted family organization hub — operability & polish milestone (v1.1)
|
||||
**Researched:** 2026-06-10
|
||||
**Confidence:** HIGH (v1.0 is shipped; v1.1 features are well-understood domain problems with clear prior art)
|
||||
|
||||
---
|
||||
|
||||
## Scope of This Document
|
||||
|
||||
This document covers **only the six v1.1 features**. v1.0 features (calendar, event CRUD, lists, push, OIDC, PWA) are shipped and validated; they are listed as dependencies, not re-researched here.
|
||||
|
||||
---
|
||||
|
||||
@@ -10,137 +16,115 @@
|
||||
|
||||
### Table Stakes (Users Expect These)
|
||||
|
||||
Features that must exist on day one. Missing any of these makes the product feel broken, not incomplete.
|
||||
Features that must exist in v1.1 to avoid the product feeling unfinished for real household use.
|
||||
|
||||
| Feature | Why Expected | Complexity | Notes |
|
||||
|---------|--------------|------------|-------|
|
||||
| Unified multi-calendar view | Core value — see all schedules at once | MEDIUM | Aggregating Fastmail shared + personal calendars via JMAP/CalDAV broker token. Color per calendar/member is the visual primitive the whole product depends on. |
|
||||
| Per-member color coding | Cannot tell whose event is whose without it | LOW | Assign a color per user in app config; render all events in that color regardless of source calendar. |
|
||||
| Day / week / month / agenda views | All competing apps offer these; absence is jarring | MEDIUM | Month view is the hardest (event overflow, multi-day spanning). Agenda view is easiest. Week view is most-used daily driver. |
|
||||
| Create / edit / delete events | Read-only calendar is not a calendar app | HIGH | Write-back to the correct Fastmail CalDAV calendar via broker token. Recurring event edits are the hard part (see dependency notes). |
|
||||
| All-day events | School holidays, birthdays, anniversaries | LOW | CalDAV `DATE` vs `DATETIME` distinction. Visual banner across top of day/week grid. |
|
||||
| Recurring events (create + display) | Weekly team standups, recurring chores, birthdays | HIGH | RRULE parsing/expansion is deceptively complex. DST handling, exception dates (EXDATE), single-instance modification (RECURRENCE-ID) are all non-trivial. Must use a library (rrule.js or equivalent). |
|
||||
| Event reminders / push notifications | Time-sensitive alerts are the whole point of a calendar | HIGH | Web Push must be wired in from the start. iOS requires PWA installed to Home Screen; service worker reliability post-device-restart is known to be fragile. Needs fallback strategy (see Pitfalls). |
|
||||
| Shared list create / check-off / reorder | Grocery list is one of the two primary list use cases | LOW | Simple CRUD in MariaDB. Checkbox state toggle + drag-to-reorder. |
|
||||
| Live list co-edit sync | Both members shop simultaneously; must not diverge | MEDIUM | WebSocket (preferred) or SSE for push. Optimistic updates in UI; server reconciliation. Redis pub/sub if multi-instance is ever needed — not needed for single-host Docker. |
|
||||
| Multiple named lists | Groceries and gift ideas are different lists | LOW | A `lists` table with name; items reference list_id. |
|
||||
| OIDC / SSO login (Authelia) | App must not have its own auth system | MEDIUM | OIDC confidential client flow. Session management. Token refresh. Wife must be able to log in without understanding what OAuth is. |
|
||||
| PWA installability (Add to Home Screen) | Native-app feel without App Store friction | MEDIUM | Web app manifest, service worker, HTTPS. iOS Safari and Chrome Android have slightly different installation prompts. Icon and splash screen assets required. |
|
||||
| Low-friction onboarding | Wife adoption is a hard constraint | LOW (UX) / MEDIUM (infra) | One URL → login via Authelia → installed PWA. No calendar credentials to enter. No separate account to create. The OIDC flow is the primary risk — it must feel seamless. |
|
||||
|
||||
---
|
||||
| Per-event reminder selector on event form | Every calendar app (Apple, Google, Fastmail) has this. The current hardcoded 15-min is a bug, not a feature. | MEDIUM | Drop-down of offsets (None / 5 min / 10 min / 15 min / 30 min / 1 hour / 2 hours / 1 day / 2 days) written as VALARM TRIGGER:-PTxM/H or TRIGGER:-P1D in the .ics. "None" default = no VALARM element emitted. |
|
||||
| "None" is the default alarm state | Apple Calendar defaults new events to "None" alert unless the user has changed their Calendar > Settings > Alerts default. Google Calendar defaults to "30 minutes". The PWA should mirror "None" as the explicit no-alarm state — no alarm = no push. | LOW | Must not silently inherit a global default from Fastmail's own app-password user preferences. Emit no VALARM when "None". |
|
||||
| Preserve existing VALARMs on edit | If an event was created in Apple Calendar or Fastmail's native client with a specific reminder, editing it in the PWA must not silently strip that reminder. | MEDIUM | tsdav + ical.js round-trip: parse VALARM on fetch, display the closest-matching preset (or "custom" fallback), write back on save. Explicitly handle the case where an existing VALARM is not in the preset list. |
|
||||
| Scheduler honors per-event lead | The push scheduler must fire at `event_start - trigger_offset`, not a hardcoded 15 min. If no VALARM, fire nothing. | MEDIUM | Depends on outbox/scheduler already built in v1.0. Requires storing the absolute fire-time in the DB so the scheduler does not re-parse ical on every tick. |
|
||||
| Admin Settings UI (role-gated) | Self-hosted apps cannot require SSH/DB-console access for routine admin. The two tasks (rotate app password, mark shared calendar) are operator-level but must be doable from the browser. | MEDIUM | Single admin flag on the users row; admin sees a Settings section hidden from the other member. Both tasks are currently manual DB writes — table stakes to remove that dependency. |
|
||||
| Initial setup wizard (first run) | Without a wizard, first-time deploy requires hand-editing env files in the right order, running VAPID key generation manually, and hoping the DB connection string is correct. Nextcloud/Gitea/Authelia all ship a first-run wizard for exactly this reason. | HIGH | Gate on a `setup_complete` flag persisted in the DB or a dotfile. Must validate each credential before advancing: DB ping, OIDC discovery endpoint reachable, VAPID keys structurally valid, app password CalDAV test connection. |
|
||||
| Event-driven outbox drain | Current ~15s latency (polling interval) makes edits feel sluggish when the user sees the event unchanged for 10+ seconds after saving. Every other calendar app (Google, Apple, Fastmail native) round-trips writes in under 2 seconds perceived. | MEDIUM | The existing transactional outbox guarantees durability. The fix is to trigger an immediate drain on INSERT to the outbox table, rather than waiting for the next scheduled tick. |
|
||||
|
||||
### Differentiators (Competitive Advantage for This Product)
|
||||
|
||||
Features where this product can outperform commercial alternatives specifically because it is self-hosted, private, and purpose-built for exactly two people.
|
||||
Features that go beyond what a user would minimally expect — meaningful for this specific 2-person self-hosted context.
|
||||
|
||||
| Feature | Value Proposition | Complexity | Notes |
|
||||
|---------|-------------------|------------|-------|
|
||||
| No ads, no freemium walls | Every commercial app (Cozi, TimeTree, Maple) gates useful features behind paid tiers; Cozi restricts free users to 30-day history | LOW (operational cost) | Self-hosted means no monetization pressure. Zero marginal cost per feature. |
|
||||
| Full personal calendar overlay | Skylight/Cozi only show a shared family calendar; this app aggregates shared + each member's personal Fastmail calendars into one view | MEDIUM | Requires Fastmail calendar sharing ACLs to be configured so the broker token can read both personal calendars. The "Skylight magic" per PROJECT.md. |
|
||||
| Privacy — data stays home | Commercial apps store your family's schedule on their servers | LOW (architecture choice) | No data leaves the home network except via the Pangolin tunnel the household already controls. |
|
||||
| Tailored to exactly two users | Commercial apps design for 4–6 family members with kids; complexity of permissions, chores, kids accounts is irrelevant overhead | LOW (scope reduction) | No "family manager" role, no parental controls, no per-member permission tiers. Two equals. |
|
||||
| Event change notifications | Google Family Calendar explicitly does not send notifications when a member creates/edits an event — a documented pain point | MEDIUM | Web Push on list changes AND calendar changes. "Wife added something to the grocery list" push. "Event was changed" push. |
|
||||
| Optimistic list UX (instant check-off) | OurGroceries is praised specifically for instant sync on check-off; most apps lag | MEDIUM | Optimistic update in React state, WebSocket confirmation, rollback on failure. |
|
||||
| Multiple reminders per event (up to 2) | Apple Calendar supports multiple alerts per event. The iCalendar spec (RFC 5545) allows multiple VALARM components in one VEVENT. Most household events benefit from a "1 day before" + "30 min before" pair. | MEDIUM | Add a second optional reminder offset selector on the form. Emit two VALARM blocks in the .ics. Parse up to 2 existing VALARMs. Do not expose this in v1.1 if it risks slipping the phase; single-alarm is the floor. |
|
||||
| All-day event reminder semantics matching Apple Calendar | Apple Calendar fires all-day alerts at 9 AM on the alert day (same day or 1 day before, etc.), not at 00:00. Google Calendar fires at 11:50 PM the night before for a "10-min" all-day offset, which is jarring. The Apple convention (morning-of) is the correct UX for this household's non-technical Apple member. | LOW | When the event is all-day and the reminder offset is "on the day" or "1 day before", the VALARM TRIGGER is written as a day-relative offset (`TRIGGER:-P1D` or `TRIGGER:P0D`). The scheduler fires at 9:00 AM on the resolved day (not midnight). This is a scheduler config constant, not user-settable. |
|
||||
| CI regression gating on PR | Prevents the class of regressions introduced during v1.0 (tsc passes but runtime breaks, API test failures not caught until manual verify). Gitea Actions is already available on the self-hosted Gitea instance. | MEDIUM | Lint + typecheck (both apps) + unit tests (Vitest) + API integration tests against a MariaDB service container. Fails PR merge if any step fails. Significantly reduces the human verification burden per phase. |
|
||||
| Mobile-emulated Playwright test harness | The CLAUDE.md preference is to use playwright-cli for validation rather than asking the operator. A mobile-emulated (iPhone viewport, touch, user-agent) authenticated harness lets the assistant catch mobile-only layout regressions, modal overflow, and form usability issues before handing off for iOS-hardware verification. | MEDIUM | Playwright `devices['iPhone 15']` or equivalent device preset, session reuse via `storageState`, DEV_AUTH_BYPASS=true for CI. Does not replace real-device iOS tests (push, standalone mode). |
|
||||
| Docker image publish from CI | Currently the image is built manually. Auto-publishing on merge to main means Unraid can pull the latest image without an SSH session. | LOW | Gitea Packages registry or Docker Hub. Triggered on PR merge to main (not on every PR). |
|
||||
|
||||
---
|
||||
### Anti-Features (Explicitly Exclude)
|
||||
|
||||
### Anti-Features (Deliberately Exclude)
|
||||
Features that appear reasonable but are wrong for a 2-person self-hosted household. Flag these as scope creep.
|
||||
|
||||
Features that appear in commercial products but are wrong for a two-person self-hosted household. Building these would bloat scope without providing value.
|
||||
|
||||
| Feature | Why Commercial Apps Have It | Why to Exclude | What to Do Instead |
|
||||
|---------|----------------------------|-----------------|--------------------|
|
||||
| Chores / rewards / star system | Skylight's chore-chart is a primary SKU driver; kid motivation is a multi-child household need | Zero children in this household. Chores as a product concept doesn't exist here. | Lists serve any "task" need. A grocery list is a chore list if you want it to be. |
|
||||
| Meal planning / recipe box | Cozi, FamCal, Maple, Skylight all have it; drives DAU | Adds a distinct domain (recipes, ingredients, nutrition) with high implementation cost. A family calendar + grocery list serves 90% of the coordination need without a recipe database. | Add grocery items manually or via list. If meal planning is ever wanted, it's a separate v3 concern. |
|
||||
| Kids / sub-accounts without email | FamCal's differentiator — create accounts for children | No children; irrelevant | N/A |
|
||||
| AI email-to-event import | Sense's primary differentiator; Skylight's Magic Import | Requires email access (out of scope), an LLM backend, and ongoing maintenance. Privacy risk. | Create events manually. Event creation UX should be fast enough that manual entry isn't painful. |
|
||||
| RSVP / event invite flows | Used in apps targeting external coordination | For a two-person household sharing one calendar, RSVP is moot — both members see all events by default. CalDAV RSVP (iTIP/iMIP) is a substantial protocol on top of the calendar work. | Both users always attend shared events. Personal calendar events are visible but don't need RSVP. |
|
||||
| Event-level comments / photos (TimeTree-style) | TimeTree's differentiator; useful for larger groups coordinating event details | Two people can just text each other. Adds a chat/media system with storage for near-zero incremental value. | Use SMS/iMessage for event-level discussion as now. |
|
||||
| Activity feed / audit log | TimeTree, some Cozi Gold features | Useful when you need to know which of 5 family members deleted the dentist appointment. With two users it's obvious. | N/A |
|
||||
| Accounts at scale / multi-household | Commercial apps target 4–6 household members, sometimes multiple households | One household, two users. Multi-tenant adds auth and data isolation complexity for zero gain. | Hardcode exactly two accounts in Authelia. |
|
||||
| Ads / monetization | Cozi free tier is ad-supported | Self-hosted; no revenue model needed | N/A |
|
||||
| Complex permissions / role tiers | "Family manager", read-only members, etc. | Two equal partners. | Both users have identical write access to all calendars and lists. |
|
||||
| Offline-first with full conflict resolution | Required for apps targeting users with spotty connectivity | Home WiFi + PWA is the primary use surface. Brief offline tolerance (optimistic updates + retry) is sufficient. CRDTs and full offline sync are engineering overhead without commensurate benefit. | Optimistic updates + graceful "offline" indicator. Retry on reconnect. |
|
||||
| Push-to-native-calendar (CalDAV subscribe URL) | Useful for Apple Calendar native integration | Optional, not v1. The PWA is the primary interface. Native calendar subscribe is a nice-to-have for the wife if she wants it — document it, don't build UI for it. | CalDAV subscribe URL for Fastmail calendars already works natively; just document how to set it up. |
|
||||
| Grocery delivery integration (Instacart, etc.) | Maple's differentiator | Third-party API dependency; not needed when the family handles their own shopping | N/A |
|
||||
| Feature | Why Requested | Why to Exclude | What to Do Instead |
|
||||
|---------|---------------|----------------|--------------------|
|
||||
| User-facing notification preferences page (per-member mute, granular notification types) | Every commercial app has this. Feels like a natural extension of reminder settings. | Two users, both presumably want reminders. This adds a settings surface that 100% of users must click through and that creates support burden (why am I not getting reminders?). The non-technical member should never need to configure this. | Set sensible defaults (all push types on) and never ask. If one member doesn't want push, they decline the browser permission prompt — the OS handles it. |
|
||||
| Reminder "snooze" in the notification payload | RFC 9074 defines a snooze mechanism via sibling VALARM components. Fantastical supports it. | Implementing reliable snooze requires creating a new VALARM in the .ics (PUT back to Fastmail) from the service worker notification click handler. That is a write path triggered from a background service worker — a significant reliability and complexity risk. | Dismiss and re-add a reminder manually if needed. The use case is too rare for this household to justify the implementation risk. |
|
||||
| Setup wizard re-run / reset | Advanced users might want to re-run parts of the wizard (e.g., rotate VAPID keys). | For a 2-person deployment, the operator can edit the env file or use the Admin Settings page for app-password rotation. A re-runnable wizard adds state-management complexity (partial completion, rollback). | Admin Settings covers the post-setup operational cases (app password rotation, shared calendar designation). VAPID key rotation is a documented manual step (generate, update env, redeploy). |
|
||||
| Full audit log in Admin Settings | Some self-hosted admin panels (Gitea, Nextcloud) ship an audit log of administrative actions. | Two users. Both equal operators. There is no adversarial scenario between two family members that requires an audit trail. | N/A — omit entirely. |
|
||||
| Calendar provider abstraction / plugin system | Would make the admin panel a "configure any CalDAV provider" experience. | The constraint is Fastmail + this specific household. A provider abstraction layer adds 3x the surface area for zero current benefit. | Hard-code Fastmail CalDAV principal discovery. Document the URL in the admin panel for transparency. If a second provider is ever needed, add it as a targeted feature in v2. |
|
||||
| Self-service member onboarding via wizard | The setup wizard bootstraps the operator. Member onboarding (per-member app password collection) is a distinct problem (backlog 999.5). | Mixing these into one wizard creates a flow that only the operator completes — the other member would encounter a half-configured wizard. | Keep operator setup wizard and member onboarding as separate concerns. Admin Settings covers the operator side of member credential management. |
|
||||
| Health dashboard / status page in Admin Settings | Uptime graphs, service health indicators, DB query stats. Seen in Nextcloud admin. | Single Docker host, two users. If the app is down, both users know immediately. There is no ops team monitoring this. | Docker logs + Unraid dashboard are sufficient. The setup wizard validates connectivity once; that is the only point of truth needed. |
|
||||
| E2E test suite that runs on real iOS Safari | Complete mobile coverage in CI | Real iOS Safari requires physical device or paid cloud service (BrowserStack). Not reproducible in a self-hosted Gitea runner. | Playwright mobile emulation covers layout/interaction. Real-device iOS tests remain a human gate for push + standalone mode (as in v1.0). |
|
||||
|
||||
---
|
||||
|
||||
## Feature Dependencies
|
||||
|
||||
```
|
||||
[OIDC Login]
|
||||
└──required by──> [All other features] (nothing works without auth)
|
||||
[Per-event reminder UI]
|
||||
└──requires──> [Event create/edit form] (v1.0, shipped)
|
||||
└──requires──> [VALARM parse on CalDAV fetch] (tsdav + ical.js, v1.0 partial)
|
||||
└──requires──> [Push scheduler with per-event fire time] (outbox/scheduler, v1.0 shipped)
|
||||
└──enhanced by──> [Faster write-back] (drain triggers scheduler sooner)
|
||||
|
||||
[CalDAV/JMAP broker token]
|
||||
└──required by──> [Calendar read]
|
||||
└──required by──> [Unified calendar view]
|
||||
└──required by──> [Per-member color coding]
|
||||
└──required by──> [Day/week/month views]
|
||||
└──required by──> [Event create/edit/delete]
|
||||
└──required by──> [All-day events] (DATE type)
|
||||
└──required by──> [Recurring event display]
|
||||
└──required by──> [Recurring event edit]
|
||||
(RECURRENCE-ID, EXDATE — hardest sub-feature)
|
||||
[Faster write-back]
|
||||
└──requires──> [Transactional outbox] (v1.0, shipped)
|
||||
└──enhanced by──> [Per-event reminder UI] (alarm fire times stored at write time)
|
||||
|
||||
[Web Push registration]
|
||||
└──required by──> [Event reminders]
|
||||
└──required by──> [List change notifications]
|
||||
└──enhances──> [Live list sync] (push as fallback to WebSocket on reconnect)
|
||||
[Admin Settings UI]
|
||||
└──requires──> [OIDC login + role field on users table] (v1.0, shipped — needs admin flag)
|
||||
└──requires──> [App password encrypted storage] (v1.0, shipped)
|
||||
└──enhances──> [Setup wizard] (wizard hands off to Admin Settings for post-setup credential rotation)
|
||||
|
||||
[PWA installability]
|
||||
└──required by──> [Web Push on iOS] (iOS only delivers push to installed PWAs)
|
||||
└──required by──> [Low-friction onboarding] (one URL → installed app)
|
||||
[Setup wizard]
|
||||
└──requires──> [DB connection] (before anything else can be validated)
|
||||
└──requires──> [VAPID key generation utility] (web-push keygen, v1.0 shipped in some form)
|
||||
└──requires──> [OIDC config storage] (env vars or DB config table)
|
||||
└──gates──> [All other features] (wizard must complete before app is usable)
|
||||
└──does NOT require──> [Admin Settings UI] (wizard is first-run only; Admin Settings is post-run)
|
||||
|
||||
[MariaDB lists schema]
|
||||
└──required by──> [Named lists]
|
||||
└──required by──> [List items CRUD]
|
||||
└──required by──> [Check-off / reorder]
|
||||
└──enhanced by──> [Live list sync via WebSocket]
|
||||
[Gitea CI]
|
||||
└──requires──> [Gitea act_runner deployed on Unraid host]
|
||||
└──requires──> [MariaDB service container support in act_runner]
|
||||
└──requires──> [Vitest unit tests + API integration tests] (v1.0, partially written)
|
||||
└──enables──> [Mobile-browser test harness] (harness runs as a CI step)
|
||||
|
||||
[Service worker]
|
||||
└──required by──> [PWA installability]
|
||||
└──required by──> [Web Push]
|
||||
└──enhances──> [Offline tolerance] (cache shell, retry queue)
|
||||
[Mobile-browser test harness]
|
||||
└──requires──> [Playwright + mobile device config]
|
||||
└──requires──> [DEV_AUTH_BYPASS or stored auth session]
|
||||
└──enhanced by──> [Gitea CI] (harness most valuable when gating PRs automatically)
|
||||
```
|
||||
|
||||
### Dependency Notes
|
||||
|
||||
- **OIDC login must come first.** Everything else is gated on auth. The OIDC flow with Authelia must be smooth enough that the non-technical member can complete it once, then never see it again (persistent session).
|
||||
- **CalDAV/JMAP broker token is the calendar foundation.** All calendar features depend on proving this integration works reliably before building display or editing UI on top of it.
|
||||
- **Recurring events require a library.** Implementing RRULE expansion manually is impractical. Use rrule.js (frontend) and a server-side equivalent for reminder scheduling. Single-instance edits (RECURRENCE-ID) and "this and following" edits add significant complexity and should be scoped carefully — basic recurring create/display can ship before full edit support.
|
||||
- **PWA install is a prerequisite for iOS Web Push.** Web Push on iOS does not work from a Safari tab — only from an installed PWA. This means the install step is not optional for the wife to receive notifications. The onboarding flow must guide her through Add to Home Screen.
|
||||
- **Live list sync requires WebSocket infrastructure.** This is a dependency on the server-side connection management (Socket.io or native WS). Redis pub/sub is only needed if the backend ever runs as multiple instances — not relevant for single-Docker-host deployment.
|
||||
- **Faster write-back is a force multiplier.** It makes the per-event reminder UX feel correct — a user sets a reminder, saves, and expects the scheduler to know about it now, not in 15 seconds. Build faster write-back before or alongside per-event reminders.
|
||||
- **Admin Settings requires an `is_admin` flag.** v1.0 shipped no role distinction. The schema migration to add `users.is_admin` (default false, first user true) is a prerequisite for the Admin Settings route guard and is trivial.
|
||||
- **Setup wizard gates the app.** Until the wizard completes, no calendar or list feature should render. The wizard is a deploy-time concern for the operator; it is not a UX concern for the non-technical member (she never sees it — the operator runs it once).
|
||||
- **Gitea CI depends on act_runner.** If the self-hosted Gitea does not yet have an act_runner container deployed, CI cannot run. This is an infrastructure prerequisite, not a code concern.
|
||||
- **Mobile harness does not block CI.** CI (lint/typecheck/unit/integration) can ship first. The mobile harness is an additive step.
|
||||
|
||||
---
|
||||
|
||||
## MVP Definition
|
||||
## MVP Definition (v1.1)
|
||||
|
||||
### Launch With (v1)
|
||||
### Must Ship (table stakes for "operability" claim)
|
||||
|
||||
- [ ] OIDC login via Authelia — required for everything else; must be seamless for non-technical user
|
||||
- [ ] CalDAV/JMAP broker integration — read Fastmail calendars (shared + personal)
|
||||
- [ ] Unified calendar view with per-member colors — day, week, month views
|
||||
- [ ] Create / edit / delete events (write-back to Fastmail) — all-day and timed; recurring create/display; single-instance edit is a stretch goal
|
||||
- [ ] Shared lists — create named list, add/check/reorder items, delete items
|
||||
- [ ] Live list sync via WebSocket — both members co-edit in real time
|
||||
- [ ] Web Push notifications — event reminders, list change alerts
|
||||
- [ ] PWA manifest + service worker — installable on iPhone and Android
|
||||
- [ ] Guided Add to Home Screen prompt on first visit (iOS)
|
||||
- [ ] Per-event reminder selector — None / preset offsets — single alarm minimum
|
||||
- [ ] Scheduler uses per-event VALARM trigger offset, fires nothing on None
|
||||
- [ ] Existing VALARMs preserved on edit round-trip
|
||||
- [ ] Admin Settings — rotate/re-enter Fastmail app password per member, mark shared calendar
|
||||
- [ ] Setup wizard — DB, OIDC, VAPID, app password — validated before completion
|
||||
- [ ] Faster write-back — event-driven outbox drain, edits land in ~1s perceived
|
||||
|
||||
### Add After Validation (v1.x)
|
||||
### Should Ship (differentiators, don't let them slip the milestone)
|
||||
|
||||
- [ ] Recurring event single-instance edit (RECURRENCE-ID) — add after core recurring display is stable and tested
|
||||
- [ ] "This and following" recurring edit — complex; only add if users report the need
|
||||
- [ ] Native CalDAV subscribe URL documentation — wife can optionally add to Apple Calendar; no new code needed, just documented
|
||||
- [ ] Timezone display toggle — show events in a secondary timezone if the household ever travels across zones
|
||||
- [ ] Gitea CI — lint + typecheck + unit + API integration on PR
|
||||
- [ ] Mobile-browser test harness — iPhone viewport, authenticated, usable by assistant in playwright-cli
|
||||
|
||||
### Future Consideration (v2+)
|
||||
### Defer (not v1.1 scope)
|
||||
|
||||
- [ ] Wall-display / kiosk dashboard — per PROJECT.md, explicitly deferred to v2
|
||||
- [ ] Upcoming events widget / agenda summary — nice home screen widget-style view for the display
|
||||
- [ ] Calendar event color override per event — current plan is color per member; per-event override adds UI complexity
|
||||
- [ ] Multiple reminders per event (2x VALARM) — v1.1 stretch; defer to v1.2 if risky
|
||||
- [ ] Self-service member onboarding via wizard (backlog 999.5) — separate feature, separate phase
|
||||
- [ ] Docker image auto-publish from CI — nice to have; ship manually until CI is stable
|
||||
|
||||
---
|
||||
|
||||
@@ -148,65 +132,170 @@ Features that appear in commercial products but are wrong for a two-person self-
|
||||
|
||||
| Feature | User Value | Implementation Cost | Priority |
|
||||
|---------|------------|---------------------|----------|
|
||||
| OIDC login | HIGH | MEDIUM | P1 |
|
||||
| CalDAV/JMAP broker | HIGH | HIGH | P1 |
|
||||
| Unified calendar view (read) | HIGH | MEDIUM | P1 |
|
||||
| Per-member color coding | HIGH | LOW | P1 |
|
||||
| Day / week / month views | HIGH | MEDIUM | P1 |
|
||||
| Event create/edit/delete | HIGH | HIGH | P1 |
|
||||
| All-day events | HIGH | LOW | P1 |
|
||||
| Recurring events (create + display) | HIGH | HIGH | P1 |
|
||||
| Shared lists CRUD | HIGH | LOW | P1 |
|
||||
| Live list sync (WebSocket) | HIGH | MEDIUM | P1 |
|
||||
| Web Push notifications | HIGH | HIGH | P1 |
|
||||
| PWA installability | HIGH | MEDIUM | P1 |
|
||||
| List change notifications | MEDIUM | LOW | P1 (shares push infra) |
|
||||
| Recurring event single-instance edit | MEDIUM | HIGH | P2 |
|
||||
| "This and following" recurring edit | LOW | HIGH | P3 |
|
||||
| Native CalDAV subscribe docs | LOW | LOW | P2 |
|
||||
| Timezone display toggle | LOW | MEDIUM | P3 |
|
||||
| Wall-display kiosk view | MEDIUM | MEDIUM | P3 (v2) |
|
||||
| Per-event reminder selector | HIGH | MEDIUM | P1 |
|
||||
| Scheduler per-event VALARM | HIGH | MEDIUM | P1 |
|
||||
| Faster write-back | HIGH | MEDIUM | P1 |
|
||||
| Admin Settings — app password mgmt | HIGH | MEDIUM | P1 |
|
||||
| Admin Settings — shared calendar toggle | HIGH | LOW | P1 |
|
||||
| Setup wizard | HIGH | HIGH | P1 |
|
||||
| Gitea CI (regression gate) | MEDIUM | MEDIUM | P1 |
|
||||
| Mobile Playwright harness | MEDIUM | LOW | P2 |
|
||||
| Multiple reminders per event | MEDIUM | MEDIUM | P2 |
|
||||
| All-day reminder at 9 AM semantics | MEDIUM | LOW | P2 |
|
||||
| Docker image auto-publish | LOW | LOW | P3 |
|
||||
|
||||
**Priority key:**
|
||||
- P1: Must have for milestone claim
|
||||
- P2: High value, ship if no risk to P1
|
||||
- P3: Nice to have, defer
|
||||
|
||||
---
|
||||
|
||||
## Competitor Feature Analysis
|
||||
## Per-Feature Expected Behavior Reference
|
||||
|
||||
| Feature | Skylight | Cozi | TimeTree | Google Family | This Product |
|
||||
|---------|----------|------|----------|---------------|--------------|
|
||||
| Color per member | Yes | Yes | Yes | No | Yes |
|
||||
| Personal + shared calendar overlay | No (shared only) | No | No | No | Yes (Fastmail aggregation) |
|
||||
| Multiple calendar views | Yes | Partial (Gold gates month) | Yes | Yes | Yes |
|
||||
| Recurring events | Yes | Yes | Yes | Yes | Yes (display v1; full edit v1.x) |
|
||||
| Shared lists | Yes | Yes | No | No | Yes |
|
||||
| Live list sync | Unknown | Yes | N/A | N/A | Yes (WebSocket) |
|
||||
| Push notifications on change | Yes | Partial | Yes | No | Yes |
|
||||
| Event-level comments | No | No | Yes | No | No (anti-feature) |
|
||||
| Chores / rewards | Yes (primary feature) | Yes | No | No | No (anti-feature) |
|
||||
| Meal planning | Yes | Yes | No | No | No (anti-feature) |
|
||||
| AI import | Yes (Magic Import) | No | No | No | No (anti-feature) |
|
||||
| RSVP | No | No | No | No | No (anti-feature) |
|
||||
| Self-hosted / private | No | No | No | No | Yes (differentiator) |
|
||||
| No ads / no paywall | No (Plus plan) | No (Gold plan) | No (Premium) | Yes | Yes |
|
||||
| Cross-ecosystem (iOS + Android) | App + hardware | App | App | App | PWA (single URL) |
|
||||
### 1. Per-Event Reminders
|
||||
|
||||
**Reminder selector options (matching Apple Calendar + Google Calendar intersection):**
|
||||
|
||||
| Label | VALARM TRIGGER value | Notes |
|
||||
|-------|---------------------|-------|
|
||||
| None | (no VALARM emitted) | Default for new events |
|
||||
| 5 minutes before | `TRIGGER:-PT5M` | |
|
||||
| 10 minutes before | `TRIGGER:-PT10M` | |
|
||||
| 15 minutes before | `TRIGGER:-PT15M` | Current hardcoded default — becomes an explicit choice |
|
||||
| 30 minutes before | `TRIGGER:-PT30M` | |
|
||||
| 1 hour before | `TRIGGER:-PT1H` | |
|
||||
| 2 hours before | `TRIGGER:-PT2H` | |
|
||||
| 1 day before | `TRIGGER:-P1D` | |
|
||||
| 2 days before | `TRIGGER:-P2D` | |
|
||||
|
||||
**All-day event semantics:** When saving an all-day event with any reminder, the scheduler fires at 09:00 AM local time on the target day (computed as `allday_date + offset_days`). Do not fire at midnight. Apple Calendar uses 9 AM as the "on the day" time for all-day alerts; this matches the wife's expectation.
|
||||
|
||||
**Round-trip on edit:** On form open for an existing event, read the first VALARM TRIGGER duration from the parsed ical.js component and select the nearest matching preset. If the existing TRIGGER does not match any preset (e.g., `TRIGGER:-PT7M` from some other client), display a "Custom (preserve)" option and do not strip it on save unless the user explicitly changes the selector. On save with a preset change, replace the VALARM. On save with "None" selected, remove all VALARMs.
|
||||
|
||||
**Scheduler contract:** Store `alarm_fire_at` (absolute UTC timestamp) in the `events` or a separate `event_alarms` table at write time. The scheduler queries `alarm_fire_at <= NOW() AND fired = false` — no ical re-parse at fire time. This is the existing outbox/scheduler pattern already in v1.0; extend it.
|
||||
|
||||
### 2. Admin Settings
|
||||
|
||||
**Scope (exactly this, no more):**
|
||||
|
||||
1. **Per-member Fastmail app password** — masked input to re-enter or rotate the encrypted credential stored in the DB. Show member name + "last updated" timestamp. On save, attempt a CalDAV `PROPFIND` test request with the new credential before committing. Surface pass/fail inline. No separate page — a section within Settings.
|
||||
|
||||
2. **Shared calendar designation** — list all synced calendars (display name + Fastmail collection URL) with a radio button selecting which one is `is_shared = true`. Currently requires a direct DB write (D-16 debt). One click + confirm. Show the current selection highlighted.
|
||||
|
||||
**Role gate:** A single `users.is_admin TINYINT(1) DEFAULT 0` flag. The first user created during the setup wizard gets `is_admin = 1`. The route `/api/admin/*` and the Admin Settings UI section return 403 for non-admin users. No role management UI — the non-technical member never sees this section.
|
||||
|
||||
**What Admin Settings is NOT:** It is not a full server configuration panel, not a user management screen, not an audit log, not a health dashboard. It is two operational tasks that currently require DB console access.
|
||||
|
||||
### 3. Setup Wizard
|
||||
|
||||
**When it runs:** On first visit to the app when a `setup_complete` record is absent from the DB (or a `SETUP_COMPLETE=false` env flag, whichever is simpler). After completion, a `setup_complete = true` record is written and the wizard never appears again.
|
||||
|
||||
**Step order (each step validates before advancing):**
|
||||
|
||||
| Step | Fields | Validation |
|
||||
|------|--------|------------|
|
||||
| 1. Welcome | None — explains what the wizard does | None |
|
||||
| 2. Database | Already connected (MariaDB creds are in env at container start). Show "connected" status. | DB ping; show error + instructions if failed |
|
||||
| 3. App URL | External URL (used for OIDC redirect) | URL format check; attempt a `HEAD` to itself if reachable |
|
||||
| 4. OIDC | Client ID, client secret, issuer URL, redirect URI (pre-filled) | Fetch `{issuer}/.well-known/openid-configuration`; show discovered endpoints; fail if unreachable |
|
||||
| 5. Session secret | Auto-generated 32-byte hex string (user can override) | Length >= 32 chars |
|
||||
| 6. Encryption key | Auto-generated 32-byte hex string for `APP_PASSWORD_ENCRYPTION_KEY` | Length == 32 bytes |
|
||||
| 7. VAPID keys | Auto-generate button (calls `webpush.generateVAPIDKeys()`) | Structural check — public key is a valid base64url-encoded P-256 point |
|
||||
| 8. Admin account | Select from OIDC-discovered members OR enter the sub/preferred_username manually | Not empty |
|
||||
| 9. Fastmail app password | App password for the primary calendar account | Test CalDAV `PROPFIND` to `https://caldav.fastmail.com/dav/principals/user/<email>/`; show pass/fail |
|
||||
| 10. Confirm + save | Summary of all inputs | Writes config to DB/env; sets `setup_complete`; redirects to app |
|
||||
|
||||
**Validation UX:** Inline per-field — show a green checkmark or red error directly below the field as soon as the user leaves it (blur event) or clicks a test button. "Next" button is disabled until the current step passes validation. Show human-readable error messages: "Could not reach the OIDC issuer — is Authelia running?" not "fetch failed: ERR_CONNECTION_REFUSED".
|
||||
|
||||
**Back navigation:** Every step allows going back. Already-validated steps retain their values. Do not re-validate automatically on back; re-validate on "Next".
|
||||
|
||||
**Failure surface:** If the DB step fails (impossible at startup in Docker but possible in dev), show a non-wizard error page with setup instructions — the wizard itself cannot run without a DB.
|
||||
|
||||
### 4. Faster Write-Back
|
||||
|
||||
**Target perceived latency:** < 2 seconds from "Save" click to the event appearing correctly in the calendar view. This matches Google Calendar and Fastmail native behavior.
|
||||
|
||||
**Current latency:** Up to ~15 seconds (outbox poll interval) + 5-minute CalDAV read-back poller.
|
||||
|
||||
**Approach:**
|
||||
1. The existing optimistic-202 response already updates the React Query cache immediately on save (latency = 0 for the UI). The gap is the CalDAV write-back actually landing, which matters for reminders and cross-device visibility.
|
||||
2. On INSERT to the `outbox` table, emit an event (in-process EventEmitter or a Redis pub/sub message if the outbox worker is in a separate process) that triggers an immediate drain attempt.
|
||||
3. On successful CalDAV PUT, emit an SSE `calendar-updated` event to connected clients so React Query invalidates the calendar cache and re-fetches. The re-fetch is the "write landed" confirmation.
|
||||
4. Reduce the read-back poller interval from 5 min to 30 s as a fallback — not the primary path, just the safety net.
|
||||
|
||||
**Durability guarantee preserved:** The outbox row is not deleted until the CalDAV PUT succeeds. The optimistic-202 pattern is unchanged. The drain is just triggered eagerly instead of lazily.
|
||||
|
||||
**What "near-immediate" does NOT mean:** CalDAV is a synchronous HTTP PUT. If Fastmail is slow (>1s), the write takes >1s. The goal is to eliminate the artificial polling delay, not to change network physics.
|
||||
|
||||
### 5. Gitea CI
|
||||
|
||||
**Trigger:** On `pull_request` to `main`.
|
||||
|
||||
**Steps:**
|
||||
1. Checkout
|
||||
2. pnpm install (cached)
|
||||
3. TypeScript typecheck — `pnpm -r tsc --noEmit` (both `apps/api` and `apps/pwa`)
|
||||
4. ESLint — `pnpm -r lint`
|
||||
5. Vitest unit tests — `pnpm -r test:unit`
|
||||
6. API integration tests — spin up MariaDB service container, run `pnpm --filter api test:integration` with `DB_HOST=127.0.0.1`
|
||||
|
||||
**Service container pattern:** Gitea Actions uses the same `services:` syntax as GitHub Actions. MariaDB `mariadb:11` with `MYSQL_ROOT_PASSWORD`, `MYSQL_DATABASE` env vars. `options: --health-cmd="mariadb-admin ping -h localhost" --health-interval=10s --health-retries=5` to gate the test step on DB readiness.
|
||||
|
||||
**Docker image publish:** Separate workflow, trigger `push` to `main` (after PR merge). Builds and pushes to the Gitea Container Registry. Not part of the PR workflow — keeps PR checks fast.
|
||||
|
||||
**Known limitation:** Gitea act_runner in Docker has incomplete service volume support. Do not mount host volumes in the services block. The MariaDB service container using env-based config (no volume) is reliable.
|
||||
|
||||
### 6. Mobile-Browser Test Harness
|
||||
|
||||
**What it is:** A reusable Playwright configuration profile using `devices['iPhone 15']` (or equivalent) with stored auth state (`DEV_AUTH_BYPASS=true` or a saved `storageState` JSON from a prior login), usable by the assistant via `playwright-cli` without re-authenticating on every run.
|
||||
|
||||
**What it covers:**
|
||||
- Mobile viewport layout (bottom nav, drawer sizing, touch targets >= 44px)
|
||||
- Calendar view rendering at iPhone screen width
|
||||
- Event form usability on mobile (reminder selector visible, not clipped)
|
||||
- List co-edit interactions on mobile
|
||||
|
||||
**What it does NOT cover:** Real iOS Safari, Web Push delivery, standalone-mode OIDC redirect, iOS-specific service worker quirks. Those remain human gates.
|
||||
|
||||
**Integration with CI:** A `test:e2e:mobile` script in `apps/pwa/package.json`. Run in CI as an optional step (allowed to fail without blocking merge) until the harness is proven stable, then graduate to blocking.
|
||||
|
||||
**Auth strategy for CI:** `DEV_AUTH_BYPASS=true` with a known test user id. The harness does not run the full OIDC flow — it injects the bypass session directly. This matches the v1.0 dev-stack bring-up pattern.
|
||||
|
||||
---
|
||||
|
||||
## Competitor / Prior Art Reference
|
||||
|
||||
| Feature | Apple Calendar | Google Calendar | Fastmail native | Nextcloud | This Product (v1.1 target) |
|
||||
|---------|---------------|-----------------|-----------------|-----------|---------------------------|
|
||||
| Reminder presets | None / 5m / 15m / 30m / 1h / 2h / 1d / 2d / 1w | None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d | None / 15m / 1h / 1d | N/A | None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d |
|
||||
| Multiple alarms | Yes (up to 5) | Yes (up to 5) | Yes | N/A | V1.1: 1; stretch: 2 |
|
||||
| All-day reminder time | 9 AM on alert day | 11:50 PM night before (jarring) | Morning | N/A | 9 AM (Apple convention) |
|
||||
| Admin credential mgmt | N/A | N/A | N/A | Yes (complex) | Minimal: 2 tasks only |
|
||||
| Setup wizard | N/A | N/A | N/A | Yes (3-step minimal) | 10-step validated |
|
||||
| Write-back latency | ~1s | ~1s | ~1s | Varies | Target ~1s (from ~15s) |
|
||||
| CI | N/A | N/A | N/A | GitHub Actions | Gitea Actions |
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Skylight Calendar product page](https://myskylight.com/calendar/)
|
||||
- [Skylight Calendar 2 TechCrunch review, Jan 2026](https://techcrunch.com/2026/01/07/skylight-debuts-calendar-2-to-keep-your-family-organized/)
|
||||
- [Cozi feature overview](https://www.cozi.com/feature-overview/)
|
||||
- [Cozi Gold features](https://www.cozi.com/cozi-gold-features/)
|
||||
- [Maple best family calendar app comparison](https://www.growmaple.com/blog-posts/best-family-calendar-app)
|
||||
- [Best Family Calendar Apps 2026 — getsense.ai](https://getsense.ai/blog/posts/best-family-calendar-apps-2026)
|
||||
- [TimeTree review and features](https://toolstack.io/tools/timetree)
|
||||
- [Google Family Calendar — support.google.com](https://support.google.com/families/answer/7157782)
|
||||
- [Nylas: The Deceptively Complex World of RRULEs](https://www.nylas.com/blog/calendar-events-rrules/)
|
||||
- [Mozilla Wiki: Calendar Recurrence and Exceptions](https://wiki.mozilla.org/Calendar:Recurrence_and_Exceptions)
|
||||
- [PWA iOS limitations 2026 — magicbell.com](https://www.magicbell.com/blog/pwa-ios-limitations-safari-support-complete-guide)
|
||||
- [PWA push notifications iOS — OneSignal docs](https://documentation.onesignal.com/docs/en/web-push-for-ios)
|
||||
- [Real-time data sync with WebSockets — GTCSys](https://gtcsys.com/real-time-communication-in-pwas-websockets-server-sent-events-and-webrtc/)
|
||||
- [Apple Calendar default alert settings (Mac support)](https://support.apple.com/guide/calendar/change-default-alert-settings-icl4407ddb59/mac)
|
||||
- [Google Calendar notifications (Android)](https://support.google.com/calendar/answer/37242?hl=en)
|
||||
- [iCalendar RFC 5545 — VALARM component](https://icalendar.org/iCalendar-RFC-5545/3-6-6-alarm-component.html)
|
||||
- [RFC 9074 — VALARM Extensions](https://datatracker.ietf.org/doc/html/rfc9074)
|
||||
- [iCalendar TRIGGER property spec](https://icalendar.org/iCalendar-RFC-5545/3-8-6-3-trigger.html)
|
||||
- [Nextcloud installation wizard](https://docs.nextcloud.com/server/stable/admin_manual/installation/installation_wizard.html)
|
||||
- [Wizard UI patterns — LogRocket UX](https://blog.logrocket.com/ux-design/creating-setup-wizard-when-you-shouldnt/)
|
||||
- [NN/G Wizards: Definition and Design Recommendations](https://www.nngroup.com/articles/wizards/)
|
||||
- [Outbox Pattern — Conduktor](https://www.conduktor.io/glossary/outbox-pattern-for-reliable-event-publishing/)
|
||||
- [Transactional Outbox with Optimistic Sending](https://www.npiontko.pro/2025/05/26/outbox-pattern-optimistic)
|
||||
- [Optimistic UI Patterns — Simon Hearne](https://simonhearne.com/2021/optimistic-ui-patterns/)
|
||||
- [Gitea Actions — Act Runner docs](https://docs.gitea.com/usage/actions/act-runner)
|
||||
- [Gitea Actions Docker builds](https://blog.diblasio.social/posts/gitea_builder/)
|
||||
- [Playwright PWA mobile testing](https://dev.to/pritig/how-playwright-simplifies-ui-testing-for-progressive-web-apps-pwas-9n8)
|
||||
- [Playwright emulation docs](https://playwright.dev/docs/emulation)
|
||||
|
||||
---
|
||||
*Feature research for: FamilySync — self-hosted family organization hub*
|
||||
*Researched: 2026-06-03*
|
||||
*Feature research for: FamilySync v1.1 — Operability & Polish*
|
||||
*Researched: 2026-06-10*
|
||||
|
||||
Reference in New Issue
Block a user