Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9e345b87 | ||
|
|
23dc3cdd3f | ||
|
|
700294a643 | ||
|
|
67430bb670 | ||
|
|
4e0a5e38e8 | ||
|
|
6e1c9ca924 | ||
|
|
303484d0ab | ||
|
|
6ade8d59bf | ||
|
|
c7955a46b9 | ||
|
|
a2890d1542 | ||
|
|
66e3b806be | ||
|
|
7fbb3cca9d | ||
|
|
6cc3b8ae27 | ||
|
|
c5cdb9c21d | ||
|
|
6e93e24df0 | ||
|
|
f83d423e1c | ||
|
|
80b20383f1 | ||
|
|
2276a254e4 | ||
|
|
0810260d0b |
+19
-11
@@ -46,12 +46,12 @@ jobs:
|
|||||||
- name: Enable pnpm
|
- name: Enable pnpm
|
||||||
run: corepack enable pnpm
|
run: corepack enable pnpm
|
||||||
|
|
||||||
# actions/cache@v4 is intentionally omitted — probe (D-PROBE-04) showed it
|
# actions/cache@v4 intentionally omitted (D-PROBE-04). Installs now target the
|
||||||
# times out on this runner (socket hang-up between runner container and job
|
# host-mounted pnpm store at /pnpm-store (see act_runner config.yaml container.options).
|
||||||
# container cache server). pnpm install without cache takes ~30s; acceptable.
|
# Without the host mount the flag still works — pnpm creates an ephemeral store there.
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile --store-dir /pnpm-store --prefer-offline
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: pnpm lint
|
run: pnpm lint
|
||||||
@@ -104,10 +104,11 @@ jobs:
|
|||||||
- name: Enable pnpm
|
- name: Enable pnpm
|
||||||
run: corepack enable pnpm
|
run: corepack enable pnpm
|
||||||
|
|
||||||
# actions/cache@v4 intentionally omitted — same reasoning as fast-checks job (D-PROBE-04).
|
# actions/cache@v4 intentionally omitted (D-PROBE-04). Installs now target the
|
||||||
|
# host-mounted pnpm store at /pnpm-store (see act_runner config.yaml container.options).
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile --store-dir /pnpm-store --prefer-offline
|
||||||
|
|
||||||
# Pitfall 11: service container healthy != MariaDB accepting connections.
|
# Pitfall 11: service container healthy != MariaDB accepting connections.
|
||||||
# No mysql CLI in the runner image (D-PROBE-03); poll via the already-installed
|
# No mysql CLI in the runner image (D-PROBE-03); poll via the already-installed
|
||||||
@@ -181,6 +182,9 @@ jobs:
|
|||||||
DB_USER: familysync
|
DB_USER: familysync
|
||||||
DB_PASSWORD: testpass
|
DB_PASSWORD: testpass
|
||||||
DB_NAME: familysync
|
DB_NAME: familysync
|
||||||
|
# Persist Playwright browser binaries across runs via host-mounted /ms-playwright.
|
||||||
|
# Without the host mount CI still works — binaries are downloaded to the ephemeral dir.
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -191,10 +195,11 @@ jobs:
|
|||||||
- name: Enable pnpm
|
- name: Enable pnpm
|
||||||
run: corepack enable pnpm
|
run: corepack enable pnpm
|
||||||
|
|
||||||
# actions/cache@v4 intentionally omitted — same reasoning as fast-checks job (D-PROBE-04).
|
# actions/cache@v4 intentionally omitted (D-PROBE-04). Installs now target the
|
||||||
|
# host-mounted pnpm store at /pnpm-store (see act_runner config.yaml container.options).
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile --store-dir /pnpm-store --prefer-offline
|
||||||
|
|
||||||
# Pitfall 11: service container healthy != MariaDB accepting connections.
|
# Pitfall 11: service container healthy != MariaDB accepting connections.
|
||||||
# No mysql CLI in the runner image (D-PROBE-03); poll via the mysql2 driver
|
# No mysql CLI in the runner image (D-PROBE-03); poll via the mysql2 driver
|
||||||
@@ -270,7 +275,9 @@ jobs:
|
|||||||
# Install Playwright browsers with system deps BEFORE starting the API, so the long
|
# Install Playwright browsers with system deps BEFORE starting the API, so the long
|
||||||
# browser download does not run during the API's lifetime.
|
# browser download does not run during the API's lifetime.
|
||||||
# Must run from apps/pwa/ where @playwright/test is installed (D-PROBE-05 confirmed exit 0).
|
# Must run from apps/pwa/ where @playwright/test is installed (D-PROBE-05 confirmed exit 0).
|
||||||
# Do NOT cache browser binaries — Playwright explicitly recommends against it in CI.
|
# PLAYWRIGHT_BROWSERS_PATH=/ms-playwright (job-level env) persists binaries across runs via
|
||||||
|
# the host-mounted dir. The --with-deps apt step cannot be cached; baking a runner image
|
||||||
|
# with browsers preinstalled would also drop the --with-deps apt step (future optimization).
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
run: npx playwright install --with-deps webkit chromium
|
run: npx playwright install --with-deps webkit chromium
|
||||||
working-directory: apps/pwa
|
working-directory: apps/pwa
|
||||||
@@ -462,7 +469,8 @@ jobs:
|
|||||||
--exit-code 1
|
--exit-code 1
|
||||||
|
|
||||||
# ── pnpm audit + outdated (code-change PRs only, D-12) ───────────────────
|
# ── pnpm audit + outdated (code-change PRs only, D-12) ───────────────────
|
||||||
# actions/cache@v4 intentionally omitted — same reasoning as fast-checks job (D-PROBE-04).
|
# actions/cache@v4 intentionally omitted (D-PROBE-04). Installs now target the
|
||||||
|
# host-mounted pnpm store at /pnpm-store (see act_runner config.yaml container.options).
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
if: needs.changes.outputs.code == 'true'
|
if: needs.changes.outputs.code == 'true'
|
||||||
@@ -475,7 +483,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: needs.changes.outputs.code == 'true'
|
if: needs.changes.outputs.code == 'true'
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile --store-dir /pnpm-store --prefer-offline
|
||||||
|
|
||||||
- name: Dependency audit (blocking on High+Critical)
|
- name: Dependency audit (blocking on High+Critical)
|
||||||
if: needs.changes.outputs.code == 'true'
|
if: needs.changes.outputs.code == 'true'
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ jobs:
|
|||||||
# Build from REPO ROOT (T-08-10): the Dockerfile copies the pnpm workspace manifest +
|
# Build from REPO ROOT (T-08-10): the Dockerfile copies the pnpm workspace manifest +
|
||||||
# lockfile from the root context; building from apps/api/ would fail to find them.
|
# lockfile from the root context; building from apps/api/ would fail to find them.
|
||||||
- name: Build production image
|
- name: Build production image
|
||||||
|
# DOCKER_BUILDKIT=1 is required: the Dockerfile uses `RUN --mount=type=cache`
|
||||||
|
# (BuildKit) to persist the pnpm store across builds. The legacy builder would
|
||||||
|
# fail on that syntax. BuildKit is default on Docker 23+, set explicitly for safety.
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: '1'
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
docker build --target production \
|
docker build --target production \
|
||||||
|
|||||||
@@ -1,5 +1,34 @@
|
|||||||
# Milestones
|
# Milestones
|
||||||
|
|
||||||
|
## v1.1 Operability & Polish (Shipped: 2026-06-18)
|
||||||
|
|
||||||
|
**Scope:** 14 phases (7–20), 57 plans, ~110 tasks. Continues v1.0 numbering; merged to `main` across a series of phase PRs (latest #27).
|
||||||
|
|
||||||
|
**Delivered:** Turned the v1.0 MVP into a configurable, administrable, and maintainable app — guided first-run setup, in-app role-gated admin, per-event reminders, near-instant write-back, local-auth (no-OIDC) mode, auto timezone — backed by a full self-hosted Gitea CI/CD pipeline (mobile + desktop e2e, security scanning, image hygiene, Docker publish) and a real lint gate. No more hand-editing env files or the database.
|
||||||
|
|
||||||
|
**Key accomplishments:**
|
||||||
|
|
||||||
|
- **Phase 7 — Mobile Test Harness:** Playwright harness (`@playwright/test`) with an iPhone/WebKit + Pixel/Chromium device matrix, SW-block, env-driven baseURL, deterministic dev-DB seed, and `DEV_AUTH_BYPASS` auth; layout/calendar/lists specs assert tap-targets, overflow, and populated/empty/error states. TEST-01/02. (Consumed by Phase 8 CI.)
|
||||||
|
- **Phase 8 — Gitea CI:** Self-hosted Gitea Actions pipeline — parallel `fast-checks` (lint/typecheck/PWA unit) + `api` (MariaDB 11 service container + migrate + DB-backed tests) + `harness` (dev-stack bring-up + Phase 7 specs on both profiles) gating every PR to `main`, plus a publish job pushing the API production image (`:latest` + `:v1.1-<sha>`, `--password-stdin`). CI-01/02.
|
||||||
|
- **Phase 9 — Faster Write-Back:** Event-driven outbox drain via a zero-dependency in-process EventEmitter (`outboxTrigger.ts`) — committed enqueues fire `signalOutboxDrain()` so edits land in ~1–2s instead of ~15s, preserving optimistic-202, create-before-delete, per-uid exactly-once, and the 15s fallback sweep. CAL-15.
|
||||||
|
- **Phase 10 — Admin Role & Settings:** v1.1 DB foundation (`users.is_admin`, `member_credentials.provider_type`, `calendar_events.reminder_lead_minutes`, `app_config`); DB-backed `requireAdmin` gating all `/api/admin/*`; one shared `validateEncryptAndStoreCredential` (CalDAV PROPFIND + AES-256-GCM) for admin rotation + member self-service; gated `/admin` PWA route. ADMIN-01/02/03.
|
||||||
|
- **Phase 11 — Per-Event Reminders:** Per-event reminder picker (None / 5m … 2d, all-day → 9 AM local) serialized as a VALARM, with a variable-lead scheduler (`uid:dtstartMs` dedup, dropped the hardcoded 15-min/shared-only restriction) that honors each event's lead, fires nothing without an alarm, and preserves VALARMs set in other clients. CAL-13/14, NOTIF-04/05/06.
|
||||||
|
- **Phase 12 — Initial Setup Wizard:** Pre-auth `/api/setup/*` first-run wizard validating DB / VAPID / OIDC / app-password before completion, generating env secrets (never persisted to DB), promoting the completing user to admin, and locking with a 423 guard on every invocation. SETUP-01/02/03/04.
|
||||||
|
- **Phase 13 — Real Lint Gate (ESLint):** ESLint flat config (typescript-eslint + React) across both apps + a Prettier `format:check` gate, turning the hollow `--if-present` no-op into a CI lint gate that actually fails; full first-run baseline cleanup to green.
|
||||||
|
- **Phase 14 — Desktop E2E Coverage:** Added a `desktop` (Desktop Chrome, no-touch) Playwright project and made the mobile-authored specs desktop-safe, so the CI regression gate validates desktop as well as iphone/pixel.
|
||||||
|
- **Phase 15 — Doc-Only CI Skip + Markdown Lint:** `dorny/paths-filter` classifies each PR so doc-only changes skip the slow `api`/`harness` jobs, with an always-running `gate` aggregate (avoids the required-check deadlock) and markdownlint-cli2 added to `fast-checks`.
|
||||||
|
- **Phase 16 — CI Dependency Audit, Security & Image Hygiene:** Boot-time refuse-to-boot guard + baked `NODE_ENV=production` confining `DEV_AUTH_BYPASS` to dev; `pnpm audit` gate with GHSA waiver allowlist + tiered outdated report; eslint-plugin-security; gitleaks (clean 613-commit baseline) + `.dockerignore`; publish-time image-hygiene assertions. SEC/DEP/IMG/CI-03.
|
||||||
|
- **Phase 17 — UI Optimization & Polish:** Fixed the long-standing phone BottomTabBar/FAB overlap (with a CI regression guard), shipped the real FamilySync logo + full favicon/PWA-icon set + brand accent, restructured `tokens.css` into a themeable semantic-token layer (light-only groundwork), and added a logout control + desktop-centered sheets + admin toasts.
|
||||||
|
- **Phase 18 — Auto Timezone Detection:** Made the household timezone an explicit, stored, browser-auto-detected, admin-changeable setting (`getHouseholdTimezone(db)` + IANA validation), routing the all-day "9 AM local" reminder computation through it instead of the implicit `process.env.TZ`.
|
||||||
|
- **Phase 19 — Local Auth (No-OIDC Mode):** Full local username/password account model (scrypt + stateless `local-session` JWT cookie, `local_credentials` table, rate-limit/lockout, login/logout, admin create/reset, self-change, OIDC-link, break-glass CLI) coexisting with the Authelia OIDC path — removing the hard dependency on a deployed Authelia.
|
||||||
|
- **Phase 20 — Admin Member Editor & Form Declutter:** Replaced per-row Rotate/Reset buttons with a single tappable member-editor sheet (display name + local password + app password) over a new `PATCH /api/admin/members/:id` with a last-admin guard, and collapsed the Add-member form — retiring the confusing "Rotate" copy.
|
||||||
|
|
||||||
|
**Requirements:** 17/17 v1.1 requirements complete (TEST, CI, CAL, ADMIN, NOTIF, SETUP). Phases 13–20 were driven by decision contracts (D-IDs / AUTH-LOCAL-*) rather than REQ-IDs. Deferred to backlog: self-service onboarding (999.5), provider abstraction (999.1), multiple reminders per event (v1.2), dark mode / theming (999.20), modern styling refresh (999.21).
|
||||||
|
|
||||||
|
**Known deferred items at close:** none carried — all phase verifications (incl. Phase 11 & 17 human-needed checks) confirmed by the operator at close.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.0 MVP (Shipped: 2026-06-10)
|
## v1.0 MVP (Shipped: 2026-06-10)
|
||||||
|
|
||||||
**Scope:** 6 phases, 42 plans, 68 tasks. Shipped via Gitea PR #1 (`gsd/v1.0-milestone` → `main`, 375 commits).
|
**Scope:** 6 phases, 42 plans, 68 tasks. Shipped via Gitea PR #1 (`gsd/v1.0-milestone` → `main`, 375 commits).
|
||||||
|
|||||||
+30
-10
@@ -8,20 +8,32 @@ FamilySync is a self-hosted, Dockerized family organization hub for a two-person
|
|||||||
|
|
||||||
The household can see and co-edit one color-coded family calendar (shared + each member's personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store, no per-member calendar credential juggling.
|
The household can see and co-edit one color-coded family calendar (shared + each member's personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store, no per-member calendar credential juggling.
|
||||||
|
|
||||||
## Current Milestone: v1.1 Operability & Polish
|
## Current State
|
||||||
|
|
||||||
**Goal:** Make FamilySync configurable, administrable, and maintainable for real multi-member use — guided setup, in-app admin, per-event reminders, faster write-back, CI/CD, and mobile test coverage — without hand-editing env files or the database.
|
**Shipped: v1.1 Operability & Polish (2026-06-18)** — 14 phases (7–20), 57 plans. Full detail in [`MILESTONES.md`](MILESTONES.md) and [`milestones/v1.1-ROADMAP.md`](milestones/v1.1-ROADMAP.md).
|
||||||
|
|
||||||
|
v1.1 turned the v1.0 MVP into a configurable, administrable, maintainable app: guided first-run setup wizard, role-gated in-app admin (credential rotation, shared-calendar designation, member editor), per-event reminders with a variable-lead scheduler, near-instant (~1–2s) event write-back, local-auth (no-OIDC) mode, and auto timezone detection — all backed by a full self-hosted Gitea CI/CD pipeline (mobile + desktop Playwright regression, a real ESLint gate, dependency/secret/security scanning, dev↔prod image hygiene, and Docker publish). No more hand-editing env files or the database.
|
||||||
|
|
||||||
|
Deferred to backlog: self-service provider onboarding (999.5), provider abstraction (999.1), dark mode / theming (999.20), and a broader modern-styling refresh (999.21 — future milestone).
|
||||||
|
|
||||||
|
## Current Milestone: v1.2 Multi-Provider, Theming & Zero-Setup
|
||||||
|
|
||||||
|
**Goal:** Open FamilySync beyond Fastmail — add Google Calendar as a first-class provider behind a provider-abstraction layer with self-service per-provider onboarding — plus dark mode, multiple reminders per event, and a database that bootstraps itself with no manual setup.
|
||||||
|
|
||||||
**Target features:**
|
**Target features:**
|
||||||
|
|
||||||
- **Per-event reminders** — reminder selector on the event form (incl. "none"), serialized as VALARM; scheduler honors each event's lead instead of a hardcoded 15-min, and fires nothing when an event has no alarm (was backlog 999.4)
|
- **Provider abstraction** — refactor Fastmail behind a provider interface so additional calendar providers plug in cleanly (999.1).
|
||||||
- **Admin Settings section** — role-gated UI to manage per-member Fastmail app passwords and designate the shared calendar, replacing manual DB writes (was backlog 999.10)
|
- **Gmail / Google Calendar, full parity** — read + write-back + reminders via the Google Calendar API + OAuth2 (not CalDAV); the second provider behind the abstraction.
|
||||||
- **Initial setup wizard** — first-run validated bootstrap of env vars, VAPID keypair, DB connection, and first app password (was backlog 999.11)
|
- **Self-service onboarding, per-provider native flow** (999.5, pivoted to generic provider) — Fastmail = app-password entry, Gmail = "Connect Google" OAuth button; low-friction for the non-technical Apple member.
|
||||||
- **Faster write-back** — event-driven outbox drain so edits land in ~1s instead of up to ~15s, preserving the optimistic-202 durability guarantees (was backlog 999.13)
|
- **Multiple reminders per event** — multiple VALARMs per event (deferred to v1.2 from v1.1).
|
||||||
- **Gitea CI** — full regression (lint/typecheck/unit/API-integration against a MariaDB service container) on PR to main + build/publish Docker image (was backlog 999.14)
|
- **PWA dark mode / theming** (999.20) — light/dark/system switch consuming the Phase 17 semantic-token groundwork.
|
||||||
- **Mobile-browser testing** ✅ **delivered (Phase 7, 2026-06-11)** — Playwright harness, two-profile mobile matrix (iPhone/WebKit + Pixel/Chromium), DEV_AUTH_BYPASS auth, deterministic dev-DB seed; 58 specs across both profiles assert layout/state. TEST-01/TEST-02 validated. Consumed by Phase 8 CI (was backlog 999.12)
|
- **Zero-manual-setup DB** — auto-create/migrate tables on boot if missing (bring-your-own MariaDB: creds + DB name → working schema), then the existing setup wizard handles admin + config.
|
||||||
|
- **Dev-user full-app exercise** (999.19) — exercise the whole app without a real calendar connected.
|
||||||
|
- **Act on CI dependency report** (999.18) — apply the outdated/vulnerable dependency updates CI surfaces.
|
||||||
|
|
||||||
Deferred to backlog: self-service provider onboarding (999.5) and provider abstraction (999.1). Admin-managed credentials (999.10) partially cover the multi-member credential gap in the interim.
|
**Explicitly out of this milestone:** modern visual refresh (999.21 stays in backlog — current UX is satisfactory). v1.3 is reserved for tablet/touchscreen UX.
|
||||||
|
|
||||||
|
Phase numbering continues from v1.1 (last phase 20) → v1.2 starts at Phase 21.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -39,6 +51,11 @@ Deferred to backlog: self-service provider onboarding (999.5) and provider abstr
|
|||||||
- [x] Faster write-back so edits reach Fastmail in ~1–2s instead of ~15s (CAL-15) — **Validated in Phase 9 (faster-write-back)**: event-driven outbox drain via a zero-dependency in-process EventEmitter (`outboxTrigger.ts`); a committed enqueue publishes a fire-and-forget `signalOutboxDrain()` that funnels through the existing `isDraining`-guarded drain with a `drainRequested` trailing-re-drain, preserving optimistic-202, create-before-delete on moves, exactly-once per uid, and the 15s `setInterval` fallback. 5/5 success criteria verified; trigger-wiring tests assert SC-1/D-05/D-07.
|
- [x] Faster write-back so edits reach Fastmail in ~1–2s instead of ~15s (CAL-15) — **Validated in Phase 9 (faster-write-back)**: event-driven outbox drain via a zero-dependency in-process EventEmitter (`outboxTrigger.ts`); a committed enqueue publishes a fire-and-forget `signalOutboxDrain()` that funnels through the existing `isDraining`-guarded drain with a `drainRequested` trailing-re-drain, preserving optimistic-202, create-before-delete on moves, exactly-once per uid, and the 15s `setInterval` fallback. 5/5 success criteria verified; trigger-wiring tests assert SC-1/D-05/D-07.
|
||||||
- [x] Per-event reminders — choose a reminder lead per event (None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d, all-day → day-granularity + 9 AM fire), serialized as a VALARM, with a variable-lead scheduler that honors each event's lead (CAL-13/CAL-14, NOTIF-04/05/06) — **Validated in Phase 11 (per-event-reminders)**: pure VALARM serialization/classification layer (`buildTimedValarm`/`buildAllDayValarm`/`classifyValarms`/`extractValarms`/`computeAlertInstantUtc`); variable-lead scheduler with `uid:dtstartMs` dedup, dropped fixed-15-min/shared-only restriction, all-day 9 AM-local branch; `reminderLeadMinutes` threaded end-to-end with preserve-on-no-change (D-08); allDay-aware reminder picker with edit pre-population. Gap-closure (Plan 11-05) fixed two code-review blockers — custom/other-client VALARMs are now preserved on edit via a surfaced `reminderIsCustom` signal (CAL-14 / Pitfall 1), and the all-day push body no longer reads "Starts in 0 min" — plus post-event-trigger classification, a server-side max bound, and helper-text gating. 5/5 must-haves verified; 347 API + 206 PWA tests green. **Deferred:** live Fastmail VALARM round-trip + on-device push fire (untestable in dev — no provider connected; backlog 999.19).
|
- [x] Per-event reminders — choose a reminder lead per event (None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d, all-day → day-granularity + 9 AM fire), serialized as a VALARM, with a variable-lead scheduler that honors each event's lead (CAL-13/CAL-14, NOTIF-04/05/06) — **Validated in Phase 11 (per-event-reminders)**: pure VALARM serialization/classification layer (`buildTimedValarm`/`buildAllDayValarm`/`classifyValarms`/`extractValarms`/`computeAlertInstantUtc`); variable-lead scheduler with `uid:dtstartMs` dedup, dropped fixed-15-min/shared-only restriction, all-day 9 AM-local branch; `reminderLeadMinutes` threaded end-to-end with preserve-on-no-change (D-08); allDay-aware reminder picker with edit pre-population. Gap-closure (Plan 11-05) fixed two code-review blockers — custom/other-client VALARMs are now preserved on edit via a surfaced `reminderIsCustom` signal (CAL-14 / Pitfall 1), and the all-day push body no longer reads "Starts in 0 min" — plus post-event-trigger classification, a server-side max bound, and helper-text gating. 5/5 must-haves verified; 347 API + 206 PWA tests green. **Deferred:** live Fastmail VALARM round-trip + on-device push fire (untestable in dev — no provider connected; backlog 999.19).
|
||||||
- [x] Admin role + role-gated settings surface to rotate member Fastmail app passwords and designate the shared calendar (ADMIN-01/02/03) — **Validated in Phase 10 (admin-role-settings)**: v1.1 DB foundation (`users.is_admin`, `member_credentials.provider_type`+`unique(user_id)`, `calendar_events.reminder_lead_minutes`, `app_config`) via an additive generate+migrate migration; DB-backed `requireAdmin` gating all `/api/admin/*` (client `isAdmin` UX-only, server 403 the real boundary, D-03); one shared `validateEncryptAndStoreCredential` helper for admin rotation + member self-service `/api/me/credential` (400-no-echo, session-userId only); exclusive shared-calendar designation made transactional + 404-guarded (CR-01 fix); gated `/admin` PWA route + conditional nav + `SetupBanner`. 12/12 must-haves verified; admin route-guard/nav-gating green in real Chromium (e2e 5/5). Deferred follow-ups: WR-01 bootstrap-race (Phase 12 reworks the bootstrap), broker `credentialSync.ts`/`CredentialSheet.tsx` crypto re-audit under full read access.
|
- [x] Admin role + role-gated settings surface to rotate member Fastmail app passwords and designate the shared calendar (ADMIN-01/02/03) — **Validated in Phase 10 (admin-role-settings)**: v1.1 DB foundation (`users.is_admin`, `member_credentials.provider_type`+`unique(user_id)`, `calendar_events.reminder_lead_minutes`, `app_config`) via an additive generate+migrate migration; DB-backed `requireAdmin` gating all `/api/admin/*` (client `isAdmin` UX-only, server 403 the real boundary, D-03); one shared `validateEncryptAndStoreCredential` helper for admin rotation + member self-service `/api/me/credential` (400-no-echo, session-userId only); exclusive shared-calendar designation made transactional + 404-guarded (CR-01 fix); gated `/admin` PWA route + conditional nav + `SetupBanner`. 12/12 must-haves verified; admin route-guard/nav-gating green in real Chromium (e2e 5/5). Deferred follow-ups: WR-01 bootstrap-race (Phase 12 reworks the bootstrap), broker `credentialSync.ts`/`CredentialSheet.tsx` crypto re-audit under full read access.
|
||||||
|
- [x] Initial setup wizard — first-run validated bootstrap of env/secrets/DB/OIDC/VAPID/app-password instead of hand-editing files; locks once complete (SETUP-01/02/03/04) — **Validated in Phase 12 (initial-setup-wizard)**: pre-auth `/api/setup/*` router mounted before the OIDC guard; each input validated (DB connects, VAPID decodes to 32 bytes + pairs with the public key, OIDC discovery resolves, app password reaches CalDAV); generated secrets shown for env copy and never persisted to the DB; completing user promoted to admin and a 423 guard enforced on every invocation. First-login-claims rework in `upsertUser` (no email coupling).
|
||||||
|
- [x] Self-hosted Gitea CI/CD + automated browser test coverage (TEST-01/02, CI-01/02) — **Validated in Phases 7/8/13/14/15/16**: a mobile (iPhone/WebKit + Pixel/Chromium) **and** desktop Playwright harness reached via `DEV_AUTH_BYPASS`; a PR pipeline gating lint (real ESLint flat config) / typecheck / unit / MariaDB-backed API integration / the headless harness; doc-only PRs skip the slow jobs via an always-running `gate` aggregate; dependency audit + gitleaks + eslint-plugin-security + dev↔prod image-hygiene assertions; and a publish job pushing the API production image on merge to `main`.
|
||||||
|
- [x] Local-auth (no-OIDC) mode (AUTH-LOCAL-*) — **Validated in Phase 19 (local-auth-no-oidc-mode)**: full local username/password account model — scrypt hashing, stateless `local-session` JWT cookie, `local_credentials` table, rate-limit/lockout login + logout, admin create/reset member, self-change password, OIDC-link to claim a local user, and a break-glass reset-admin CLI — coexisting with the Authelia OIDC path, removing the hard dependency on a deployed Authelia for solo/small self-hosters.
|
||||||
|
- [x] Household timezone as an explicit, stored, auto-detected, admin-changeable setting (Phase 18 D-01..D-07) — **Validated in Phase 18 (auto-timezone-detection)**: `getHouseholdTimezone(db)` with IANA validation is the source of truth for the all-day "9 AM local" reminder computation (replacing the implicit `process.env.TZ`), seeded from the browser at first run and changeable from `/admin`; browser-local display/timed-write path untouched.
|
||||||
|
- [x] PWA visual identity + phone-layout polish + admin member editor (Phase 17 D-01..D-10, Phase 20 D-01..D-07) — **Validated in Phases 17 & 20**: fixed the phone BottomTabBar/FAB overlap (with a CI regression guard), shipped the real FamilySync logo + full favicon/PWA-icon set + brand accent, restructured `tokens.css` into a themeable semantic-token layer (light-only groundwork), added a logout control + desktop-centered sheets; and replaced the per-row Rotate/Reset buttons with a single tappable member-editor sheet over `PATCH /api/admin/members/:id` (last-admin guard), retiring the confusing "Rotate" copy.
|
||||||
|
|
||||||
### Active
|
### Active
|
||||||
|
|
||||||
@@ -100,6 +117,9 @@ Deferred to backlog: self-service provider onboarding (999.5) and provider abstr
|
|||||||
| **D-15:** Validate the real external topology via a **local Newt connector + test subdomain** through existing Pangolin (Mode A), not an Unraid deploy. Unraid (Mode B) reserved for go-live. | Authelia OIDC + SSE pass-through behaviour live in Authelia + Pangolin/Newt, not in where the origin runs — so a local Newt rig faithfully tests both, decoupling "does the topology work" from "is it in production." Newt dials outbound (no open ports). Only shared touch is an additive, reversible Authelia client. | ✓ Validated (Gate 2 executed live in Phase 3 / D-17) |
|
| **D-15:** Validate the real external topology via a **local Newt connector + test subdomain** through existing Pangolin (Mode A), not an Unraid deploy. Unraid (Mode B) reserved for go-live. | Authelia OIDC + SSE pass-through behaviour live in Authelia + Pangolin/Newt, not in where the origin runs — so a local Newt rig faithfully tests both, decoupling "does the topology work" from "is it in production." Newt dials outbound (no open ports). Only shared touch is an additive, reversible Authelia client. | ✓ Validated (Gate 2 executed live in Phase 3 / D-17) |
|
||||||
| **D-16 (2026-06-05, Phase 2):** No dedicated Fastmail "broker" account. The **shared-family calendar is a calendar collection created on the operator's primary Fastmail account** (`me@lucasberger.ca`) and shared out to the wife + others via Fastmail's own calendar sharing. The app's single app password enumerates it like any other collection; the `calendars.is_shared` flag (operator-set) marks which row is the shared one. | Clarified during the Wave 2 checkpoint: "broker account" was only ever the role the primary account's app password plays. id=1 ("Calendar") is the operator's **personal** calendar, not the shared one — so it must NOT be marked `is_shared`. Aggregating each _other_ member's **personal** calendar still follows the D-09 per-member app-password model (open for Phase 3 onboarding: a member may get a personal color lane, or only the shared calendar). | ✓ Resolved (2026-06-10): "FamilySync" shared calendar created on the primary account, synced as `calendars.id=10`, marked `is_shared=1`; shared lane + reminders now active |
|
| **D-16 (2026-06-05, Phase 2):** No dedicated Fastmail "broker" account. The **shared-family calendar is a calendar collection created on the operator's primary Fastmail account** (`me@lucasberger.ca`) and shared out to the wife + others via Fastmail's own calendar sharing. The app's single app password enumerates it like any other collection; the `calendars.is_shared` flag (operator-set) marks which row is the shared one. | Clarified during the Wave 2 checkpoint: "broker account" was only ever the role the primary account's app password plays. id=1 ("Calendar") is the operator's **personal** calendar, not the shared one — so it must NOT be marked `is_shared`. Aggregating each _other_ member's **personal** calendar still follows the D-09 per-member app-password model (open for Phase 3 onboarding: a member may get a personal color lane, or only the shared calendar). | ✓ Resolved (2026-06-10): "FamilySync" shared calendar created on the primary account, synced as `calendars.id=10`, marked `is_shared=1`; shared lane + reminders now active |
|
||||||
| **D-17 (2026-06-07, Phase 3):** Phase 1 Gate 2 (deferred per D-14) was executed live during Phase 3 against real Authelia OIDC over Pangolin/Newt (Mode A), clearing the load-bearing iOS-standalone-login risk. The full event write path (create/all-day/recurring/edit/delete/conflict) is verified end-to-end to Fastmail. | Live bring-up surfaced bugs the dev-bypass build could not (newt MTU blackhole, OIDC state-cookie race, write-path timezone/identity/join/cache bugs, all-day off-by-one, color collisions). All fixed; UX gaps captured as backlog 999.3–999.9. | — Validated (Gate 2, `03-GATE2-RESULTS.md`). Carried: Android install (B5), SSE smoke (Phase 4 entry gate, D-14). |
|
| **D-17 (2026-06-07, Phase 3):** Phase 1 Gate 2 (deferred per D-14) was executed live during Phase 3 against real Authelia OIDC over Pangolin/Newt (Mode A), clearing the load-bearing iOS-standalone-login risk. The full event write path (create/all-day/recurring/edit/delete/conflict) is verified end-to-end to Fastmail. | Live bring-up surfaced bugs the dev-bypass build could not (newt MTU blackhole, OIDC state-cookie race, write-path timezone/identity/join/cache bugs, all-day off-by-one, color collisions). All fixed; UX gaps captured as backlog 999.3–999.9. | — Validated (Gate 2, `03-GATE2-RESULTS.md`). Carried: Android install (B5), SSE smoke (Phase 4 entry gate, D-14). |
|
||||||
|
| **D-18 (2026-06-12, Phase 9):** Faster write-back uses a **zero-dependency in-process EventEmitter** drain signal, not Redis — the drain is single-process by design; Redis stays only for list SSE. | The optimistic-202 outbox is single-process; an in-process signal funnelled through the existing `isDraining` guard preserves all durability guarantees without a new external dependency. (Redis was later removed entirely — quick 260618-smr — as it was unused at runtime.) | ✓ Validated (v1.1, Phase 9, CAL-15) |
|
||||||
|
| **D-19 (2026-06-17, Phase 19):** FamilySync ships **local username/password auth as a first-class mode coexisting with Authelia OIDC**, not OIDC-only. | The operator runs it this way; a hard dependency on a deployed Authelia is too heavy for solo/small self-hosters. A local user can be linked to an OIDC identity later (claim flow, never email-matched per D-10). | ✓ Validated (v1.1, Phase 19) |
|
||||||
|
| **D-20 (2026-06-11, Phase 8):** CI runs on the self-hosted Gitea runner with `runs-on: ubuntu-latest` (no self-hosted label) in Docker-executor mode; MariaDB readiness uses `healthcheck.sh --connect`, never `mysqladmin ping` (removed in MariaDB 11); the secret is `REGISTRY_PAT` (the `GITEA_` prefix is silently dropped). | Established by the runner-probe-first approach (PITFALLS 11/12); these constraints are load-bearing for every CI workflow in the repo. | ✓ Validated (v1.1, Phases 8/16, CI-01/02) |
|
||||||
|
|
||||||
## Evolution
|
## Evolution
|
||||||
|
|
||||||
@@ -122,4 +142,4 @@ This document evolves at phase transitions and milestone boundaries.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
_Last updated: 2026-06-18 — Phase 20 (Admin Member Editor & Form Declutter) complete; single member-editor sheet (D-01..D-07) over new PATCH /api/admin/members/:id with last-admin guard; "Rotate"/"Reset password" copy retired._
|
_Last updated: 2026-06-19 — v1.2 milestone started (Multi-Provider, Theming & Zero-Setup): provider abstraction + Gmail full parity, self-service per-provider onboarding, multiple reminders per event, PWA dark mode, zero-manual-setup DB bootstrap, dev-user full-app exercise, and CI dependency updates. Phases continue from 21._
|
||||||
|
|||||||
+61
-64
@@ -1,92 +1,89 @@
|
|||||||
# Requirements: FamilySync — v1.1 "Operability & Polish"
|
# Requirements: FamilySync — v1.2 "Multi-Provider, Theming & Zero-Setup"
|
||||||
|
|
||||||
**Defined:** 2026-06-10
|
**Defined:** 2026-06-19
|
||||||
**Milestone:** v1.1 (continues from v1.0 MVP, shipped 2026-06-10)
|
**Milestone:** v1.2 (continues from v1.1 "Operability & Polish", shipped 2026-06-18)
|
||||||
**Core Value:** The household can see and co-edit one color-coded family calendar (shared + each member's personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store. v1.1 makes that app **configurable, administrable, and maintainable** without hand-editing env files or the database.
|
**Core Value:** The household can see and co-edit one color-coded family calendar (shared + each member's personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store. v1.2 **opens the app beyond Fastmail** (Google Calendar as a first-class provider behind a provider abstraction, with self-service per-provider onboarding), adds **dark mode** and **multiple reminders per event**, and makes the database **bootstrap itself with no manual setup**.
|
||||||
|
|
||||||
REQ-IDs continue v1.0 numbering (CAL ≤12, NOTIF ≤3 already used). New categories: ADMIN, SETUP, CI, TEST.
|
REQ-IDs continue prior numbering (CAL ≤15, SETUP ≤04, TEST ≤02 already used). New categories: **PROV** (providers & onboarding), **THEME** (PWA theming), **DEP** (dependency maintenance).
|
||||||
|
|
||||||
## v1.1 Requirements
|
## v1.2 Requirements
|
||||||
|
|
||||||
Each requirement maps to exactly one roadmap phase (see Traceability).
|
Each requirement maps to exactly one roadmap phase (see Traceability).
|
||||||
|
|
||||||
### Calendar — Per-event reminders & write-back latency
|
### Providers — abstraction & self-service onboarding
|
||||||
|
|
||||||
- [x] **CAL-13**: User can choose a reminder lead time when creating or editing an event from a preset list (None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d), with **"None" as the default**; the choice is serialized as a VALARM on the event written back to Fastmail.
|
- [ ] **PROV-01**: All calendar read, write-back, and reminder operations flow through a single `CalendarProvider` abstraction; the existing Fastmail/CalDAV broker is refactored behind it as one implementation with **no behavior regression** to the working Fastmail path (outbox durability, RRULE/VALARM preservation, optimistic-202, per-uid exactly-once all preserved).
|
||||||
- [x] **CAL-14**: Editing an event **preserves any existing reminder/VALARM** set in another client (Fastmail or native) — reminders are never silently stripped on round-trip.
|
- [ ] **PROV-02**: A member can connect a **Google account via a "Connect Google" OAuth flow** (self-service, no admin involvement); the OAuth refresh token is stored encrypted (AES-256-GCM, same path as Fastmail app passwords) and `member_credentials` supports one credential **per provider per user** (`UNIQUE(user_id, provider_type)`).
|
||||||
- [x] **CAL-15**: A created, edited, or deleted event reaches Fastmail within ~2 seconds (event-driven outbox drain) instead of up to ~15s, while preserving the optimistic-202 accept and all outbox durability guarantees (create-before-delete ordering, drain concurrency guard, fresh-etag-before-PUT, per-uid exactly-once).
|
- [ ] **PROV-03**: A member can self-service connect or re-enter **their own** provider credential using each provider's native mechanism — Fastmail = app-password entry, Google = OAuth consent — without an admin rotating it for them (generalizes the v1.1 admin-managed ADMIN-01 path).
|
||||||
|
- [ ] **PROV-04**: When a provider connection expires or is revoked, the user sees a **clear, in-calendar banner** (not hidden in settings) and can **reconnect in one tap** by re-running the provider's auth flow, without removing and re-adding the account.
|
||||||
|
|
||||||
### Notifications — Variable-lead reminder scheduling
|
### Calendar — Google parity & multiple reminders
|
||||||
|
|
||||||
- [x] **NOTIF-04**: An event reminder push fires at the event's **chosen lead time**, not a hardcoded 15-minute lead.
|
- [ ] **CAL-16**: Events from a connected **Google calendar appear in the unified color-coded calendar** (read sync), with recurring events expanded correctly (Google recurrence/cancelled-instance model reconciled to the app's RRULE/EXDATE model) and all-day events not shifted.
|
||||||
- [x] **NOTIF-05**: An event with **no reminder set produces no reminder push** (no default 15-min fire).
|
- [ ] **CAL-17**: A user can **create / edit / delete events on a connected Google calendar** from the app, written back to Google — full parity with the Fastmail write path, including recurring-event create and whole-series delete.
|
||||||
- [x] **NOTIF-06**: An all-day event's reminder fires at a sensible local time (9 AM on the alert day), not at midnight, and reminder delivery remains exactly-once across catch-up scans and rescheduled events.
|
- [ ] **CAL-18**: A user can set **multiple reminders on a single event** (up to 5), each firing independently; the set serializes to the connected provider's native model (multiple VALARMs for Fastmail / multiple `overrides` for Google), **preserves reminders authored in another client** on edit, and the scheduler fires each lead exactly once (dedup key includes the lead).
|
||||||
|
|
||||||
### Administration — Settings section (role-gated)
|
### Theming — PWA dark mode
|
||||||
|
|
||||||
> Role-agnostic design: ship operator-only (`is_admin`), but the role check is member-count-agnostic so more admins can be added later without rework.
|
- [ ] **THEME-01**: A user can switch the PWA between **Light / Dark / System**, the choice is persisted, there is **no flash-of-wrong-theme** on load, and the dark theme renders cleanly across every route (calendar, lists, admin, settings sheet, login) including the Schedule-X calendar component's `--sx-color-*` variables.
|
||||||
|
|
||||||
- [x] **ADMIN-01**: An admin can view household members and update (rotate / re-enter) a member's Fastmail app password from the UI; the credential is validated against CalDAV before saving and stored encrypted (existing `APP_PASSWORD_ENCRYPTION_KEY` path) — the password is never displayed, logged, or echoed.
|
### Setup — zero-manual-setup database bootstrap
|
||||||
- [x] **ADMIN-02**: An admin can designate which synced calendar is the shared family calendar (set `calendars.is_shared`) from the UI, replacing the manual DB write.
|
|
||||||
- [x] **ADMIN-03**: Admin Settings routes and UI are gated by a role check; a non-admin member cannot reach or invoke them.
|
|
||||||
|
|
||||||
### Setup — First-run configuration wizard
|
- [ ] **SETUP-05**: On boot, the app **auto-creates and migrates the full schema** against a bring-your-own MariaDB — the operator supplies only DB credentials and a database name — running drizzle migrations programmatically (`drizzle-orm/mysql2/migrator`, never `drizzle-kit push`), **idempotently and concurrency-safely** (migration-lock sentinel), completing **before requests are served**, so no manual `db:migrate` step is required.
|
||||||
|
|
||||||
- [x] **SETUP-01**: On first run (no admin/credentials configured), the operator is guided through a setup wizard to define bootstrap configuration (app/external URL, OIDC client, session secret, encryption key, VAPID keypair, MariaDB connection, first member's Fastmail app password) instead of hand-editing `.env` / `docker-compose.yml`.
|
### Test — dev/mock provider (full-app exercise without a live calendar)
|
||||||
- [x] **SETUP-02**: The wizard **validates each input before completing** — DB connectivity test, VAPID private key decodes to 32 bytes and pairs with the public key, OIDC discovery resolves, and the Fastmail app password reaches CalDAV (PROPFIND).
|
|
||||||
- [x] **SETUP-03**: The wizard generates secrets (session secret, encryption key, VAPID keypair) for the operator to copy into env; secrets are **never written to the database or returned in a response body**.
|
|
||||||
- [x] **SETUP-04**: Once setup is complete, the setup endpoints are no longer accessible (guard checked on every invocation, not only at startup).
|
|
||||||
|
|
||||||
### CI — Gitea continuous integration
|
- [ ] **TEST-03**: A **mock/in-memory `CalendarProvider`** lets the dev-bypass user and the Playwright CI harness exercise the **full app** — calendar create/edit/delete, reminders, and lists — with **no live calendar account**, producing deterministic seed data covering recurring, all-day, past, and future events.
|
||||||
|
|
||||||
- [x] **CI-01**: Every pull request targeting `main` runs full regression — lint, typecheck (both apps), unit tests, API integration tests against a MariaDB service container, **and the Phase 7 mobile Playwright harness as a UI-regression step (CI brings up the API + PWA dev servers + MariaDB service container with `DEV_AUTH_BYPASS` in the runner and runs the harness specs headlessly against the authed PWA)** — and the result gates the merge.
|
### Dependencies — apply CI-surfaced updates
|
||||||
- [x] **CI-02**: On merge to `main`, the API Docker image is built and published to the Gitea container registry.
|
|
||||||
|
|
||||||
### Test — Mobile-emulated authed browser harness
|
- [ ] **DEP-01**: Apply the **outdated/vulnerable dependency upgrades** surfaced by CI (`pnpm audit` / `pnpm outdated`), respecting the existing version pins and constraints (ESLint held at 9.x until `eslint-plugin-react` supports 10; Drizzle/drizzle-kit kept coupled; Schedule-X minor-only and not bumped before dark mode is locked), with **all CI gates green** after the update.
|
||||||
|
|
||||||
- [x] **TEST-01**: The assistant can drive the PWA in a **mobile-emulated viewport** (device profile + mobile UA + touch) for automated UI/layout verification.
|
## Future Requirements (deferred, not in v1.2)
|
||||||
- [x] **TEST-02**: Automated runs reach the **authenticated** PWA via the existing `DEV_AUTH_BYPASS` on the host-side dev stack (no manual login, no Authelia/OIDC mocking). Targets the dev build; real prod-service-worker mobile testing is out of scope (see below). The harness specs are also consumed by Phase 8 (Gitea CI) as the PR UI-regression step.
|
|
||||||
|
|
||||||
## Future Requirements (deferred, not in v1.1)
|
- **Additional calendar providers** beyond Fastmail + Google (the PROV-01 abstraction makes them cheaper to add later).
|
||||||
|
- **Google Calendar webhook push** (`events.watch`) for lower-latency sync — the existing 5-min ctag/poll model is sufficient for a two-person household; evaluate only if latency becomes a problem.
|
||||||
- **Multiple reminders per event** (2× VALARM) — stretch; deferred to v1.2.
|
- **Wizard re-run / reconfigure** flow after first setup (carried from v1.1 backlog).
|
||||||
- **Self-service provider onboarding** (backlog 999.5) — a member adds their _own_ Fastmail app password on first login. v1.1 covers this admin-managed (ADMIN-01); self-service deferred.
|
|
||||||
- **Calendar provider abstraction** (backlog 999.1) — provider interface so Fastmail is one of several backends.
|
|
||||||
- **Android PWA install walkthrough** verified on a real device (carried from v1.0).
|
- **Android PWA install walkthrough** verified on a real device (carried from v1.0).
|
||||||
- **Wizard re-run / reconfigure** flow after first setup.
|
- **Tablet / touchscreen UX** — explicitly reserved for **v1.3**.
|
||||||
|
|
||||||
## Out of Scope (explicit exclusions)
|
## Out of Scope (explicit exclusions)
|
||||||
|
|
||||||
- **Notification-preferences UI, reminder snooze** — over-build for a 2-member household; the per-event selector (CAL-13) is sufficient.
|
- **Modern visual styling refresh** (backlog 999.21) — current UX is satisfactory; this is a separate future redesign track, not v1.2.
|
||||||
- **Admin audit log, health dashboard, user-management/CRUD** — scope creep for a tiny self-hosted app.
|
- **Per-event Google color (`colorId`) honoring** — FamilySync uses its own per-calendar colors for consistency; Google's per-event colors are ignored.
|
||||||
- **Real-device iOS push / standalone CI** — remains a human/device gate, as in v1.0; the mobile harness covers responsive layout + authed flows only, not iOS-Safari-standalone behavior.
|
- **CalDAV/JMAP for Google** — Google integration uses the Google Calendar API + OAuth2; no CalDAV bridge.
|
||||||
- **Mobile testing against the prod service-worker build past real Authelia** — `DEV_AUTH_BYPASS` only reaches the dev build (no real SW). A reusable Authelia storage-state to drive the prod-SW PWA is deferred; not worth the complexity for v1.1's layout/flow goal.
|
- **Notification-preferences UI / reminder snooze** — the per-event multi-reminder selector (CAL-18) is sufficient for a 2-member household.
|
||||||
- **Redis pub/sub for the outbox drain** — the drain is single-process by design; an in-process EventEmitter is correct. (Redis stays for list SSE.)
|
- **`googleapis` monolith** — use the scoped `@googleapis/calendar` + `google-auth-library` to avoid the ~50 MB footprint.
|
||||||
- **node-cron** — silently skips ticks in the long-lived process; schedulers stay on `setInterval`.
|
- **`drizzle-kit push` at boot** — emits a false destructive diff on populated MariaDB 11; boot migration uses the `migrate()` API only.
|
||||||
- **drizzle-kit push** — emits a false destructive diff on populated MariaDB; migrations use generate+migrate.
|
- **Bulk `pnpm up`** for DEP-01 — selective, pin-aware updates only.
|
||||||
|
|
||||||
## Traceability
|
## Traceability
|
||||||
|
|
||||||
Maps each REQ-ID to its phase. v1.1 phases continue v1.0 numbering (v1.0 ended at Phase 6) → v1.1 starts at Phase 7. **Coverage: 17/17 v1.1 requirements mapped, no orphans, no duplicates.**
|
Maps each REQ-ID to its phase. v1.2 phases continue prior numbering (v1.1 ended at Phase 20) → v1.2 starts at **Phase 21**.
|
||||||
|
|
||||||
| REQ-ID | Phase | Status |
|
| REQ-ID | Phase | Status |
|
||||||
| -------- | -------------------------------- | -------- |
|
| -------- | -------- | ----------- |
|
||||||
| TEST-01 | Phase 7 (Mobile Test Harness) | Complete |
|
| SETUP-05 | Phase 21 | Not started |
|
||||||
| TEST-02 | Phase 7 (Mobile Test Harness) | Complete |
|
| PROV-01 | Phase 22 | Not started |
|
||||||
| CI-01 | Phase 8 (Gitea CI) | Complete |
|
| CAL-18 | Phase 23 | Not started |
|
||||||
| CI-02 | Phase 8 (Gitea CI) | Complete |
|
| TEST-03 | Phase 24 | Not started |
|
||||||
| CAL-15 | Phase 9 (Faster Write-Back) | Complete |
|
| CAL-16 | Phase 25 | Not started |
|
||||||
| ADMIN-01 | Phase 10 (Admin Role & Settings) | Complete |
|
| CAL-17 | Phase 25 | Not started |
|
||||||
| ADMIN-02 | Phase 10 (Admin Role & Settings) | Complete |
|
| PROV-02 | Phase 25 | Not started |
|
||||||
| ADMIN-03 | Phase 10 (Admin Role & Settings) | Complete |
|
| PROV-03 | Phase 26 | Not started |
|
||||||
| CAL-13 | Phase 11 (Per-Event Reminders) | Complete |
|
| PROV-04 | Phase 26 | Not started |
|
||||||
| CAL-14 | Phase 11 (Per-Event Reminders) | Complete |
|
| THEME-01 | Phase 27 | Not started |
|
||||||
| NOTIF-04 | Phase 11 (Per-Event Reminders) | Complete |
|
| DEP-01 | Phase 28 | Not started |
|
||||||
| NOTIF-05 | Phase 11 (Per-Event Reminders) | Complete |
|
|
||||||
| NOTIF-06 | Phase 11 (Per-Event Reminders) | Complete |
|
|
||||||
| SETUP-01 | Phase 12 (Initial Setup Wizard) | Complete |
|
|
||||||
| SETUP-02 | Phase 12 (Initial Setup Wizard) | Complete |
|
|
||||||
| SETUP-03 | Phase 12 (Initial Setup Wizard) | Complete |
|
|
||||||
| SETUP-04 | Phase 12 (Initial Setup Wizard) | Complete |
|
|
||||||
|
|
||||||
**DB foundation note:** The v1.1 schema migration (`users.is_admin`, `calendar_events.reminder_lead_minutes`, `app_config` table) is not a standalone requirement — it is carried by **Phase 10 (Admin Role & Settings)** (which owns is_admin + app_config) and consumed by **Phase 11 (Per-Event Reminders)** (reminder_lead_minutes) and **Phase 12 (Initial Setup Wizard)** (app_config.setup_complete). Folded per ARCHITECTURE.md ordering rather than created as a migration-only phase. This makes Phase 10 the head of the admin chain (10 → 11, 10 → 12).
|
**Coverage:** 11/11 requirements mapped to exactly one phase — no orphans, no duplicates.
|
||||||
|
|
||||||
|
| Phase | Requirements |
|
||||||
|
| ----- | ------------ |
|
||||||
|
| Phase 21 — Zero-Setup DB Bootstrap | SETUP-05 |
|
||||||
|
| Phase 22 — Provider Abstraction | PROV-01 |
|
||||||
|
| Phase 23 — Multiple Reminders Per Event | CAL-18 |
|
||||||
|
| Phase 24 — Dev/Mock Provider | TEST-03 |
|
||||||
|
| Phase 25 — Google Calendar Provider | CAL-16, CAL-17, PROV-02 |
|
||||||
|
| Phase 26 — Self-Service Provider Onboarding | PROV-03, PROV-04 |
|
||||||
|
| Phase 27 — PWA Dark Mode | THEME-01 |
|
||||||
|
| Phase 28 — Dependency Updates | DEP-01 |
|
||||||
|
|||||||
@@ -46,6 +46,52 @@ _A living document updated after each milestone. Lessons feed forward into futur
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Milestone: v1.1 — Operability & Polish
|
||||||
|
|
||||||
|
**Shipped:** 2026-06-18
|
||||||
|
**Phases:** 14 (7–20) | **Plans:** 57 | **Sessions:** not tracked
|
||||||
|
|
||||||
|
### What Was Built
|
||||||
|
|
||||||
|
- A self-hosted Gitea CI/CD pipeline: PR-gating lint (real ESLint flat config) / typecheck / MariaDB-backed API integration / a mobile + desktop Playwright regression harness, plus dependency-audit / gitleaks / eslint-plugin-security / dev↔prod image-hygiene gates and a Docker publish on merge.
|
||||||
|
- In-app operability: role-gated admin (credential rotation, shared-calendar designation, member editor), a validated first-run setup wizard, per-event reminders with a variable-lead scheduler, auto timezone detection, and ~1–2s event write-back.
|
||||||
|
- A first-class local-auth (no-OIDC) mode coexisting with Authelia OIDC, removing the hard dependency on a deployed Authelia.
|
||||||
|
|
||||||
|
### What Worked
|
||||||
|
|
||||||
|
- **Backlog → phase promotion pipeline:** most of v1.1 (999.4/10/11/12/13/14/15/16) was captured as backlog during v1.0, then promoted cleanly into scoped phases — the deferred-idea capture paid off directly.
|
||||||
|
- **Runner-probe-first for self-hosted CI (PITFALL 12):** probing `node`/`pnpm`/Docker/registry access on the Gitea runner *before* authoring any test/build steps surfaced every fork answer (Docker-executor, `ubuntu-latest`, artifact-fork, `REGISTRY_PAT` naming) up front and avoided blind CI iteration.
|
||||||
|
- **Zero-dependency in-process solutions:** the EventEmitter outbox-drain signal (CAL-15) hit the latency goal with no new infra; the project later removed Redis entirely as unused.
|
||||||
|
- **TDD discipline on the admin/auth chain** (Phases 10/11/12/19) kept the role boundary and credential-handling correct, with route-level 403/423/409 guards asserted in tests.
|
||||||
|
|
||||||
|
### What Was Inefficient
|
||||||
|
|
||||||
|
- **Dev user can't exercise calendar features end-to-end:** `DEV_AUTH_BYPASS` user 1 has no `member_credentials`/calendars, so per-event reminders (Phase 11) could only be verified via tests + a route-mocked smoke, not hands-on by the operator (→ backlog 999.19). Recurring dev-testability friction.
|
||||||
|
- **Gitea-specific quirks cost cycles:** secrets with the `GITEA_` prefix are silently dropped (→ `REGISTRY_PAT`); `actions/upload-artifact@v4` is broken on Gitea (needs the `ChristopherHX` fork); `actions/cache@v4` timed out; skipped jobs may not emit a commit-status (drove the always-running `gate` aggregate). None are documented as GitHub-incompatible up front.
|
||||||
|
- **Scope grew mid-milestone:** the milestone planned as 7–17 but accreted 18/19/20 via `/gsd-phase`, and the ROADMAP header wasn't kept in sync — the phase-detail sections for 18–20 ended up appended after the Backlog. Keep the roadmap header + section ordering current when inserting late phases.
|
||||||
|
|
||||||
|
### Patterns Established
|
||||||
|
|
||||||
|
- **Runner-probe-first** for any new self-hosted-CI capability — never author steps against an unprobed runner.
|
||||||
|
- **Always-running `gate` aggregate** (`if: always()`, passes on success-or-skipped) is the only safe required-check surface when path-filtering jobs — never mark a path-filtered job itself required (deadlock).
|
||||||
|
- **In-process EventEmitter over Redis** for single-process work (the outbox drain); reserve external infra for genuinely cross-process needs.
|
||||||
|
- **Local auth is a first-class mode**, not a fallback — identity stays OIDC-`iss+sub` (never email); a local user is *linked* to an OIDC identity via an explicit claim flow (D-10/D-19).
|
||||||
|
- **Confine dev-only affordances at build + boot:** bake `NODE_ENV=production` into the prod image and refuse-to-boot if `DEV_AUTH_BYPASS` is set — defense-in-depth beyond the runtime guard.
|
||||||
|
|
||||||
|
### Key Lessons
|
||||||
|
|
||||||
|
1. Capturing deferred ideas as structured backlog entries during one milestone makes the next milestone's roadmap nearly write-itself — invest in the capture.
|
||||||
|
2. Self-hosted GitHub-Actions-compatible runners are *not* drop-in GitHub — probe the runtime, the action ecosystem (forks), and the status/secret semantics before designing the pipeline.
|
||||||
|
3. Dev-environment testability is a feature: if the dev user can't exercise the real flows, every feature regresses to test-only verification and the operator can't UAT — fix the dev seed/provider story early (999.19).
|
||||||
|
|
||||||
|
### Cost Observations
|
||||||
|
|
||||||
|
- Model mix: not tracked
|
||||||
|
- Sessions: not tracked
|
||||||
|
- Notable: 14 phases shipped in ~8 days (2026-06-10 → 2026-06-18) with heavy parallelization across independent tracks (CI chain vs admin chain vs polish) once the harness landed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Cross-Milestone Trends
|
## Cross-Milestone Trends
|
||||||
|
|
||||||
### Process Evolution
|
### Process Evolution
|
||||||
@@ -53,13 +99,17 @@ _A living document updated after each milestone. Lessons feed forward into futur
|
|||||||
| Milestone | Sessions | Phases | Key Change |
|
| Milestone | Sessions | Phases | Key Change |
|
||||||
| --------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- |
|
| --------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| v1.0 | n/a | 6 | Established GSD plan→execute→verify→ship→complete loop; dev-auth bypass for gated infra; milestone-branch + Gitea PR shipping |
|
| v1.0 | n/a | 6 | Established GSD plan→execute→verify→ship→complete loop; dev-auth bypass for gated infra; milestone-branch + Gitea PR shipping |
|
||||||
|
| v1.1 | n/a | 14 | Self-hosted Gitea CI/CD as the merge gate; per-phase branch + PR shipping; backlog→phase promotion pipeline; parallel independent tracks |
|
||||||
|
|
||||||
### Cumulative Quality
|
### Cumulative Quality
|
||||||
|
|
||||||
| Milestone | Tests | Coverage | Zero-Dep Additions |
|
| Milestone | Tests | Coverage | Zero-Dep Additions |
|
||||||
| --------- | ------------------------------------- | ------------ | ------------------ |
|
| --------- | ------------------------------------- | ------------ | ------------------------------------------- |
|
||||||
| v1.0 | PWA 191 + API broker/events 114 green | not measured | n/a |
|
| v1.0 | PWA 191 + API broker/events 114 green | not measured | n/a |
|
||||||
|
| v1.1 | PWA ~249 + API ~347 green | not measured | `outboxTrigger.ts` EventEmitter (CAL-15); Redis later removed entirely as unused |
|
||||||
|
|
||||||
### Top Lessons (Verified Across Milestones)
|
### Top Lessons (Verified Across Milestones)
|
||||||
|
|
||||||
1. (pending second milestone to cross-validate)
|
1. **Capture deferred ideas as structured backlog during the milestone** — v1.1's roadmap came almost entirely from v1.0-era backlog entries.
|
||||||
|
2. **iOS-Safari standalone / on-device push stays a human gate** across both milestones — automated harnesses (desktop + mobile-emulated) cover layout/flows, never the device-only behavior.
|
||||||
|
3. **`setInterval` + in-process signals over external schedulers/brokers** for this single-process app — node-cron silently no-ops (v1.0), Redis went unused (v1.1).
|
||||||
|
|||||||
+155
-711
@@ -3,7 +3,8 @@
|
|||||||
## Milestones
|
## Milestones
|
||||||
|
|
||||||
- ✅ **v1.0 MVP** — Phases 1–6 (shipped 2026-06-10) — see [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md)
|
- ✅ **v1.0 MVP** — Phases 1–6 (shipped 2026-06-10) — see [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md)
|
||||||
- 🚧 **v1.1 Operability & Polish** — Phases 7–17 (planning) — mobile test harness, Gitea CI (runs the harness), faster write-back, in-app admin, per-event reminders, guided setup, real lint gate, desktop e2e, doc-only CI skip + markdown lint, CI dependency audit + security checks + image hygiene, UI optimization & polish
|
- ✅ **v1.1 Operability & Polish** — Phases 7–20 (shipped 2026-06-18) — see [`milestones/v1.1-ROADMAP.md`](milestones/v1.1-ROADMAP.md)
|
||||||
|
- 🟡 **v1.2 Multi-Provider, Theming & Zero-Setup** — Phases 21–28 (in progress)
|
||||||
|
|
||||||
## Phases
|
## Phases
|
||||||
|
|
||||||
@@ -21,673 +22,187 @@ Full phase detail archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROA
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### 🚧 v1.1 Operability & Polish (Phases 7–17)
|
<details>
|
||||||
|
<summary>✅ v1.1 Operability & Polish (Phases 7–20) — SHIPPED 2026-06-18</summary>
|
||||||
|
|
||||||
Make FamilySync configurable, administrable, and maintainable for real multi-member use — without hand-editing env files or the database. The new critical path runs **mobile test harness → Gitea CI** (CI consumes the harness specs for UI regression), and the **admin role → reminders / setup wizard** chain (a single `/api/admin` + `/api/setup` route surface carrying the v1.1 DB migration). Faster write-back is a fully independent track.
|
- [x] Phase 7: Mobile Test Harness (4/4 plans) — completed 2026-06-11
|
||||||
|
- [x] Phase 8: Gitea CI (4/4 plans) — completed 2026-06-11
|
||||||
|
- [x] Phase 9: Faster Write-Back (2/2 plans) — completed 2026-06-12
|
||||||
|
- [x] Phase 10: Admin Role & Settings (4/4 plans) — completed 2026-06-13
|
||||||
|
- [x] Phase 11: Per-Event Reminders (5/5 plans) — completed 2026-06-14
|
||||||
|
- [x] Phase 12: Initial Setup Wizard (7/7 plans) — completed 2026-06-16
|
||||||
|
- [x] Phase 13: Real Lint Gate (ESLint) (3/3 plans) — completed 2026-06-12
|
||||||
|
- [x] Phase 14: Desktop E2E Coverage (1/1 plans) — completed 2026-06-12
|
||||||
|
- [x] Phase 15: Doc-Only CI Skip + Markdown Lint (3/3 plans) — completed 2026-06-12
|
||||||
|
- [x] Phase 16: CI Dependency Audit, Security & Image Hygiene (6/6 plans) — completed 2026-06-13
|
||||||
|
- [x] Phase 17: UI Optimization & Polish (6/6 plans) — completed 2026-06-18
|
||||||
|
- [x] Phase 18: Auto Timezone Detection (4/4 plans) — completed 2026-06-14
|
||||||
|
- [x] Phase 19: Local Auth (No-OIDC Mode) (5/5 plans) — completed 2026-06-17
|
||||||
|
- [x] Phase 20: Admin Member Editor & Form Declutter (3/3 plans) — completed 2026-06-18
|
||||||
|
|
||||||
- [x] **Phase 7: Mobile Test Harness** - Mobile-emulated, authenticated PWA browser harness so the assistant (and CI) can catch mobile-only defects (completed 2026-06-11)
|
Full phase detail archived in [`milestones/v1.1-ROADMAP.md`](milestones/v1.1-ROADMAP.md).
|
||||||
- [x] **Phase 8: Gitea CI** - Full regression on PR to main (lint/typecheck/unit/API-integration vs a MariaDB service container **+ the Phase 7 mobile harness as a UI-regression step against a CI-hosted dev stack**) + Docker image publish on merge (completed 2026-06-11)
|
|
||||||
- [x] **Phase 9: Faster Write-Back** - Event-driven outbox drain so edits land in ~1-2s instead of ~15s, preserving every outbox durability guarantee (completed 2026-06-12)
|
</details>
|
||||||
- [x] **Phase 10: Admin Role & Settings** - DB foundation (is_admin / reminder_lead / app_config) + role-gated admin UI to rotate app passwords and designate the shared calendar (completed 2026-06-13)
|
|
||||||
- [x] **Phase 11: Per-Event Reminders** - Reminder selector on the event form (incl. "None") serialized as VALARM, with a variable-lead scheduler that honors each event's choice (completed 2026-06-14)
|
### 🟡 v1.2 Multi-Provider, Theming & Zero-Setup (Phases 21–28)
|
||||||
- [x] **Phase 12: Initial Setup Wizard** - First-run validated bootstrap of env/VAPID/DB/OIDC + first app password, reusing the admin route surface (completed 2026-06-16)
|
|
||||||
- [x] **Phase 13: Real Lint Gate (ESLint)** - Wire ESLint flat config (typescript-eslint + React) across both apps so the Phase 8 CI lint slot actually fails on violations instead of no-op'ing (completed 2026-06-12)
|
- [ ] **Phase 21: Zero-Setup DB Bootstrap** - App auto-creates/migrates its schema on boot against a bring-your-own MariaDB — no manual `db:migrate`
|
||||||
- [x] **Phase 14: Desktop E2E Coverage** - Add a Desktop Chrome Playwright profile + make the mobile-authored specs desktop-safe so the Phase 8 regression gate validates desktop, not just mobile (completed 2026-06-12)
|
- [ ] **Phase 22: Provider Abstraction** - Refactor Fastmail/CalDAV behind a `CalendarProvider` interface with no Fastmail regression; widen credential uniqueness to `(user_id, provider_type)`
|
||||||
- [x] **Phase 15: Doc-Only CI Skip + Markdown Lint** - Aggregate-gate the slow api/harness CI jobs so doc-only PRs to main merge without running them (no branch-protection deadlock), and add markdownlint to `fast-checks` so docs get a fast format+lint gate (promoted from backlog 999.17) (completed 2026-06-12)
|
- [ ] **Phase 23: Multiple Reminders Per Event** - Up to 5 independent reminders per event, serialized to N VALARMs, other-client alarms preserved
|
||||||
- [x] **Phase 16: CI Dependency Audit, Security Checks & Image Hygiene** - Extend Gitea CI with outdated-dependency reporting + vulnerability audit + a baseline of additional security checks, and enforce the dev/prod image boundary so no dev-bypass, secret, or family data ships in published images (absorbs backlog 999.17); independent of the admin chain (completed 2026-06-13)
|
- [ ] **Phase 24: Dev/Mock Provider** - In-memory provider lets the dev user + Playwright exercise the full app with no live calendar account
|
||||||
- [x] **Phase 17: UI Optimization & Polish** - Phone-layout polish + branding + theme groundwork: fix the long-standing phone-layout overlap where the fixed BottomTabBar covers the New Event FAB and the colour legend (+ small-viewport sweep), finish the branding assets (real FamilySync logo into the BrandSlot seam + a complete favicon/PWA-icon set replacing the placeholder stubs), and restructure tokens.css into a themeable token layer (light-only groundwork for future dark mode). Shipped dark theme → backlog 999.20; broader styling refresh → backlog 999.21 (future milestone) (completed 2026-06-18)
|
- [ ] **Phase 25: Google Calendar Provider** - Connect a Google account via OAuth; Google events read + write + reminders at Fastmail parity
|
||||||
|
- [ ] **Phase 26: Self-Service Provider Onboarding** - Members connect/reconnect their own provider (Fastmail app-password / Google OAuth) without admin help, with a one-tap reconnect banner
|
||||||
|
- [ ] **Phase 27: PWA Dark Mode** - Light/Dark/System theme switch, persisted, no flash-of-wrong-theme, clean across every route incl. Schedule-X
|
||||||
|
- [ ] **Phase 28: Dependency Updates** - Apply CI-surfaced outdated/vulnerable upgrades, pin-aware and per-ecosystem, with all CI gates green
|
||||||
|
|
||||||
## Phase Details
|
## Phase Details
|
||||||
|
|
||||||
> v1.0 phase detail (Phases 1–6) is archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md).
|
### Phase 21: Zero-Setup DB Bootstrap
|
||||||
|
**Goal**: A bring-your-own MariaDB (creds + DB name only) becomes a working FamilySync schema with no manual migration step — the app migrates itself on boot, idempotently and concurrency-safely, before it serves any request.
|
||||||
### Phase 7: Mobile Test Harness
|
**Depends on**: Nothing (touches only `index.ts` startup + `db/migrations`)
|
||||||
|
**Requirements**: SETUP-05
|
||||||
**Goal**: The assistant can drive the PWA in a mobile-emulated, authenticated browser context against the host-side dev stack, so mobile-only layout and flow defects can be caught automatically instead of only by the operator on real devices. This harness is also the artifact Phase 8 (CI) runs for UI regression.
|
|
||||||
**Mode:** standard
|
|
||||||
**Depends on**: Nothing (fully independent; goes first. One new dev dependency `@playwright/test` in `apps/pwa`; no backend changes).
|
|
||||||
**Requirements**: TEST-01, TEST-02
|
|
||||||
**Success Criteria** (what must be TRUE):
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. Pointing the app at an empty MariaDB (DB credentials + database name supplied) and starting it produces the full, current schema with no manual `db:migrate` / `docker exec` step.
|
||||||
|
2. Restarting an already-migrated app is a no-op — no duplicate-migration error, no destructive diff, and existing data is untouched.
|
||||||
|
3. Two app instances starting against the same fresh database concurrently both come up cleanly — the migration runs exactly once (migration-lock sentinel), the loser waits rather than racing.
|
||||||
|
4. If the database is unreachable or migration fails, the app refuses to start (fatal) rather than serving on a partial schema.
|
||||||
|
5. The first request the setup wizard receives already has a complete schema — no setup-wizard call ever hits a missing table.
|
||||||
|
**Plans**: TBD
|
||||||
|
|
||||||
1. An automated run can load the PWA in a mobile-emulated viewport (device profile + mobile UA + touch) and assert on responsive layout / tap targets.
|
### Phase 22: Provider Abstraction (CalDAV only, Fastmail unchanged)
|
||||||
2. The automated run reaches the authenticated PWA via the existing `DEV_AUTH_BYPASS` on the host-side dev stack — no manual login and no Authelia/OIDC mocking.
|
**Goal**: All calendar read, write-back, and reminder operations flow through one `CalendarProvider` seam, with the existing Fastmail/CalDAV broker wrapped behind it verbatim — so future providers plug in without touching the poller, outbox worker, or scheduler, and the live Fastmail path behaves identically.
|
||||||
3. The harness runs repeatably day-over-day without re-capturing any session state (no stale storage-state failures).
|
**Depends on**: Phase 21 (schema auto-migrates before the integration suite runs)
|
||||||
4. The harness specs are structured so they can run headlessly in CI (Phase 8) against a dev stack the runner brings up — no dependence on a developer's already-running host stack.
|
**Requirements**: PROV-01
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. The existing Fastmail calendar continues to read, write (create/edit/delete, recurring + whole-series), and remind with no observable change — every v1.1 outbox/poller/scheduler integration test passes unchanged against the new interface.
|
||||||
|
2. Optimistic-202, create-before-delete on moves, per-uid exactly-once, the `isDraining` drain guard, and RRULE/VALARM preservation on edit all still hold (no per-provider drain locks introduced).
|
||||||
|
3. The poller and outbox worker select the implementation per member credential via a `createProvider(cred)` factory keyed on `provider_type`, with `caldav` as the default branch.
|
||||||
|
4. The `member_credentials` uniqueness constraint is widened to `UNIQUE(user_id, provider_type)` (migrated, no data loss), unblocking one credential per provider per user, and the token-storage shape (separate `provider_tokens` table vs. extended `member_credentials`) is decided and recorded.
|
||||||
|
**Plans**: TBD
|
||||||
|
|
||||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
### Phase 23: Multiple Reminders Per Event
|
||||||
|
**Goal**: A user can set several independent reminders on one event (up to 5), each firing once at its own lead, serialized to the connected provider's native model, without clobbering reminders authored in another client.
|
||||||
|
**Depends on**: Phase 22 (the CalDAV provider's write path accepts a reminder-leads array)
|
||||||
|
**Requirements**: CAL-18
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. In the event form a user can add and remove multiple reminder rows (up to 5); on edit, all existing reminders pre-populate.
|
||||||
|
2. Each reminder fires independently and exactly once — the scheduler dedup key includes the lead (`uid:dtstartMs:lead`), so two reminders on the same event don't collapse or double-fire.
|
||||||
|
3. Saving an event writes one VALARM per chosen lead to Fastmail (multiple sub-components in the VCALENDAR); reminders authored in another client are preserved on edit (preserve-vs-replace driven by a remindersChanged signal).
|
||||||
|
4. Existing single-reminder events keep their reminder after the schema migration (the old `reminder_lead_minutes` value is migrated into the new `reminder_leads` JSON array before the old column is dropped).
|
||||||
|
**Plans**: TBD
|
||||||
|
|
||||||
- **No stale storage-state** (Pitfall 14): use `DEV_AUTH_BYPASS=true` for the automated harness rather than a checked-in storage-state.json with an expiring session cookie; decide the auth strategy before the first test.
|
### Phase 24: Dev/Mock Provider (full-app exercise without a live calendar)
|
||||||
- **Service worker block** (Pitfall 15): set `serviceWorkers: 'block'` (or explicitly unregister) in the context so a previous run's SW does not intercept requests / return stale cached responses; verify no SW-sourced responses in the trace.
|
**Goal**: The dev-bypass user and the Playwright CI harness can exercise the entire app — calendar create/edit/delete, reminders, and lists — with no real calendar account connected, against deterministic seed data.
|
||||||
- Hard constraints: targets the dev build via `DEV_AUTH_BYPASS` (DEV_AUTH_BYPASS user 1 has no CalDAV credential/calendars — verify layout/flows, not live event-create); real prod-service-worker / iOS-Safari-standalone mobile testing stays a human/device gate (out of scope).
|
**Depends on**: Phase 22 (the `CalendarProvider` interface + factory); benefits from Phase 23 (reminder model) for reminder coverage
|
||||||
|
**Requirements**: TEST-03
|
||||||
**Plans**: 4 plans (3 waves)Plans:
|
**Success Criteria** (what must be TRUE):
|
||||||
**Wave 1**
|
1. Under `DEV_AUTH_BYPASS` (dev user, `provider_type='mock'` / no credential row), the calendar populates with seeded events — recurring, all-day, past, and future — with no live provider and no network I/O.
|
||||||
|
2. The dev user can create, edit, and delete events and set reminders end-to-end through the UI; the mock provider absorbs the outbox writes and reflects them back.
|
||||||
- [x] 07-01-PLAN.md — Harness foundation: @playwright/test + WebKit/Chromium browsers, playwright.config.ts (iPhone/WebKit + Pixel/Chromium matrix, serviceWorkers block, env baseURL, vite webServer), vitest exclude, scripts (Wave 1)
|
3. The Playwright harness exercises calendar CRUD hermetically (no live Fastmail/Google account), so CI can validate the full flow.
|
||||||
|
4. The mock path is strictly dev-only (same hard production guard as the existing dev bypass) and never activates in a production image.
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
**Plans**: TBD
|
||||||
|
|
||||||
- [x] 07-02-PLAN.md — global-setup.ts: /health readiness poll + deterministic mysql2 reset-and-seed (calendar id 10 INSERT IGNORE guard, list + items) + e2e README/guardrails (Wave 2)
|
|
||||||
|
|
||||||
**Wave 3** *(blocked on Wave 2 completion)*
|
|
||||||
|
|
||||||
- [x] 07-03-PLAN.md — layout.spec.ts: tap targets >=44px, no overflow, in-viewport, accessible names (UI-SPEC Rules 1-4) + harness self-validation injected-defect proofs (Wave 3)
|
|
||||||
- [x] 07-04-PLAN.md — calendar.spec.ts + lists.spec.ts: populated/empty/error states (Rules 4/5) + DEV_AUTH_BYPASS auth-reached + no-SW-controller precondition (Wave 3)
|
|
||||||
|
|
||||||
|
### Phase 25: Google Calendar Provider
|
||||||
|
**Goal**: A member can connect a Google account and have their Google calendar reach full Fastmail parity — events appear in the unified color-coded view (recurrence + all-day correct), and create/edit/delete (including recurring + whole-series) and reminders write back to Google.
|
||||||
|
**Depends on**: Phase 22 (provider interface), Phase 23 (reminder serialization contract), Phase 21 (token table exists after migrate-on-boot)
|
||||||
|
**Requirements**: CAL-16, CAL-17, PROV-02
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. After connecting a Google account, that calendar's events appear in the unified color-coded calendar — recurring events expand correctly (Google recurrence/cancelled-instance model reconciled to the app's RRULE/EXDATE model) and all-day events are not shifted.
|
||||||
|
2. A user can create, edit, and delete events on the connected Google calendar from the app — including recurring-event create and whole-series delete — written back to Google.
|
||||||
|
3. Reminders set in the app serialize to Google `overrides` (and parse back), capped at 5, preserving other-client alarms.
|
||||||
|
4. The OAuth refresh token is stored AES-256-GCM encrypted (never in `app_config`), via the `UNIQUE(user_id, provider_type)` model; access tokens refresh inline and a Google `syncToken` 410 triggers a safe full re-sync without overwriting un-drained local writes.
|
||||||
|
5. The Google OAuth callback is mounted pre-auth (distinct path + distinct state cookie, redirect URI from `EXTERNAL_BASE_URL`) and round-trips correctly through the Pangolin hostname.
|
||||||
|
**Plans**: TBD
|
||||||
**UI hint**: yes
|
**UI hint**: yes
|
||||||
|
|
||||||
### Phase 8: Gitea CI
|
### Phase 26: Self-Service Provider Onboarding
|
||||||
|
**Goal**: A member can connect, re-enter, or reconnect their own provider credential using each provider's native mechanism — Fastmail app-password entry, Google "Connect Google" OAuth — without an admin acting for them, and a clear in-calendar banner makes a broken connection a one-tap fix.
|
||||||
**Goal**: Every PR to `main` runs a full regression that gates the merge — lint, typecheck, unit, API-integration against a MariaDB service container, **and the Phase 7 mobile Playwright harness as a UI-regression step against a CI-hosted dev stack** — and a merge to `main` builds and publishes the API Docker image, all on the existing self-hosted Gitea Actions runner.
|
**Depends on**: Phase 25 (the Google OAuth authorize/callback must exist before the UI redirects to it)
|
||||||
**Mode:** standard
|
**Requirements**: PROV-03, PROV-04
|
||||||
**Depends on**: Phase 7 (the PR regression runs the Phase 7 mobile harness specs as its UI-regression step; without the harness there is nothing to run). No other code dependencies. Start with a runner-probe step.
|
|
||||||
**Requirements**: CI-01, CI-02
|
|
||||||
**Success Criteria** (what must be TRUE):
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. A member sees which providers they have connected and can self-service connect their own credential — Fastmail via an app-password form (live-validated), Google via a single "Connect Google Calendar" button — with no admin involvement.
|
||||||
1. Opening or updating a PR targeting `main` triggers a workflow that runs lint, typecheck (both apps), unit tests, and API integration tests against a MariaDB service container — and a failing run blocks the merge.
|
2. When a provider connection expires or is revoked, the user sees a clear banner in the calendar view (not buried in settings), not a silent failure.
|
||||||
2. The API integration tests connect to the service-container MariaDB (DB_HOST=127.0.0.1, service creds) and pass reliably on a cold first run, not only on re-run.
|
3. From that banner the user can reconnect in one tap by re-running the provider's auth flow, without removing and re-adding the account.
|
||||||
3. The same PR workflow brings up the dev stack inside the runner — the API dev server, the PWA dev server, and the MariaDB service container, with `DEV_AUTH_BYPASS=true` — and runs the Phase 7 mobile Playwright harness specs headlessly against that authed PWA; a harness failure blocks the merge.
|
4. A member can disconnect a provider, which removes its credential/tokens and its synced calendars + events.
|
||||||
4. The harness step waits for both the API and PWA dev servers to be ready (readiness probe / poll) before launching Playwright, so it does not flake on startup races.
|
**Plans**: TBD
|
||||||
5. On merge to `main`, the API Docker image is built and pushed to the Gitea container registry under a sensible tag.
|
|
||||||
6. Registry credentials never appear in plaintext in the CI logs.
|
|
||||||
|
|
||||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
|
||||||
|
|
||||||
- **Runner-probe first** (Pitfall 12): the first workflow only probes `node --version` / `pnpm --version` / Docker access on the `self-hosted` runner before any test or build steps are designed; pin Node 22 explicitly, do not assume `actions/setup-node` works as on GitHub.
|
|
||||||
- **MariaDB readiness wait** (Pitfall 11): add an explicit readiness loop (e.g. `healthcheck.sh --connect --innodb_initialized`, NOT `mysqladmin ping` which is removed in MariaDB 11) before any `drizzle-kit migrate` / integration test step; healthy ≠ accepting connections.
|
|
||||||
- **Dev-stack readiness races (NEW for the harness step):** running the PWA and API dev servers *inside* CI adds startup/readiness races on top of the MariaDB-11 readiness race. The harness step must wait for **both** the API and PWA dev servers to be accepting connections (poll their URLs / health endpoints) before Playwright launches — do not race the browser against a not-yet-listening server. Run with `DEV_AUTH_BYPASS=true` so the harness reaches the authed PWA exactly as in Phase 7.
|
|
||||||
- **--password-stdin** (Pitfall 13): `docker login` via `--password-stdin` with the token piped from a registered Gitea secret (PAT with `write:package`); never `-p $TOKEN` on the command line.
|
|
||||||
- Hard constraints: API integration tests need a real MariaDB and live in `apps/api/tests/` (never `src/`); cache the pnpm store; Drizzle generate+migrate to set up the CI DB schema; the harness step reuses the Phase 7 specs unchanged (CI owns only the stack bring-up + readiness wait, not the spec content).
|
|
||||||
|
|
||||||
**Plans**: 4 plans (4 waves)Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 08-01-PLAN.md — Runner probe + operator runner/PAT registration (W0; answers the Docker-vs-host fork)
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
|
||||||
|
|
||||||
- [x] 08-02-PLAN.md — ci.yml: fast-checks (lint/typecheck/PWA unit) + API job (MariaDB service + migrate + DB-backed tests)
|
|
||||||
|
|
||||||
**Wave 3** *(blocked on Wave 2 completion)*
|
|
||||||
|
|
||||||
- [x] 08-03-PLAN.md — ci.yml: harness job (dev-stack bring-up + readiness waits + Phase 7 Playwright specs, both profiles)
|
|
||||||
|
|
||||||
**Wave 4** *(blocked on Wave 3 completion)*
|
|
||||||
|
|
||||||
- [x] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1-<sha> via --password-stdin)
|
|
||||||
|
|
||||||
**UI hint**: yes
|
**UI hint**: yes
|
||||||
|
|
||||||
### Phase 9: Faster Write-Back
|
### Phase 27: PWA Dark Mode
|
||||||
|
**Goal**: A user can switch the PWA between Light / Dark / System, the choice persists, and the dark theme renders cleanly everywhere with no flash-of-wrong-theme on load.
|
||||||
**Goal**: A created, edited, or deleted event reaches Fastmail within ~1-2 seconds (event-driven outbox drain) instead of waiting up to ~15s for the next interval tick — with every existing durability guarantee intact.
|
**Depends on**: Nothing (pure frontend; parallelizable with Phases 22–26)
|
||||||
**Mode:** standard
|
**Requirements**: THEME-01
|
||||||
**Depends on**: Nothing (fully independent; the only new artifact is a zero-dependency in-process EventEmitter, `lib/outboxTrigger.ts`). Can run in parallel with any other v1.1 track.
|
|
||||||
**Requirements**: CAL-15
|
|
||||||
**Success Criteria** (what must be TRUE):
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. A theme toggle in Settings lets the user pick Light / Dark / System; the choice is persisted (per-device localStorage) and survives reload.
|
||||||
1. After creating/editing/deleting an event, the change lands in Fastmail in ~1-2s in the common case (drain is signalled on enqueue, not waited-for on the interval) — observable as the change appearing in the Fastmail native app well before the old ~15s window.
|
2. On load there is no flash-of-wrong-theme — the resolved theme is applied before first paint (inline pre-paint script reading the stored preference).
|
||||||
2. The route handler still returns an optimistic 202 immediately and never makes a CalDAV call inline — the event-driven signal is fire-and-forget.
|
3. The dark theme renders cleanly across every route — calendar, lists, admin, settings sheet, and login — including the Schedule-X calendar component's `--sx-color-*` variables and the per-member color lanes.
|
||||||
3. Edit-as-move still writes the new event before deleting the old one (create-before-delete ordering preserved); no event is ever lost when a move drains under rapid enqueues.
|
4. "System" tracks the OS `prefers-color-scheme` and updates live when the OS theme changes; the iOS `theme-color` meta reflects the active theme.
|
||||||
4. No duplicate CalDAV PUTs occur for the same outbox row when the signal and the 15s fallback interval overlap (exactly-once per uid preserved).
|
**Plans**: TBD
|
||||||
5. The 15s `setInterval` fallback still runs and recovers any rows missed by the signal path (startup catch-up, transient errors).
|
|
||||||
|
|
||||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
|
||||||
|
|
||||||
- **No double-drain** (Pitfall 5): the trigger must set a `drainRequested` flag funnelled through the single setInterval-controlled path / the existing `isDraining` guard — never call `runOutboxDrain()` directly from the signal in a way that bypasses the guard or escapes the error-caught wrapper.
|
|
||||||
- **Create-before-delete under concurrent enqueues** (Pitfall 6): enqueue CREATE before DELETE; do not fire the signal between the two inserts of a move (publish after both inserts / after the transaction commits).
|
|
||||||
- Hard constraints: `setInterval` only (no node-cron); single-process by design — **no Redis** for the drain (Redis stays for list SSE); all outbox guarantees (fresh-etag-before-PUT, 412 conflict flow, per-uid exactly-once) unchanged.
|
|
||||||
|
|
||||||
**Plans**: 2 plans (2 waves)
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 09-01-PLAN.md — TDD: outboxTrigger.ts (zero-dep EventEmitter signal) + scheduleOutboxDrain wrapper / drainRequested trailing-re-drain loop + initOutboxTrigger in outboxWorker.ts; trigger-wiring tests (SC-1, SC-4/D-07, D-05) (Wave 1)
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
|
||||||
|
|
||||||
- [x] 09-02-PLAN.md — Four post-commit signalOutboxDrain() publish sites in events.ts (create / edit-as-move-after-transaction / same-cal update / delete) + initOutboxTrigger() startup wiring under isMainModule() in index.ts (Wave 2)
|
|
||||||
|
|
||||||
### Phase 10: Admin Role & Settings
|
|
||||||
|
|
||||||
**Goal**: An admin can manage household configuration that previously required manual DB writes — rotating a member's Fastmail app password and designating the shared family calendar — from a role-gated in-app Settings section, on top of the v1.1 DB foundation this phase introduces.
|
|
||||||
**Mode:** standard
|
|
||||||
**Depends on**: Nothing required upstream; this phase **carries the v1.1 DB migration** (users.is_admin, calendar_events.reminder_lead_minutes, app_config table) that Phases 11 and 12 build on. It is the head of the admin chain (10 → 11, 10 → 12).
|
|
||||||
**Requirements**: ADMIN-01, ADMIN-02, ADMIN-03
|
|
||||||
**Success Criteria** (what must be TRUE):
|
|
||||||
|
|
||||||
1. An admin sees an Admin section in Settings and can list household members with their credential status; a non-admin member never sees it and cannot invoke any `/api/admin/*` route (gets 403).
|
|
||||||
2. An admin can enter or rotate a member's Fastmail app password; it is validated against CalDAV (PROPFIND) before saving and stored encrypted — and the password is never displayed, echoed in a response, or logged.
|
|
||||||
3. An admin can pick which synced calendar is the shared family calendar from a list, and the `calendars.is_shared` flag updates accordingly (replacing the manual `UPDATE calendars SET is_shared=1` step).
|
|
||||||
4. The role check is role-agnostic and member-count-agnostic: it gates on `users.is_admin`, so more admins can be added later without reworking the guard.
|
|
||||||
5. The DB migration (is_admin, reminder_lead_minutes, app_config) is applied via generate+migrate and is in place for downstream phases (reminder_lead_minutes for Phase 11, app_config.setup_complete for Phase 12).
|
|
||||||
|
|
||||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
|
||||||
|
|
||||||
- **Admin role check inside the sub-router** (Pitfall 9): apply `requireAdmin` with `.use('*', ...)` inside `adminRouter`, not only at the parent mount; integration test must assert 403 for a non-admin authenticated user.
|
|
||||||
- **App password never logged/echoed** (Pitfall 7): custom zod-validator `hook` returns a generic 400 (no Zod `received`/`value` field); no `console.log` of request bodies in `routes/admin*`.
|
|
||||||
- Hard constraints: Drizzle **generate+migrate, never push** (false destructive diff on populated MariaDB); reuse `broker/crypto.ts` `encryptPassword` (no changes to crypto); `/api/admin/credentials` and `/api/admin/calendars/:id/shared` are the single shared surface — do NOT duplicate them into `/api/setup/*` in Phase 12.
|
|
||||||
|
|
||||||
**Folded-in scope** (from backlog 999.5, self-service member onboarding): the credential surface this phase builds is the same one a member needs on first login. Expose a `needsProviderSetup` signal (member has no `member_credentials` row) and let a member enter/validate (CalDAV PROPFIND) + encrypt their **own** Fastmail app password — the self-service counterpart of the admin-managed flow, sharing the validation/encryption/initial-sync path. Non-technical-friendly instructions (link to Fastmail's app-password page, required Calendars/CalDAV scope) are the hard UX constraint. Member-scoped: a member can only set their own credential; never log/echo the password.
|
|
||||||
|
|
||||||
**Plans**: 4 plans (4 waves)Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 10-01-PLAN.md — v1.1 DB foundation migration (is_admin, provider_type+unique, reminder_lead_minutes, app_config) + dev-bypass admin seed
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
|
||||||
|
|
||||||
- [x] 10-02-PLAN.md — requireAdmin guard + first-login-wins bootstrap + /api/me isAdmin/needsProviderSetup (TDD)
|
|
||||||
|
|
||||||
**Wave 3** *(blocked on Wave 2 completion)*
|
|
||||||
|
|
||||||
- [x] 10-03-PLAN.md — adminRouter (members/credentials/calendars/shared) + member self-service credential, validate→encrypt→sync (TDD)
|
|
||||||
|
|
||||||
**Wave 4** *(blocked on Wave 3 completion)*
|
|
||||||
|
|
||||||
- [x] 10-04-PLAN.md — PWA /admin route + nav gating + CredentialSheet + SetupBanner (playwright-cli verified)
|
|
||||||
|
|
||||||
**UI hint**: yes
|
**UI hint**: yes
|
||||||
|
|
||||||
### Phase 11: Per-Event Reminders
|
### Phase 28: Dependency Updates
|
||||||
|
**Goal**: The outdated/vulnerable dependency upgrades that CI surfaces are applied — selectively and pin-aware — leaving the app on current, non-CVE-bearing dependencies with every CI gate green.
|
||||||
**Goal**: A user can choose a reminder lead time per event (None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d, default None), serialized as a VALARM on the event, and the push scheduler fires at that exact lead — firing nothing when there is no alarm and never stripping reminders set in other clients.
|
**Depends on**: Phase 27 (Schedule-X is not bumped until dark mode is locked); runs last so all other v1.2 work is already merged
|
||||||
**Mode:** standard
|
**Requirements**: DEP-01
|
||||||
**Depends on**: Phase 10 (the `calendar_events.reminder_lead_minutes` column from the v1.1 migration is the scheduler's ground truth). Independent of Phases 7/8/9/12.
|
|
||||||
**Requirements**: CAL-13, CAL-14, NOTIF-04, NOTIF-05, NOTIF-06
|
|
||||||
**Success Criteria** (what must be TRUE):
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. The CI dependency report's outdated/vulnerable packages are upgraded (no bulk `pnpm up` / `audit --fix`), per-ecosystem and pin-aware — ESLint held at 9.x until `eslint-plugin-react` supports 10, Drizzle orm + drizzle-kit bumped together, Schedule-X minor-only.
|
||||||
1. When creating or editing a timed event, the user can pick a reminder lead from the preset list (None default); the choice round-trips to Fastmail as a VALARM and is visible/honored on re-open.
|
2. After the updates, all CI gates pass — lint, typecheck, unit, API integration, the Playwright harness, audit, and image hygiene are green.
|
||||||
2. Editing an event that already has a reminder set in another client (Fastmail / Apple Calendar) preserves that VALARM — it is never silently dropped on round-trip.
|
3. No runtime regression to the calendar, lists, push, provider, or theming paths after the bumps (existing tests + harness still pass).
|
||||||
3. A reminder push fires at the event's chosen lead time (e.g. T-30 for a 30-minute lead), not a hardcoded 15-minute lead.
|
**Plans**: TBD
|
||||||
4. An event with no reminder set produces no reminder push (no default 15-minute fire).
|
|
||||||
5. An all-day event's reminder fires at a sensible local time (9 AM on the alert day), not midnight; the reminder selector is disabled/hidden for all-day events in the UI; and reminder delivery stays exactly-once across catch-up scans and rescheduled events.
|
|
||||||
|
|
||||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
|
||||||
|
|
||||||
- **Preserve-on-edit** (Pitfall 1): the update path extracts and preserves existing VALARM sub-components from `rawVevent` (mirroring the WR-01 RRULE-preserve pattern) — never rebuild-from-scratch and silently strip; `outboxPayloadSchema` distinguishes "no change" from explicit "no reminder".
|
|
||||||
- **No TRIGGER VALUE=TEXT** (Pitfall 2): build the trigger with `ICAL.Duration.fromSeconds(-n*60)`, not a bare string; unit-test that the ICS emits a DURATION trigger with no `VALUE=TEXT`.
|
|
||||||
- **All-day 9AM semantics** (Pitfall 3): guard `buildVeventString` (`if (!allDay && reminderMinutes > 0)`), disable the selector when allDay, keep the scheduler's all-day handling at 9 AM local.
|
|
||||||
- **uid:dtstartMs dedup** (Pitfall 4): change the scheduler dedup key from bare `uid` to compound `uid:dtstartMs` and widen the scan to a variable per-event window so long leads fire and rescheduled events re-fire; keep `eventFieldsSchema` and `outboxPayloadSchema` in sync (IN-03).
|
|
||||||
- Hard constraints: `setInterval` only; scheduler reads `reminder_lead_minutes` from the DB (ground truth), not the outbox payload; drop the `isShared`-only reminder restriction (a user who set an alarm wants it regardless of calendar).
|
|
||||||
|
|
||||||
**Plans**: 4 plans (3 waves)
|
|
||||||
Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 11-01-PLAN.md — VALARM builders + classifier + extractor + computeAlertInstantUtc (vevent.ts, TDD)
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
|
||||||
|
|
||||||
- [x] 11-02-PLAN.md — Variable-lead scheduler: uid:dtstartMs dedup, drop isShared, all-day 9 AM, humanized body (TDD)
|
|
||||||
- [x] 11-03-PLAN.md — Backend plumbing: schema field, outbox preserve-on-edit, sync upsert, occurrence surfacing
|
|
||||||
|
|
||||||
**Wave 3** *(blocked on Wave 2 completion)*
|
|
||||||
|
|
||||||
- [x] 11-04-PLAN.md — EventForm reminder picker (allDay swap, edit pre-population) + client types + Playwright smoke
|
|
||||||
|
|
||||||
**UI hint**: yes
|
|
||||||
|
|
||||||
### Phase 12: Initial Setup Wizard
|
|
||||||
|
|
||||||
**Goal**: On first run (no admin/credentials configured), the operator is guided through a validated, step-by-step wizard to bootstrap the app — env presence, generated secrets to copy, DB/OIDC/VAPID/app-password validation — instead of hand-editing `.env` / `docker-compose.yml`; once complete, the setup endpoints lock.
|
|
||||||
**Mode:** standard
|
|
||||||
**Depends on**: Phase 10 (reuses the admin role + `/api/admin/credentials` and `/api/admin/calendars/:id/shared` routes; the wizard is the second frontend consumer of that surface, and `app_config` from the Phase 10 migration holds `setup_complete`). Goes last. Independent of Phases 7/8/9/11.
|
|
||||||
**Requirements**: SETUP-01, SETUP-02, SETUP-03, SETUP-04
|
|
||||||
**Success Criteria** (what must be TRUE):
|
|
||||||
|
|
||||||
1. On a fresh install with nothing configured, the operator reaches a setup wizard (via `GET /api/setup/status` mounted before the OIDC guard) and walks through bootstrap steps instead of editing files by hand.
|
|
||||||
2. Each input is validated before the step can complete: DB connects, VAPID private key decodes to exactly 32 bytes and pairs with the public key, OIDC discovery resolves, and the Fastmail app password reaches CalDAV (PROPFIND).
|
|
||||||
3. Generated secrets (session secret, encryption key, VAPID keypair) are displayed for the operator to copy into env; they are never written to the DB or returned in a way that persists, and `APP_PASSWORD_ENCRYPTION_KEY`/`VAPID_PRIVATE_KEY` never enter the DB at all.
|
|
||||||
4. After completion, the wizard-completing user is promoted to admin (`is_admin`), `app_config.setup_complete` is set, and any further call to a setup endpoint returns 423 Locked.
|
|
||||||
5. The 423 guard is enforced on every invocation (checked against member-credentials + VAPID env present), not only at startup.
|
|
||||||
|
|
||||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
|
||||||
|
|
||||||
- **Guard on every invocation** (Pitfall 8): the "already set up" guard returns 423 from all setup routes once configured — implement and test the guard before the happy path; a second POST after completion must return 423, not 200.
|
|
||||||
- **Secrets stay in env, never in DB** (Pitfalls 8 & 10): the wizard validates secrets by performing a test operation (test encrypt/decrypt, structural VAPID check), never by accepting/storing the key value; no DB column for `vapid_private_key` or `app_password_encryption_key`; never log/echo the app password.
|
|
||||||
- Hard constraints: `GET /api/setup/status` mounts **before** the OIDC guard (like `/health`); do NOT create `/api/setup/credentials` — reuse the Phase 10 admin routes; Drizzle generate+migrate (any `app_config` seeding via migration).
|
|
||||||
|
|
||||||
**Plans**: 7 plans in 4 waves (4 original + 3 gap-closure for 12-UAT.md gaps 1-6)
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 12-01-PLAN.md — Schema migration (nullable OIDC + claimed) + generate-secrets helper (SETUP-03) + Wave-0 scaffolds
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
|
||||||
|
|
||||||
- [x] 12-02-PLAN.md — Pre-auth /api/setup/* router + isSetupLocked 423 guard + index mount + OIDC boot fallback (SETUP-01/02/04)
|
|
||||||
- [x] 12-03-PLAN.md — First-login-claims rework in upsertUser (D-08, SETUP-01)
|
|
||||||
|
|
||||||
**Wave 3** *(blocked on Wave 2 completion)*
|
|
||||||
|
|
||||||
- [x] 12-04-PLAN.md — PWA SetupPage wizard + App.tsx gate + UI-SPEC revision (SETUP-01/02)
|
|
||||||
|
|
||||||
**Wave 4 — Gap closure** *(UAT 12-UAT.md gaps 1-6; 06+07 parallel, 05 blocked on 06)*
|
|
||||||
|
|
||||||
- [x] 12-06-PLAN.md — Backend: validate/vapid asserts wizard key == env VAPID_PUBLIC_KEY (gap 2) + status exposes non-secret DB name (gap 3) (SETUP-02)
|
|
||||||
- [x] 12-07-PLAN.md — App.tsx: reverse-gate /setup post-completion (gap 5) + reconcile ['me'] so calendar banner clears after wizard (gap 6) (SETUP-01/04)
|
|
||||||
- [x] 12-05-PLAN.md — SetupPage: drop DB-vs-env aside (gap 1) + read-only DB-name field (gap 3) + persist fields across Back (gap 4) (SETUP-01) — depends on 12-06
|
|
||||||
|
|
||||||
**UI hint**: yes
|
|
||||||
|
|
||||||
### Phase 13: Real Lint Gate (ESLint)
|
|
||||||
|
|
||||||
**Goal**: The CI lint gate actually fails on lint violations. A real ESLint flat config (`eslint.config.js`, `typescript-eslint`; React + react-hooks plugins for `apps/pwa`) plus a package-level `lint` script in `apps/api` and `apps/pwa` makes the existing root `pnpm -r --if-present lint` run a real linter, replacing the hollow no-op gate that exits 0 because no linter exists.
|
|
||||||
**Mode:** standard
|
|
||||||
**Depends on**: Phase 8 (the CI `fast-checks` job already runs `pnpm lint`; this fills the slot Phase 8 shipped wired to auto-activate once a package `lint` script lands). Independent of all other phases.
|
|
||||||
**Requirements**: TBD (promoted from backlog 999.16)
|
|
||||||
**Success Criteria** (what must be TRUE):
|
|
||||||
|
|
||||||
1. `pnpm lint` runs ESLint across both `apps/api` and `apps/pwa` and exits non-zero on an introduced violation (verified by a deliberate test violation), where today it exits 0 with no linter present.
|
|
||||||
2. The CI `fast-checks` lint step blocks a PR to main on lint violations — the gate can now fail.
|
|
||||||
3. The first real run's existing violations are resolved (fix / warn / disable decided per rule) so the baseline gate ends green.
|
|
||||||
|
|
||||||
**Pitfalls this phase owns**:
|
|
||||||
|
|
||||||
- Pick a baseline ruleset (recommended vs strict-type-checked) deliberately — strict surfaces a large upfront cleanup; decide blocking vs advisory before flipping the gate to blocking.
|
|
||||||
- `typecheck`/tsc already gates type errors; ESLint should not duplicate type-checking rules unnecessarily.
|
|
||||||
|
|
||||||
**Plans**: 3 plans — all complete (scope expanded during planning to add a Prettier `format:check` gate)
|
|
||||||
|
|
||||||
- [x] 13-01-PLAN.md — Install ESLint/Prettier deps + flat config + package scripts + prove the gate fails (SC-1)
|
|
||||||
- [x] 13-02-PLAN.md — Fix all first-run lint violations across both apps, green `pnpm lint` (D-13-05/06)
|
|
||||||
- [x] 13-03-PLAN.md — Prettier reformat (isolated commit) + CI format:check step + green baseline (SC-2/SC-3)
|
|
||||||
|
|
||||||
**UI hint**: no
|
|
||||||
|
|
||||||
### Phase 14: Desktop E2E Coverage
|
|
||||||
|
|
||||||
**Goal**: The Phase 8 regression gate exercises the desktop layout and flows, not just mobile. A `desktop` Playwright project (`devices['Desktop Chrome']`, no touch, wide viewport) is added to `apps/pwa/playwright.config.ts`, and the existing mobile-authored specs are reviewed/adjusted (or appropriately skipped) so `pnpm test:e2e` passes on a no-touch desktop viewport as well as the `iphone`/`pixel` profiles.
|
|
||||||
**Mode:** standard
|
|
||||||
**Depends on**: Phase 7 (the harness it extends) and Phase 8 (CI runs `pnpm test:e2e` and picks up the new project automatically — no CI plumbing change needed beyond any desktop-profile runtime/wait). Independent of Phases 9–13.
|
|
||||||
**Requirements**: TBD (promoted from backlog 999.15)
|
|
||||||
**Success Criteria** (what must be TRUE):
|
|
||||||
|
|
||||||
1. A `desktop` project exists in `playwright.config.ts` (Desktop Chrome, wide viewport, no `hasTouch`).
|
|
||||||
2. The existing e2e specs pass (or are explicitly, justifiably skipped) on the desktop profile — touch-gesture / mobile-drawer / mobile-only-layout assumptions are handled.
|
|
||||||
3. `pnpm test:e2e` in CI runs and gates on both mobile and desktop profiles (blocking-vs-advisory for desktop decided when planned).
|
|
||||||
|
|
||||||
**Pitfalls this phase owns**:
|
|
||||||
|
|
||||||
- The real work is the spec-compat pass, not CI plumbing — Phase 8 reused the Phase 7 harness unchanged, so the config addition is small but specs authored for touch/mobile need per-spec review.
|
|
||||||
- Desktop WebKit is optional — the Apple member is already covered on mobile Safari via `iphone`; Desktop Chrome is likely sufficient for a shared/wall browser.
|
|
||||||
|
|
||||||
**Plans**: 1 plan
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [x] 14-01-PLAN.md — Add the `desktop` Playwright project, desktop-skip the two mobile-only layout assertions (+ D-04 parity), update spec/README docs, and prove `pnpm test:e2e` is green on iphone + pixel + desktop with a blocking CI gate.
|
|
||||||
|
|
||||||
**UI hint**: no
|
|
||||||
|
|
||||||
### Phase 15: Doc-Only CI Skip
|
|
||||||
|
|
||||||
**Goal**: Doc-only PRs to `main` merge without running the slow `harness` (Playwright e2e + dev-stack bring-up, ~5 min) and `api` (MariaDB integration) jobs, while `fast-checks` (Prettier `format:check` + markdown linting) still runs — and branch protection never deadlocks on a required check that never reports. Docs get a *fast but real* gate: format + lint, none of the slow code jobs.
|
|
||||||
**Mode:** standard
|
|
||||||
**Depends on**: Phase 8 (the `.gitea/workflows/ci.yml` it modifies) and Phase 13 (the `fast-checks` job + `format:check` step this extends). Independent of Phases 9–12.
|
|
||||||
**Requirements**: TBD (promoted from backlog 999.17)
|
|
||||||
**Success Criteria** (what must be TRUE):
|
|
||||||
|
|
||||||
1. A doc-only PR to `main` (only `docs/` or `*.md` changed) skips the `api` and `harness` jobs but still runs `fast-checks`.
|
|
||||||
2. A PR touching code runs `fast-checks`, `api`, and `harness` as today; a failure in any blocks the merge.
|
|
||||||
3. Branch protection requires `CI / fast-checks` + an always-running `CI / gate` aggregate (passes when each heavy job is `success` OR `skipped`) — the direct `api`/`harness` requirements are dropped so a skipped heavy job never deadlocks the merge.
|
|
||||||
4. `fast-checks` runs a markdown linter (markdownlint-cli2) over `**/*.md`; an introduced markdown-lint violation fails the gate, and the existing markdown baseline passes (violations fixed or rules configured) so the gate starts green.
|
|
||||||
|
|
||||||
**Pitfalls this phase owns**:
|
|
||||||
|
|
||||||
- **Required-check deadlock** — never path-filter a required context directly; a required job that never reports blocks the PR forever. The always-running `gate` job (`if: always()`, passes on `success`/`skipped`) is the only safe gating surface.
|
|
||||||
- **Gitea skipped-status quirk** — Gitea may not emit a commit-status for a `skipped` job; rely on the always-running `gate`, not on marking `api`/`harness` skipped-but-required.
|
|
||||||
- **Prettier vs markdownlint overlap** — Prettier already owns markdown *formatting*; scope markdownlint to *content* rules (heading increments, no broken/duplicate link refs, list/code-fence conventions) and disable its purely-stylistic rules that fight Prettier (e.g. line-length, list-indent), so the two don't conflict on the same `.md`.
|
|
||||||
- **`.planning/*` is push-direct, never linted** — planning bookkeeping bypasses CI via the Unprotected file pattern, so markdownlint never sees it; scope the lint glob to `docs/` + repo-root/app `*.md` and exclude `.planning/**` (and any generated markdown) to avoid a baseline cleanup of churny bookkeeping files.
|
|
||||||
|
|
||||||
**Plans**: 3 plans (3 waves)
|
|
||||||
Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 15-01-PLAN.md — markdownlint-cli2 + `.markdownlint-cli2.jsonc` + `md:lint` script + fast-checks step + fix 13 baseline violations (SC-4)
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
|
||||||
|
|
||||||
- [x] 15-02-PLAN.md — ci.yml: `changes` (dorny/paths-filter@v4) + conditional api/harness + always-running `gate` aggregate (SC-1/SC-2, SC-3 YAML)
|
|
||||||
|
|
||||||
**Wave 3** *(blocked on Wave 2 completion)*
|
|
||||||
|
|
||||||
- [x] 15-03-PLAN.md — operator branch-protection checkpoint (require `CI / fast-checks` + `CI / gate`, drop api/harness) + publish.yml comment update (SC-3)
|
|
||||||
|
|
||||||
**UI hint**: no
|
|
||||||
|
|
||||||
### Phase 16: CI Dependency Audit, Security Checks & Image Hygiene
|
|
||||||
|
|
||||||
**Goal**: The CI pipeline surfaces outdated and vulnerable dependencies, runs a baseline of additional security checks, and enforces a clean dev↔prod boundary in the images it publishes — so the two-person household app doesn't silently rot on stale/CVE-bearing packages, and no dev-only affordance, secret, or family-specific data ever ships in a production image. Extends the existing Gitea CI (Phase 8) workflow with dependency/security/image-hygiene gates rather than standing up a separate pipeline. **Absorbs backlog 999.17 (dev/prod image boundary).**
|
|
||||||
**Mode:** standard
|
|
||||||
**Depends on**: Phase 8 (Gitea CI — adds steps to the existing workflow + publish job; no admin-chain dependency). Independent of Phases 10–12.
|
|
||||||
**Requirements**: SEC-01 (secret scanning), SEC-02 (static security lint), DEP-01 (vuln audit gate), DEP-02 (outdated advisory), IMG-01 (NODE_ENV+boot-guard), IMG-02 (.dockerignore), IMG-03 (publish image-hygiene assertions), CI-03 (security job + gate wiring)
|
|
||||||
|
|
||||||
**Candidate scope (to be sharpened in `/gsd-discuss-phase 16`):**
|
|
||||||
|
|
||||||
- **Outdated dependencies:** a CI step that reports dependencies behind their latest (e.g. `pnpm outdated -r`), surfaced on the PR. Decide gating vs advisory, and how to handle the pinned-version table in CLAUDE.md (the stack pins exact versions — "outdated" must not fight intentional pins).
|
|
||||||
- **Vulnerability audit:** `pnpm audit` (or equivalent) against the lockfile, failing on a chosen severity threshold (e.g. high/critical). Decide the threshold and an allowlist/waiver mechanism for unfixable transitive advisories.
|
|
||||||
- **Additional security checks (user is open to these — pick a sensible baseline, avoid over-build):** candidates — secret scanning on the diff (gitleaks/trufflehog), a CodeQL/`eslint-plugin-security` static pass, dependency-review on PRs, Dockerfile/image scan (e.g. trivy) of the published image.
|
|
||||||
- **Dev/prod boundary definition & enforcement (from 999.17):** the `DEV_AUTH_BYPASS` concept (and any dev-only affordance) must be provably confined to local dev — never to production, never baked into published images. Today the guard is runtime-only (`NODE_ENV !== 'production' && DEV_AUTH_BYPASS === 'true'` in `apps/api/src/auth/devBypass.ts`); add (a) explicit documentation of what "dev image" vs "shipped image" means, and (b) build-time / boot-time enforcement (a `production` image refuses to boot — or the build aborts — if dev-bypass is enabled) as defense-in-depth.
|
|
||||||
- **No data/secrets in published images (from 999.17):** audit the Dockerfile(s) + the Phase 8 publish job (`publish.yml`) to confirm `.env`, dev seed SQL, local DB dumps, encryption keys, OIDC secrets, the `DEV_USER` seed, and family-specific fixtures are `.dockerignore`d and never `COPY`'d. Add a CI assertion that fails the publish if a dev-bypass code path is active, a forbidden env/secret is present, or personal/seed data is staged into the image context. The dev-stack seed path (`DEV_USER` id 1 + sample calendar/list data) must be unreachable from the production image/compose.
|
|
||||||
- **Noise control:** these gates are notorious for flaky/advisory-churn failures; decide blocking-on-merge vs warn-only per check, and where results surface (PR annotation vs job log), mirroring Phase 15's gate-aggregation approach.
|
|
||||||
|
|
||||||
**Boundary:** Extends the existing Gitea CI workflow + publish job; does not remove dev-bypass (still needed for local verification and the Phase 7/8 harness) and does not add a new external service or a runtime dependency to the app. Automated dependency *upgrades* (e.g. Renovate/Dependabot bots) are a separate concern — decide in discuss whether they're in scope or deferred.
|
|
||||||
|
|
||||||
**Plans**: 6 plans in 2 waves
|
|
||||||
Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 16-01-PLAN.md — Image-hygiene runtime: bake NODE_ENV=production + boot-time refuse-to-boot guard (IMG-01)
|
|
||||||
- [x] 16-02-PLAN.md — pnpm audit gate + waiver allowlist + advisory-only tiered outdated report (DEP-01, DEP-02)
|
|
||||||
- [x] 16-03-PLAN.md — Fold eslint-plugin-security into the lint gate as blocking errors + triage (SEC-02)
|
|
||||||
- [x] 16-04-PLAN.md — gitleaks config + full-history baseline + .dockerignore (SEC-01, IMG-02)
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
|
||||||
|
|
||||||
- [x] 16-05-PLAN.md — Add the security job to ci.yml (gitleaks always; audit/outdated code-gated) + gate wiring (CI-03)
|
|
||||||
- [x] 16-06-PLAN.md — publish.yml static image-hygiene assertion + boot-smoke before push (IMG-03)
|
|
||||||
|
|
||||||
**UI hint**: no
|
|
||||||
|
|
||||||
### Phase 17: UI Optimization & Polish
|
|
||||||
|
|
||||||
**Goal**: A visual-identity & polish pass for the PWA spanning three workstreams: **(A) phone-layout polish** so the phone (≤767px) layout has no fixed-chrome overlap and small-viewport spacing reads cleanly — starting with the long-standing BottomTabBar overlap that hides the New Event FAB and the colour legend, plus a small-viewport sweep; **(B) branding assets** — generate a real FamilySync logo into the existing `BrandSlot` seam (`apps/pwa/src/components/BrandSlot.tsx`) and a complete favicon/PWA-icon set replacing the placeholder stubs in `apps/pwa/public/`; **(C) theme-token groundwork** — restructure `apps/pwa/src/styles/tokens.css` into a themeable semantic-token layer (swappable by `data-theme`/`prefers-color-scheme`), light staying the only shipped theme, so a future dark theme is cheap.
|
|
||||||
**Mode:** standard
|
|
||||||
**Depends on**: Nothing structural (CSS/layout + assets only). Best sequenced after Phase 10 merges (the BottomTabBar gained an Admin tab and the new SetupBanner adds top pressure on phone), but otherwise independent of the admin chain.
|
|
||||||
**Requirements**: No REQ-IDs — decisions D-01…D-10 (17-CONTEXT.md) stand in. Coverage: D-01/D-02 (A, phone overlap+guard) → 17-03; D-03/D-04 (B, assets) → 17-02; D-04/D-05 (B, wiring) → 17-04; D-06 (C, token groundwork) → 17-01; D-07/D-09 (D, logout+sheet centering) → 17-05; D-08/D-09/D-10 (D, admin toasts+reset-sheet+two-tab nav) → 17-06.
|
|
||||||
**Scope boundary (set in `/gsd-discuss-phase 17`, 2026-06-17):** Workstream C ships token groundwork **only** — no dark palette, no theme toggle (→ backlog **999.20**). A broader "modern styling" visual refresh is **out of scope** and routed to backlog **999.21** (future milestone). Keep Phase 17 a focused polish + branding + groundwork pass, not a redesign.
|
|
||||||
|
|
||||||
**Seed defect — phone-layout bottom-bar overlap (documented 2026-06-13; long-standing, NOT introduced by Phase 10 — the BottomTabBar dates to Phase 04):**
|
|
||||||
|
|
||||||
At ≤767px (`window.matchMedia('(max-width: 767px)')` in `apps/pwa/src/App.tsx`) the layout switches to a 48px top AppNav + a `position: fixed` BottomTabBar (`height: calc(56px + env(safe-area-inset-bottom))`, z-index 200; `apps/pwa/src/components/BottomTabBar.tsx`) + a floating "New Event" FAB (`position: fixed; bottom: var(--space-6); right: var(--space-6)`; `apps/pwa/src/components/CalendarShell.tsx`). Two problems:
|
|
||||||
|
|
||||||
1. **FAB sits inside the bar** — the FAB's `bottom` offset (~`--space-6`, ≈24px) is smaller than the bar's 56px height, so the round New Event button overlaps the bottom tab bar (lands on the Admin tab).
|
|
||||||
2. **Content occluded** — the content area (`contentStyle` in `App.tsx`) reserves no `padding-bottom` for the fixed bar, so the bottom of the calendar and the colour-legend chips (e.g. the "Dev User" / member legend) slide under the bar and are partially hidden.
|
|
||||||
|
|
||||||
**Fix sketch (CSS-only, no behaviour change):** on phone, lift the FAB to `bottom: calc(56px + env(safe-area-inset-bottom, 0px) + var(--space-6))` and add a matching `padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px))` to the phone content/scroll area (or reduce the `100dvh` column by the bar height). Verify across the `iphone`/`pixel`/`desktop` Playwright profiles and a real narrow Chromium via playwright-cli.
|
|
||||||
|
|
||||||
**Evidence:** reproduced 2026-06-13 with playwright-cli at 390×844 (FAB over the Admin tab; "Dev User" legend clipped) vs 1280×800 (desktop sidebar, no overlap). Full detail in todo `2026-06-13-pwa-phone-bottombar-overlap.md`.
|
|
||||||
|
|
||||||
**Candidate scope (to sharpen in `/gsd-discuss-phase 17`):** the seed defect above, plus a sweep for other small-viewport spacing / tap-target / overlap issues (the Phase 7 `layout.spec.ts` tap-target/overflow assertions are a ready checklist) and any phone/desktop visual inconsistencies noticed in use. Keep it a focused polish pass, not a redesign.
|
|
||||||
|
|
||||||
**Plans**: 6/6 plans complete
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 17-01-PLAN.md — C: tokens.css themeable-layer groundwork + --bottom-chrome-h token (D-06) [Wave 1]
|
|
||||||
- [x] 17-02-PLAN.md — B: generate logo + full icon set, operator approval checkpoint (D-03, D-04) [Wave 1]
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
|
||||||
|
|
||||||
- [x] 17-03-PLAN.md — A: phone FAB/BottomTabBar overlap fix + sweep + CI overlap assertion (D-01, D-02) [Wave 2, dep 01]
|
|
||||||
- [x] 17-04-PLAN.md — B: wire logo into BrandSlot + index.html favicons + manifest maskable + accent (D-04, D-05) [Wave 2, dep 01,02]
|
|
||||||
- [x] 17-05-PLAN.md — D: logout control + sheet desktop-centering (SettingsSheet/CredentialSheet) (D-07, D-09) [Wave 2, dep 01]
|
|
||||||
- [x] 17-06-PLAN.md — D: admin success toasts + two-tab ARIA nav + reset-sheet centering + admin.spec.ts (D-08, D-09, D-10) [Wave 2, dep 01]
|
|
||||||
|
|
||||||
**UI hint**: yes
|
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
| Phase | Milestone | Plans Complete | Status | Completed |
|
| Phase | Milestone | Plans Complete | Status | Completed |
|
||||||
| ----- | --------- | -------------- | -------- | ---------- |
|
| ----- | --------- | -------------- | -------- | ---------- |
|
||||||
| 1. Foundation + Broker Spike | v1.0 | 4/4 | Complete | 2026-06-04 |
|
| 1. Foundation + Broker Spike | v1.0 | 4/4 | Complete | 2026-06-04 |
|
||||||
| 2. Calendar Display | v1.0 | 5/5 | Complete | 2026-06-05 |
|
| 2. Calendar Display | v1.0 | 5/5 | Complete | 2026-06-05 |
|
||||||
| 3. Event Write-Back + PWA Install| v1.0 | 12/12 | Complete | 2026-06-07 |
|
| 3. Event Write-Back + PWA Install | v1.0 | 12/12 | Complete | 2026-06-07 |
|
||||||
| 4. Shared Lists + Live Sync | v1.0 | 7/7 | Complete | 2026-06-09 |
|
| 4. Shared Lists + Live Sync | v1.0 | 7/7 | Complete | 2026-06-09 |
|
||||||
| 5. Web Push Notifications | v1.0 | 8/8 | Complete | 2026-06-10 |
|
| 5. Web Push Notifications | v1.0 | 8/8 | Complete | 2026-06-10 |
|
||||||
| 6. UX Polish | v1.0 | 6/6 | Complete | 2026-06-10 |
|
| 6. UX Polish | v1.0 | 6/6 | Complete | 2026-06-10 |
|
||||||
| 7. Mobile Test Harness | v1.1 | 4/4 | Complete | 2026-06-11 |
|
| 7. Mobile Test Harness | v1.1 | 4/4 | Complete | 2026-06-11 |
|
||||||
| 8. Gitea CI | v1.1 | 4/4 | Complete | 2026-06-11 |
|
| 8. Gitea CI | v1.1 | 4/4 | Complete | 2026-06-11 |
|
||||||
| 9. Faster Write-Back | v1.1 | 2/2 | Complete | 2026-06-12 |
|
| 9. Faster Write-Back | v1.1 | 2/2 | Complete | 2026-06-12 |
|
||||||
| 10. Admin Role & Settings | v1.1 | 4/4 | Complete | 2026-06-13 |
|
| 10. Admin Role & Settings | v1.1 | 4/4 | Complete | 2026-06-13 |
|
||||||
| 11. Per-Event Reminders | v1.1 | 5/5 | Complete | 2026-06-14 |
|
| 11. Per-Event Reminders | v1.1 | 5/5 | Complete | 2026-06-14 |
|
||||||
| 12. Initial Setup Wizard | v1.1 | 7/7 | Complete | 2026-06-16 |
|
| 12. Initial Setup Wizard | v1.1 | 7/7 | Complete | 2026-06-16 |
|
||||||
| 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 |
|
| 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 |
|
||||||
| 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 |
|
| 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 |
|
||||||
| 15. Doc-Only CI Skip + MD Lint | v1.1 | 3/3 | Complete | 2026-06-12 |
|
| 15. Doc-Only CI Skip + MD Lint | v1.1 | 3/3 | Complete | 2026-06-12 |
|
||||||
| 16. CI Dep Audit, Sec & Img Hyg | v1.1 | 6/6 | Complete | 2026-06-13 |
|
| 16. CI Dep Audit, Sec & Img Hyg | v1.1 | 6/6 | Complete | 2026-06-13 |
|
||||||
| 17. UI Optimization & Polish | v1.1 | 6/6 | Complete | 2026-06-18 |
|
| 17. UI Optimization & Polish | v1.1 | 6/6 | Complete | 2026-06-18 |
|
||||||
| 18. Auto Timezone Detection | v1.1 | 4/4 | Complete | 2026-06-14 |
|
| 18. Auto Timezone Detection | v1.1 | 4/4 | Complete | 2026-06-14 |
|
||||||
| 19. Local Auth (No-OIDC Mode) | v1.1 | 5/5 | Complete | 2026-06-17 |
|
| 19. Local Auth (No-OIDC Mode) | v1.1 | 5/5 | Complete | 2026-06-17 |
|
||||||
| 20. Admin Member Editor & Declutter | v1.1 | 3/3 | Complete | 2026-06-18 |
|
| 20. Admin Member Editor & Declutter | v1.1 | 3/3 | Complete | 2026-06-18 |
|
||||||
|
| 21. Zero-Setup DB Bootstrap | v1.2 | 0/? | Not started | - |
|
||||||
|
| 22. Provider Abstraction | v1.2 | 0/? | Not started | - |
|
||||||
|
| 23. Multiple Reminders Per Event | v1.2 | 0/? | Not started | - |
|
||||||
|
| 24. Dev/Mock Provider | v1.2 | 0/? | Not started | - |
|
||||||
|
| 25. Google Calendar Provider | v1.2 | 0/? | Not started | - |
|
||||||
|
| 26. Self-Service Provider Onboarding | v1.2 | 0/? | Not started | - |
|
||||||
|
| 27. PWA Dark Mode | v1.2 | 0/? | Not started | - |
|
||||||
|
| 28. Dependency Updates | v1.2 | 0/? | Not started | - |
|
||||||
|
|
||||||
|
## Dependencies & Critical Path (v1.2)
|
||||||
|
|
||||||
|
```
|
||||||
|
21 (DB Bootstrap) ──► 22 (Provider Abstraction) ──┬─► 23 (Multiple Reminders) ──┐
|
||||||
|
│ ├─► 25 (Google) ──► 26 (Onboarding)
|
||||||
|
└─► 24 (Mock Provider) ───────┘
|
||||||
|
27 (Dark Mode) — independent, parallelizable with 22–26
|
||||||
|
28 (Dependency Updates) — last; after 27 (Schedule-X hold) and all other v1.2 work
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Critical path:** 21 → 22 → 23 → 25 → 26, then 28 last.
|
||||||
|
- **Parallelizable:** 24 after 22 (alongside 23/25); 27 anytime.
|
||||||
|
- **HIGH-risk gate:** Phase 22 must not regress the live Fastmail path — golden-path integration tests pass unchanged before 23/24/25 build on it.
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
### Phase 999.1: Treat Fastmail as one calendar provider; framework supports adding more providers (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern.
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 6/6 plans complete
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.4: Per-event reminder configuration (VALARM authoring + scheduler honors it) (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] End-to-end per-event reminders — let the user choose *when* (or whether) to be reminded per event, and make the push scheduler honor that choice instead of a hardcoded lead.
|
|
||||||
|
|
||||||
**Half A — author the VALARM (event form):** The event create/edit form has no UI to set a reminder ("remind me 10 min / 1 hour / 1 day before", or **no reminder**), so the written `.ics` carries no `VALARM` and no reminder can fire — in native clients or via web push. Add a reminder selector (including an explicit "none"), serialize chosen offsets as `VALARM` (TRIGGER) on write-back, and parse existing `VALARM`s on read so edits preserve them. Feeds the Phase 5 web-push requirement (push needs reminder data to notify about).
|
|
||||||
|
|
||||||
**Half B — scheduler honors the provider's value (NEW, surfaced 2026-06-10):** Today `apps/api/src/broker/reminderScheduler.ts` runs a **hardcoded 15-minute** scan for shared timed events (`index.ts:139` "starting in ~15 min"; reminderScheduler header "15-min reminder scan") and never reads the event's actual alarm. So every reminder fires 15 min before regardless of what the event (or the calendar provider) specifies, and an event with **no** alarm still gets a 15-min push. Change the scheduler to read each event's `VALARM` `TRIGGER` (the value written in Half A / set in Fastmail or another native client) and fire at that lead — and fire **nothing** when the event has no alarm. The current fixed 15-min window/dedup logic (catch-up scan, per-uid exactly-once — see quick 260610-hbu) must be generalized to a variable per-event lead.
|
|
||||||
|
|
||||||
**Boundary:** preserve the reminder scheduler's resilience guarantees (catch-up on a missed tick, per-uid exactly-once dedup). This makes the lead per-event/variable rather than constant; it is not a rewrite of the scan/dedup design.
|
|
||||||
|
|
||||||
**Severity:** medium — feature gap surfaced during Phase 03 Gate 2 testing; Half B surfaced 2026-06-10. Tags: phase-03, phase-05, calendar, write-back, reminders, valarm, push, scheduler, phase-05-dependency.
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
> **Promoted into v1.1 Phase 11 (Per-Event Reminders) — CAL-13/CAL-14/NOTIF-04/05/06.** Backlog entry retained for history.
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.10: Admin Settings / Administration section — manage app passwords + designate the shared calendar via UI (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] Add an in-app **Settings/Administration** section, gated to an administrator role, for configuration that today requires manual backend/DB steps:
|
|
||||||
|
|
||||||
- **View/update per-member Fastmail app passwords** (stored encrypted via `APP_PASSWORD_ENCRYPTION_KEY`, existing crypto path) — rotate or re-enter a member's credential and re-trigger sync.
|
|
||||||
- **Designate which synced calendar is the "shared" calendar** by toggling `calendars.is_shared` from the UI. Today this is a manual DB write: e.g. `UPDATE calendars SET is_shared=1 WHERE id=<row>` — done by hand on 2026-06-10 to mark the "FamilySync" calendar (id 10) shared after the poller synced it (D-16). The admin should pick the shared calendar from a list of synced collections instead of relying on a backend process. (The poller's upsert already leaves `is_shared` untouched, so a UI-set flag persists.)
|
|
||||||
|
|
||||||
**Context:** Motivated by the manual D-16 resolution (2026-06-10). **Related:** 999.5 (per-member first-login app-password onboarding) — this is the ongoing admin-managed counterpart; and 999.11 (initial setup wizard) — bootstrap-time vs. ongoing config. Tags: admin, settings, calendar, app-passwords, D-16.
|
|
||||||
|
|
||||||
> **Promoted into v1.1 Phase 10 (Admin Role & Settings) — ADMIN-01/ADMIN-02/ADMIN-03.** Backlog entry retained for history.
|
|
||||||
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.11: Initial setup wizard — first-run config of env vars, app passwords, DB connection (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] Add a first-run **setup wizard** that walks the administrator through defining all bootstrap configuration instead of hand-editing `.env` / `docker-compose.yml`:
|
|
||||||
|
|
||||||
- **App environment variables:** OIDC client id/secret/issuer/redirect URI + external URL, session signing secret (`OIDC_AUTH_SECRET`), `APP_PASSWORD_ENCRYPTION_KEY`, and the **VAPID keypair** (subject + public + private).
|
|
||||||
- **MariaDB connection:** host/port/user/password/db, with a connectivity test.
|
|
||||||
- **First Fastmail app password** for the initial member, encrypted on save.
|
|
||||||
|
|
||||||
Wizard should **validate inputs before completing** — e.g. VAPID private key decodes to 32 bytes AND pairs with the public key, OIDC discovery resolves, DB connects, app-password reaches CalDAV.
|
|
||||||
|
|
||||||
**Context:** Motivated by setup friction observed 2026-06-10 — a VAPID private key truncated on paste into `.env` silently broke push (`setVapidDetails failed — 32 bytes`), and `DB_HOST` / dev overrides must currently be set by hand. A guided + validated wizard would have caught these. **Related:** 999.10 (ongoing admin Settings) and 999.5 (member onboarding). Tags: onboarding, setup, install, env, vapid, mariadb, oidc.
|
|
||||||
|
|
||||||
> **Promoted into v1.1 Phase 12 (Initial Setup Wizard) — SETUP-01/02/03/04.** Backlog entry retained for history.
|
|
||||||
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.12: Assistant-driven mobile-browser UI testing (mobile viewport + authed PWA) (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] Give the assistant a way to validate UI/UX changes in a **mobile** browser experience, not just desktop Chromium. Today `playwright-cli` drives a desktop viewport, and the prod stack enforces OIDC (Authelia) so the authed PWA can't be reached headlessly — which is exactly why a string of mobile-only defects this milestone (silent Android notifications, the dead "How to enable" link, iOS/Android session-cookie persistence, install/standalone behaviour) could only be found by the operator on real devices, not by the assistant.
|
|
||||||
|
|
||||||
**What this needs (any subset):**
|
|
||||||
|
|
||||||
- **Mobile viewport + UA emulation** in the browser harness (e.g. Playwright device descriptors — iPhone/Pixel viewport, touch, mobile user-agent) so layout, tap targets, and responsive behaviour can be checked.
|
|
||||||
- **An authenticated entry path for automated runs** so the assistant can reach the real PWA past Authelia — e.g. a reusable saved storage-state/cookie, a test-only bypass on a non-prod host, or driving the Authelia login once and reusing the session. (Note: this overlaps the existing `DEV_AUTH_BYPASS`, but that only works on the host-side dev stack, not the prod-mode PWA that has the real service worker. A mobile, authed, SW-enabled target is the gap.)
|
|
||||||
- Optionally: a documented way to point the harness at the Pangolin HTTPS URL with a persisted session, and/or remote-debug a real device.
|
|
||||||
|
|
||||||
**Boundary:** genuinely device-only behaviour (iOS-Safari standalone push, real APNs/FCM delivery, OS notification-channel importance) still needs a human — this item is about everything SHORT of that (responsive layout, tap flows, in-page notification UI states, auth redirects) which a mobile-emulated authed browser *could* cover but currently can't.
|
|
||||||
|
|
||||||
**Context:** Surfaced 2026-06-10 during Phase 5 UAT — repeated mobile-only bugs were caught only by the operator because the assistant had no mobile, authenticated browser to test in. **Related:** [[feedback-playwright-verify]] (use playwright-cli over manual verification — this extends it to mobile/authed). Tags: testing, playwright, mobile, pwa, oidc, dx.
|
|
||||||
|
|
||||||
> **Promoted into v1.1 Phase 7 (Mobile Test Harness) — TEST-01/TEST-02.** v1.1 scopes the `DEV_AUTH_BYPASS` dev-build path; the prod-SW authed-mobile target stays deferred. Backlog entry retained for history.
|
|
||||||
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.13: Reduce event write-back latency to the calendar provider (outbox drain) (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] Calendar create/edit/delete writes are enqueue-only (`calendarOutbox`, 202 optimistic-accept; D-12/D-05 — no Fastmail call in the route) and flushed to Fastmail by `runOutboxDrain` on a **15-second `setInterval`** (`apps/api/src/broker/outboxWorker.ts`). So a change can take up to ~15s to land in Fastmail (and longer to reflect back in the app, which depends on the separate 5-min poller). Reduce that perceived sync delay so edits feel near-immediate.
|
|
||||||
|
|
||||||
**Options to weigh when picking this up:**
|
|
||||||
|
|
||||||
- **Event-driven drain (preferred):** trigger an outbox drain immediately after a successful enqueue (in-process signal, or Redis pub/sub which is already available) so the write fires within ~1s instead of waiting for the next tick — keep the 15s `setInterval` as a fallback/retry sweep. Must preserve the existing per-row etag/412 handling and the rapid-successive-edit ordering (see outboxWorker comments ~L312 — each edit carries its enqueue-time etag).
|
|
||||||
- **Shorter interval:** simplest, but more idle DB polling; a floor (e.g. 3–5s) trades latency for load.
|
|
||||||
- **Faster read-back too:** the user also sees latency from the 5-min poller reflecting the change back. Consider invalidating/short-poll after a local write, or optimistic UI already covering it — confirm whether the perceived delay is the write (15s) or the read-back (5min).
|
|
||||||
|
|
||||||
**Boundary:** the optimistic 202 + outbox durability design (create-before-delete, drain concurrency guard, fresh-etag-before-PUT) must be preserved — this is a latency tune, not a rewrite of the write path.
|
|
||||||
|
|
||||||
**Context:** Surfaced 2026-06-10. Tags: calendar, write-back, outbox, latency, redis, performance.
|
|
||||||
|
|
||||||
> **Promoted into v1.1 Phase 9 (Faster Write-Back) — CAL-15.** In-process EventEmitter chosen (not Redis); the drain is single-process by design. Backlog entry retained for history.
|
|
||||||
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.14: Gitea CI — full regression on PR to main + build/publish Docker image (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] The repo is committed against a self-hosted Gitea instance with a registered Actions runner, but there is no CI yet (no `.gitea/workflows/` or `.github/workflows/`). Two things should run automatically: (1) **full regression** on every PR targeting `main` — gating the merge; (2) **build the app's Docker image and publish it** to the Gitea container registry.
|
|
||||||
|
|
||||||
**Options / decisions to make when picking this up:**
|
|
||||||
|
|
||||||
- **Test scope:** "full regression" = lint + typecheck + unit + the API integration tests. Integration tests need a real MariaDB (see [[api-integration-test-db]]) — the workflow must spin up a MariaDB service container, bind it, and set `DB_HOST=127.0.0.1` + `.env` creds. The PWA build/test also runs.
|
|
||||||
- **Monorepo:** pnpm workspace (`apps/api`, `apps/pwa`, shared). Cache the pnpm store.
|
|
||||||
- **Docker images:** only `apps/api/Dockerfile` exists today — there is no PWA Dockerfile yet. Decide one image (API) vs. also building/serving the PWA. Tag scheme + when to publish (only on merge to `main`? on tags? per-PR?).
|
|
||||||
- **Registry auth:** push to the Gitea registry using the runner's Gitea-provided token or a dedicated package-write token.
|
|
||||||
- Gitea Actions are GitHub-Actions-compatible syntax but run on the self-hosted runner — confirm runner labels and available images, and that Actions is enabled, before authoring.
|
|
||||||
|
|
||||||
**Likely shape:** a `.gitea/workflows/ci.yml` — `on: pull_request` (to `main`) → install (pnpm), lint, typecheck, unit, API integration vs. a `mariadb` service container, PWA build; `on: push` to `main`/tag → `docker build apps/api/Dockerfile`, login, push tagged image.
|
|
||||||
|
|
||||||
**Context:** Promoted from STATE.md pending todo (`.planning/todos/pending/2026-06-10-gitea-ci-regression-and-docker-publish.md`), surfaced 2026-06-10. Tags: tooling, ci, gitea, docker, mariadb, monorepo.
|
|
||||||
|
|
||||||
> **Promoted into v1.1 Phase 8 (Gitea CI) — CI-01/CI-02.** v1.1 also extends CI-01 to run the Phase 7 mobile harness as a UI-regression step (CI brings up the dev stack in the runner). Backlog entry retained for history.
|
|
||||||
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.15: Desktop e2e coverage — add a Desktop Playwright profile + desktop-safe specs (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] The Playwright harness (`apps/pwa/playwright.config.ts`) defines only **mobile** device profiles — `iphone` (iPhone 14 / WebKit) and `pixel` (Pixel 7 / Chromium), both with touch and a mobile viewport. The Phase 8 CI regression gate runs `pnpm test:e2e`, so it currently validates the **mobile experience only**. Add desktop coverage so the regression gate exercises the desktop layout/flows as well.
|
|
||||||
|
|
||||||
**Options / decisions to make when picking this up:**
|
|
||||||
|
|
||||||
- **Add a Desktop profile:** a new `desktop` project in `playwright.config.ts` (e.g. `devices['Desktop Chrome']`, no `hasTouch`, wide viewport). Optionally a Desktop WebKit/Safari profile too — but the family's Apple member is already covered on mobile Safari via `iphone`; Desktop Chrome is likely sufficient for a shared/wall browser.
|
|
||||||
- **Spec-compat pass (the real work):** the existing e2e specs were authored for mobile — they may assume touch gestures, a mobile nav/drawer, or mobile-only layout. Each spec needs review/adjustment so it passes (or is appropriately skipped) on a no-touch, wide-viewport desktop. This is harness/spec work, not CI plumbing.
|
|
||||||
- **Gating choice:** decide whether desktop runs block the merge immediately, or run advisory (non-blocking) until the specs are confirmed desktop-safe.
|
|
||||||
|
|
||||||
**Boundary:** Phase 8 deliberately reused the Phase 7 harness **unchanged** (CI owns only stack bring-up + readiness waits, not spec content), which is why this was deferred. Once a Desktop project is added to the config, Phase 8 CI picks it up automatically via `pnpm test:e2e` — no CI changes needed beyond whatever runtime/wait the desktop profile requires.
|
|
||||||
|
|
||||||
**Context:** Deferred from Phase 8 (Gitea CI) planning, 2026-06-11 — user wants both mobile and desktop validated, but desktop needs a config addition + spec review that is out of Phase 8's CI-plumbing scope. Tags: testing, playwright, e2e, desktop, harness, ci.
|
|
||||||
|
|
||||||
> **Promoted into v1.1 Phase 14 (Desktop E2E Coverage) — 2026-06-11.** Backlog entry retained for history.
|
|
||||||
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.16: Wire a real linter (ESLint) so the CI lint gate actually fails on violations (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] The Phase 8 CI `fast-checks` job runs `pnpm lint`, but **no linter exists** in the repo — the root `lint` script is `pnpm -r --if-present lint`, which finds no package-level lint script and exits 0. The lint gate is a hollow placeholder that can never fail. Wire up a real linter so it runs and gates merges on lint violations. (`typecheck`/tsc already gates type errors meanwhile.)
|
|
||||||
|
|
||||||
**Options / decisions to make when picking this up:**
|
|
||||||
|
|
||||||
- **Tooling:** ESLint flat config (`eslint.config.js`) with `typescript-eslint`; add React + react-hooks plugins for `apps/pwa`. Add `eslint` (+ plugins) as devDeps and a `lint` script to `apps/api` and `apps/pwa` — `pnpm -r --if-present lint` then picks them up automatically, no CI change needed.
|
|
||||||
- **Rule strictness:** pick a baseline (recommended vs strict-type-checked). Stricter = more upfront violations to fix.
|
|
||||||
- **Violation cleanup (the real work):** the first run surfaces existing violations across both apps. Decide per-rule: fix, downgrade to warn, or disable. The gate must end green.
|
|
||||||
- **Gating choice:** blocking on merge immediately, or advisory (warn-only) until the codebase is clean.
|
|
||||||
|
|
||||||
**Boundary:** Phase 8 deliberately scoped lint wiring out (CI-plumbing-only); it shipped the gate slot wired to auto-activate once a package `lint` script lands. This item is that follow-up.
|
|
||||||
|
|
||||||
**Context:** Raised during Phase 8 execution, 2026-06-11 — user noted the `--if-present` lint step "didn't fix the linter, just made it so it didn't have to exist to proceed" and wants a lint gate that actually fails. Tags: ci, lint, eslint, typescript-eslint, quality, gitea.
|
|
||||||
|
|
||||||
> **Promoted into v1.1 Phase 13 (Real Lint Gate / ESLint) — 2026-06-11.** Backlog entry retained for history.
|
|
||||||
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.18: Update dependencies as found during CI (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] When the CI dependency-audit gate (Phase 16) surfaces outdated or vulnerable packages, bump them rather than letting the report accumulate. Establish a lightweight, recurring "act on the CI dependency report" loop so the two-person household app doesn't drift onto stale/CVE-bearing deps. Scope is the upkeep workflow (review → bump → verify gate green), not a one-time audit.
|
|
||||||
|
|
||||||
**Context:** Captured 2026-06-13 during Phase 10 work. Companion to the audit *reporting* shipped in Phase 16 (CI Dependency Audit) — that phase makes outdated/vulnerable deps *visible*; this item is the standing follow-through to *resolve* what it finds. Tags: ci, dependencies, maintenance, security, upkeep.
|
|
||||||
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.19: Dev user exercises full app functionality without syncing to a real calendar (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] Let the `DEV_AUTH_BYPASS` dev user (currently hardcoded `DEV_USER` id 1 in `apps/api/src/auth/devBypass.ts`) exercise the full app — create/edit/delete events, set per-event reminders, manage lists — against a local/in-app calendar store, WITHOUT requiring a connected Fastmail/CalDAV provider and WITHOUT writing anything to a real calendar. Today the dev user has no `member_credentials` row and no `calendars`, so `writable-calendars` is empty and `POST /api/events/create` returns `422 "No writable calendar found for user"` — making hands-on UAT of event/reminder features impossible in dev. Options to explore: seed the dev user a fake local calendar + short-circuit the outbox/CalDAV write path under dev-bypass (no Fastmail round-trip), or a dev-only in-memory calendar provider. Must stay strictly dev-only (same hard `NODE_ENV !== 'production'` guard) and never ship in production images.
|
|
||||||
|
|
||||||
**Context:** Captured 2026-06-14 during Phase 11 (Per-Event Reminders) UAT. The reminder picker and backend were verified via automated tests + a route-mocked playwright smoke, but the operator could not manually create an event to see reminders end-to-end because no provider is connected in the dev DB (`needsProviderSetup: true`). This is a recurring dev-testability friction (see MEMORY: "Dev user 1 has no calendars"). Tags: dev-tooling, dev-bypass, testability, calendars, outbox, uat.
|
|
||||||
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.20: PWA dark mode / theming — ship a full dark theme + light/dark/system switch (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] Ship a complete dark theme for the PWA plus a light/dark/system theme switch. **Phase 17 lays the token-architecture groundwork** — it restructures `apps/pwa/src/styles/tokens.css` from a single light `:root` into a themeable semantic-token layer that can be swapped via `data-theme` / `prefers-color-scheme`, with light staying the default and only-shipped theme. This backlog item is the follow-through that consumes that seam: author the actual dark palette values (including the Schedule-X `--sx-color-*` calendar overrides at the bottom of tokens.css), wire `prefers-color-scheme`, add a persisted in-app toggle in the /admin or Settings surface (light / dark / system), and verify both themes render cleanly across every route (calendar, lists, admin, settings sheet, login) via `playwright-cli` + the Phase 7 `layout.spec` profiles.
|
|
||||||
|
|
||||||
**Context:** Deferred out of Phase 17 (2026-06-17) during `/gsd-discuss-phase 17` to keep that phase scoped to phone-layout polish + branding assets. Phase 17's token restructure is the explicit enabling groundwork, so this should be cheap to pick up afterward. Related: Phase 17 (UI Optimization & Polish — the groundwork), 999.21 (modern styling refresh). Tags: pwa, theming, dark-mode, tokens, accessibility, settings, prefers-color-scheme.
|
|
||||||
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.21: PWA modern visual styling refresh — contemporary look across the app (BACKLOG)
|
### Phase 999.21: PWA modern visual styling refresh — contemporary look across the app (BACKLOG)
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] A broader "more modern, visually appealing" styling pass across the PWA — beyond the bounded in-system polish of Phase 17. Candidate scope: a contemporary refresh of high-visibility surfaces (login, calendar shell, event form, lists, admin), revisiting elevation/shadows, radii, spacing rhythm, typography scale, and control states, potentially reworking specific component layouts. Explicitly **flagged for a future milestone**, not v1.1 — it is a visual-overhaul track with real redesign risk and should be scoped/sequenced on its own rather than bolted onto a polish phase. Best sequenced after the Phase 17 token groundwork and 999.20 (dark mode) so the refresh is theme-aware from the start.
|
**Goal:** [Captured for future planning] A broader "more modern, visually appealing" styling pass across the PWA — beyond the bounded in-system polish of Phase 17. Candidate scope: a contemporary refresh of high-visibility surfaces (login, calendar shell, event form, lists, admin), revisiting elevation/shadows, radii, spacing rhythm, typography scale, and control states, potentially reworking specific component layouts. Explicitly **flagged for a future milestone**, not v1.2 — it is a visual-overhaul track with real redesign risk and should be scoped/sequenced on its own rather than bolted onto a polish phase. Best sequenced after the Phase 17 token groundwork and Phase 27 (dark mode) so the refresh is theme-aware from the start.
|
||||||
|
|
||||||
**Context:** Deferred out of Phase 17 (2026-06-17) during `/gsd-discuss-phase 17`. The user scoped Phase 17 to layout polish + branding (logo/favicon/icon assets) + theme-token groundwork, and routed the open-ended styling refresh here for a future milestone to avoid an unbounded redesign inside a polish phase. Related: Phase 17 (the polish baseline), 999.20 (dark mode / theming). Tags: pwa, ui, styling, redesign, design-system, future-milestone.
|
**Context:** Deferred out of Phase 17 (2026-06-17) during `/gsd-discuss-phase 17`. The user scoped Phase 17 to layout polish + branding (logo/favicon/icon assets) + theme-token groundwork, and routed the open-ended styling refresh here for a future milestone to avoid an unbounded redesign inside a polish phase. Related: Phase 17 (the polish baseline), Phase 27 / 999.20 (dark mode / theming). Tags: pwa, ui, styling, redesign, design-system, future-milestone.
|
||||||
|
|
||||||
**Requirements:** TBD
|
**Requirements:** TBD
|
||||||
**Plans:** 0 plans
|
**Plans:** 0 plans
|
||||||
@@ -696,77 +211,6 @@ Plans:
|
|||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
### Phase 18: Auto timezone detection and ability to change timezone
|
---
|
||||||
|
|
||||||
**Goal:** Make the household timezone an explicit, stored, user-changeable setting — auto-detected from the browser at first run, changeable from the role-gated /admin Settings — and route the server-side all-day "9 AM local" reminder computation through it (replacing the implicit `process.env.TZ` fallback), without touching the already-correct browser-local display/timed-write path.
|
_Backlog items 999.1 (provider abstraction), 999.5 (self-service onboarding), 999.18 (CI dependency updates), 999.19 (dev-user full-app exercise), and 999.20 (dark mode) were promoted into v1.2 Phases 22, 26, 28, 24, and 27 respectively. Historical promotion provenance for v1.0/v1.1 backlog items (999.1/4/5/10/11/12/13/14/15/16) is preserved in `milestones/v1.1-ROADMAP.md`._
|
||||||
**Requirements**: TBD (decision contract D-01..D-07 from 18-CONTEXT.md)
|
|
||||||
**Depends on:** Phase 10 (admin role + `/admin` Settings + `app_config`); Phase 11 (all-day reminder computation this rewires). Independent of Phase 17. Phase 12 (setup wizard) not required — seeding is self-contained.
|
|
||||||
**Plans:** 4/4 plans complete
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 18-01-PLAN.md — TDD: getHouseholdTimezone(db) accessor + isValidIanaTimezone (D-05/D-06)
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
|
||||||
|
|
||||||
- [x] 18-02-PLAN.md — TDD: admin GET/PUT/seed timezone endpoints on adminRouter, requireAdmin + IANA validation + no-overwrite seed (D-01/D-02/D-03/D-04)
|
|
||||||
- [x] 18-03-PLAN.md — TDD: route all-day reminder TZ at reminderScheduler:247 + outboxWorker:501,607 through the accessor (D-05/D-06/D-07)
|
|
||||||
|
|
||||||
**Wave 3** *(blocked on Wave 2 completion)*
|
|
||||||
|
|
||||||
- [x] 18-04-PLAN.md — PWA Timezone section in /admin Settings (searchable IANA picker + detected-zone seed) + client fns (D-02/D-04)
|
|
||||||
|
|
||||||
**UI hint**: yes
|
|
||||||
|
|
||||||
### Phase 19: Local Auth (No-OIDC Mode)
|
|
||||||
|
|
||||||
**Goal:** Let an operator run FamilySync entirely on **local DB users with no OIDC** — username/password accounts and a local login flow that coexists with the Authelia OIDC path — and **optionally wire OIDC in later** by claiming/linking an existing local user to an OIDC identity. Removes the hard dependency on a deployed Authelia for small/solo self-hosters.
|
|
||||||
**Mode:** standard
|
|
||||||
**Depends on:** Phase 12 (Initial Setup Wizard) — builds directly on the pre-OIDC **local-user foundation** introduced there: nullable `users.oidc_iss`/`oidc_sub` + the claimed/pending marker, and the first-login-claims merge. Phase 19 generalizes that single bootstrap local user into a full local-account model + login.
|
|
||||||
**Requirements**: AUTH-LOCAL-01..AUTH-LOCAL-20 (derived during planning 2026-06-17) — local_credentials schema (01), scrypt hash/verify (02), login route (03), localAuthMiddleware (04), auth-mode endpoint (05), logout (06), admin create-member (07), admin reset (08), self-change (09), OIDC-link (10), break-glass CLI (11), LoginPage (12), admin UI (13), settings UI (14), routing gate (15), dev-bypass/harness rework (16), hasLocalCredential (17), de-Authelia copy (18), rate-limit/lockout (19), auth unit tests (20). Plus `LOCAL_SESSION_SECRET` env + boot assertion (D-05).
|
|
||||||
**Plans:** 5/5 plans complete
|
|
||||||
|
|
||||||
**Provenance:** Deferred from the Phase 12 discussion (2026-06-15) — see `.planning/phases/12-initial-setup-wizard/12-CONTEXT.md` §Deferred Ideas. The operator runs FamilySync this way themselves and wants no-OIDC operation as a first-class mode.
|
|
||||||
|
|
||||||
**Open questions for discuss/spec:**
|
|
||||||
|
|
||||||
- Password hashing/storage choice (e.g. argon2id/bcrypt) and how it sits alongside the env-only secret kernel from Phase 12.
|
|
||||||
- How local login coexists with `oidcAuthMiddleware` ordering in `apps/api/src/index.ts` (route-level auth strategy selection vs. a mode flag in `app_config`).
|
|
||||||
- The OIDC-link flow: claiming an existing local user into an `oidc_iss+oidc_sub` identity without violating the D-10 "identity is OIDC, never email" rule.
|
|
||||||
- Whether "local mode vs OIDC mode" is a deploy-time switch or both can be live simultaneously.
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 19-01-PLAN.md — Foundation (TDD): local_credentials schema + 0003 migration, scrypt hash/verify, local-session JWT helpers, LOCAL_SESSION_SECRET boot guard + generate-secrets, .dockerignore scripts exclusion (AUTH-LOCAL-01/02)
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1)*
|
|
||||||
|
|
||||||
- [x] 19-02-PLAN.md — Backend account mgmt (TDD): admin create/reset member, self-change password, hasLocalCredential, linkOidcToUser helper + /api/me/link-oidc (AUTH-LOCAL-07/08/09/10/17)
|
|
||||||
|
|
||||||
**Wave 3** *(blocked on Wave 2)*
|
|
||||||
|
|
||||||
- [x] 19-03-PLAN.md — Middleware + routes + wiring (TDD): localAuthMiddleware, /api/auth/mode, login (rate-limit/lockout) + logout, index.ts mount + OIDC-guard skip + /callback link branch, de-Authelia comments (AUTH-LOCAL-03/04/05/06/18/19/20)
|
|
||||||
|
|
||||||
**Wave 4** *(blocked on Wave 3; 04 + 05 parallel)*
|
|
||||||
|
|
||||||
- [x] 19-04-PLAN.md — PWA: LoginPage + BrandSlot + App.tsx gate + client.ts + AdminPage + SettingsSheet (AUTH-LOCAL-12/13/14/15)
|
|
||||||
- [x] 19-05-PLAN.md — Dev-bypass Option C + break-glass CLI + harness/CI rework + login.spec.ts (AUTH-LOCAL-11/16)
|
|
||||||
|
|
||||||
### Phase 20: Admin Member Editor & Form Declutter
|
|
||||||
|
|
||||||
**Goal:** Replace the per-member-row action buttons (Rotate/Add credential + Reset password) in the admin Members panel with a single edit affordance — clicking a member's name or an edit button opens a member-detail editor where an admin modifies all of that member's details in one place: display name, local-login password, and the Fastmail/CalDAV app password (calendar credential) — using clear, non-jargon labels that retire the confusing "Rotate" term. Also collapse the "Add member" section so its input fields are hidden behind a single "Add member" trigger by default, decluttering the panel. Client-side AdminPage + CredentialSheet rework over the existing `/api/admin` endpoints; no new auth/authorization boundary (seeded by the gripe that "Rotate" for the app password is not intuitive).
|
|
||||||
**Requirements**: TBD (refine in /gsd-discuss-phase 20 — open scope: which fields count as "all" (color swatch? admin toggle? OIDC link?), whether to keep any standalone reset-password flow, and the exact edit affordance — clickable name vs. row edit button)
|
|
||||||
**Depends on:** Phase 19
|
|
||||||
**Plans:** 3/3 plans complete
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
**Wave 1**
|
|
||||||
|
|
||||||
- [x] 20-01-PLAN.md — Server: PATCH /api/admin/members/:id (displayName + is_admin) with last-admin demotion guard (TDD) + isAdmin in GET /members
|
|
||||||
- [x] 20-02-PLAN.md — PWA API client: AdminMember.isAdmin field + updateMemberProfile fetcher (last-admin sentinel)
|
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
|
||||||
|
|
||||||
- [x] 20-03-PLAN.md — PWA: unified MemberEditorSheet (edit/create, per-section saves) + decluttered tappable Members panel; retire Rotate/Reset-password buttons
|
|
||||||
|
|||||||
+24
-21
@@ -1,36 +1,35 @@
|
|||||||
---
|
---
|
||||||
gsd_state_version: 1.0
|
gsd_state_version: 1.0
|
||||||
milestone: v1.1
|
milestone: v1.2
|
||||||
milestone_name: Operability & Polish
|
milestone_name: Multi-Provider, Theming & Zero-Setup
|
||||||
current_phase: 999.1
|
status: planning
|
||||||
current_phase_name: BACKLOG
|
last_updated: "2026-06-19T14:30:00.000Z"
|
||||||
status: "Phase 20 shipped — PR #25"
|
last_activity: 2026-06-19
|
||||||
stopped_at: Phase 20 UI-SPEC approved
|
|
||||||
last_updated: "2026-06-19T00:26:05.257Z"
|
|
||||||
last_activity: 2026-06-18
|
|
||||||
progress:
|
progress:
|
||||||
total_phases: 27
|
total_phases: 8
|
||||||
completed_phases: 13
|
completed_phases: 0
|
||||||
total_plans: 58
|
total_plans: 0
|
||||||
completed_plans: 57
|
completed_plans: 0
|
||||||
percent: 48
|
percent: 0
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State
|
# Project State
|
||||||
|
|
||||||
## Project Reference
|
## Project Reference
|
||||||
|
|
||||||
See: .planning/PROJECT.md (updated 2026-06-16)
|
See: .planning/PROJECT.md (updated 2026-06-18 after v1.1 milestone)
|
||||||
|
|
||||||
**Core value:** One color-coded family calendar (shared + personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store
|
**Core value:** One color-coded family calendar (shared + personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store
|
||||||
**Current focus:** Phase 20 — admin-member-editor-form-declutter
|
**Current focus:** Phase 21 — Zero-Setup DB Bootstrap (v1.2 roadmap created; awaiting plan)
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 999.1 — Treat Fastmail as one calendar provider; framework supports adding more providers (BACKLOG)
|
Phase: 21 of 28 (Zero-Setup DB Bootstrap) — first v1.2 phase
|
||||||
Plan: Not started
|
Plan: — (not yet planned)
|
||||||
Status: Phase 20 shipped — PR #25
|
Status: Ready to plan
|
||||||
Last activity: 2026-06-18
|
Last activity: 2026-06-19 — Completed quick task 260619-jp4: added app screenshots + README Features section
|
||||||
|
|
||||||
|
Progress: [░░░░░░░░░░] 0% (0/8 v1.2 phases)
|
||||||
|
|
||||||
### ✅ Resolved Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action)
|
### ✅ Resolved Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action)
|
||||||
|
|
||||||
@@ -209,6 +208,8 @@ Recent decisions affecting current work:
|
|||||||
|
|
||||||
### Roadmap Evolution
|
### Roadmap Evolution
|
||||||
|
|
||||||
|
- **v1.2 roadmap created (2026-06-19):** 8 phases (21–28), continuing v1.1 numbering (last phase 20). 11/11 requirements mapped to exactly one phase, no orphans, no duplicates. Dependency-ordered per research ARCHITECTURE.md "Dependency-Ordered Build Sequence": **21 Zero-Setup DB Bootstrap** (SETUP-05, no deps, unlocks auto-migrate) → **22 Provider Abstraction** (PROV-01, HIGH-risk, CalDAV-only/Fastmail unchanged, carries the `UNIQUE(user_id, provider_type)` migration) → **23 Multiple Reminders** (CAL-18, dep 22) and **24 Dev/Mock Provider** (TEST-03, dep 22) → **25 Google Calendar Provider** (CAL-16/17/PROV-02, deps 22/23/21) → **26 Self-Service Onboarding** (PROV-03/04, dep 25). **27 PWA Dark Mode** (THEME-01) is independent/parallelizable with 22–26. **28 Dependency Updates** (DEP-01) is last — after 27 (Schedule-X hold) and all other v1.2 work. Backlog promotions: 999.1→Provider Abstraction (22), 999.20→Dark Mode (27), 999.5→Self-Service Onboarding (26), 999.19→Dev/Mock Provider (24), 999.18→Dependency Updates (28). DEP-01 kept a standalone closing phase (not folded) to preserve its hard "last, after dark-mode-locked" ordering. Critical path: 21→22→23→25→26, then 28.
|
||||||
|
|
||||||
- Phase 6 added (2026-06-07): UX Polish — all-day visual distinction, event-form date/recurrence behavior, recurring-series edit, auth-flow smoothing. Candidate scope pulls from backlog 999.2/999.3/999.6/999.7/999.8/999.9.
|
- Phase 6 added (2026-06-07): UX Polish — all-day visual distinction, event-form date/recurrence behavior, recurring-series edit, auth-flow smoothing. Candidate scope pulls from backlog 999.2/999.3/999.6/999.7/999.8/999.9.
|
||||||
- Phase 6 complete (2026-06-10): all 6 plans executed + 2 phase-level UX fixes (AppNav persistence + BottomTabBar desktop hide). Residual device-only checkpoints documented above.
|
- Phase 6 complete (2026-06-10): all 6 plans executed + 2 phase-level UX fixes (AppNav persistence + BottomTabBar desktop hide). Residual device-only checkpoints documented above.
|
||||||
- Backlog reviewed (2026-06-10, /gsd-review-backlog): removed 6 stale duplicates (999.2/3/6/7/8/9 — already promoted into Phase 6) from the Backlog section + deleted the 999.2 dir; kept 999.1/4/5/10/11/12/13; added 999.14 (Gitea CI, promoted from STATE pending todo); archived stale kickoff-new-project todo.
|
- Backlog reviewed (2026-06-10, /gsd-review-backlog): removed 6 stale duplicates (999.2/3/6/7/8/9 — already promoted into Phase 6) from the Backlog section + deleted the 999.2 dir; kept 999.1/4/5/10/11/12/13; added 999.14 (Gitea CI, promoted from STATE pending todo); archived stale kickoff-new-project todo.
|
||||||
@@ -256,6 +257,9 @@ Recent decisions affecting current work:
|
|||||||
| 260613-dmw | Exclude `.gitea/**` from the CI `changes` `code` paths-filter so workflow-only PRs skip the heavy api/harness jobs (treated like docs) while fast-checks + gate still run. Single `- '!.gitea/**'` negation appended after the yml/yaml globs (index 11 vs 5). Rides along on the Phase 16 branch / PR #15. | 2026-06-13 | 2d329a9 | | [260613-dmw-exclude-gitea-workflow-config-changes-fr](./quick/260613-dmw-exclude-gitea-workflow-config-changes-fr/) |
|
| 260613-dmw | Exclude `.gitea/**` from the CI `changes` `code` paths-filter so workflow-only PRs skip the heavy api/harness jobs (treated like docs) while fast-checks + gate still run. Single `- '!.gitea/**'` negation appended after the yml/yaml globs (index 11 vs 5). Rides along on the Phase 16 branch / PR #15. | 2026-06-13 | 2d329a9 | | [260613-dmw-exclude-gitea-workflow-config-changes-fr](./quick/260613-dmw-exclude-gitea-workflow-config-changes-fr/) |
|
||||||
| 260613-fp9 | `.gitea`/`.planning`-only pushes to main no longer trigger the Docker publish — added `paths-ignore: ['.gitea/**', '.planning/**']` under `on.push` in `.gitea/workflows/publish.yml` (skips only when EVERY changed file matches; mixed code+docs pushes still publish). `.dockerignore` already excludes `.planning` so the image is byte-identical. Done in isolated worktree (phase-10 agent held main tree). | 2026-06-13 | cd5a88c | | [260613-fp9-gitea-and-planning-pushes-should-not-tri](./quick/260613-fp9-gitea-and-planning-pushes-should-not-tri/) |
|
| 260613-fp9 | `.gitea`/`.planning`-only pushes to main no longer trigger the Docker publish — added `paths-ignore: ['.gitea/**', '.planning/**']` under `on.push` in `.gitea/workflows/publish.yml` (skips only when EVERY changed file matches; mixed code+docs pushes still publish). `.dockerignore` already excludes `.planning` so the image is byte-identical. Done in isolated worktree (phase-10 agent held main tree). | 2026-06-13 | cd5a88c | | [260613-fp9-gitea-and-planning-pushes-should-not-tri](./quick/260613-fp9-gitea-and-planning-pushes-should-not-tri/) |
|
||||||
| 260613-ndv | Isolate local apps/api integration tests to a dedicated `familysync_test` DB so test runs stop polluting the dev `familysync` DB. New CI-gated vitest globalSetup root-provisions (CREATE DATABASE + GRANT) + migrates + truncate-resets `familysync_test` each run; `vitest.config.ts` forces `DB_NAME=familysync_test` for local workers (no-op under CI, so CI's `familysync` service DB + db:migrate are untouched). Verified: dev `familysync` users stays 3 across a run, `familysync_test` resets (186→93, not doubled), 244/244 tests pass (flaky list_shares timeout gone), typecheck 0. Branch off main. | 2026-06-13 | 07d5161 | Verified | [260613-ndv-wire-apps-api-integration-tests-to-a-ded](./quick/260613-ndv-wire-apps-api-integration-tests-to-a-ded/) |
|
| 260613-ndv | Isolate local apps/api integration tests to a dedicated `familysync_test` DB so test runs stop polluting the dev `familysync` DB. New CI-gated vitest globalSetup root-provisions (CREATE DATABASE + GRANT) + migrates + truncate-resets `familysync_test` each run; `vitest.config.ts` forces `DB_NAME=familysync_test` for local workers (no-op under CI, so CI's `familysync` service DB + db:migrate are untouched). Verified: dev `familysync` users stays 3 across a run, `familysync_test` resets (186→93, not doubled), 244/244 tests pass (flaky list_shares timeout gone), typecheck 0. Branch off main. | 2026-06-13 | 07d5161 | Verified | [260613-ndv-wire-apps-api-integration-tests-to-a-ded](./quick/260613-ndv-wire-apps-api-integration-tests-to-a-ded/) |
|
||||||
|
| 260618-smr | Remove unused Redis service and all references — Redis confirmed unused at runtime (no ioredis/redis client import, no `REDIS_*` env, not a dependency in any package.json). Dropped the `redis` service from both compose files and cleaned all references in CLAUDE.md, README.md, and docs/* + e2e config. Kept the in-memory-vs-Redis design-rationale comments (D-12/D-18) in listEmitter/reminderScheduler/linkNonceStore/localAuth. `docker compose config` parses clean (0 redis); `format:check` green. Branch off main. | 2026-06-18 | 0b42666 | Verified | [260618-smr-remove-unused-redis-service-and-referenc](./quick/260618-smr-remove-unused-redis-service-and-referenc/) |
|
||||||
|
| 260618-tg2 | Persistent CI dependency caches — point all 4 CI `pnpm install` steps at a host-mounted `/pnpm-store` (`--store-dir /pnpm-store --prefer-offline`) and persist Playwright browsers via `PLAYWRIGHT_BROWSERS_PATH=/ms-playwright` on the harness job; added BuildKit `--mount=type=cache` to all 3 Dockerfile install stages + `DOCKER_BUILDKIT=1` on the publish build. Avoids `actions/cache` (D-PROBE-04 timeout). In-repo only — requires act_runner `config.yaml` `container.options` host mounts (manual host change). Verdaccio deferred. Branch off main. | 2026-06-18 | 6e93e24 | Verified | [260618-tg2-persistent-ci-dependency-caches-pnpm-sto](./quick/260618-tg2-persistent-ci-dependency-caches-pnpm-sto/) |
|
||||||
|
| 260619-jp4 | Add six app screenshots (Calendar, Lists, List detail, Login, Setup wizard, Admin) to `docs/screenshots/` and rebuild the README intro into a "Features" section that expands the description, one screenshot per capability. Captured via playwright-cli (Chromium) against the live dev stack with demo data mocked per-browser via `route` interception — nothing written to the real Fastmail account. prettier + md:lint green. Branch: gsd/v1.2-multi-provider-theming-zero-setup. | 2026-06-19 | 23dc3cd | | [260619-jp4-add-app-screenshots-to-readme](./quick/260619-jp4-add-app-screenshots-to-readme/) |
|
||||||
|
|
||||||
## Deferred Items
|
## Deferred Items
|
||||||
|
|
||||||
@@ -281,5 +285,4 @@ Resume file: .planning/phases/20-admin-member-editor-form-declutter/20-UI-SPEC.m
|
|||||||
|
|
||||||
## Operator Next Steps
|
## Operator Next Steps
|
||||||
|
|
||||||
- **Phase 8 is complete.** CI pipeline is fully operational on the self-hosted Gitea runner.
|
- v1.2 roadmap created (Phases 21–28). Plan the first phase with `/gsd-plan-phase 21`.
|
||||||
- Next: `/gsd-plan-phase 9` (Faster Write-Back — fully independent, lowest risk) or `/gsd-plan-phase 10` (Admin Role & Settings — carries the v1.1 DB migration that Phases 11 & 12 depend on). These can run in parallel once planned.
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"firecrawl": false,
|
"firecrawl": false,
|
||||||
"exa_search": false,
|
"exa_search": false,
|
||||||
"git": {
|
"git": {
|
||||||
"branching_strategy": "phase",
|
"branching_strategy": "milestone",
|
||||||
"create_tag": true,
|
"create_tag": true,
|
||||||
"phase_branch_template": "gsd/phase-{phase}-{slug}",
|
"phase_branch_template": "gsd/phase-{phase}-{slug}",
|
||||||
"milestone_branch_template": "gsd/{milestone}-{slug}",
|
"milestone_branch_template": "gsd/{milestone}-{slug}",
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Requirements Archive: v1.1 Operability & Polish
|
||||||
|
|
||||||
|
**Archived:** 2026-06-19
|
||||||
|
**Status:** SHIPPED
|
||||||
|
|
||||||
|
For current requirements, see `.planning/REQUIREMENTS.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements: FamilySync — v1.1 "Operability & Polish"
|
||||||
|
|
||||||
|
**Defined:** 2026-06-10
|
||||||
|
**Milestone:** v1.1 (continues from v1.0 MVP, shipped 2026-06-10)
|
||||||
|
**Core Value:** The household can see and co-edit one color-coded family calendar (shared + each member's personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store. v1.1 makes that app **configurable, administrable, and maintainable** without hand-editing env files or the database.
|
||||||
|
|
||||||
|
REQ-IDs continue v1.0 numbering (CAL ≤12, NOTIF ≤3 already used). New categories: ADMIN, SETUP, CI, TEST.
|
||||||
|
|
||||||
|
## v1.1 Requirements
|
||||||
|
|
||||||
|
Each requirement maps to exactly one roadmap phase (see Traceability).
|
||||||
|
|
||||||
|
### Calendar — Per-event reminders & write-back latency
|
||||||
|
|
||||||
|
- [x] **CAL-13**: User can choose a reminder lead time when creating or editing an event from a preset list (None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d), with **"None" as the default**; the choice is serialized as a VALARM on the event written back to Fastmail.
|
||||||
|
- [x] **CAL-14**: Editing an event **preserves any existing reminder/VALARM** set in another client (Fastmail or native) — reminders are never silently stripped on round-trip.
|
||||||
|
- [x] **CAL-15**: A created, edited, or deleted event reaches Fastmail within ~2 seconds (event-driven outbox drain) instead of up to ~15s, while preserving the optimistic-202 accept and all outbox durability guarantees (create-before-delete ordering, drain concurrency guard, fresh-etag-before-PUT, per-uid exactly-once).
|
||||||
|
|
||||||
|
### Notifications — Variable-lead reminder scheduling
|
||||||
|
|
||||||
|
- [x] **NOTIF-04**: An event reminder push fires at the event's **chosen lead time**, not a hardcoded 15-minute lead.
|
||||||
|
- [x] **NOTIF-05**: An event with **no reminder set produces no reminder push** (no default 15-min fire).
|
||||||
|
- [x] **NOTIF-06**: An all-day event's reminder fires at a sensible local time (9 AM on the alert day), not at midnight, and reminder delivery remains exactly-once across catch-up scans and rescheduled events.
|
||||||
|
|
||||||
|
### Administration — Settings section (role-gated)
|
||||||
|
|
||||||
|
> Role-agnostic design: ship operator-only (`is_admin`), but the role check is member-count-agnostic so more admins can be added later without rework.
|
||||||
|
|
||||||
|
- [x] **ADMIN-01**: An admin can view household members and update (rotate / re-enter) a member's Fastmail app password from the UI; the credential is validated against CalDAV before saving and stored encrypted (existing `APP_PASSWORD_ENCRYPTION_KEY` path) — the password is never displayed, logged, or echoed.
|
||||||
|
- [x] **ADMIN-02**: An admin can designate which synced calendar is the shared family calendar (set `calendars.is_shared`) from the UI, replacing the manual DB write.
|
||||||
|
- [x] **ADMIN-03**: Admin Settings routes and UI are gated by a role check; a non-admin member cannot reach or invoke them.
|
||||||
|
|
||||||
|
### Setup — First-run configuration wizard
|
||||||
|
|
||||||
|
- [x] **SETUP-01**: On first run (no admin/credentials configured), the operator is guided through a setup wizard to define bootstrap configuration (app/external URL, OIDC client, session secret, encryption key, VAPID keypair, MariaDB connection, first member's Fastmail app password) instead of hand-editing `.env` / `docker-compose.yml`.
|
||||||
|
- [x] **SETUP-02**: The wizard **validates each input before completing** — DB connectivity test, VAPID private key decodes to 32 bytes and pairs with the public key, OIDC discovery resolves, and the Fastmail app password reaches CalDAV (PROPFIND).
|
||||||
|
- [x] **SETUP-03**: The wizard generates secrets (session secret, encryption key, VAPID keypair) for the operator to copy into env; secrets are **never written to the database or returned in a response body**.
|
||||||
|
- [x] **SETUP-04**: Once setup is complete, the setup endpoints are no longer accessible (guard checked on every invocation, not only at startup).
|
||||||
|
|
||||||
|
### CI — Gitea continuous integration
|
||||||
|
|
||||||
|
- [x] **CI-01**: Every pull request targeting `main` runs full regression — lint, typecheck (both apps), unit tests, API integration tests against a MariaDB service container, **and the Phase 7 mobile Playwright harness as a UI-regression step (CI brings up the API + PWA dev servers + MariaDB service container with `DEV_AUTH_BYPASS` in the runner and runs the harness specs headlessly against the authed PWA)** — and the result gates the merge.
|
||||||
|
- [x] **CI-02**: On merge to `main`, the API Docker image is built and published to the Gitea container registry.
|
||||||
|
|
||||||
|
### Test — Mobile-emulated authed browser harness
|
||||||
|
|
||||||
|
- [x] **TEST-01**: The assistant can drive the PWA in a **mobile-emulated viewport** (device profile + mobile UA + touch) for automated UI/layout verification.
|
||||||
|
- [x] **TEST-02**: Automated runs reach the **authenticated** PWA via the existing `DEV_AUTH_BYPASS` on the host-side dev stack (no manual login, no Authelia/OIDC mocking). Targets the dev build; real prod-service-worker mobile testing is out of scope (see below). The harness specs are also consumed by Phase 8 (Gitea CI) as the PR UI-regression step.
|
||||||
|
|
||||||
|
## Future Requirements (deferred, not in v1.1)
|
||||||
|
|
||||||
|
- **Multiple reminders per event** (2× VALARM) — stretch; deferred to v1.2.
|
||||||
|
- **Self-service provider onboarding** (backlog 999.5) — a member adds their _own_ Fastmail app password on first login. v1.1 covers this admin-managed (ADMIN-01); self-service deferred.
|
||||||
|
- **Calendar provider abstraction** (backlog 999.1) — provider interface so Fastmail is one of several backends.
|
||||||
|
- **Android PWA install walkthrough** verified on a real device (carried from v1.0).
|
||||||
|
- **Wizard re-run / reconfigure** flow after first setup.
|
||||||
|
|
||||||
|
## Out of Scope (explicit exclusions)
|
||||||
|
|
||||||
|
- **Notification-preferences UI, reminder snooze** — over-build for a 2-member household; the per-event selector (CAL-13) is sufficient.
|
||||||
|
- **Admin audit log, health dashboard, user-management/CRUD** — scope creep for a tiny self-hosted app.
|
||||||
|
- **Real-device iOS push / standalone CI** — remains a human/device gate, as in v1.0; the mobile harness covers responsive layout + authed flows only, not iOS-Safari-standalone behavior.
|
||||||
|
- **Mobile testing against the prod service-worker build past real Authelia** — `DEV_AUTH_BYPASS` only reaches the dev build (no real SW). A reusable Authelia storage-state to drive the prod-SW PWA is deferred; not worth the complexity for v1.1's layout/flow goal.
|
||||||
|
- **Redis pub/sub for the outbox drain** — the drain is single-process by design; an in-process EventEmitter is correct. (Redis stays for list SSE.)
|
||||||
|
- **node-cron** — silently skips ticks in the long-lived process; schedulers stay on `setInterval`.
|
||||||
|
- **drizzle-kit push** — emits a false destructive diff on populated MariaDB; migrations use generate+migrate.
|
||||||
|
|
||||||
|
## Traceability
|
||||||
|
|
||||||
|
Maps each REQ-ID to its phase. v1.1 phases continue v1.0 numbering (v1.0 ended at Phase 6) → v1.1 starts at Phase 7. **Coverage: 17/17 v1.1 requirements mapped, no orphans, no duplicates.**
|
||||||
|
|
||||||
|
| REQ-ID | Phase | Status |
|
||||||
|
| -------- | -------------------------------- | -------- |
|
||||||
|
| TEST-01 | Phase 7 (Mobile Test Harness) | Complete |
|
||||||
|
| TEST-02 | Phase 7 (Mobile Test Harness) | Complete |
|
||||||
|
| CI-01 | Phase 8 (Gitea CI) | Complete |
|
||||||
|
| CI-02 | Phase 8 (Gitea CI) | Complete |
|
||||||
|
| CAL-15 | Phase 9 (Faster Write-Back) | Complete |
|
||||||
|
| ADMIN-01 | Phase 10 (Admin Role & Settings) | Complete |
|
||||||
|
| ADMIN-02 | Phase 10 (Admin Role & Settings) | Complete |
|
||||||
|
| ADMIN-03 | Phase 10 (Admin Role & Settings) | Complete |
|
||||||
|
| CAL-13 | Phase 11 (Per-Event Reminders) | Complete |
|
||||||
|
| CAL-14 | Phase 11 (Per-Event Reminders) | Complete |
|
||||||
|
| NOTIF-04 | Phase 11 (Per-Event Reminders) | Complete |
|
||||||
|
| NOTIF-05 | Phase 11 (Per-Event Reminders) | Complete |
|
||||||
|
| NOTIF-06 | Phase 11 (Per-Event Reminders) | Complete |
|
||||||
|
| SETUP-01 | Phase 12 (Initial Setup Wizard) | Complete |
|
||||||
|
| SETUP-02 | Phase 12 (Initial Setup Wizard) | Complete |
|
||||||
|
| SETUP-03 | Phase 12 (Initial Setup Wizard) | Complete |
|
||||||
|
| SETUP-04 | Phase 12 (Initial Setup Wizard) | Complete |
|
||||||
|
|
||||||
|
**DB foundation note:** The v1.1 schema migration (`users.is_admin`, `calendar_events.reminder_lead_minutes`, `app_config` table) is not a standalone requirement — it is carried by **Phase 10 (Admin Role & Settings)** (which owns is_admin + app_config) and consumed by **Phase 11 (Per-Event Reminders)** (reminder_lead_minutes) and **Phase 12 (Initial Setup Wizard)** (app_config.setup_complete). Folded per ARCHITECTURE.md ordering rather than created as a migration-only phase. This makes Phase 10 the head of the admin chain (10 → 11, 10 → 12).
|
||||||
@@ -0,0 +1,772 @@
|
|||||||
|
# Roadmap: FamilySync
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
- ✅ **v1.0 MVP** — Phases 1–6 (shipped 2026-06-10) — see [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md)
|
||||||
|
- ✅ **v1.1 Operability & Polish** — Phases 7–20 (shipped 2026-06-18) — mobile test harness, Gitea CI (runs the harness), faster write-back, in-app admin, per-event reminders, guided setup, real lint gate, desktop e2e, doc-only CI skip + markdown lint, CI dependency audit + security checks + image hygiene, UI optimization & polish, auto timezone detection, local auth (no-OIDC mode), admin member editor & declutter
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>✅ v1.0 MVP (Phases 1–6) — SHIPPED 2026-06-10</summary>
|
||||||
|
|
||||||
|
- [x] Phase 1: Foundation + Broker Spike (4/4 plans) — completed 2026-06-04
|
||||||
|
- [x] Phase 2: Calendar Display (5/5 plans) — completed 2026-06-05
|
||||||
|
- [x] Phase 3: Event Write-Back + PWA Install (12/12 plans) — completed 2026-06-07
|
||||||
|
- [x] Phase 4: Shared Lists + Live Sync (7/7 plans) — completed 2026-06-09
|
||||||
|
- [x] Phase 5: Web Push Notifications (8/8 plans) — completed 2026-06-10
|
||||||
|
- [x] Phase 6: UX Polish (6/6 plans) — completed 2026-06-10
|
||||||
|
|
||||||
|
Full phase detail archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### ✅ v1.1 Operability & Polish (Phases 7–20) — SHIPPED 2026-06-18
|
||||||
|
|
||||||
|
Make FamilySync configurable, administrable, and maintainable for real multi-member use — without hand-editing env files or the database. The new critical path runs **mobile test harness → Gitea CI** (CI consumes the harness specs for UI regression), and the **admin role → reminders / setup wizard** chain (a single `/api/admin` + `/api/setup` route surface carrying the v1.1 DB migration). Faster write-back is a fully independent track.
|
||||||
|
|
||||||
|
- [x] **Phase 7: Mobile Test Harness** - Mobile-emulated, authenticated PWA browser harness so the assistant (and CI) can catch mobile-only defects (completed 2026-06-11)
|
||||||
|
- [x] **Phase 8: Gitea CI** - Full regression on PR to main (lint/typecheck/unit/API-integration vs a MariaDB service container **+ the Phase 7 mobile harness as a UI-regression step against a CI-hosted dev stack**) + Docker image publish on merge (completed 2026-06-11)
|
||||||
|
- [x] **Phase 9: Faster Write-Back** - Event-driven outbox drain so edits land in ~1-2s instead of ~15s, preserving every outbox durability guarantee (completed 2026-06-12)
|
||||||
|
- [x] **Phase 10: Admin Role & Settings** - DB foundation (is_admin / reminder_lead / app_config) + role-gated admin UI to rotate app passwords and designate the shared calendar (completed 2026-06-13)
|
||||||
|
- [x] **Phase 11: Per-Event Reminders** - Reminder selector on the event form (incl. "None") serialized as VALARM, with a variable-lead scheduler that honors each event's choice (completed 2026-06-14)
|
||||||
|
- [x] **Phase 12: Initial Setup Wizard** - First-run validated bootstrap of env/VAPID/DB/OIDC + first app password, reusing the admin route surface (completed 2026-06-16)
|
||||||
|
- [x] **Phase 13: Real Lint Gate (ESLint)** - Wire ESLint flat config (typescript-eslint + React) across both apps so the Phase 8 CI lint slot actually fails on violations instead of no-op'ing (completed 2026-06-12)
|
||||||
|
- [x] **Phase 14: Desktop E2E Coverage** - Add a Desktop Chrome Playwright profile + make the mobile-authored specs desktop-safe so the Phase 8 regression gate validates desktop, not just mobile (completed 2026-06-12)
|
||||||
|
- [x] **Phase 15: Doc-Only CI Skip + Markdown Lint** - Aggregate-gate the slow api/harness CI jobs so doc-only PRs to main merge without running them (no branch-protection deadlock), and add markdownlint to `fast-checks` so docs get a fast format+lint gate (promoted from backlog 999.17) (completed 2026-06-12)
|
||||||
|
- [x] **Phase 16: CI Dependency Audit, Security Checks & Image Hygiene** - Extend Gitea CI with outdated-dependency reporting + vulnerability audit + a baseline of additional security checks, and enforce the dev/prod image boundary so no dev-bypass, secret, or family data ships in published images (absorbs backlog 999.17); independent of the admin chain (completed 2026-06-13)
|
||||||
|
- [x] **Phase 17: UI Optimization & Polish** - Phone-layout polish + branding + theme groundwork: fix the long-standing phone-layout overlap where the fixed BottomTabBar covers the New Event FAB and the colour legend (+ small-viewport sweep), finish the branding assets (real FamilySync logo into the BrandSlot seam + a complete favicon/PWA-icon set replacing the placeholder stubs), and restructure tokens.css into a themeable token layer (light-only groundwork for future dark mode). Shipped dark theme → backlog 999.20; broader styling refresh → backlog 999.21 (future milestone) (completed 2026-06-18)
|
||||||
|
|
||||||
|
## Phase Details
|
||||||
|
|
||||||
|
> v1.0 phase detail (Phases 1–6) is archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md).
|
||||||
|
|
||||||
|
### Phase 7: Mobile Test Harness
|
||||||
|
|
||||||
|
**Goal**: The assistant can drive the PWA in a mobile-emulated, authenticated browser context against the host-side dev stack, so mobile-only layout and flow defects can be caught automatically instead of only by the operator on real devices. This harness is also the artifact Phase 8 (CI) runs for UI regression.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Nothing (fully independent; goes first. One new dev dependency `@playwright/test` in `apps/pwa`; no backend changes).
|
||||||
|
**Requirements**: TEST-01, TEST-02
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
|
||||||
|
1. An automated run can load the PWA in a mobile-emulated viewport (device profile + mobile UA + touch) and assert on responsive layout / tap targets.
|
||||||
|
2. The automated run reaches the authenticated PWA via the existing `DEV_AUTH_BYPASS` on the host-side dev stack — no manual login and no Authelia/OIDC mocking.
|
||||||
|
3. The harness runs repeatably day-over-day without re-capturing any session state (no stale storage-state failures).
|
||||||
|
4. The harness specs are structured so they can run headlessly in CI (Phase 8) against a dev stack the runner brings up — no dependence on a developer's already-running host stack.
|
||||||
|
|
||||||
|
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||||
|
|
||||||
|
- **No stale storage-state** (Pitfall 14): use `DEV_AUTH_BYPASS=true` for the automated harness rather than a checked-in storage-state.json with an expiring session cookie; decide the auth strategy before the first test.
|
||||||
|
- **Service worker block** (Pitfall 15): set `serviceWorkers: 'block'` (or explicitly unregister) in the context so a previous run's SW does not intercept requests / return stale cached responses; verify no SW-sourced responses in the trace.
|
||||||
|
- Hard constraints: targets the dev build via `DEV_AUTH_BYPASS` (DEV_AUTH_BYPASS user 1 has no CalDAV credential/calendars — verify layout/flows, not live event-create); real prod-service-worker / iOS-Safari-standalone mobile testing stays a human/device gate (out of scope).
|
||||||
|
|
||||||
|
**Plans**: 4 plans (3 waves)Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 07-01-PLAN.md — Harness foundation: @playwright/test + WebKit/Chromium browsers, playwright.config.ts (iPhone/WebKit + Pixel/Chromium matrix, serviceWorkers block, env baseURL, vite webServer), vitest exclude, scripts (Wave 1)
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 07-02-PLAN.md — global-setup.ts: /health readiness poll + deterministic mysql2 reset-and-seed (calendar id 10 INSERT IGNORE guard, list + items) + e2e README/guardrails (Wave 2)
|
||||||
|
|
||||||
|
**Wave 3** *(blocked on Wave 2 completion)*
|
||||||
|
|
||||||
|
- [x] 07-03-PLAN.md — layout.spec.ts: tap targets >=44px, no overflow, in-viewport, accessible names (UI-SPEC Rules 1-4) + harness self-validation injected-defect proofs (Wave 3)
|
||||||
|
- [x] 07-04-PLAN.md — calendar.spec.ts + lists.spec.ts: populated/empty/error states (Rules 4/5) + DEV_AUTH_BYPASS auth-reached + no-SW-controller precondition (Wave 3)
|
||||||
|
|
||||||
|
**UI hint**: yes
|
||||||
|
|
||||||
|
### Phase 8: Gitea CI
|
||||||
|
|
||||||
|
**Goal**: Every PR to `main` runs a full regression that gates the merge — lint, typecheck, unit, API-integration against a MariaDB service container, **and the Phase 7 mobile Playwright harness as a UI-regression step against a CI-hosted dev stack** — and a merge to `main` builds and publishes the API Docker image, all on the existing self-hosted Gitea Actions runner.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Phase 7 (the PR regression runs the Phase 7 mobile harness specs as its UI-regression step; without the harness there is nothing to run). No other code dependencies. Start with a runner-probe step.
|
||||||
|
**Requirements**: CI-01, CI-02
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
|
||||||
|
1. Opening or updating a PR targeting `main` triggers a workflow that runs lint, typecheck (both apps), unit tests, and API integration tests against a MariaDB service container — and a failing run blocks the merge.
|
||||||
|
2. The API integration tests connect to the service-container MariaDB (DB_HOST=127.0.0.1, service creds) and pass reliably on a cold first run, not only on re-run.
|
||||||
|
3. The same PR workflow brings up the dev stack inside the runner — the API dev server, the PWA dev server, and the MariaDB service container, with `DEV_AUTH_BYPASS=true` — and runs the Phase 7 mobile Playwright harness specs headlessly against that authed PWA; a harness failure blocks the merge.
|
||||||
|
4. The harness step waits for both the API and PWA dev servers to be ready (readiness probe / poll) before launching Playwright, so it does not flake on startup races.
|
||||||
|
5. On merge to `main`, the API Docker image is built and pushed to the Gitea container registry under a sensible tag.
|
||||||
|
6. Registry credentials never appear in plaintext in the CI logs.
|
||||||
|
|
||||||
|
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||||
|
|
||||||
|
- **Runner-probe first** (Pitfall 12): the first workflow only probes `node --version` / `pnpm --version` / Docker access on the `self-hosted` runner before any test or build steps are designed; pin Node 22 explicitly, do not assume `actions/setup-node` works as on GitHub.
|
||||||
|
- **MariaDB readiness wait** (Pitfall 11): add an explicit readiness loop (e.g. `healthcheck.sh --connect --innodb_initialized`, NOT `mysqladmin ping` which is removed in MariaDB 11) before any `drizzle-kit migrate` / integration test step; healthy ≠ accepting connections.
|
||||||
|
- **Dev-stack readiness races (NEW for the harness step):** running the PWA and API dev servers *inside* CI adds startup/readiness races on top of the MariaDB-11 readiness race. The harness step must wait for **both** the API and PWA dev servers to be accepting connections (poll their URLs / health endpoints) before Playwright launches — do not race the browser against a not-yet-listening server. Run with `DEV_AUTH_BYPASS=true` so the harness reaches the authed PWA exactly as in Phase 7.
|
||||||
|
- **--password-stdin** (Pitfall 13): `docker login` via `--password-stdin` with the token piped from a registered Gitea secret (PAT with `write:package`); never `-p $TOKEN` on the command line.
|
||||||
|
- Hard constraints: API integration tests need a real MariaDB and live in `apps/api/tests/` (never `src/`); cache the pnpm store; Drizzle generate+migrate to set up the CI DB schema; the harness step reuses the Phase 7 specs unchanged (CI owns only the stack bring-up + readiness wait, not the spec content).
|
||||||
|
|
||||||
|
**Plans**: 4 plans (4 waves)Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 08-01-PLAN.md — Runner probe + operator runner/PAT registration (W0; answers the Docker-vs-host fork)
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 08-02-PLAN.md — ci.yml: fast-checks (lint/typecheck/PWA unit) + API job (MariaDB service + migrate + DB-backed tests)
|
||||||
|
|
||||||
|
**Wave 3** *(blocked on Wave 2 completion)*
|
||||||
|
|
||||||
|
- [x] 08-03-PLAN.md — ci.yml: harness job (dev-stack bring-up + readiness waits + Phase 7 Playwright specs, both profiles)
|
||||||
|
|
||||||
|
**Wave 4** *(blocked on Wave 3 completion)*
|
||||||
|
|
||||||
|
- [x] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1-<sha> via --password-stdin)
|
||||||
|
|
||||||
|
**UI hint**: yes
|
||||||
|
|
||||||
|
### Phase 9: Faster Write-Back
|
||||||
|
|
||||||
|
**Goal**: A created, edited, or deleted event reaches Fastmail within ~1-2 seconds (event-driven outbox drain) instead of waiting up to ~15s for the next interval tick — with every existing durability guarantee intact.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Nothing (fully independent; the only new artifact is a zero-dependency in-process EventEmitter, `lib/outboxTrigger.ts`). Can run in parallel with any other v1.1 track.
|
||||||
|
**Requirements**: CAL-15
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
|
||||||
|
1. After creating/editing/deleting an event, the change lands in Fastmail in ~1-2s in the common case (drain is signalled on enqueue, not waited-for on the interval) — observable as the change appearing in the Fastmail native app well before the old ~15s window.
|
||||||
|
2. The route handler still returns an optimistic 202 immediately and never makes a CalDAV call inline — the event-driven signal is fire-and-forget.
|
||||||
|
3. Edit-as-move still writes the new event before deleting the old one (create-before-delete ordering preserved); no event is ever lost when a move drains under rapid enqueues.
|
||||||
|
4. No duplicate CalDAV PUTs occur for the same outbox row when the signal and the 15s fallback interval overlap (exactly-once per uid preserved).
|
||||||
|
5. The 15s `setInterval` fallback still runs and recovers any rows missed by the signal path (startup catch-up, transient errors).
|
||||||
|
|
||||||
|
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||||
|
|
||||||
|
- **No double-drain** (Pitfall 5): the trigger must set a `drainRequested` flag funnelled through the single setInterval-controlled path / the existing `isDraining` guard — never call `runOutboxDrain()` directly from the signal in a way that bypasses the guard or escapes the error-caught wrapper.
|
||||||
|
- **Create-before-delete under concurrent enqueues** (Pitfall 6): enqueue CREATE before DELETE; do not fire the signal between the two inserts of a move (publish after both inserts / after the transaction commits).
|
||||||
|
- Hard constraints: `setInterval` only (no node-cron); single-process by design — **no Redis** for the drain (Redis stays for list SSE); all outbox guarantees (fresh-etag-before-PUT, 412 conflict flow, per-uid exactly-once) unchanged.
|
||||||
|
|
||||||
|
**Plans**: 2 plans (2 waves)
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 09-01-PLAN.md — TDD: outboxTrigger.ts (zero-dep EventEmitter signal) + scheduleOutboxDrain wrapper / drainRequested trailing-re-drain loop + initOutboxTrigger in outboxWorker.ts; trigger-wiring tests (SC-1, SC-4/D-07, D-05) (Wave 1)
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 09-02-PLAN.md — Four post-commit signalOutboxDrain() publish sites in events.ts (create / edit-as-move-after-transaction / same-cal update / delete) + initOutboxTrigger() startup wiring under isMainModule() in index.ts (Wave 2)
|
||||||
|
|
||||||
|
### Phase 10: Admin Role & Settings
|
||||||
|
|
||||||
|
**Goal**: An admin can manage household configuration that previously required manual DB writes — rotating a member's Fastmail app password and designating the shared family calendar — from a role-gated in-app Settings section, on top of the v1.1 DB foundation this phase introduces.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Nothing required upstream; this phase **carries the v1.1 DB migration** (users.is_admin, calendar_events.reminder_lead_minutes, app_config table) that Phases 11 and 12 build on. It is the head of the admin chain (10 → 11, 10 → 12).
|
||||||
|
**Requirements**: ADMIN-01, ADMIN-02, ADMIN-03
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
|
||||||
|
1. An admin sees an Admin section in Settings and can list household members with their credential status; a non-admin member never sees it and cannot invoke any `/api/admin/*` route (gets 403).
|
||||||
|
2. An admin can enter or rotate a member's Fastmail app password; it is validated against CalDAV (PROPFIND) before saving and stored encrypted — and the password is never displayed, echoed in a response, or logged.
|
||||||
|
3. An admin can pick which synced calendar is the shared family calendar from a list, and the `calendars.is_shared` flag updates accordingly (replacing the manual `UPDATE calendars SET is_shared=1` step).
|
||||||
|
4. The role check is role-agnostic and member-count-agnostic: it gates on `users.is_admin`, so more admins can be added later without reworking the guard.
|
||||||
|
5. The DB migration (is_admin, reminder_lead_minutes, app_config) is applied via generate+migrate and is in place for downstream phases (reminder_lead_minutes for Phase 11, app_config.setup_complete for Phase 12).
|
||||||
|
|
||||||
|
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||||
|
|
||||||
|
- **Admin role check inside the sub-router** (Pitfall 9): apply `requireAdmin` with `.use('*', ...)` inside `adminRouter`, not only at the parent mount; integration test must assert 403 for a non-admin authenticated user.
|
||||||
|
- **App password never logged/echoed** (Pitfall 7): custom zod-validator `hook` returns a generic 400 (no Zod `received`/`value` field); no `console.log` of request bodies in `routes/admin*`.
|
||||||
|
- Hard constraints: Drizzle **generate+migrate, never push** (false destructive diff on populated MariaDB); reuse `broker/crypto.ts` `encryptPassword` (no changes to crypto); `/api/admin/credentials` and `/api/admin/calendars/:id/shared` are the single shared surface — do NOT duplicate them into `/api/setup/*` in Phase 12.
|
||||||
|
|
||||||
|
**Folded-in scope** (from backlog 999.5, self-service member onboarding): the credential surface this phase builds is the same one a member needs on first login. Expose a `needsProviderSetup` signal (member has no `member_credentials` row) and let a member enter/validate (CalDAV PROPFIND) + encrypt their **own** Fastmail app password — the self-service counterpart of the admin-managed flow, sharing the validation/encryption/initial-sync path. Non-technical-friendly instructions (link to Fastmail's app-password page, required Calendars/CalDAV scope) are the hard UX constraint. Member-scoped: a member can only set their own credential; never log/echo the password.
|
||||||
|
|
||||||
|
**Plans**: 4 plans (4 waves)Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 10-01-PLAN.md — v1.1 DB foundation migration (is_admin, provider_type+unique, reminder_lead_minutes, app_config) + dev-bypass admin seed
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 10-02-PLAN.md — requireAdmin guard + first-login-wins bootstrap + /api/me isAdmin/needsProviderSetup (TDD)
|
||||||
|
|
||||||
|
**Wave 3** *(blocked on Wave 2 completion)*
|
||||||
|
|
||||||
|
- [x] 10-03-PLAN.md — adminRouter (members/credentials/calendars/shared) + member self-service credential, validate→encrypt→sync (TDD)
|
||||||
|
|
||||||
|
**Wave 4** *(blocked on Wave 3 completion)*
|
||||||
|
|
||||||
|
- [x] 10-04-PLAN.md — PWA /admin route + nav gating + CredentialSheet + SetupBanner (playwright-cli verified)
|
||||||
|
|
||||||
|
**UI hint**: yes
|
||||||
|
|
||||||
|
### Phase 11: Per-Event Reminders
|
||||||
|
|
||||||
|
**Goal**: A user can choose a reminder lead time per event (None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d, default None), serialized as a VALARM on the event, and the push scheduler fires at that exact lead — firing nothing when there is no alarm and never stripping reminders set in other clients.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Phase 10 (the `calendar_events.reminder_lead_minutes` column from the v1.1 migration is the scheduler's ground truth). Independent of Phases 7/8/9/12.
|
||||||
|
**Requirements**: CAL-13, CAL-14, NOTIF-04, NOTIF-05, NOTIF-06
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
|
||||||
|
1. When creating or editing a timed event, the user can pick a reminder lead from the preset list (None default); the choice round-trips to Fastmail as a VALARM and is visible/honored on re-open.
|
||||||
|
2. Editing an event that already has a reminder set in another client (Fastmail / Apple Calendar) preserves that VALARM — it is never silently dropped on round-trip.
|
||||||
|
3. A reminder push fires at the event's chosen lead time (e.g. T-30 for a 30-minute lead), not a hardcoded 15-minute lead.
|
||||||
|
4. An event with no reminder set produces no reminder push (no default 15-minute fire).
|
||||||
|
5. An all-day event's reminder fires at a sensible local time (9 AM on the alert day), not midnight; the reminder selector is disabled/hidden for all-day events in the UI; and reminder delivery stays exactly-once across catch-up scans and rescheduled events.
|
||||||
|
|
||||||
|
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||||
|
|
||||||
|
- **Preserve-on-edit** (Pitfall 1): the update path extracts and preserves existing VALARM sub-components from `rawVevent` (mirroring the WR-01 RRULE-preserve pattern) — never rebuild-from-scratch and silently strip; `outboxPayloadSchema` distinguishes "no change" from explicit "no reminder".
|
||||||
|
- **No TRIGGER VALUE=TEXT** (Pitfall 2): build the trigger with `ICAL.Duration.fromSeconds(-n*60)`, not a bare string; unit-test that the ICS emits a DURATION trigger with no `VALUE=TEXT`.
|
||||||
|
- **All-day 9AM semantics** (Pitfall 3): guard `buildVeventString` (`if (!allDay && reminderMinutes > 0)`), disable the selector when allDay, keep the scheduler's all-day handling at 9 AM local.
|
||||||
|
- **uid:dtstartMs dedup** (Pitfall 4): change the scheduler dedup key from bare `uid` to compound `uid:dtstartMs` and widen the scan to a variable per-event window so long leads fire and rescheduled events re-fire; keep `eventFieldsSchema` and `outboxPayloadSchema` in sync (IN-03).
|
||||||
|
- Hard constraints: `setInterval` only; scheduler reads `reminder_lead_minutes` from the DB (ground truth), not the outbox payload; drop the `isShared`-only reminder restriction (a user who set an alarm wants it regardless of calendar).
|
||||||
|
|
||||||
|
**Plans**: 4 plans (3 waves)
|
||||||
|
Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 11-01-PLAN.md — VALARM builders + classifier + extractor + computeAlertInstantUtc (vevent.ts, TDD)
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 11-02-PLAN.md — Variable-lead scheduler: uid:dtstartMs dedup, drop isShared, all-day 9 AM, humanized body (TDD)
|
||||||
|
- [x] 11-03-PLAN.md — Backend plumbing: schema field, outbox preserve-on-edit, sync upsert, occurrence surfacing
|
||||||
|
|
||||||
|
**Wave 3** *(blocked on Wave 2 completion)*
|
||||||
|
|
||||||
|
- [x] 11-04-PLAN.md — EventForm reminder picker (allDay swap, edit pre-population) + client types + Playwright smoke
|
||||||
|
|
||||||
|
**UI hint**: yes
|
||||||
|
|
||||||
|
### Phase 12: Initial Setup Wizard
|
||||||
|
|
||||||
|
**Goal**: On first run (no admin/credentials configured), the operator is guided through a validated, step-by-step wizard to bootstrap the app — env presence, generated secrets to copy, DB/OIDC/VAPID/app-password validation — instead of hand-editing `.env` / `docker-compose.yml`; once complete, the setup endpoints lock.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Phase 10 (reuses the admin role + `/api/admin/credentials` and `/api/admin/calendars/:id/shared` routes; the wizard is the second frontend consumer of that surface, and `app_config` from the Phase 10 migration holds `setup_complete`). Goes last. Independent of Phases 7/8/9/11.
|
||||||
|
**Requirements**: SETUP-01, SETUP-02, SETUP-03, SETUP-04
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
|
||||||
|
1. On a fresh install with nothing configured, the operator reaches a setup wizard (via `GET /api/setup/status` mounted before the OIDC guard) and walks through bootstrap steps instead of editing files by hand.
|
||||||
|
2. Each input is validated before the step can complete: DB connects, VAPID private key decodes to exactly 32 bytes and pairs with the public key, OIDC discovery resolves, and the Fastmail app password reaches CalDAV (PROPFIND).
|
||||||
|
3. Generated secrets (session secret, encryption key, VAPID keypair) are displayed for the operator to copy into env; they are never written to the DB or returned in a way that persists, and `APP_PASSWORD_ENCRYPTION_KEY`/`VAPID_PRIVATE_KEY` never enter the DB at all.
|
||||||
|
4. After completion, the wizard-completing user is promoted to admin (`is_admin`), `app_config.setup_complete` is set, and any further call to a setup endpoint returns 423 Locked.
|
||||||
|
5. The 423 guard is enforced on every invocation (checked against member-credentials + VAPID env present), not only at startup.
|
||||||
|
|
||||||
|
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||||
|
|
||||||
|
- **Guard on every invocation** (Pitfall 8): the "already set up" guard returns 423 from all setup routes once configured — implement and test the guard before the happy path; a second POST after completion must return 423, not 200.
|
||||||
|
- **Secrets stay in env, never in DB** (Pitfalls 8 & 10): the wizard validates secrets by performing a test operation (test encrypt/decrypt, structural VAPID check), never by accepting/storing the key value; no DB column for `vapid_private_key` or `app_password_encryption_key`; never log/echo the app password.
|
||||||
|
- Hard constraints: `GET /api/setup/status` mounts **before** the OIDC guard (like `/health`); do NOT create `/api/setup/credentials` — reuse the Phase 10 admin routes; Drizzle generate+migrate (any `app_config` seeding via migration).
|
||||||
|
|
||||||
|
**Plans**: 7 plans in 4 waves (4 original + 3 gap-closure for 12-UAT.md gaps 1-6)
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 12-01-PLAN.md — Schema migration (nullable OIDC + claimed) + generate-secrets helper (SETUP-03) + Wave-0 scaffolds
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 12-02-PLAN.md — Pre-auth /api/setup/* router + isSetupLocked 423 guard + index mount + OIDC boot fallback (SETUP-01/02/04)
|
||||||
|
- [x] 12-03-PLAN.md — First-login-claims rework in upsertUser (D-08, SETUP-01)
|
||||||
|
|
||||||
|
**Wave 3** *(blocked on Wave 2 completion)*
|
||||||
|
|
||||||
|
- [x] 12-04-PLAN.md — PWA SetupPage wizard + App.tsx gate + UI-SPEC revision (SETUP-01/02)
|
||||||
|
|
||||||
|
**Wave 4 — Gap closure** *(UAT 12-UAT.md gaps 1-6; 06+07 parallel, 05 blocked on 06)*
|
||||||
|
|
||||||
|
- [x] 12-06-PLAN.md — Backend: validate/vapid asserts wizard key == env VAPID_PUBLIC_KEY (gap 2) + status exposes non-secret DB name (gap 3) (SETUP-02)
|
||||||
|
- [x] 12-07-PLAN.md — App.tsx: reverse-gate /setup post-completion (gap 5) + reconcile ['me'] so calendar banner clears after wizard (gap 6) (SETUP-01/04)
|
||||||
|
- [x] 12-05-PLAN.md — SetupPage: drop DB-vs-env aside (gap 1) + read-only DB-name field (gap 3) + persist fields across Back (gap 4) (SETUP-01) — depends on 12-06
|
||||||
|
|
||||||
|
**UI hint**: yes
|
||||||
|
|
||||||
|
### Phase 13: Real Lint Gate (ESLint)
|
||||||
|
|
||||||
|
**Goal**: The CI lint gate actually fails on lint violations. A real ESLint flat config (`eslint.config.js`, `typescript-eslint`; React + react-hooks plugins for `apps/pwa`) plus a package-level `lint` script in `apps/api` and `apps/pwa` makes the existing root `pnpm -r --if-present lint` run a real linter, replacing the hollow no-op gate that exits 0 because no linter exists.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Phase 8 (the CI `fast-checks` job already runs `pnpm lint`; this fills the slot Phase 8 shipped wired to auto-activate once a package `lint` script lands). Independent of all other phases.
|
||||||
|
**Requirements**: TBD (promoted from backlog 999.16)
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
|
||||||
|
1. `pnpm lint` runs ESLint across both `apps/api` and `apps/pwa` and exits non-zero on an introduced violation (verified by a deliberate test violation), where today it exits 0 with no linter present.
|
||||||
|
2. The CI `fast-checks` lint step blocks a PR to main on lint violations — the gate can now fail.
|
||||||
|
3. The first real run's existing violations are resolved (fix / warn / disable decided per rule) so the baseline gate ends green.
|
||||||
|
|
||||||
|
**Pitfalls this phase owns**:
|
||||||
|
|
||||||
|
- Pick a baseline ruleset (recommended vs strict-type-checked) deliberately — strict surfaces a large upfront cleanup; decide blocking vs advisory before flipping the gate to blocking.
|
||||||
|
- `typecheck`/tsc already gates type errors; ESLint should not duplicate type-checking rules unnecessarily.
|
||||||
|
|
||||||
|
**Plans**: 3 plans — all complete (scope expanded during planning to add a Prettier `format:check` gate)
|
||||||
|
|
||||||
|
- [x] 13-01-PLAN.md — Install ESLint/Prettier deps + flat config + package scripts + prove the gate fails (SC-1)
|
||||||
|
- [x] 13-02-PLAN.md — Fix all first-run lint violations across both apps, green `pnpm lint` (D-13-05/06)
|
||||||
|
- [x] 13-03-PLAN.md — Prettier reformat (isolated commit) + CI format:check step + green baseline (SC-2/SC-3)
|
||||||
|
|
||||||
|
**UI hint**: no
|
||||||
|
|
||||||
|
### Phase 14: Desktop E2E Coverage
|
||||||
|
|
||||||
|
**Goal**: The Phase 8 regression gate exercises the desktop layout and flows, not just mobile. A `desktop` Playwright project (`devices['Desktop Chrome']`, no touch, wide viewport) is added to `apps/pwa/playwright.config.ts`, and the existing mobile-authored specs are reviewed/adjusted (or appropriately skipped) so `pnpm test:e2e` passes on a no-touch desktop viewport as well as the `iphone`/`pixel` profiles.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Phase 7 (the harness it extends) and Phase 8 (CI runs `pnpm test:e2e` and picks up the new project automatically — no CI plumbing change needed beyond any desktop-profile runtime/wait). Independent of Phases 9–13.
|
||||||
|
**Requirements**: TBD (promoted from backlog 999.15)
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
|
||||||
|
1. A `desktop` project exists in `playwright.config.ts` (Desktop Chrome, wide viewport, no `hasTouch`).
|
||||||
|
2. The existing e2e specs pass (or are explicitly, justifiably skipped) on the desktop profile — touch-gesture / mobile-drawer / mobile-only-layout assumptions are handled.
|
||||||
|
3. `pnpm test:e2e` in CI runs and gates on both mobile and desktop profiles (blocking-vs-advisory for desktop decided when planned).
|
||||||
|
|
||||||
|
**Pitfalls this phase owns**:
|
||||||
|
|
||||||
|
- The real work is the spec-compat pass, not CI plumbing — Phase 8 reused the Phase 7 harness unchanged, so the config addition is small but specs authored for touch/mobile need per-spec review.
|
||||||
|
- Desktop WebKit is optional — the Apple member is already covered on mobile Safari via `iphone`; Desktop Chrome is likely sufficient for a shared/wall browser.
|
||||||
|
|
||||||
|
**Plans**: 1 plan
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [x] 14-01-PLAN.md — Add the `desktop` Playwright project, desktop-skip the two mobile-only layout assertions (+ D-04 parity), update spec/README docs, and prove `pnpm test:e2e` is green on iphone + pixel + desktop with a blocking CI gate.
|
||||||
|
|
||||||
|
**UI hint**: no
|
||||||
|
|
||||||
|
### Phase 15: Doc-Only CI Skip
|
||||||
|
|
||||||
|
**Goal**: Doc-only PRs to `main` merge without running the slow `harness` (Playwright e2e + dev-stack bring-up, ~5 min) and `api` (MariaDB integration) jobs, while `fast-checks` (Prettier `format:check` + markdown linting) still runs — and branch protection never deadlocks on a required check that never reports. Docs get a *fast but real* gate: format + lint, none of the slow code jobs.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Phase 8 (the `.gitea/workflows/ci.yml` it modifies) and Phase 13 (the `fast-checks` job + `format:check` step this extends). Independent of Phases 9–12.
|
||||||
|
**Requirements**: TBD (promoted from backlog 999.17)
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
|
||||||
|
1. A doc-only PR to `main` (only `docs/` or `*.md` changed) skips the `api` and `harness` jobs but still runs `fast-checks`.
|
||||||
|
2. A PR touching code runs `fast-checks`, `api`, and `harness` as today; a failure in any blocks the merge.
|
||||||
|
3. Branch protection requires `CI / fast-checks` + an always-running `CI / gate` aggregate (passes when each heavy job is `success` OR `skipped`) — the direct `api`/`harness` requirements are dropped so a skipped heavy job never deadlocks the merge.
|
||||||
|
4. `fast-checks` runs a markdown linter (markdownlint-cli2) over `**/*.md`; an introduced markdown-lint violation fails the gate, and the existing markdown baseline passes (violations fixed or rules configured) so the gate starts green.
|
||||||
|
|
||||||
|
**Pitfalls this phase owns**:
|
||||||
|
|
||||||
|
- **Required-check deadlock** — never path-filter a required context directly; a required job that never reports blocks the PR forever. The always-running `gate` job (`if: always()`, passes on `success`/`skipped`) is the only safe gating surface.
|
||||||
|
- **Gitea skipped-status quirk** — Gitea may not emit a commit-status for a `skipped` job; rely on the always-running `gate`, not on marking `api`/`harness` skipped-but-required.
|
||||||
|
- **Prettier vs markdownlint overlap** — Prettier already owns markdown *formatting*; scope markdownlint to *content* rules (heading increments, no broken/duplicate link refs, list/code-fence conventions) and disable its purely-stylistic rules that fight Prettier (e.g. line-length, list-indent), so the two don't conflict on the same `.md`.
|
||||||
|
- **`.planning/*` is push-direct, never linted** — planning bookkeeping bypasses CI via the Unprotected file pattern, so markdownlint never sees it; scope the lint glob to `docs/` + repo-root/app `*.md` and exclude `.planning/**` (and any generated markdown) to avoid a baseline cleanup of churny bookkeeping files.
|
||||||
|
|
||||||
|
**Plans**: 3 plans (3 waves)
|
||||||
|
Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 15-01-PLAN.md — markdownlint-cli2 + `.markdownlint-cli2.jsonc` + `md:lint` script + fast-checks step + fix 13 baseline violations (SC-4)
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 15-02-PLAN.md — ci.yml: `changes` (dorny/paths-filter@v4) + conditional api/harness + always-running `gate` aggregate (SC-1/SC-2, SC-3 YAML)
|
||||||
|
|
||||||
|
**Wave 3** *(blocked on Wave 2 completion)*
|
||||||
|
|
||||||
|
- [x] 15-03-PLAN.md — operator branch-protection checkpoint (require `CI / fast-checks` + `CI / gate`, drop api/harness) + publish.yml comment update (SC-3)
|
||||||
|
|
||||||
|
**UI hint**: no
|
||||||
|
|
||||||
|
### Phase 16: CI Dependency Audit, Security Checks & Image Hygiene
|
||||||
|
|
||||||
|
**Goal**: The CI pipeline surfaces outdated and vulnerable dependencies, runs a baseline of additional security checks, and enforces a clean dev↔prod boundary in the images it publishes — so the two-person household app doesn't silently rot on stale/CVE-bearing packages, and no dev-only affordance, secret, or family-specific data ever ships in a production image. Extends the existing Gitea CI (Phase 8) workflow with dependency/security/image-hygiene gates rather than standing up a separate pipeline. **Absorbs backlog 999.17 (dev/prod image boundary).**
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Phase 8 (Gitea CI — adds steps to the existing workflow + publish job; no admin-chain dependency). Independent of Phases 10–12.
|
||||||
|
**Requirements**: SEC-01 (secret scanning), SEC-02 (static security lint), DEP-01 (vuln audit gate), DEP-02 (outdated advisory), IMG-01 (NODE_ENV+boot-guard), IMG-02 (.dockerignore), IMG-03 (publish image-hygiene assertions), CI-03 (security job + gate wiring)
|
||||||
|
|
||||||
|
**Candidate scope (to be sharpened in `/gsd-discuss-phase 16`):**
|
||||||
|
|
||||||
|
- **Outdated dependencies:** a CI step that reports dependencies behind their latest (e.g. `pnpm outdated -r`), surfaced on the PR. Decide gating vs advisory, and how to handle the pinned-version table in CLAUDE.md (the stack pins exact versions — "outdated" must not fight intentional pins).
|
||||||
|
- **Vulnerability audit:** `pnpm audit` (or equivalent) against the lockfile, failing on a chosen severity threshold (e.g. high/critical). Decide the threshold and an allowlist/waiver mechanism for unfixable transitive advisories.
|
||||||
|
- **Additional security checks (user is open to these — pick a sensible baseline, avoid over-build):** candidates — secret scanning on the diff (gitleaks/trufflehog), a CodeQL/`eslint-plugin-security` static pass, dependency-review on PRs, Dockerfile/image scan (e.g. trivy) of the published image.
|
||||||
|
- **Dev/prod boundary definition & enforcement (from 999.17):** the `DEV_AUTH_BYPASS` concept (and any dev-only affordance) must be provably confined to local dev — never to production, never baked into published images. Today the guard is runtime-only (`NODE_ENV !== 'production' && DEV_AUTH_BYPASS === 'true'` in `apps/api/src/auth/devBypass.ts`); add (a) explicit documentation of what "dev image" vs "shipped image" means, and (b) build-time / boot-time enforcement (a `production` image refuses to boot — or the build aborts — if dev-bypass is enabled) as defense-in-depth.
|
||||||
|
- **No data/secrets in published images (from 999.17):** audit the Dockerfile(s) + the Phase 8 publish job (`publish.yml`) to confirm `.env`, dev seed SQL, local DB dumps, encryption keys, OIDC secrets, the `DEV_USER` seed, and family-specific fixtures are `.dockerignore`d and never `COPY`'d. Add a CI assertion that fails the publish if a dev-bypass code path is active, a forbidden env/secret is present, or personal/seed data is staged into the image context. The dev-stack seed path (`DEV_USER` id 1 + sample calendar/list data) must be unreachable from the production image/compose.
|
||||||
|
- **Noise control:** these gates are notorious for flaky/advisory-churn failures; decide blocking-on-merge vs warn-only per check, and where results surface (PR annotation vs job log), mirroring Phase 15's gate-aggregation approach.
|
||||||
|
|
||||||
|
**Boundary:** Extends the existing Gitea CI workflow + publish job; does not remove dev-bypass (still needed for local verification and the Phase 7/8 harness) and does not add a new external service or a runtime dependency to the app. Automated dependency *upgrades* (e.g. Renovate/Dependabot bots) are a separate concern — decide in discuss whether they're in scope or deferred.
|
||||||
|
|
||||||
|
**Plans**: 6 plans in 2 waves
|
||||||
|
Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 16-01-PLAN.md — Image-hygiene runtime: bake NODE_ENV=production + boot-time refuse-to-boot guard (IMG-01)
|
||||||
|
- [x] 16-02-PLAN.md — pnpm audit gate + waiver allowlist + advisory-only tiered outdated report (DEP-01, DEP-02)
|
||||||
|
- [x] 16-03-PLAN.md — Fold eslint-plugin-security into the lint gate as blocking errors + triage (SEC-02)
|
||||||
|
- [x] 16-04-PLAN.md — gitleaks config + full-history baseline + .dockerignore (SEC-01, IMG-02)
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 16-05-PLAN.md — Add the security job to ci.yml (gitleaks always; audit/outdated code-gated) + gate wiring (CI-03)
|
||||||
|
- [x] 16-06-PLAN.md — publish.yml static image-hygiene assertion + boot-smoke before push (IMG-03)
|
||||||
|
|
||||||
|
**UI hint**: no
|
||||||
|
|
||||||
|
### Phase 17: UI Optimization & Polish
|
||||||
|
|
||||||
|
**Goal**: A visual-identity & polish pass for the PWA spanning three workstreams: **(A) phone-layout polish** so the phone (≤767px) layout has no fixed-chrome overlap and small-viewport spacing reads cleanly — starting with the long-standing BottomTabBar overlap that hides the New Event FAB and the colour legend, plus a small-viewport sweep; **(B) branding assets** — generate a real FamilySync logo into the existing `BrandSlot` seam (`apps/pwa/src/components/BrandSlot.tsx`) and a complete favicon/PWA-icon set replacing the placeholder stubs in `apps/pwa/public/`; **(C) theme-token groundwork** — restructure `apps/pwa/src/styles/tokens.css` into a themeable semantic-token layer (swappable by `data-theme`/`prefers-color-scheme`), light staying the only shipped theme, so a future dark theme is cheap.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on**: Nothing structural (CSS/layout + assets only). Best sequenced after Phase 10 merges (the BottomTabBar gained an Admin tab and the new SetupBanner adds top pressure on phone), but otherwise independent of the admin chain.
|
||||||
|
**Requirements**: No REQ-IDs — decisions D-01…D-10 (17-CONTEXT.md) stand in. Coverage: D-01/D-02 (A, phone overlap+guard) → 17-03; D-03/D-04 (B, assets) → 17-02; D-04/D-05 (B, wiring) → 17-04; D-06 (C, token groundwork) → 17-01; D-07/D-09 (D, logout+sheet centering) → 17-05; D-08/D-09/D-10 (D, admin toasts+reset-sheet+two-tab nav) → 17-06.
|
||||||
|
**Scope boundary (set in `/gsd-discuss-phase 17`, 2026-06-17):** Workstream C ships token groundwork **only** — no dark palette, no theme toggle (→ backlog **999.20**). A broader "modern styling" visual refresh is **out of scope** and routed to backlog **999.21** (future milestone). Keep Phase 17 a focused polish + branding + groundwork pass, not a redesign.
|
||||||
|
|
||||||
|
**Seed defect — phone-layout bottom-bar overlap (documented 2026-06-13; long-standing, NOT introduced by Phase 10 — the BottomTabBar dates to Phase 04):**
|
||||||
|
|
||||||
|
At ≤767px (`window.matchMedia('(max-width: 767px)')` in `apps/pwa/src/App.tsx`) the layout switches to a 48px top AppNav + a `position: fixed` BottomTabBar (`height: calc(56px + env(safe-area-inset-bottom))`, z-index 200; `apps/pwa/src/components/BottomTabBar.tsx`) + a floating "New Event" FAB (`position: fixed; bottom: var(--space-6); right: var(--space-6)`; `apps/pwa/src/components/CalendarShell.tsx`). Two problems:
|
||||||
|
|
||||||
|
1. **FAB sits inside the bar** — the FAB's `bottom` offset (~`--space-6`, ≈24px) is smaller than the bar's 56px height, so the round New Event button overlaps the bottom tab bar (lands on the Admin tab).
|
||||||
|
2. **Content occluded** — the content area (`contentStyle` in `App.tsx`) reserves no `padding-bottom` for the fixed bar, so the bottom of the calendar and the colour-legend chips (e.g. the "Dev User" / member legend) slide under the bar and are partially hidden.
|
||||||
|
|
||||||
|
**Fix sketch (CSS-only, no behaviour change):** on phone, lift the FAB to `bottom: calc(56px + env(safe-area-inset-bottom, 0px) + var(--space-6))` and add a matching `padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px))` to the phone content/scroll area (or reduce the `100dvh` column by the bar height). Verify across the `iphone`/`pixel`/`desktop` Playwright profiles and a real narrow Chromium via playwright-cli.
|
||||||
|
|
||||||
|
**Evidence:** reproduced 2026-06-13 with playwright-cli at 390×844 (FAB over the Admin tab; "Dev User" legend clipped) vs 1280×800 (desktop sidebar, no overlap). Full detail in todo `2026-06-13-pwa-phone-bottombar-overlap.md`.
|
||||||
|
|
||||||
|
**Candidate scope (to sharpen in `/gsd-discuss-phase 17`):** the seed defect above, plus a sweep for other small-viewport spacing / tap-target / overlap issues (the Phase 7 `layout.spec.ts` tap-target/overflow assertions are a ready checklist) and any phone/desktop visual inconsistencies noticed in use. Keep it a focused polish pass, not a redesign.
|
||||||
|
|
||||||
|
**Plans**: 6/6 plans complete
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 17-01-PLAN.md — C: tokens.css themeable-layer groundwork + --bottom-chrome-h token (D-06) [Wave 1]
|
||||||
|
- [x] 17-02-PLAN.md — B: generate logo + full icon set, operator approval checkpoint (D-03, D-04) [Wave 1]
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 17-03-PLAN.md — A: phone FAB/BottomTabBar overlap fix + sweep + CI overlap assertion (D-01, D-02) [Wave 2, dep 01]
|
||||||
|
- [x] 17-04-PLAN.md — B: wire logo into BrandSlot + index.html favicons + manifest maskable + accent (D-04, D-05) [Wave 2, dep 01,02]
|
||||||
|
- [x] 17-05-PLAN.md — D: logout control + sheet desktop-centering (SettingsSheet/CredentialSheet) (D-07, D-09) [Wave 2, dep 01]
|
||||||
|
- [x] 17-06-PLAN.md — D: admin success toasts + two-tab ARIA nav + reset-sheet centering + admin.spec.ts (D-08, D-09, D-10) [Wave 2, dep 01]
|
||||||
|
|
||||||
|
**UI hint**: yes
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
| Phase | Milestone | Plans Complete | Status | Completed |
|
||||||
|
| ----- | --------- | -------------- | -------- | ---------- |
|
||||||
|
| 1. Foundation + Broker Spike | v1.0 | 4/4 | Complete | 2026-06-04 |
|
||||||
|
| 2. Calendar Display | v1.0 | 5/5 | Complete | 2026-06-05 |
|
||||||
|
| 3. Event Write-Back + PWA Install| v1.0 | 12/12 | Complete | 2026-06-07 |
|
||||||
|
| 4. Shared Lists + Live Sync | v1.0 | 7/7 | Complete | 2026-06-09 |
|
||||||
|
| 5. Web Push Notifications | v1.0 | 8/8 | Complete | 2026-06-10 |
|
||||||
|
| 6. UX Polish | v1.0 | 6/6 | Complete | 2026-06-10 |
|
||||||
|
| 7. Mobile Test Harness | v1.1 | 4/4 | Complete | 2026-06-11 |
|
||||||
|
| 8. Gitea CI | v1.1 | 4/4 | Complete | 2026-06-11 |
|
||||||
|
| 9. Faster Write-Back | v1.1 | 2/2 | Complete | 2026-06-12 |
|
||||||
|
| 10. Admin Role & Settings | v1.1 | 4/4 | Complete | 2026-06-13 |
|
||||||
|
| 11. Per-Event Reminders | v1.1 | 5/5 | Complete | 2026-06-14 |
|
||||||
|
| 12. Initial Setup Wizard | v1.1 | 7/7 | Complete | 2026-06-16 |
|
||||||
|
| 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 |
|
||||||
|
| 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 |
|
||||||
|
| 15. Doc-Only CI Skip + MD Lint | v1.1 | 3/3 | Complete | 2026-06-12 |
|
||||||
|
| 16. CI Dep Audit, Sec & Img Hyg | v1.1 | 6/6 | Complete | 2026-06-13 |
|
||||||
|
| 17. UI Optimization & Polish | v1.1 | 6/6 | Complete | 2026-06-18 |
|
||||||
|
| 18. Auto Timezone Detection | v1.1 | 4/4 | Complete | 2026-06-14 |
|
||||||
|
| 19. Local Auth (No-OIDC Mode) | v1.1 | 5/5 | Complete | 2026-06-17 |
|
||||||
|
| 20. Admin Member Editor & Declutter | v1.1 | 3/3 | Complete | 2026-06-18 |
|
||||||
|
|
||||||
|
## Backlog
|
||||||
|
|
||||||
|
### Phase 999.1: Treat Fastmail as one calendar provider; framework supports adding more providers (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern.
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 6/6 plans complete
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.4: Per-event reminder configuration (VALARM authoring + scheduler honors it) (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] End-to-end per-event reminders — let the user choose *when* (or whether) to be reminded per event, and make the push scheduler honor that choice instead of a hardcoded lead.
|
||||||
|
|
||||||
|
**Half A — author the VALARM (event form):** The event create/edit form has no UI to set a reminder ("remind me 10 min / 1 hour / 1 day before", or **no reminder**), so the written `.ics` carries no `VALARM` and no reminder can fire — in native clients or via web push. Add a reminder selector (including an explicit "none"), serialize chosen offsets as `VALARM` (TRIGGER) on write-back, and parse existing `VALARM`s on read so edits preserve them. Feeds the Phase 5 web-push requirement (push needs reminder data to notify about).
|
||||||
|
|
||||||
|
**Half B — scheduler honors the provider's value (NEW, surfaced 2026-06-10):** Today `apps/api/src/broker/reminderScheduler.ts` runs a **hardcoded 15-minute** scan for shared timed events (`index.ts:139` "starting in ~15 min"; reminderScheduler header "15-min reminder scan") and never reads the event's actual alarm. So every reminder fires 15 min before regardless of what the event (or the calendar provider) specifies, and an event with **no** alarm still gets a 15-min push. Change the scheduler to read each event's `VALARM` `TRIGGER` (the value written in Half A / set in Fastmail or another native client) and fire at that lead — and fire **nothing** when the event has no alarm. The current fixed 15-min window/dedup logic (catch-up scan, per-uid exactly-once — see quick 260610-hbu) must be generalized to a variable per-event lead.
|
||||||
|
|
||||||
|
**Boundary:** preserve the reminder scheduler's resilience guarantees (catch-up on a missed tick, per-uid exactly-once dedup). This makes the lead per-event/variable rather than constant; it is not a rewrite of the scan/dedup design.
|
||||||
|
|
||||||
|
**Severity:** medium — feature gap surfaced during Phase 03 Gate 2 testing; Half B surfaced 2026-06-10. Tags: phase-03, phase-05, calendar, write-back, reminders, valarm, push, scheduler, phase-05-dependency.
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
> **Promoted into v1.1 Phase 11 (Per-Event Reminders) — CAL-13/CAL-14/NOTIF-04/05/06.** Backlog entry retained for history.
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.10: Admin Settings / Administration section — manage app passwords + designate the shared calendar via UI (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] Add an in-app **Settings/Administration** section, gated to an administrator role, for configuration that today requires manual backend/DB steps:
|
||||||
|
|
||||||
|
- **View/update per-member Fastmail app passwords** (stored encrypted via `APP_PASSWORD_ENCRYPTION_KEY`, existing crypto path) — rotate or re-enter a member's credential and re-trigger sync.
|
||||||
|
- **Designate which synced calendar is the "shared" calendar** by toggling `calendars.is_shared` from the UI. Today this is a manual DB write: e.g. `UPDATE calendars SET is_shared=1 WHERE id=<row>` — done by hand on 2026-06-10 to mark the "FamilySync" calendar (id 10) shared after the poller synced it (D-16). The admin should pick the shared calendar from a list of synced collections instead of relying on a backend process. (The poller's upsert already leaves `is_shared` untouched, so a UI-set flag persists.)
|
||||||
|
|
||||||
|
**Context:** Motivated by the manual D-16 resolution (2026-06-10). **Related:** 999.5 (per-member first-login app-password onboarding) — this is the ongoing admin-managed counterpart; and 999.11 (initial setup wizard) — bootstrap-time vs. ongoing config. Tags: admin, settings, calendar, app-passwords, D-16.
|
||||||
|
|
||||||
|
> **Promoted into v1.1 Phase 10 (Admin Role & Settings) — ADMIN-01/ADMIN-02/ADMIN-03.** Backlog entry retained for history.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.11: Initial setup wizard — first-run config of env vars, app passwords, DB connection (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] Add a first-run **setup wizard** that walks the administrator through defining all bootstrap configuration instead of hand-editing `.env` / `docker-compose.yml`:
|
||||||
|
|
||||||
|
- **App environment variables:** OIDC client id/secret/issuer/redirect URI + external URL, session signing secret (`OIDC_AUTH_SECRET`), `APP_PASSWORD_ENCRYPTION_KEY`, and the **VAPID keypair** (subject + public + private).
|
||||||
|
- **MariaDB connection:** host/port/user/password/db, with a connectivity test.
|
||||||
|
- **First Fastmail app password** for the initial member, encrypted on save.
|
||||||
|
|
||||||
|
Wizard should **validate inputs before completing** — e.g. VAPID private key decodes to 32 bytes AND pairs with the public key, OIDC discovery resolves, DB connects, app-password reaches CalDAV.
|
||||||
|
|
||||||
|
**Context:** Motivated by setup friction observed 2026-06-10 — a VAPID private key truncated on paste into `.env` silently broke push (`setVapidDetails failed — 32 bytes`), and `DB_HOST` / dev overrides must currently be set by hand. A guided + validated wizard would have caught these. **Related:** 999.10 (ongoing admin Settings) and 999.5 (member onboarding). Tags: onboarding, setup, install, env, vapid, mariadb, oidc.
|
||||||
|
|
||||||
|
> **Promoted into v1.1 Phase 12 (Initial Setup Wizard) — SETUP-01/02/03/04.** Backlog entry retained for history.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.12: Assistant-driven mobile-browser UI testing (mobile viewport + authed PWA) (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] Give the assistant a way to validate UI/UX changes in a **mobile** browser experience, not just desktop Chromium. Today `playwright-cli` drives a desktop viewport, and the prod stack enforces OIDC (Authelia) so the authed PWA can't be reached headlessly — which is exactly why a string of mobile-only defects this milestone (silent Android notifications, the dead "How to enable" link, iOS/Android session-cookie persistence, install/standalone behaviour) could only be found by the operator on real devices, not by the assistant.
|
||||||
|
|
||||||
|
**What this needs (any subset):**
|
||||||
|
|
||||||
|
- **Mobile viewport + UA emulation** in the browser harness (e.g. Playwright device descriptors — iPhone/Pixel viewport, touch, mobile user-agent) so layout, tap targets, and responsive behaviour can be checked.
|
||||||
|
- **An authenticated entry path for automated runs** so the assistant can reach the real PWA past Authelia — e.g. a reusable saved storage-state/cookie, a test-only bypass on a non-prod host, or driving the Authelia login once and reusing the session. (Note: this overlaps the existing `DEV_AUTH_BYPASS`, but that only works on the host-side dev stack, not the prod-mode PWA that has the real service worker. A mobile, authed, SW-enabled target is the gap.)
|
||||||
|
- Optionally: a documented way to point the harness at the Pangolin HTTPS URL with a persisted session, and/or remote-debug a real device.
|
||||||
|
|
||||||
|
**Boundary:** genuinely device-only behaviour (iOS-Safari standalone push, real APNs/FCM delivery, OS notification-channel importance) still needs a human — this item is about everything SHORT of that (responsive layout, tap flows, in-page notification UI states, auth redirects) which a mobile-emulated authed browser *could* cover but currently can't.
|
||||||
|
|
||||||
|
**Context:** Surfaced 2026-06-10 during Phase 5 UAT — repeated mobile-only bugs were caught only by the operator because the assistant had no mobile, authenticated browser to test in. **Related:** [[feedback-playwright-verify]] (use playwright-cli over manual verification — this extends it to mobile/authed). Tags: testing, playwright, mobile, pwa, oidc, dx.
|
||||||
|
|
||||||
|
> **Promoted into v1.1 Phase 7 (Mobile Test Harness) — TEST-01/TEST-02.** v1.1 scopes the `DEV_AUTH_BYPASS` dev-build path; the prod-SW authed-mobile target stays deferred. Backlog entry retained for history.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.13: Reduce event write-back latency to the calendar provider (outbox drain) (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] Calendar create/edit/delete writes are enqueue-only (`calendarOutbox`, 202 optimistic-accept; D-12/D-05 — no Fastmail call in the route) and flushed to Fastmail by `runOutboxDrain` on a **15-second `setInterval`** (`apps/api/src/broker/outboxWorker.ts`). So a change can take up to ~15s to land in Fastmail (and longer to reflect back in the app, which depends on the separate 5-min poller). Reduce that perceived sync delay so edits feel near-immediate.
|
||||||
|
|
||||||
|
**Options to weigh when picking this up:**
|
||||||
|
|
||||||
|
- **Event-driven drain (preferred):** trigger an outbox drain immediately after a successful enqueue (in-process signal, or Redis pub/sub which is already available) so the write fires within ~1s instead of waiting for the next tick — keep the 15s `setInterval` as a fallback/retry sweep. Must preserve the existing per-row etag/412 handling and the rapid-successive-edit ordering (see outboxWorker comments ~L312 — each edit carries its enqueue-time etag).
|
||||||
|
- **Shorter interval:** simplest, but more idle DB polling; a floor (e.g. 3–5s) trades latency for load.
|
||||||
|
- **Faster read-back too:** the user also sees latency from the 5-min poller reflecting the change back. Consider invalidating/short-poll after a local write, or optimistic UI already covering it — confirm whether the perceived delay is the write (15s) or the read-back (5min).
|
||||||
|
|
||||||
|
**Boundary:** the optimistic 202 + outbox durability design (create-before-delete, drain concurrency guard, fresh-etag-before-PUT) must be preserved — this is a latency tune, not a rewrite of the write path.
|
||||||
|
|
||||||
|
**Context:** Surfaced 2026-06-10. Tags: calendar, write-back, outbox, latency, redis, performance.
|
||||||
|
|
||||||
|
> **Promoted into v1.1 Phase 9 (Faster Write-Back) — CAL-15.** In-process EventEmitter chosen (not Redis); the drain is single-process by design. Backlog entry retained for history.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.14: Gitea CI — full regression on PR to main + build/publish Docker image (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] The repo is committed against a self-hosted Gitea instance with a registered Actions runner, but there is no CI yet (no `.gitea/workflows/` or `.github/workflows/`). Two things should run automatically: (1) **full regression** on every PR targeting `main` — gating the merge; (2) **build the app's Docker image and publish it** to the Gitea container registry.
|
||||||
|
|
||||||
|
**Options / decisions to make when picking this up:**
|
||||||
|
|
||||||
|
- **Test scope:** "full regression" = lint + typecheck + unit + the API integration tests. Integration tests need a real MariaDB (see [[api-integration-test-db]]) — the workflow must spin up a MariaDB service container, bind it, and set `DB_HOST=127.0.0.1` + `.env` creds. The PWA build/test also runs.
|
||||||
|
- **Monorepo:** pnpm workspace (`apps/api`, `apps/pwa`, shared). Cache the pnpm store.
|
||||||
|
- **Docker images:** only `apps/api/Dockerfile` exists today — there is no PWA Dockerfile yet. Decide one image (API) vs. also building/serving the PWA. Tag scheme + when to publish (only on merge to `main`? on tags? per-PR?).
|
||||||
|
- **Registry auth:** push to the Gitea registry using the runner's Gitea-provided token or a dedicated package-write token.
|
||||||
|
- Gitea Actions are GitHub-Actions-compatible syntax but run on the self-hosted runner — confirm runner labels and available images, and that Actions is enabled, before authoring.
|
||||||
|
|
||||||
|
**Likely shape:** a `.gitea/workflows/ci.yml` — `on: pull_request` (to `main`) → install (pnpm), lint, typecheck, unit, API integration vs. a `mariadb` service container, PWA build; `on: push` to `main`/tag → `docker build apps/api/Dockerfile`, login, push tagged image.
|
||||||
|
|
||||||
|
**Context:** Promoted from STATE.md pending todo (`.planning/todos/pending/2026-06-10-gitea-ci-regression-and-docker-publish.md`), surfaced 2026-06-10. Tags: tooling, ci, gitea, docker, mariadb, monorepo.
|
||||||
|
|
||||||
|
> **Promoted into v1.1 Phase 8 (Gitea CI) — CI-01/CI-02.** v1.1 also extends CI-01 to run the Phase 7 mobile harness as a UI-regression step (CI brings up the dev stack in the runner). Backlog entry retained for history.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.15: Desktop e2e coverage — add a Desktop Playwright profile + desktop-safe specs (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] The Playwright harness (`apps/pwa/playwright.config.ts`) defines only **mobile** device profiles — `iphone` (iPhone 14 / WebKit) and `pixel` (Pixel 7 / Chromium), both with touch and a mobile viewport. The Phase 8 CI regression gate runs `pnpm test:e2e`, so it currently validates the **mobile experience only**. Add desktop coverage so the regression gate exercises the desktop layout/flows as well.
|
||||||
|
|
||||||
|
**Options / decisions to make when picking this up:**
|
||||||
|
|
||||||
|
- **Add a Desktop profile:** a new `desktop` project in `playwright.config.ts` (e.g. `devices['Desktop Chrome']`, no `hasTouch`, wide viewport). Optionally a Desktop WebKit/Safari profile too — but the family's Apple member is already covered on mobile Safari via `iphone`; Desktop Chrome is likely sufficient for a shared/wall browser.
|
||||||
|
- **Spec-compat pass (the real work):** the existing e2e specs were authored for mobile — they may assume touch gestures, a mobile nav/drawer, or mobile-only layout. Each spec needs review/adjustment so it passes (or is appropriately skipped) on a no-touch, wide-viewport desktop. This is harness/spec work, not CI plumbing.
|
||||||
|
- **Gating choice:** decide whether desktop runs block the merge immediately, or run advisory (non-blocking) until the specs are confirmed desktop-safe.
|
||||||
|
|
||||||
|
**Boundary:** Phase 8 deliberately reused the Phase 7 harness **unchanged** (CI owns only stack bring-up + readiness waits, not spec content), which is why this was deferred. Once a Desktop project is added to the config, Phase 8 CI picks it up automatically via `pnpm test:e2e` — no CI changes needed beyond whatever runtime/wait the desktop profile requires.
|
||||||
|
|
||||||
|
**Context:** Deferred from Phase 8 (Gitea CI) planning, 2026-06-11 — user wants both mobile and desktop validated, but desktop needs a config addition + spec review that is out of Phase 8's CI-plumbing scope. Tags: testing, playwright, e2e, desktop, harness, ci.
|
||||||
|
|
||||||
|
> **Promoted into v1.1 Phase 14 (Desktop E2E Coverage) — 2026-06-11.** Backlog entry retained for history.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.16: Wire a real linter (ESLint) so the CI lint gate actually fails on violations (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] The Phase 8 CI `fast-checks` job runs `pnpm lint`, but **no linter exists** in the repo — the root `lint` script is `pnpm -r --if-present lint`, which finds no package-level lint script and exits 0. The lint gate is a hollow placeholder that can never fail. Wire up a real linter so it runs and gates merges on lint violations. (`typecheck`/tsc already gates type errors meanwhile.)
|
||||||
|
|
||||||
|
**Options / decisions to make when picking this up:**
|
||||||
|
|
||||||
|
- **Tooling:** ESLint flat config (`eslint.config.js`) with `typescript-eslint`; add React + react-hooks plugins for `apps/pwa`. Add `eslint` (+ plugins) as devDeps and a `lint` script to `apps/api` and `apps/pwa` — `pnpm -r --if-present lint` then picks them up automatically, no CI change needed.
|
||||||
|
- **Rule strictness:** pick a baseline (recommended vs strict-type-checked). Stricter = more upfront violations to fix.
|
||||||
|
- **Violation cleanup (the real work):** the first run surfaces existing violations across both apps. Decide per-rule: fix, downgrade to warn, or disable. The gate must end green.
|
||||||
|
- **Gating choice:** blocking on merge immediately, or advisory (warn-only) until the codebase is clean.
|
||||||
|
|
||||||
|
**Boundary:** Phase 8 deliberately scoped lint wiring out (CI-plumbing-only); it shipped the gate slot wired to auto-activate once a package `lint` script lands. This item is that follow-up.
|
||||||
|
|
||||||
|
**Context:** Raised during Phase 8 execution, 2026-06-11 — user noted the `--if-present` lint step "didn't fix the linter, just made it so it didn't have to exist to proceed" and wants a lint gate that actually fails. Tags: ci, lint, eslint, typescript-eslint, quality, gitea.
|
||||||
|
|
||||||
|
> **Promoted into v1.1 Phase 13 (Real Lint Gate / ESLint) — 2026-06-11.** Backlog entry retained for history.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.18: Update dependencies as found during CI (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] When the CI dependency-audit gate (Phase 16) surfaces outdated or vulnerable packages, bump them rather than letting the report accumulate. Establish a lightweight, recurring "act on the CI dependency report" loop so the two-person household app doesn't drift onto stale/CVE-bearing deps. Scope is the upkeep workflow (review → bump → verify gate green), not a one-time audit.
|
||||||
|
|
||||||
|
**Context:** Captured 2026-06-13 during Phase 10 work. Companion to the audit *reporting* shipped in Phase 16 (CI Dependency Audit) — that phase makes outdated/vulnerable deps *visible*; this item is the standing follow-through to *resolve* what it finds. Tags: ci, dependencies, maintenance, security, upkeep.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.19: Dev user exercises full app functionality without syncing to a real calendar (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] Let the `DEV_AUTH_BYPASS` dev user (currently hardcoded `DEV_USER` id 1 in `apps/api/src/auth/devBypass.ts`) exercise the full app — create/edit/delete events, set per-event reminders, manage lists — against a local/in-app calendar store, WITHOUT requiring a connected Fastmail/CalDAV provider and WITHOUT writing anything to a real calendar. Today the dev user has no `member_credentials` row and no `calendars`, so `writable-calendars` is empty and `POST /api/events/create` returns `422 "No writable calendar found for user"` — making hands-on UAT of event/reminder features impossible in dev. Options to explore: seed the dev user a fake local calendar + short-circuit the outbox/CalDAV write path under dev-bypass (no Fastmail round-trip), or a dev-only in-memory calendar provider. Must stay strictly dev-only (same hard `NODE_ENV !== 'production'` guard) and never ship in production images.
|
||||||
|
|
||||||
|
**Context:** Captured 2026-06-14 during Phase 11 (Per-Event Reminders) UAT. The reminder picker and backend were verified via automated tests + a route-mocked playwright smoke, but the operator could not manually create an event to see reminders end-to-end because no provider is connected in the dev DB (`needsProviderSetup: true`). This is a recurring dev-testability friction (see MEMORY: "Dev user 1 has no calendars"). Tags: dev-tooling, dev-bypass, testability, calendars, outbox, uat.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.20: PWA dark mode / theming — ship a full dark theme + light/dark/system switch (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] Ship a complete dark theme for the PWA plus a light/dark/system theme switch. **Phase 17 lays the token-architecture groundwork** — it restructures `apps/pwa/src/styles/tokens.css` from a single light `:root` into a themeable semantic-token layer that can be swapped via `data-theme` / `prefers-color-scheme`, with light staying the default and only-shipped theme. This backlog item is the follow-through that consumes that seam: author the actual dark palette values (including the Schedule-X `--sx-color-*` calendar overrides at the bottom of tokens.css), wire `prefers-color-scheme`, add a persisted in-app toggle in the /admin or Settings surface (light / dark / system), and verify both themes render cleanly across every route (calendar, lists, admin, settings sheet, login) via `playwright-cli` + the Phase 7 `layout.spec` profiles.
|
||||||
|
|
||||||
|
**Context:** Deferred out of Phase 17 (2026-06-17) during `/gsd-discuss-phase 17` to keep that phase scoped to phone-layout polish + branding assets. Phase 17's token restructure is the explicit enabling groundwork, so this should be cheap to pick up afterward. Related: Phase 17 (UI Optimization & Polish — the groundwork), 999.21 (modern styling refresh). Tags: pwa, theming, dark-mode, tokens, accessibility, settings, prefers-color-scheme.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.21: PWA modern visual styling refresh — contemporary look across the app (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] A broader "more modern, visually appealing" styling pass across the PWA — beyond the bounded in-system polish of Phase 17. Candidate scope: a contemporary refresh of high-visibility surfaces (login, calendar shell, event form, lists, admin), revisiting elevation/shadows, radii, spacing rhythm, typography scale, and control states, potentially reworking specific component layouts. Explicitly **flagged for a future milestone**, not v1.1 — it is a visual-overhaul track with real redesign risk and should be scoped/sequenced on its own rather than bolted onto a polish phase. Best sequenced after the Phase 17 token groundwork and 999.20 (dark mode) so the refresh is theme-aware from the start.
|
||||||
|
|
||||||
|
**Context:** Deferred out of Phase 17 (2026-06-17) during `/gsd-discuss-phase 17`. The user scoped Phase 17 to layout polish + branding (logo/favicon/icon assets) + theme-token groundwork, and routed the open-ended styling refresh here for a future milestone to avoid an unbounded redesign inside a polish phase. Related: Phase 17 (the polish baseline), 999.20 (dark mode / theming). Tags: pwa, ui, styling, redesign, design-system, future-milestone.
|
||||||
|
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 18: Auto timezone detection and ability to change timezone
|
||||||
|
|
||||||
|
**Goal:** Make the household timezone an explicit, stored, user-changeable setting — auto-detected from the browser at first run, changeable from the role-gated /admin Settings — and route the server-side all-day "9 AM local" reminder computation through it (replacing the implicit `process.env.TZ` fallback), without touching the already-correct browser-local display/timed-write path.
|
||||||
|
**Requirements**: TBD (decision contract D-01..D-07 from 18-CONTEXT.md)
|
||||||
|
**Depends on:** Phase 10 (admin role + `/admin` Settings + `app_config`); Phase 11 (all-day reminder computation this rewires). Independent of Phase 17. Phase 12 (setup wizard) not required — seeding is self-contained.
|
||||||
|
**Plans:** 4/4 plans complete
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 18-01-PLAN.md — TDD: getHouseholdTimezone(db) accessor + isValidIanaTimezone (D-05/D-06)
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 18-02-PLAN.md — TDD: admin GET/PUT/seed timezone endpoints on adminRouter, requireAdmin + IANA validation + no-overwrite seed (D-01/D-02/D-03/D-04)
|
||||||
|
- [x] 18-03-PLAN.md — TDD: route all-day reminder TZ at reminderScheduler:247 + outboxWorker:501,607 through the accessor (D-05/D-06/D-07)
|
||||||
|
|
||||||
|
**Wave 3** *(blocked on Wave 2 completion)*
|
||||||
|
|
||||||
|
- [x] 18-04-PLAN.md — PWA Timezone section in /admin Settings (searchable IANA picker + detected-zone seed) + client fns (D-02/D-04)
|
||||||
|
|
||||||
|
**UI hint**: yes
|
||||||
|
|
||||||
|
### Phase 19: Local Auth (No-OIDC Mode)
|
||||||
|
|
||||||
|
**Goal:** Let an operator run FamilySync entirely on **local DB users with no OIDC** — username/password accounts and a local login flow that coexists with the Authelia OIDC path — and **optionally wire OIDC in later** by claiming/linking an existing local user to an OIDC identity. Removes the hard dependency on a deployed Authelia for small/solo self-hosters.
|
||||||
|
**Mode:** standard
|
||||||
|
**Depends on:** Phase 12 (Initial Setup Wizard) — builds directly on the pre-OIDC **local-user foundation** introduced there: nullable `users.oidc_iss`/`oidc_sub` + the claimed/pending marker, and the first-login-claims merge. Phase 19 generalizes that single bootstrap local user into a full local-account model + login.
|
||||||
|
**Requirements**: AUTH-LOCAL-01..AUTH-LOCAL-20 (derived during planning 2026-06-17) — local_credentials schema (01), scrypt hash/verify (02), login route (03), localAuthMiddleware (04), auth-mode endpoint (05), logout (06), admin create-member (07), admin reset (08), self-change (09), OIDC-link (10), break-glass CLI (11), LoginPage (12), admin UI (13), settings UI (14), routing gate (15), dev-bypass/harness rework (16), hasLocalCredential (17), de-Authelia copy (18), rate-limit/lockout (19), auth unit tests (20). Plus `LOCAL_SESSION_SECRET` env + boot assertion (D-05).
|
||||||
|
**Plans:** 5/5 plans complete
|
||||||
|
|
||||||
|
**Provenance:** Deferred from the Phase 12 discussion (2026-06-15) — see `.planning/phases/12-initial-setup-wizard/12-CONTEXT.md` §Deferred Ideas. The operator runs FamilySync this way themselves and wants no-OIDC operation as a first-class mode.
|
||||||
|
|
||||||
|
**Open questions for discuss/spec:**
|
||||||
|
|
||||||
|
- Password hashing/storage choice (e.g. argon2id/bcrypt) and how it sits alongside the env-only secret kernel from Phase 12.
|
||||||
|
- How local login coexists with `oidcAuthMiddleware` ordering in `apps/api/src/index.ts` (route-level auth strategy selection vs. a mode flag in `app_config`).
|
||||||
|
- The OIDC-link flow: claiming an existing local user into an `oidc_iss+oidc_sub` identity without violating the D-10 "identity is OIDC, never email" rule.
|
||||||
|
- Whether "local mode vs OIDC mode" is a deploy-time switch or both can be live simultaneously.
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 19-01-PLAN.md — Foundation (TDD): local_credentials schema + 0003 migration, scrypt hash/verify, local-session JWT helpers, LOCAL_SESSION_SECRET boot guard + generate-secrets, .dockerignore scripts exclusion (AUTH-LOCAL-01/02)
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1)*
|
||||||
|
|
||||||
|
- [x] 19-02-PLAN.md — Backend account mgmt (TDD): admin create/reset member, self-change password, hasLocalCredential, linkOidcToUser helper + /api/me/link-oidc (AUTH-LOCAL-07/08/09/10/17)
|
||||||
|
|
||||||
|
**Wave 3** *(blocked on Wave 2)*
|
||||||
|
|
||||||
|
- [x] 19-03-PLAN.md — Middleware + routes + wiring (TDD): localAuthMiddleware, /api/auth/mode, login (rate-limit/lockout) + logout, index.ts mount + OIDC-guard skip + /callback link branch, de-Authelia comments (AUTH-LOCAL-03/04/05/06/18/19/20)
|
||||||
|
|
||||||
|
**Wave 4** *(blocked on Wave 3; 04 + 05 parallel)*
|
||||||
|
|
||||||
|
- [x] 19-04-PLAN.md — PWA: LoginPage + BrandSlot + App.tsx gate + client.ts + AdminPage + SettingsSheet (AUTH-LOCAL-12/13/14/15)
|
||||||
|
- [x] 19-05-PLAN.md — Dev-bypass Option C + break-glass CLI + harness/CI rework + login.spec.ts (AUTH-LOCAL-11/16)
|
||||||
|
|
||||||
|
### Phase 20: Admin Member Editor & Form Declutter
|
||||||
|
|
||||||
|
**Goal:** Replace the per-member-row action buttons (Rotate/Add credential + Reset password) in the admin Members panel with a single edit affordance — clicking a member's name or an edit button opens a member-detail editor where an admin modifies all of that member's details in one place: display name, local-login password, and the Fastmail/CalDAV app password (calendar credential) — using clear, non-jargon labels that retire the confusing "Rotate" term. Also collapse the "Add member" section so its input fields are hidden behind a single "Add member" trigger by default, decluttering the panel. Client-side AdminPage + CredentialSheet rework over the existing `/api/admin` endpoints; no new auth/authorization boundary (seeded by the gripe that "Rotate" for the app password is not intuitive).
|
||||||
|
**Requirements**: TBD (refine in /gsd-discuss-phase 20 — open scope: which fields count as "all" (color swatch? admin toggle? OIDC link?), whether to keep any standalone reset-password flow, and the exact edit affordance — clickable name vs. row edit button)
|
||||||
|
**Depends on:** Phase 19
|
||||||
|
**Plans:** 3/3 plans complete
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
**Wave 1**
|
||||||
|
|
||||||
|
- [x] 20-01-PLAN.md — Server: PATCH /api/admin/members/:id (displayName + is_admin) with last-admin demotion guard (TDD) + isAdmin in GET /members
|
||||||
|
- [x] 20-02-PLAN.md — PWA API client: AdminMember.isAdmin field + updateMemberProfile fetcher (last-admin sentinel)
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
|
- [x] 20-03-PLAN.md — PWA: unified MemberEditorSheet (edit/create, per-section saves) + decluttered tappable Members panel; retire Rotate/Reset-password buttons
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user