style(13-03): apply Prettier formatting across repo

Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
Lucas Berger
2026-06-11 20:35:18 -04:00
parent 4bc0445173
commit 982438dc10
398 changed files with 19050 additions and 16382 deletions
+78 -73
View File
@@ -18,42 +18,42 @@ This document covers **only the six v1.1 features**. v1.0 features (calendar, ev
Features that must exist in v1.1 to avoid the product feeling unfinished for real household use.
| Feature | Why Expected | Complexity | Notes |
|---------|--------------|------------|-------|
| 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. |
| Feature | Why Expected | Complexity | Notes |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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 that go beyond what a user would minimally expect — meaningful for this specific 2-person self-hosted context.
| Feature | Value Proposition | Complexity | Notes |
|---------|-------------------|------------|-------|
| 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). |
| Feature | Value Proposition | Complexity | Notes |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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)
Features that appear reasonable but are wrong for a 2-person self-hosted household. Flag these as scope creep.
| 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 | 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). |
---
@@ -130,21 +130,22 @@ Features that appear reasonable but are wrong for a 2-person self-hosted househo
## Feature Prioritization Matrix
| Feature | User Value | Implementation Cost | Priority |
|---------|------------|---------------------|----------|
| 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 |
| Feature | User Value | Implementation Cost | Priority |
| --------------------------------------- | ---------- | ------------------- | -------- |
| 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
@@ -157,17 +158,17 @@ Features that appear reasonable but are wrong for a 2-person self-hosted househo
**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` | |
| 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.
@@ -193,18 +194,18 @@ Features that appear reasonable but are wrong for a 2-person self-hosted househo
**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 |
| 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".
@@ -219,6 +220,7 @@ Features that appear reasonable but are wrong for a 2-person self-hosted househo
**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.
@@ -233,6 +235,7 @@ Features that appear reasonable but are wrong for a 2-person self-hosted househo
**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`)
@@ -251,6 +254,7 @@ Features that appear reasonable but are wrong for a 2-person self-hosted househo
**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)
@@ -266,15 +270,15 @@ Features that appear reasonable but are wrong for a 2-person self-hosted househo
## 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 |
| 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 |
---
@@ -297,5 +301,6 @@ Features that appear reasonable but are wrong for a 2-person self-hosted househo
- [Playwright emulation docs](https://playwright.dev/docs/emulation)
---
*Feature research for: FamilySync v1.1 — Operability & Polish*
*Researched: 2026-06-10*
_Feature research for: FamilySync v1.1 — Operability & Polish_
_Researched: 2026-06-10_