diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a2dae27 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,362 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +env: + MILESTONE: v1.1 + +jobs: + fast-checks: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Enable pnpm + run: corepack enable pnpm + + # actions/cache@v4 is intentionally omitted — probe (D-PROBE-04) showed it + # times out on this runner (socket hang-up between runner container and job + # container cache server). pnpm install without cache takes ~30s; acceptable. + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # lint is currently a no-op: no package defines a `lint` script and ESLint is + # not installed. `pnpm -r lint` prints ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT but + # exits 0, so this step passes. Wiring lint is out of this phase's scope. + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: PWA unit tests + run: pnpm --filter @familysync/pwa test + + api: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + # Runs in PARALLEL with fast-checks (D-03) — no needs: dependency. + services: + mariadb: + image: mariadb:11 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: familysync + MARIADB_USER: familysync + MARIADB_PASSWORD: testpass + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + --health-start-period=30s + # Throwaway creds scoped to the ephemeral service container — never production secrets (T-08-03). + env: + DB_HOST: mariadb + DB_PORT: 3306 + DB_USER: familysync + DB_PASSWORD: testpass + DB_NAME: familysync + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Enable pnpm + run: corepack enable pnpm + + # actions/cache@v4 intentionally omitted — same reasoning as fast-checks job (D-PROBE-04). + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # Pitfall 11: service container healthy != MariaDB accepting connections. + # No mysql CLI in the runner image (D-PROBE-03); poll via the already-installed + # mysql2 driver using an inline Node script. 90s deadline covers cold-start InnoDB init. + - name: Wait for MariaDB to accept connections + # No mysql CLI in the runner image (D-PROBE-03). Poll via the mysql2 driver + # already installed in apps/pwa (devDependency). --input-type=commonjs forces + # CJS mode even though apps/pwa has "type":"module" in its package.json. + run: | + node --input-type=commonjs - <<'EOF' + const mysql = require('mysql2/promise'); + const deadline = Date.now() + 90_000; + (async () => { + while (true) { + try { + const conn = await mysql.createConnection({ + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT ?? 3306), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }); + await conn.query('SELECT 1'); + await conn.end(); + console.log('MariaDB ready'); + process.exit(0); + } catch (err) { + if (Date.now() >= deadline) { + console.error('MariaDB did not become ready within 90s:', err.message); + process.exit(1); + } + await new Promise(r => setTimeout(r, 3000)); + } + } + })(); + EOF + working-directory: apps/pwa + + # Apply schema migrations. Uses drizzle-kit migrate (applies committed SQL files). + # Never use drizzle push — unsafe on MariaDB (emits destructive TRUNCATE diff, T-08-04). + - name: Run DB migrations + run: pnpm --filter @familysync/api db:migrate + + # Full DB-backed API test suite (all tests in apps/api/tests/ require a real MariaDB). + - name: Run API tests + run: pnpm --filter @familysync/api test + + harness: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + # Runs in PARALLEL with fast-checks + api (D-03) — no needs: dependency. + services: + mariadb: + image: mariadb:11 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: familysync + MARIADB_USER: familysync + MARIADB_PASSWORD: testpass + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + --health-start-period=30s + # Throwaway creds scoped to the ephemeral service container — never production secrets (T-08-06). + env: + DB_HOST: mariadb + DB_PORT: 3306 + DB_USER: familysync + DB_PASSWORD: testpass + DB_NAME: familysync + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Enable pnpm + run: corepack enable pnpm + + # actions/cache@v4 intentionally omitted — same reasoning as fast-checks job (D-PROBE-04). + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # Pitfall 11: service container healthy != MariaDB accepting connections. + # No mysql CLI in the runner image (D-PROBE-03); poll via the mysql2 driver + # already installed in apps/pwa (devDependency). --input-type=commonjs forces + # CJS mode even though apps/pwa has "type":"module" in its package.json. + - name: Wait for MariaDB to accept connections + run: | + node --input-type=commonjs - <<'EOF' + const mysql = require('mysql2/promise'); + const deadline = Date.now() + 90_000; + (async () => { + while (true) { + try { + const conn = await mysql.createConnection({ + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT ?? 3306), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }); + await conn.query('SELECT 1'); + await conn.end(); + console.log('MariaDB ready'); + process.exit(0); + } catch (err) { + if (Date.now() >= deadline) { + console.error('MariaDB did not become ready within 90s:', err.message); + process.exit(1); + } + await new Promise(r => setTimeout(r, 3000)); + } + } + })(); + EOF + working-directory: apps/pwa + + # Apply schema migrations. Uses drizzle-kit migrate (applies committed SQL files). + # Never use drizzle push — unsafe on MariaDB (emits destructive TRUNCATE diff, T-08-07). + - name: Run DB migrations + run: pnpm --filter @familysync/api db:migrate + + # Seed the dev user (id=1). DEV_AUTH_BYPASS injects DEV_USER (id=1) into the request + # context in-memory only — it never writes a users row (devBypass.ts). global-setup.ts + # seeds calendars/lists/events for user_id=1 but ASSUMES that user row already exists + # (true on the dev DB, false on a fresh CI DB): without it the calendars INSERT IGNORE is + # silently skipped on the users FK, so calendar 10 is missing and the calendar_events + # insert fails its FK. Idempotent INSERT IGNORE; matches DEV_USER (oidc dev/dev-user, #4A90D9). + - name: Seed dev user (id=1) + run: | + node --input-type=commonjs - <<'EOF' + const mysql = require('mysql2/promise'); + (async () => { + const conn = await mysql.createConnection({ + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT ?? 3306), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }); + await conn.execute( + "INSERT IGNORE INTO users (id, oidc_iss, oidc_sub, display_name, color) VALUES (1, 'dev', 'dev-user', 'Dev User', '#4A90D9')", + ); + console.log('seeded dev user id=1'); + await conn.end(); + })(); + EOF + working-directory: apps/pwa + + # Build the API before starting it — dist/ is gitignored and does not exist in CI (Pitfall 4). + - name: Build API + run: pnpm --filter @familysync/api build + + # Install Playwright browsers with system deps BEFORE starting the API, so the long + # 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). + # Do NOT cache browser binaries — Playwright explicitly recommends against it in CI. + - name: Install Playwright browsers + run: npx playwright install --with-deps webkit chromium + working-directory: apps/pwa + + # Start the API AND run the harness in ONE step. A bare `node &` started in an EARLIER + # step is reaped at the step boundary: CI run #7 proved :3000 was healthy during a + # separate "wait" step but dead by the time global-setup polled :5173/health → :3000 + # (after the multi-minute browser install). Keeping the API a child of THIS step's shell + # guarantees it stays alive for the entire Playwright run. + # DEV_AUTH_BYPASS=true + NODE_ENV=development are set both inline and in env: — global-setup.ts + # refuses NODE_ENV=production and the API devBypass.ts checks development. DB_* come from env:. + # CI=true makes Playwright start Vite :5173 itself (reuseExistingServer=false), use + # retries:2/workers:1, and apply reporter:'github' — which --reporter=list,html overrides + # because Gitea does not render github annotations (Pitfall 5 / D-06). Both projects run. + - name: Run harness (start API + Playwright iphone + pixel) + env: + CI: 'true' + # Use 127.0.0.1 (not localhost): the runner image resolves `localhost` to ::1 first, + # but the Vite dev server binds IPv4-only (127.0.0.1:5173). global-setup.ts uses Node + # fetch (no IPv4 fallback, unlike curl), so localhost→::1:5173 → ECONNREFUSED → its + # /health poll never returns 200. Proven via [::1]:5173 ECONNREFUSED vs 127.0.0.1:5173 200. + # --dns-result-order=ipv4first is defense-in-depth for any remaining localhost hop + # (Vite's /health proxy → localhost:3000; the API is dual-stack so that hop already works). + PLAYWRIGHT_BASE_URL: http://127.0.0.1:5173 + NODE_OPTIONS: '--dns-result-order=ipv4first' + DEV_AUTH_BYPASS: 'true' + NODE_ENV: development + DB_HOST: mariadb + DB_PORT: 3306 + DB_USER: familysync + DB_PASSWORD: testpass + DB_NAME: familysync + run: | + NODE_ENV=development DEV_AUTH_BYPASS=true node apps/api/dist/index.js & + API_PID=$! + echo "API PID: $API_PID" + + # Wait for the API :3000/health before launching Playwright (D-02 / T-08-08). + deadline=$((SECONDS + 60)) + until curl -sf http://localhost:3000/health > /dev/null 2>&1; do + if ! kill -0 "$API_PID" 2>/dev/null; then echo "API process exited before becoming ready"; exit 1; fi + if [ $SECONDS -ge $deadline ]; then echo "API did not become ready within 60s"; kill "$API_PID" 2>/dev/null || true; exit 1; fi + sleep 2 + done + echo "API ready at :3000" + + # Run the Phase 7 harness across both profiles; preserve its exit code, always kill the API. + # Call the pwa test:e2e script DIRECTLY (single pnpm layer) and append --reporter without a + # `--` separator: `pnpm test:e2e -- ` double-forwards the `--` into + # `playwright test -- `, where playwright treats --reporter as a test-file filter → + # "No tests found" (run #10). The filtered single-layer form forwards the flag cleanly. + set +e + pnpm --filter @familysync/pwa test:e2e --reporter=list,html + rc=$? + kill "$API_PID" 2>/dev/null || true + exit $rc + + # Upload traces/screenshots/videos on failure for debugging (D-06). + # MUST use ChristopherHX/gitea-upload-artifact@v4 — the standard upload-artifact action + # detects Gitea as GHES and aborts (Pitfall 6 / D-PROBE-06). + - name: Upload Playwright test artifacts + if: failure() + uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4 + with: + name: playwright-traces-${{ github.run_id }} + path: apps/pwa/test-results/ + retention-days: 14 + + publish: + runs-on: ubuntu-latest + # Push to main only — never on pull_request (D-03). No dev-bypass flag in this job (T-08-09). + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + # Compute both image tags per D-04: + # :latest — moving pointer for easy pulls + # :- — immutable, rollback-traceable (e.g. v1.1-4303a1b) + # GITHUB_SHA is confirmed available in Gitea Actions (probe P-13). + # MILESTONE is read from the workflow-level env var (set to v1.1 above) — update at milestone boundaries. + - name: Compute image tags + id: tags + run: | + SHORT_SHA=${GITHUB_SHA:0:7} + MILESTONE="${{ env.MILESTONE }}" + echo "latest=git.bergerhouse.net/luckberg/familysync-api:latest" >> $GITHUB_OUTPUT + echo "sha_tag=git.bergerhouse.net/luckberg/familysync-api:${MILESTONE}-${SHORT_SHA}" >> $GITHUB_OUTPUT + + # Pitfall 13 (load-bearing security step): PAT piped via stdin — never via -p/--password. + # GITEA_TOKEN/GITHUB_TOKEN cannot push packages; a PAT with write:package scope is required + # (confirmed: Gitea forum + registry docs). Token is masked by Gitea's secret-log scrubber + # and never echoed elsewhere or set as a plain env var. + # Secret is named REGISTRY_PAT (not GITEA_REGISTRY_PAT): Gitea reserves the GITEA_ prefix + # for secret names, so the GITEA_-prefixed name cannot be created. + - name: Docker login + run: | + echo "${{ secrets.REGISTRY_PAT }}" | \ + docker login git.bergerhouse.net \ + --username luckberg \ + --password-stdin + + # 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. + - name: Build and push + run: | + docker build --target production \ + -f apps/api/Dockerfile \ + -t ${{ steps.tags.outputs.latest }} \ + -t ${{ steps.tags.outputs.sha_tag }} \ + . + docker push ${{ steps.tags.outputs.latest }} + docker push ${{ steps.tags.outputs.sha_tag }} + + # Always drop the stored credential from the runner after push (defence in depth). + - name: Docker logout + if: always() + run: docker logout git.bergerhouse.net || true diff --git a/.gitignore b/.gitignore index 88e97e9..467b726 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,8 @@ graphify-out/ # Transient workflow scratch .planning/tmp/ + +# Playwright e2e harness outputs (regenerated every run; Phase 7 mobile test harness) +apps/pwa/test-results/ +apps/pwa/playwright-report/ +apps/pwa/blob-report/ diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index e4cf75a..06709e2 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -18,7 +18,7 @@ The household can see and co-edit one color-coded family calendar (shared + each - **Initial setup wizard** — first-run validated bootstrap of env vars, VAPID keypair, DB connection, and first app password (was backlog 999.11) - **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) - **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) -- **Mobile-browser testing** — mobile viewport + authenticated PWA harness so the assistant can catch mobile-only defects (was backlog 999.12) +- **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) 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. @@ -114,4 +114,4 @@ This document evolves at phase transitions and milestone boundaries. 4. Update Context with current state --- -*Last updated: 2026-06-10 — started milestone v1.1 Operability & Polish* +*Last updated: 2026-06-11 — Phase 7 (Mobile Test Harness) complete; TEST-01/TEST-02 validated* diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 6f53053..fc9ba16 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -44,8 +44,8 @@ Each requirement maps to exactly one roadmap phase (see Traceability). ### Test — Mobile-emulated authed browser harness -- [ ] **TEST-01**: The assistant can drive the PWA in a **mobile-emulated viewport** (device profile + mobile UA + touch) for automated UI/layout verification. -- [ ] **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. +- [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) @@ -71,9 +71,9 @@ Maps each REQ-ID to its phase. v1.1 phases continue v1.0 numbering (v1.0 ended a | REQ-ID | Phase | Status | |--------|-------|--------| -| TEST-01 | Phase 7 (Mobile Test Harness) | Pending | -| TEST-02 | Phase 7 (Mobile Test Harness) | Pending | -| CI-01 | Phase 8 (Gitea CI) | Pending | +| TEST-01 | Phase 7 (Mobile Test Harness) | Complete | +| TEST-02 | Phase 7 (Mobile Test Harness) | Complete | +| CI-01 | Phase 8 (Gitea CI) | In progress | | CI-02 | Phase 8 (Gitea CI) | Pending | | CAL-15 | Phase 9 (Faster Write-Back) | Pending | | ADMIN-01 | Phase 10 (Admin Role & Settings) | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 4cc3ef4..d23dec0 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -25,7 +25,7 @@ Full phase detail archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROA 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. -- [ ] **Phase 7: Mobile Test Harness** - Mobile-emulated, authenticated PWA browser harness so the assistant (and CI) can catch mobile-only defects +- [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) - [ ] **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 - [ ] **Phase 9: Faster Write-Back** - Event-driven outbox drain so edits land in ~1-2s instead of ~15s, preserving every outbox durability guarantee - [ ] **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 @@ -37,113 +37,172 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem > 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**: TBD + +**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**: TBD + +**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)* + +- [ ] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1- 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**: TBD ### 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. + **Plans**: TBD **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**: TBD **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**: TBD **UI hint**: yes @@ -157,21 +216,20 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem | 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 | 0/? | Not started | - | -| 8. Gitea CI | v1.1 | 0/? | Not started | - | +| 7. Mobile Test Harness | v1.1 | 4/4 | Complete | 2026-06-11 | +| 8. Gitea CI | v1.1 | 3/4 | In Progress| | | 9. Faster Write-Back | v1.1 | 0/? | Not started | - | | 10. Admin Role & Settings | v1.1 | 0/? | Not started | - | | 11. Per-Event Reminders | v1.1 | 0/? | Not started | - | | 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - | - ## 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:** 3/6 plans executed +**Plans:** 3/4 plans executed Plans: @@ -266,6 +324,7 @@ Plans: **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. @@ -288,6 +347,7 @@ Plans: **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). @@ -310,6 +370,7 @@ Plans: **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?). @@ -328,3 +389,46 @@ 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. + +**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. + +**Requirements:** TBD +**Plans:** 0 plans + +Plans: + +- [ ] TBD (promote with /gsd-review-backlog when ready) diff --git a/.planning/STATE.md b/.planning/STATE.md index a0c7a5f..80f24bb 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,15 +2,16 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Operability & Polish -status: planning -last_updated: "2026-06-10T23:30:00.000Z" -last_activity: 2026-06-10 +status: executing +stopped_at: 08-03 complete — advancing to 08-04 (Wave 4, publish job) +last_updated: "2026-06-11T20:00:00.000Z" +last_activity: "2026-06-11 -- 08-03 complete; harness job green on cold CI run (run #11): 58 specs passed across iphone/WebKit + pixel/Chromium in 1.6 min; 4 infrastructure fixes (API-reap, IPv4-first, dev-user seed, reporter forwarding); no Phase 7 harness file modified; advancing to 08-04" progress: - total_phases: 6 - completed_phases: 0 - total_plans: 0 - completed_plans: 0 - percent: 0 + total_phases: 16 + completed_phases: 1 + total_plans: 8 + completed_plans: 6 + percent: 6 --- # Project State @@ -20,20 +21,20 @@ progress: See: .planning/PROJECT.md (updated 2026-06-10) **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:** v1.1 Operability & Polish — roadmap reordered (Phases 7–12). Next: plan Phase 7 with `/gsd-plan-phase 7` (Mobile Test Harness — independent, goes first; Phase 8 CI consumes its specs) or Phase 9 (Faster Write-Back — independent, lowest risk) or Phase 10 (Admin Role & Settings — carries the DB migration that Phases 11 & 12 need). +**Current focus:** Phase 08 — gitea-ci ## Current Position -Phase: Not started (roadmap reordered — Phases 7–12) -Plan: — -Status: Roadmap complete, awaiting phase planning -Last activity: 2026-06-10 — v1.1 roadmap reordered (6 phases, 17/17 requirements mapped) +Phase: 08 (gitea-ci) — EXECUTING +Plan: 4 of 4 (08-04 next — Wave 4, publish job) +Status: Executing — 08-03 complete, harness green (58 specs, both profiles, cold CI run) +Last activity: 2026-06-11 -- 08-03 complete; harness job green on cold CI run (run #11): 58 specs passed across iphone/WebKit + pixel/Chromium in 1.6 min; 4 infrastructure fixes (API-reap, IPv4-first, dev-user seed, reporter forwarding); no Phase 7 harness file modified; advancing to 08-04 ## Performance Metrics **Velocity:** -- Total plans completed: 17 +- Total plans completed: 21 - Average duration: - - Total execution time: 0 hours @@ -43,6 +44,7 @@ Last activity: 2026-06-10 — v1.1 roadmap reordered (6 phases, 17/17 requiremen |-------|-------|-------|----------| | 02 | 5 | - | - | | 03 | 12 | - | - | +| 07 | 4 | - | - | **Recent Trend:** @@ -76,6 +78,10 @@ Last activity: 2026-06-10 — v1.1 roadmap reordered (6 phases, 17/17 requiremen | Phase 06-ux-polish P04 | 5 | 2 tasks | 2 files | | Phase 06-ux-polish P05 | 35 | 4 tasks | 6 files | | Phase 06-ux-polish P06 | 45 | 4 tasks | 5 files | +| Phase 07 P01 | 310 | 3 tasks | 7 files | +| Phase 07 P02 | 196 | 2 tasks | 4 files | +| Phase 07-mobile-test-harness P03 | 480 | 2 tasks | 2 files | +| Phase 07-mobile-test-harness P04 | 22 | 2 tasks | 2 files | ## Accumulated Context @@ -84,6 +90,14 @@ Last activity: 2026-06-10 — v1.1 roadmap reordered (6 phases, 17/17 requiremen Decisions are logged in PROJECT.md Key Decisions table. Recent decisions affecting current work: +- D-PROBE-01 (2026-06-11, 08-01): runs-on must be ubuntu-latest — runner has no self-hosted label; all downstream ci.yml workflows use ubuntu-latest. +- D-PROBE-02 (2026-06-11, 08-01): Docker-executor confirmed (/.dockerenv present); services: works; DB_HOST=mariadb in all downstream jobs. +- D-PROBE-03 (2026-06-11, 08-01): No mysql CLI in runner image — DB readiness uses healthcheck.sh --connect --innodb_initialized or Node mysql2 wait; no mysql shell-out. +- D-PROBE-04 (2026-06-11, 08-01): actions/cache@v4 timed out — skip cache in Plans 02/03 critical path; best-effort with continue-on-error if used. +- D-PROBE-05 (2026-06-11, 08-01): Playwright WebKit + Chromium deps install cleanly (exit 0); Phase-7 harness CI-feasible. +- D-PROBE-06 (2026-06-11, 08-01): ChristopherHX/gitea-upload-artifact@v4 works — MUST use this fork; actions/upload-artifact@v4 broken on Gitea. +- D-PROBE-07 (2026-06-11, 08-01): ${GITHUB_SHA:0:7} produces 7 chars — D-04 publish tag expression valid. +- D-PROBE-08 (2026-06-11, 08-01): GITEA_REGISTRY_PAT deferred to Plan 04; PAT not exercised in probe. - CAL-08 RESOLVED → GO (Phase 1): per-member Fastmail app password reaches all of that account's calendars; no cross-account ACL needed. Unified view stands; no shared-only fallback. See CAL-08-DECISION.md. - D-14 (2026-06-04): Phase 1 Gate 2 (live Authelia/Pangolin) deferred. SSE-over-Pangolin smoke = hard gate before Phase 4; live AUTH smoke incl. iOS standalone-PWA folded into Phase 3. Phases 2–3 build behind a dev-auth bypass. Tracked in 01-HUMAN-UAT.md + docs/deployment.md. - D-15 (2026-06-04): Validate real topology via local Newt connector + test subdomain through Pangolin (Mode A), not an Unraid deploy; Unraid reserved for go-live. @@ -123,6 +137,8 @@ Recent decisions affecting current work: - [Phase 06-05]: AuthSplash state machine: loading/redirecting/dead-end; CalendarContent renders only on meQuery.isSuccess (D-10); sessionExpired flag via Zustand + global QueryCache/MutationCache onError (D-11); one-shot redirect guard re-armed only on explicit user tap - [Phase 06-06]: Schedule-X all-day CSS: .sx__all-day-event does not exist in v4.6.0; real selectors are .sx__date-grid-event (week/day) + .sx__month-grid-event:not(:has(.sx__month-grid-event-time)) (month); --sx-color-primary-container remapped as fallback - [Phase 06]: Phase-level UX fixes (surfaced during UAT, not in any single plan): AppNav made persistent across routes — nav no longer disappears on /lists (commits 6070437 RED + 051874b fix); BottomTabBar hidden on desktop — no longer overlaps sidebar Settings affordance (commits 740e342 RED + 089b53d fix) +- [Phase ?]: D-04-SCHEDULE-X-LOCATOR: Used .sx-react-calendar-wrapper CSS class to assert Schedule-X grid — no semantic role on outer wrapper div +- [Phase ?]: D-04-EMPTY-NETWORK-SIM: Lists empty state simulated via page.route to 200 empty array — preserves seeded DB for parallel workers (D-06 / T-07-11) ### Roadmap Evolution @@ -183,9 +199,9 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-10T23:30:00.000Z -Stopped at: v1.1 roadmap reordered (Phases 7–12; 17/17 requirements mapped) -Resume file: None +Last session: 2026-06-11T20:00:00.000Z +Stopped at: 08-03 complete — advancing to 08-04 (Wave 4, publish job) +Resume file: .planning/phases/08-gitea-ci/08-04-PLAN.md ## Operator Next Steps diff --git a/.planning/phases/07-mobile-test-harness/07-01-PLAN.md b/.planning/phases/07-mobile-test-harness/07-01-PLAN.md new file mode 100644 index 0000000..b1380e4 --- /dev/null +++ b/.planning/phases/07-mobile-test-harness/07-01-PLAN.md @@ -0,0 +1,179 @@ +--- +phase: 07-mobile-test-harness +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - apps/pwa/package.json + - apps/pwa/playwright.config.ts + - apps/pwa/vitest.config.ts + - apps/pwa/tsconfig.json + - package.json +autonomous: true +requirements: [TEST-01, TEST-02] +user_setup: [] + +must_haves: + truths: + - "playwright test --list reports exactly two projects: iphone and pixel" + - "Vitest does NOT pick up e2e/*.spec.ts files (no glob collision)" + - "Both WebKit and Chromium browser engines are installed for @playwright/test" + - "tsc --noEmit passes in apps/pwa with the new playwright.config.ts and e2e/ files in scope" + artifacts: + - path: "apps/pwa/playwright.config.ts" + provides: "Two-project device matrix (iPhone/WebKit, Pixel/Chromium), serviceWorkers block, env baseURL, globalSetup ref, vite-only webServer, trace/artifact config" + contains: "devices['iPhone 14']" + - path: "apps/pwa/vitest.config.ts" + provides: "exclude e2e/** so Vitest's default *.spec.ts glob does not collide with Playwright specs" + contains: "exclude" + - path: "apps/pwa/package.json" + provides: "@playwright/test devDependency + test:e2e scripts" + contains: "test:e2e" + key_links: + - from: "apps/pwa/playwright.config.ts" + to: "apps/pwa/e2e/global-setup.ts" + via: "globalSetup config option" + pattern: "globalSetup.*global-setup" + - from: "apps/pwa/playwright.config.ts" + to: "PLAYWRIGHT_BASE_URL env" + via: "use.baseURL env-driven" + pattern: "PLAYWRIGHT_BASE_URL" +--- + + +Stand up the Playwright test-harness foundation in `apps/pwa`: add `@playwright/test` as a dev dependency, install the WebKit + Chromium browser engines, author `playwright.config.ts` with the two-profile device matrix (iPhone/WebKit + Pixel/Chromium), and isolate the new `e2e/*.spec.ts` glob from the existing Vitest `*.spec.ts` default glob. This is the blocking dependency for the seed plan and all spec plans. + +Purpose: Every downstream plan (global-setup, layout/calendar/lists specs) imports from `@playwright/test` and runs under this config. Nothing else in the phase can land until the config matrix, browser engines, and glob isolation exist. + +Output: `apps/pwa/playwright.config.ts`, the `@playwright/test` dev dep + installed browsers, `vitest.config.ts` exclude, package.json scripts, and `e2e/` brought into the `tsc --noEmit` gate. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/07-mobile-test-harness/07-CONTEXT.md +@.planning/phases/07-mobile-test-harness/07-RESEARCH.md +@.planning/phases/07-mobile-test-harness/07-PATTERNS.md +@.planning/phases/07-mobile-test-harness/07-VALIDATION.md + + + + + + Task 1: Install @playwright/test + browser engines, wire package.json scripts + apps/pwa/package.json, package.json + + - apps/pwa/package.json — current scripts block (`dev`, `build`, `preview`, `typecheck`, `test`) and devDependencies; mirror naming + - package.json (root) — workspace script convention: `pnpm --filter @familysync/