Compare commits
88
Commits
v1.0
...
bcc9682a01
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcc9682a01 | ||
|
|
b12e10d129 | ||
|
|
234384c142 | ||
|
|
92353e1860 | ||
|
|
bb331fd110 | ||
|
|
e14054c264 | ||
|
|
f153a72c36 | ||
|
|
b1851f475e | ||
|
|
98acff8108 | ||
|
|
73eecf7559 | ||
|
|
dcf42423a5 | ||
|
|
ebcc38d810 | ||
|
|
212d8c1691 | ||
|
|
03e8088238 | ||
|
|
e486c6be9f | ||
|
|
73897407c7 | ||
|
|
53a989c3fb | ||
|
|
71c89093b1 | ||
|
|
d55e347a09 | ||
|
|
2a34a94cc6 | ||
|
|
694ffe713b | ||
|
|
78229168b9 | ||
|
|
181d161da6 | ||
|
|
0b148b96f8 | ||
|
|
dc31d4e1ec | ||
|
|
c0f892cae5 | ||
|
|
3343f36e97 | ||
|
|
667f01702c | ||
|
|
087d9af117 | ||
|
|
134d4db08a | ||
|
|
e57b76ef59 | ||
|
|
b333d7b7ea | ||
|
|
4a510f1d79 | ||
|
|
41b0b60291 | ||
|
|
4e474cdd9c | ||
|
|
92acf02989 | ||
|
|
e8a9ce4ea9 | ||
|
|
c3cee0baae | ||
|
|
4303a1b680 | ||
|
|
9c38dd33ff | ||
|
|
5322cfc2b0 | ||
|
|
2b745adb29 | ||
|
|
c564fc67a1 | ||
|
|
789e87a360 | ||
|
|
53c3ca56b8 | ||
|
|
f52b722b9b | ||
|
|
84921a0464 | ||
|
|
407bf1e91c | ||
|
|
3b4fd9a5b1 | ||
|
|
e105dce9de | ||
|
|
fcc680e553 | ||
|
|
8458dc25eb | ||
|
|
10e570b21d | ||
|
|
b074b4abb2 | ||
|
|
17b625b6fe | ||
|
|
c44bcc9c37 | ||
|
|
4cd7931c3c | ||
|
|
f8227855dc | ||
|
|
52e14a88db | ||
|
|
d3c6726301 | ||
|
|
8f0846c8c6 | ||
|
|
535ba11cda | ||
|
|
53498e3408 | ||
|
|
e7fdf798dd | ||
|
|
4536987915 | ||
|
|
44fea2c5e2 | ||
|
|
0c24f77bdb | ||
|
|
32deab91fd | ||
|
|
ed206c3732 | ||
|
|
7a089421dd | ||
|
|
ad927e04d2 | ||
|
|
3cee2b394d | ||
|
|
179766d447 | ||
|
|
c4b2f55ccf | ||
|
|
2477741337 | ||
|
|
16d031629e | ||
|
|
ab02cdb1ba | ||
|
|
a31636e718 | ||
|
|
9bd0f536fd | ||
|
|
a373df15ab | ||
|
|
3696a85afc | ||
|
|
361bd8f800 | ||
|
|
3ed7fc8da8 | ||
|
|
a9c3304c4e | ||
|
|
581b31916b | ||
|
|
b2dacf9940 | ||
|
|
53b916601f | ||
|
|
1f361a9473 |
@@ -0,0 +1,307 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
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 <root> test:e2e -- <args>` double-forwards the `--` into
|
||||
# `playwright test -- <args>`, 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
|
||||
@@ -0,0 +1,77 @@
|
||||
# Publishing / Releases
|
||||
#
|
||||
# Trigger: push to main — i.e. when any PR merges.
|
||||
# Image: git.bergerhouse.net/luckberg/familysync-api
|
||||
# Tags:
|
||||
# :latest — moving pointer for easy pulls
|
||||
# :<MILESTONE>-<shortsha> — immutable, rollback-traceable (e.g. v1.1-98acff8)
|
||||
#
|
||||
# Required secret: REGISTRY_PAT — a Gitea Actions secret holding a PAT with write:package scope.
|
||||
# Named REGISTRY_PAT (not GITEA_*): Gitea reserves the GITEA_ prefix for secret names, so
|
||||
# GITEA_-prefixed names cannot be created. GITEA_TOKEN / GITHUB_TOKEN cannot push packages.
|
||||
#
|
||||
# Safety gate: branch protection on main, NOT a needs: dependency in this file.
|
||||
# The PR test jobs (fast-checks, api, harness in ci.yml) run on pull_request — they never
|
||||
# run in the same workflow invocation as publish.yml. Tests gate the PR; main is trusted to
|
||||
# be green because direct push and force push are blocked and the three required checks
|
||||
# (CI / fast-checks, CI / api, CI / harness) must pass before merge.
|
||||
#
|
||||
# To bump the milestone tag at a milestone boundary: edit MILESTONE below.
|
||||
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
MILESTONE: v1.1
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Compute both image tags per D-04:
|
||||
# :latest — moving pointer for easy pulls
|
||||
# :<milestone>-<shortsha> — 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
|
||||
@@ -49,4 +49,13 @@ graphify-out/
|
||||
# Intel / graph diff baselines (local-only; regenerated on each refresh/build)
|
||||
.planning/intel/.last-refresh.json
|
||||
.planning/graphs/.last-build-snapshot.json
|
||||
.planning/graphs/.last-build-status.json
|
||||
.planning/research/.cache/
|
||||
|
||||
# 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/
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Milestones
|
||||
|
||||
## 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).
|
||||
|
||||
**Delivered:** A self-hosted, Dockerized family-organization PWA on the household's existing Fastmail account — unified color-coded calendar (shared + personal) with full event CRUD written back via CalDAV, installable PWA, shared collaborative lists with real-time co-edit sync, and Web Push notifications — usable cross-ecosystem with no app store.
|
||||
|
||||
**Key accomplishments:**
|
||||
|
||||
- **Phase 1 — Foundation + Broker Spike:** Authelia OIDC sessions with stable per-member identity (iss+sub) + auto-assigned color, AES-256-GCM credential encryption, and a tsdav CalDAV broker reading Fastmail calendars. CAL-08 resolved **GO** (per-member app-password model — no cross-account ACL).
|
||||
- **Phase 2 — Calendar Display:** Unified color-coded calendar across day/week/month/agenda with server-side occurrence expansion (VTIMEZONE/DST, all-day, EXDATE), each member's events in their assigned color.
|
||||
- **Phase 3 — Event Write-Back + PWA Install:** Full event CRUD written back to Fastmail (enqueue-only outbox, 202 optimistic-accept), installable PWA with auth-safe service worker and guided iOS install. Taken live over real Authelia/Pangolin and verified end-to-end on desktop and iOS.
|
||||
- **Phase 4 — Shared Lists + Live Sync:** Named collaborative lists with item CRUD, fractional-rank drag-reorder, and real-time SSE co-edit sync — member-scoped with no cross-tenant leak proven at the route layer.
|
||||
- **Phase 5 — Web Push Notifications:** VAPID push for event reminders, event-change alerts, and coalesced list-change notifications; resilient `setInterval` broker workers (poller/outbox/reminder) with catch-up + per-uid exactly-once dedup.
|
||||
- **Phase 6 — UX Polish:** All-day visual distinction, duration-preserving event end-tracking, RRULE UNTIL/COUNT bounding + whole-series edit prompt, pre-auth AuthSplash gating, session-expiry interstitial, and persistent nav chrome.
|
||||
|
||||
**Requirements:** 20/20 v1 requirements complete (AUTH, CAL, PWA, LIST, NOTIF). Deferred out of scope: CAL-09…CAL-12 (v1.x), DISP-01/DISP-02 (v2).
|
||||
|
||||
**Known deferred items at close** (acknowledged, device-only / live-infra — not regressions):
|
||||
- Phase 01 Gate-2 live checks (Authelia OIDC over Pangolin, iOS standalone install/redirect, session persistence) — carried under D-14; spot-check at go-live.
|
||||
- Phase 06 iOS device-only residuals: standalone cold-load + OIDC redirect (D-10/D-11), PushPermissionPrompt spinner (CP-04.3).
|
||||
- Android event-change push: server-side FCM delivery proven (201); on-device confirmation + operator channel-importance bump pending (05-UAT Test 4).
|
||||
- Stale `kickoff-new-project` todo (superseded; archived during 2026-06-10 backlog review).
|
||||
|
||||
---
|
||||
+33
-17
@@ -8,6 +8,20 @@ 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.
|
||||
|
||||
## Current Milestone: v1.1 Operability & Polish
|
||||
|
||||
**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.
|
||||
|
||||
**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)
|
||||
- **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)
|
||||
- **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** ✅ **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.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Validated
|
||||
@@ -18,13 +32,13 @@ The household can see and co-edit one color-coded family calendar (shared + each
|
||||
- [x] Create / edit / delete events written back to the correct Fastmail calendar via the app's single broker token — **Validated in Phase 3 (Gate 2, live 2026-06-07)**: create (timed/all-day/weekly-recurring), edit, delete, and recurring-series delete all round-trip to caldav.fastmail.com; 412-conflict handled. Recurring repeat-bound + per-occurrence-duration UX are "create+display only in v1" gaps (backlog 999.7/999.8).
|
||||
- [x] Authelia OIDC login for every member (true SSO) — **Validated in Phase 3 (Gate 2, live 2026-06-07)**: both members log in via real Authelia OIDC over Pangolin; distinct stable colors; session carried transparently by Authelia SSO. (Full-name legend needs an Authelia ID-token `claims_policy` — operator step.)
|
||||
- [x] React PWA installable on iPhone via "Add to Home Screen" (no App Store) — **Validated in Phase 3 (Gate 2, live 2026-06-07)**: iOS install + full-screen standalone launch + standalone OIDC login (load-bearing) confirmed on the wife's iPhone. Android install walkthrough deferred (B5, not yet device-tested).
|
||||
- [x] Shared collaborative lists (groceries, gift ideas) co-edited by both members, stored in MariaDB — **Validated in Phase 4 (shared-lists-live-sync)**: list + item CRUD, fractional-rank drag-reorder, member-scoped access (no cross-tenant leak proven at route layer).
|
||||
- [x] Live list sync so co-edits appear without manual refresh — **Validated in Phase 4**: scoped SSE fan-out over Pangolin (transport smoke-tested), bounded-backoff reconnect, co-edits land within seconds.
|
||||
- [x] Web Push notifications for event reminders and list changes — **Validated in Phase 5 (web-push-notifications)**: VAPID push for reminders, event-change, and coalesced list alerts; on-device UAT 1/2/5 PASS (iOS reminder delivery, iOS push, coalescing). Android event-change on-device confirmation + iOS standalone spinner remain device-only spot-checks at go-live.
|
||||
|
||||
### Active
|
||||
|
||||
<!-- v1 scope. Hypotheses until shipped and validated. -->
|
||||
- [ ] Shared collaborative lists (groceries, gift ideas) that both members co-edit, stored in MariaDB
|
||||
- [ ] Live list sync so co-edits appear without manual refresh (Redis optional)
|
||||
- [ ] Web Push notifications for event reminders and list changes
|
||||
<!-- Carried into v1.x — partially validated or device-pending. -->
|
||||
- [ ] Android PWA install walkthrough verified on a real Android device (iOS validated Phase 3; Android = carried Gate 2 row B5)
|
||||
- [ ] Low-friction onboarding for the non-technical Apple member — visit one URL, sign in. **Partially validated Phase 3** (wife logged in + installed unaided); the per-member Fastmail app-password provider-setup step is still missing (backlog 999.5)
|
||||
|
||||
@@ -47,10 +61,12 @@ The household can see and co-edit one color-coded family calendar (shared + each
|
||||
- **One app to build:** backend + MariaDB + React PWA behind Authelia, sitting on top of Fastmail. This is far lighter than the prior Baikal + Vikunja + PWA design that was evaluated and dropped.
|
||||
- **Calendars are all on Fastmail.** Personal calendars are also Fastmail-hosted calendar collections, so the app reads and writes everything (shared + personal) through a single Fastmail JMAP/CalDAV broker token — no external ICS feeds, no per-member credential management. The primary user gets the shared calendar natively in the Fastmail app; Apple members use the PWA (optionally subscribe in native Apple Calendar via CalDAV).
|
||||
- **Prior exploration:** Architecture was revised across two sessions (explore → Opus verification). See `.planning/notes/familysync-architecture.md` for full reasoning behind the dropped options.
|
||||
- **Open questions (for phase research):**
|
||||
- Fastmail API — JMAP vs CalDAV for the app's calendar read/write. Which is cleaner to build against?
|
||||
- PWA Web Push — sufficient/reliable enough for family alerts on iOS, or is a fallback needed?
|
||||
- Mechanics of surfacing each member's *personal* Fastmail calendar to the broker token (calendar sharing/ACLs within Fastmail).
|
||||
- **Shipped state (v1.0, 2026-06-10):** pnpm monorepo — `apps/api` (Hono + Drizzle/MariaDB + tsdav/ical.js CalDAV broker) and `apps/pwa` (React 19 + Vite + vite-plugin-pwa + Schedule-X). Live over real Authelia OIDC + Pangolin/Newt. ~338 files changed across the milestone.
|
||||
- **Open questions — resolved this milestone:**
|
||||
- Fastmail API → **CalDAV via tsdav** (JMAP calendars unavailable on Fastmail); locked.
|
||||
- PWA Web Push → **sufficient with caveats**: VAPID direct push works on iOS 16.4+ installed PWAs and Android; iOS revokes subscriptions after 3 silent pushes (every push must be visible) and standalone install is mandatory. No FCM broker needed.
|
||||
- Per-member personal calendar → **CAL-08 GO**: a per-member Fastmail app password reaches all of that account's calendars; no cross-account ACL. Onboarding flow to collect each member's app password is backlog 999.5.
|
||||
- **Known issues / tech debt carried to v1.x:** event write-back latency (15s outbox drain, 999.13); per-event reminder config / VALARM authoring (999.4); first-login provider setup (999.5); admin Settings + setup wizard (999.10/999.11); Gitea CI (999.14); mobile-emulated authed browser testing (999.12).
|
||||
|
||||
## Constraints
|
||||
|
||||
@@ -68,16 +84,16 @@ The household can see and co-edit one color-coded family calendar (shared + each
|
||||
|
||||
| Decision | Rationale | Outcome |
|
||||
|----------|-----------|---------|
|
||||
| Calendar hosted on Fastmail, not self-hosted (Baikal dropped) | Fastmail reaches the whole household native-or-PWA with one fewer self-hosted service; Fastmail Android app can't show self-hosted CalDAV anyway | — Pending |
|
||||
| Personal calendars also Fastmail-hosted collections, aggregated via one broker token | Avoids external ICS feeds and per-member credential management; uniform read/write path | — Pending |
|
||||
| Shared lists in custom MariaDB, not Vikunja | Cross-ecosystem native task sync is impossible; a list table in the backend being built anyway is trivial vs another container + SSO integration | — Pending |
|
||||
| React PWA only, no React Native / App Store | App-like UX with one codebase for all surfaces; lowest onboarding friction (one URL) | — Pending |
|
||||
| Authelia OIDC for all members | True SSO consistent with existing infra; no separate calendar-credential problem | — Pending |
|
||||
| Web Push notifications in v1 (not deferred) | Family alerts judged essential day one; build push into the foundation | — Pending |
|
||||
| Calendar hosted on Fastmail, not self-hosted (Baikal dropped) | Fastmail reaches the whole household native-or-PWA with one fewer self-hosted service; Fastmail Android app can't show self-hosted CalDAV anyway | ✓ Validated (v1.0) |
|
||||
| Personal calendars also Fastmail-hosted collections, aggregated via one broker token | Avoids external ICS feeds and per-member credential management; uniform read/write path | ✓ Validated (v1.0, CAL-08 GO) |
|
||||
| Shared lists in custom MariaDB, not Vikunja | Cross-ecosystem native task sync is impossible; a list table in the backend being built anyway is trivial vs another container + SSO integration | ✓ Validated (v1.0, Phase 4) |
|
||||
| React PWA only, no React Native / App Store | App-like UX with one codebase for all surfaces; lowest onboarding friction (one URL) | ✓ Validated (v1.0, Phase 3) |
|
||||
| Authelia OIDC for all members | True SSO consistent with existing infra; no separate calendar-credential problem | ✓ Validated (v1.0, Phase 3) |
|
||||
| Web Push notifications in v1 (not deferred) | Family alerts judged essential day one; build push into the foundation | ✓ Validated (v1.0, Phase 5) |
|
||||
| Personal-calendar overlay in v1 (not shared-only) | Unified view of everyone's schedules is the Skylight magic worth shipping early | — Validated (CAL-08 GO, Phase 1): per-member app password reaches all of an account's calendars; no cross-account ACL needed |
|
||||
| **D-14:** Defer Phase 1 Gate 2 (live Authelia/Pangolin verification). SSE-over-Pangolin smoke = hard gate before Phase 4; live AUTH smoke incl. iOS standalone-PWA folded into Phase 3; full 2-member prod login verified there. Phases 2–3 develop behind a documented dev-auth bypass. | Gate 2 needs operator infra (Authelia config + tunnel) + docs that didn't exist; deferring unblocks Phase 2/3 code without rework risk, since the broker data path (CAL-01/CAL-08) is already proven live. SSE must still be verified before Phase 4 to avoid building live-sync on an unverified transport (#1034). | Tracked: `01-HUMAN-UAT.md`, `docs/deployment.md` |
|
||||
| **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. | — Pending (Gate 2) |
|
||||
| **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). | — Pending (shared calendar not yet created) |
|
||||
| **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-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). |
|
||||
|
||||
## Evolution
|
||||
@@ -98,4 +114,4 @@ This document evolves at phase transitions and milestone boundaries.
|
||||
4. Update Context with current state
|
||||
|
||||
---
|
||||
*Last updated: 2026-06-07 after Phase 3 (event-write-back-pwa-install)*
|
||||
*Last updated: 2026-06-11 — Phase 7 (Mobile Test Harness) complete; TEST-01/TEST-02 validated*
|
||||
|
||||
+66
-103
@@ -1,129 +1,92 @@
|
||||
# Requirements: FamilySync
|
||||
# Requirements: FamilySync — v1.1 "Operability & Polish"
|
||||
|
||||
**Defined:** 2026-06-03
|
||||
**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.
|
||||
**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.
|
||||
|
||||
## v1 Requirements
|
||||
REQ-IDs continue v1.0 numbering (CAL ≤12, NOTIF ≤3 already used). New categories: ADMIN, SETUP, CI, TEST.
|
||||
|
||||
Requirements for initial release. Each maps to roadmap phases.
|
||||
## v1.1 Requirements
|
||||
|
||||
### Authentication & Onboarding
|
||||
Each requirement maps to exactly one roadmap phase (see Traceability).
|
||||
|
||||
> **Given:** Authelia is already deployed and both household members already have Authelia accounts. Auth scope is therefore app-side only — register FamilySync as an OIDC confidential client in Authelia and integrate the login flow. No Authelia deployment, no account provisioning.
|
||||
### Calendar — Per-event reminders & write-back latency
|
||||
|
||||
- [ ] **AUTH-01**: User can log in through Authelia (OIDC SSO) — no separate FamilySync account or password to create
|
||||
- [ ] **AUTH-02**: User stays logged in across sessions so re-authentication is rare (persistent session)
|
||||
- [ ] **AUTH-03**: Each member maps to a stable identity (OIDC `iss`+`sub`) and is assigned a consistent per-member color
|
||||
- [ ] **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.
|
||||
- [ ] **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.
|
||||
- [ ] **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).
|
||||
|
||||
### Calendar
|
||||
### Notifications — Variable-lead reminder scheduling
|
||||
|
||||
- [ ] **CAL-01**: App reads the shared family Fastmail calendar via a CalDAV broker token and caches it locally (ctag polling)
|
||||
- [x] **CAL-02**: User sees a unified, color-coded calendar that aggregates every accessible calendar into one view
|
||||
- [x] **CAL-03**: User can switch between week, month, day, and agenda/list views
|
||||
- [x] **CAL-04**: User can create a timed or all-day event, written back to the correct Fastmail calendar
|
||||
- [x] **CAL-05**: User can edit an existing event
|
||||
- [x] **CAL-06**: User can delete an event
|
||||
- [x] **CAL-07**: User can create a recurring event and see all its occurrences expanded correctly (single-occurrence editing deferred to v1.x)
|
||||
- [ ] **CAL-08**: Each member's personal Fastmail calendar is overlaid into the unified view — *spike-gated in Phase 1*; if cross-account CalDAV sharing proves infeasible, v1 falls back to shared-family-only and this moves to v1.x
|
||||
- [ ] **NOTIF-04**: An event reminder push fires at the event's **chosen lead time**, not a hardcoded 15-minute lead.
|
||||
- [ ] **NOTIF-05**: An event with **no reminder set produces no reminder push** (no default 15-min fire).
|
||||
- [ ] **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.
|
||||
|
||||
### Lists
|
||||
### Administration — Settings section (role-gated)
|
||||
|
||||
- [x] **LIST-01**: User can create and delete named lists (e.g. Groceries, Gift Ideas)
|
||||
- [x] **LIST-02**: User can add items to a list, check them off, and delete them
|
||||
- [x] **LIST-03**: User can reorder items within a list
|
||||
- [x] **LIST-04**: Both members' list edits appear live for the other member without manual refresh
|
||||
> 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.
|
||||
|
||||
### Notifications
|
||||
- [ ] **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.
|
||||
- [ ] **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.
|
||||
- [ ] **ADMIN-03**: Admin Settings routes and UI are gated by a role check; a non-admin member cannot reach or invoke them.
|
||||
|
||||
- [x] **NOTIF-01**: User receives a Web Push reminder before an event starts
|
||||
- [x] **NOTIF-02**: User receives a Web Push alert when the other member changes a shared list
|
||||
- [x] **NOTIF-03**: User receives a Web Push alert when an event is added or changed
|
||||
### Setup — First-run configuration wizard
|
||||
|
||||
### PWA & Install
|
||||
- [ ] **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`.
|
||||
- [ ] **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).
|
||||
- [ ] **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**.
|
||||
- [ ] **SETUP-04**: Once setup is complete, the setup endpoints are no longer accessible (guard checked on every invocation, not only at startup).
|
||||
|
||||
- [x] **PWA-01**: App is installable to the Home Screen on iPhone and Android (web manifest + service worker, served over HTTPS)
|
||||
- [x] **PWA-02**: First-time users get a guided "Add to Home Screen" prompt (prerequisite for iOS Web Push)
|
||||
### CI — Gitea continuous integration
|
||||
|
||||
## v1.x Requirements
|
||||
- [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.
|
||||
|
||||
Deferred to a near-term follow-up release. Tracked but not in the v1 roadmap.
|
||||
### Test — Mobile-emulated authed browser harness
|
||||
|
||||
### Calendar
|
||||
- [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.
|
||||
|
||||
- **CAL-09**: User can edit/delete a single occurrence of a recurring event (RECURRENCE-ID / EXDATE)
|
||||
- **CAL-10**: User can apply a "this and following" edit to a recurring series
|
||||
- **CAL-11**: Documentation for subscribing to the Fastmail calendar natively in Apple Calendar via CalDAV (no new code)
|
||||
- **CAL-12**: Secondary-timezone display toggle for travel
|
||||
## Future Requirements (deferred, not in v1.1)
|
||||
|
||||
## v2 Requirements
|
||||
- **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.
|
||||
|
||||
### Display
|
||||
## Out of Scope (explicit exclusions)
|
||||
|
||||
- **DISP-01**: Always-on wall-display / kiosk dashboard view (Skylight-style)
|
||||
- **DISP-02**: Upcoming-events / agenda summary widget tuned for the wall display
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Explicitly excluded. Documented to prevent scope creep. Anti-features sourced from research (`.planning/research/FEATURES.md`).
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| Email features | Members keep existing mail clients; never the product's job |
|
||||
| Self-hosted calendar server (Baikal/Radicale) | Fastmail hosts all calendars via CalDAV; one fewer service |
|
||||
| Vikunja / external task backend | Lists live in MariaDB; cross-ecosystem native task sync is impossible anyway |
|
||||
| React Native / App Store app | PWA delivers app-like UX without publishing overhead |
|
||||
| PostgreSQL | Not in the stack; MariaDB is the database |
|
||||
| Chores / rewards / star system | No children in the household; lists cover any task need |
|
||||
| Meal planning / recipe box | Separate domain, high cost; grocery list covers the coordination need |
|
||||
| Kids / sub-accounts | No children; irrelevant |
|
||||
| AI email-to-event import | Requires email access (out of scope) + LLM backend; privacy risk |
|
||||
| RSVP / invite flows (iTIP/iMIP) | Two people share one calendar; both attend by default |
|
||||
| Event-level comments / photos | Two people can text; adds chat/media storage for ~zero value |
|
||||
| Activity feed / audit log | Obvious with two users |
|
||||
| Multi-household / accounts at scale | One household, two hardcoded Authelia accounts |
|
||||
| Ads / monetization | Self-hosted; no revenue model |
|
||||
| Complex permissions / role tiers | Two equal partners with identical write access |
|
||||
| Offline-first with CRDT conflict resolution | Home WiFi is primary; optimistic updates + retry suffice |
|
||||
| Grocery delivery integration | Third-party dependency; not needed |
|
||||
- **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
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| AUTH-01 | Phase 1 | Pending |
|
||||
| AUTH-02 | Phase 1 | Pending |
|
||||
| AUTH-03 | Phase 1 | Pending |
|
||||
| CAL-01 | Phase 1 | Pending |
|
||||
| CAL-08 | Phase 1 | Pending |
|
||||
| CAL-02 | Phase 2 | Complete |
|
||||
| CAL-03 | Phase 2 | Complete |
|
||||
| CAL-04 | Phase 3 | Complete |
|
||||
| CAL-05 | Phase 3 | Complete |
|
||||
| CAL-06 | Phase 3 | Complete |
|
||||
| CAL-07 | Phase 3 | Complete |
|
||||
| PWA-01 | Phase 3 | Complete |
|
||||
| PWA-02 | Phase 3 | Complete |
|
||||
| LIST-01 | Phase 4 | Complete |
|
||||
| LIST-02 | Phase 4 | Complete |
|
||||
| LIST-03 | Phase 4 | Complete |
|
||||
| LIST-04 | Phase 4 | Complete |
|
||||
| NOTIF-01 | Phase 5 | Complete |
|
||||
| NOTIF-02 | Phase 5 | Complete |
|
||||
| NOTIF-03 | Phase 5 | Complete |
|
||||
| CAL-09 | v1.x | Deferred |
|
||||
| CAL-10 | v1.x | Deferred |
|
||||
| CAL-11 | v1.x | Deferred |
|
||||
| CAL-12 | v1.x | Deferred |
|
||||
| DISP-01 | v2 | Deferred |
|
||||
| DISP-02 | v2 | Deferred |
|
||||
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.**
|
||||
|
||||
**Coverage:**
|
||||
| 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) | Pending |
|
||||
| ADMIN-01 | Phase 10 (Admin Role & Settings) | Pending |
|
||||
| ADMIN-02 | Phase 10 (Admin Role & Settings) | Pending |
|
||||
| ADMIN-03 | Phase 10 (Admin Role & Settings) | Pending |
|
||||
| CAL-13 | Phase 11 (Per-Event Reminders) | Pending |
|
||||
| CAL-14 | Phase 11 (Per-Event Reminders) | Pending |
|
||||
| NOTIF-04 | Phase 11 (Per-Event Reminders) | Pending |
|
||||
| NOTIF-05 | Phase 11 (Per-Event Reminders) | Pending |
|
||||
| NOTIF-06 | Phase 11 (Per-Event Reminders) | Pending |
|
||||
| SETUP-01 | Phase 12 (Initial Setup Wizard) | Pending |
|
||||
| SETUP-02 | Phase 12 (Initial Setup Wizard) | Pending |
|
||||
| SETUP-03 | Phase 12 (Initial Setup Wizard) | Pending |
|
||||
| SETUP-04 | Phase 12 (Initial Setup Wizard) | Pending |
|
||||
|
||||
- v1 requirements: 20 total
|
||||
- Mapped to phases: 20
|
||||
- Unmapped: 0 ✓
|
||||
- Deferred (not in v1 scope): 6 — CAL-09…CAL-12 (v1.x), DISP-01/DISP-02 (v2)
|
||||
|
||||
---
|
||||
*Requirements defined: 2026-06-03*
|
||||
*Last updated: 2026-06-10 — added deferred REQ-IDs (CAL-09…CAL-12, DISP-01/02) to traceability table*
|
||||
**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,59 @@
|
||||
# Project Retrospective
|
||||
|
||||
*A living document updated after each milestone. Lessons feed forward into future planning.*
|
||||
|
||||
## Milestone: v1.0 — MVP
|
||||
|
||||
**Shipped:** 2026-06-10
|
||||
**Phases:** 6 | **Plans:** 42 | **Sessions:** not tracked
|
||||
|
||||
### What Was Built
|
||||
- Unified color-coded Fastmail calendar (shared + personal) with full event CRUD written back via CalDAV — read views, recurrence/DST expansion, all-day, and an enqueue-only outbox write path.
|
||||
- Installable React PWA behind Authelia OIDC, taken live over Pangolin/Newt and verified end-to-end on desktop and iOS.
|
||||
- Shared collaborative lists with real-time SSE co-edit sync, and VAPID Web Push for reminders / event-change / list alerts.
|
||||
|
||||
### What Worked
|
||||
- **Dev-auth bypass to build behind a deferred gate (D-14):** Phases 2–3 were built against a documented bypass while live Authelia/Pangolin infra wasn't ready, with no rework when Gate 2 finally ran live in Phase 3.
|
||||
- **Wave-based parallel plans** within phases kept large phases (Phase 3 = 12 plans) moving.
|
||||
- **Enqueue-only outbox with optimistic 202** cleanly separated request handling from the slow CalDAV write, and made create-before-delete ordering + etag/412 handling tractable.
|
||||
|
||||
### What Was Inefficient
|
||||
- **A long tail of bugs only reproduced under live conditions** (Newt MTU blackhole, OIDC state-cookie race, write-path timezone/identity/join/cache bugs, all-day off-by-one, color collisions, silent Android notifications, session-cookie expiry). Building behind the bypass too long delayed their discovery — they all surfaced at once during live bring-up.
|
||||
- **Background workers silently failed:** node-cron 4.2.1 skipped *every* scheduled tick in the long-lived API process, so reminders/poller/outbox never fired on schedule — caught late, during Phase 5 UAT, not by tests.
|
||||
- **Repeated mobile-only defects could only be found by the operator on real devices** because the test harness is desktop-Chromium and the prod PWA is behind OIDC (→ backlog 999.12).
|
||||
|
||||
### Patterns Established
|
||||
- **`setInterval`, not node-cron, for in-process schedulers** (node-cron silently no-ops in a long-lived process). Do not reintroduce node-cron.
|
||||
- **drizzle-kit `generate`+`migrate`, never `push`, on MariaDB** — `push` emits a false destructive (truncate) diff against populated MariaDB.
|
||||
- **iOS-Safari standalone behavior is a human/device checkpoint**, not a playwright-cli check — keep those as explicit manual gates.
|
||||
- **Run `tsc --noEmit` (both apps) in the post-merge gate** — esbuild strips types so vitest stays green while tsc fails.
|
||||
|
||||
### Key Lessons
|
||||
1. Bring the real external topology (auth + tunnel) up *early* and behind a small reversible config, rather than deferring all live verification — the live-only bug class is large and clusters at first contact.
|
||||
2. Long-running Node schedulers need an integration-level "does it actually fire on a tick" check; unit tests pass while the scheduler silently does nothing.
|
||||
3. Push has hard platform footguns (iOS revokes after 3 silent pushes; standalone install mandatory; VAPID key truncation = silent Apple 403) — encode them as guards from day one, not after a missed notification.
|
||||
|
||||
### Cost Observations
|
||||
- Model mix: not tracked
|
||||
- Sessions: not tracked
|
||||
- Notable: TDD red→green discipline is visible in commit history, but per-commit `gate_status:` trailers were never emitted across the milestone — the ship-time TDD audit had nothing structured to aggregate. Wire gate_status trailers in v1.x if the audit is wanted.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Milestone Trends
|
||||
|
||||
### Process Evolution
|
||||
|
||||
| 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 |
|
||||
|
||||
### Cumulative Quality
|
||||
|
||||
| Milestone | Tests | Coverage | Zero-Dep Additions |
|
||||
|-----------|-------|----------|-------------------|
|
||||
| v1.0 | PWA 191 + API broker/events 114 green | not measured | n/a |
|
||||
|
||||
### Top Lessons (Verified Across Milestones)
|
||||
|
||||
1. (pending second milestone to cross-validate)
|
||||
+271
-219
@@ -1,266 +1,273 @@
|
||||
# Roadmap: FamilySync
|
||||
|
||||
## Overview
|
||||
## Milestones
|
||||
|
||||
FamilySync is built in five phases, each delivering an end-to-end user-observable capability. Phase 1 is both the foundation and the highest-risk gate: OIDC auth must work and the CalDAV broker must prove it can read personal Fastmail calendars before any calendar UI is built. Phases 2–3 complete the calendar. Phase 4 delivers shared lists with live co-edit sync. Phase 5 wires up Web Push notifications. The dependency chain is strict: each phase is a prerequisite for the next, except the lists track (Phase 4) which is independent of the calendar write path.
|
||||
- ✅ **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–14 (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
|
||||
|
||||
## Phases
|
||||
|
||||
**Phase Numbering:**
|
||||
<details>
|
||||
<summary>✅ v1.0 MVP (Phases 1–6) — SHIPPED 2026-06-10</summary>
|
||||
|
||||
- Integer phases (1, 2, 3): Planned milestone work
|
||||
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
|
||||
- [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
|
||||
|
||||
Decimal phases appear between their surrounding integers in numeric order.
|
||||
Full phase detail archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md).
|
||||
|
||||
- [x] **Phase 1: Foundation + Broker Spike** - Auth, Docker scaffold, CalDAV broker read path, and personal-calendar ACL spike (go/no-go gate) (completed 2026-06-04)
|
||||
- [x] **Phase 2: Calendar Display** - Read-only unified color-coded calendar (week/month/day/agenda) built on the confirmed broker (completed 2026-06-05)
|
||||
- [x] **Phase 3: Event Write-Back + PWA Install** - Full event CRUD written back to Fastmail, PWA manifest + service worker, guided iOS install flow (completed 2026-06-07)
|
||||
- [x] **Phase 4: Shared Lists + Live Sync** - Named collaborative lists with item CRUD and real-time SSE co-edit sync (completed 2026-06-09)
|
||||
- [x] **Phase 5: Web Push Notifications** - VAPID push for event reminders, event changes, and list-change alerts (completed 2026-06-10; on-device UAT 1/2/5 PASS, T3 dropped as non-gating, T4 Android event-change push deferred to Phase 6 verification — see 05-UAT.md)
|
||||
- [x] **Phase 6: UX Polish** - All-day visual distinction, event-form date/recurrence behavior, recurring-series edit, and auth-flow smoothing (completed 2026-06-10)
|
||||
</details>
|
||||
|
||||
### 🚧 v1.1 Operability & Polish (Phases 7–14)
|
||||
|
||||
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)
|
||||
- [ ] **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
|
||||
- [ ] **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
|
||||
- [ ] **Phase 12: Initial Setup Wizard** - First-run validated bootstrap of env/VAPID/DB/OIDC + first app password, reusing the admin route surface
|
||||
- [ ] **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
|
||||
- [ ] **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
|
||||
|
||||
## Phase Details
|
||||
|
||||
### Phase 1: Foundation + Broker Spike
|
||||
> v1.0 phase detail (Phases 1–6) is archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md).
|
||||
|
||||
**Goal**: The app stack is running, both members can authenticate, and the CalDAV broker can read Fastmail calendars — with a confirmed go/no-go decision on personal-calendar cross-account sharing
|
||||
**Mode:** mvp
|
||||
**Depends on**: Nothing (first phase)
|
||||
**Requirements**: AUTH-01, AUTH-02, AUTH-03, CAL-01, CAL-08
|
||||
### 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. Both members can reach the app URL, authenticate through Authelia OIDC, and land on the app home page without entering any Fastmail credentials
|
||||
2. Sessions persist across browser restarts — neither member is asked to log in again on the next visit
|
||||
3. Each member is assigned a stable, distinct display color that does not change between sessions
|
||||
4. The broker successfully fetches and caches at least one event from the shared Fastmail calendar via CalDAV PROPFIND/REPORT
|
||||
5. The personal-calendar ACL spike produces a documented go/no-go decision: either the broker token sees the wife's personal calendar after Fastmail share+accept, or the fallback strategy (shared-family-only or per-member app password) is chosen and recorded
|
||||
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.
|
||||
|
||||
**Verification status (D-14, 2026-06-04):** Code + **Gate 1** complete. Gate 1 = stack up (`/health` live), CAL-01 proven live (503 real events cached via REPORT), CAL-08 = **GO** (per-member app-password model, see `CAL-08-DECISION.md`). **Gate 2 deferred** — criteria 1/2/3 (live Authelia OIDC login over Pangolin, session persistence, distinct colors in a real browser) and the SSE-over-Pangolin smoke test require the operator's Authelia + Pangolin/Newt infra; tracked in `01-HUMAN-UAT.md` and `docs/deployment.md`. The live AUTH smoke (incl. iOS) is folded into **Phase 3**; the SSE smoke is a hard gate before **Phase 4**. Phases 2–3 develop behind a documented dev-auth bypass.
|
||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||
|
||||
**Plans**: 4 plans
|
||||
Plans:
|
||||
- **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).
|
||||
|
||||
- [x] 01-01-PLAN.md — Walking skeleton: monorepo scaffold + Docker/MariaDB + Drizzle schema (push) + /health end-to-end slice + Vitest Wave 0 harness
|
||||
- [x] 01-02-PLAN.md — Authelia OIDC slice: stable identity (iss+sub) + auto-assigned member color + /api/me + authenticated PWA shell (AUTH-01/02/03)
|
||||
- [x] 01-03-PLAN.md — CalDAV broker slice: AES-256-GCM credential encryption + tsdav broker + ical.js sync (all-day DATE) + ctag poller + /api/events (CAL-01)
|
||||
- [x] 01-04-PLAN.md — Integration + gate: wire poller/routes, event-proof landing page, CAL-08 spike + go/no-go doc, live Pangolin deploy + SSE smoke test
|
||||
|
||||
### Phase 2: Calendar Display
|
||||
|
||||
**Goal**: Both members can see a unified, color-coded calendar aggregating all accessible Fastmail calendars across day, week, month, and agenda views — read-only, no write-back yet
|
||||
**Mode:** mvp
|
||||
**Depends on**: Phase 1
|
||||
**Requirements**: CAL-02, CAL-03, CAL-07
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. Opening the app shows a color-coded calendar where each member's events appear in their assigned color, with shared events distinguishable from personal events
|
||||
2. The user can switch between day, week, month, and agenda views and all events render correctly in each view
|
||||
3. A recurring event (e.g., weekly meeting) displays all its occurrences correctly in the current view window, including correct behavior across DST boundaries
|
||||
4. All-day events (birthdays, holidays) appear as full-day banners on the correct date with no timezone shift
|
||||
|
||||
**Plans**: 5 plansPlans:
|
||||
**Plans**: 4 plans (3 waves)Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 02-01-PLAN.md — Foundation: schema (hasRrule + calendars.isShared, pushed) + dev-auth bypass + PWA vitest/jsdom harness + ICS fixtures + RED test stubs
|
||||
- [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] 02-02-PLAN.md — Backend slice: expandOccurrences() (VTIMEZONE/DST + all-day + EXDATE) + windowed/joined/zod-validated /api/events + shared-calendar checkpoint (CAL-02/CAL-07)
|
||||
- [x] 02-03-PLAN.md — Frontend foundation: CSS token layer + colorUtils + calendarConfig (firstDayOfWeek 0→7) + hydrateEvents (Temporal/PlainDate guard) + Zustand store + windowed fetchEvents
|
||||
- [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] 02-04-PLAN.md — Vertical slice: CalendarShell mounts Schedule-X, renders real windowed Fastmail events color-coded across all four views (CAL-02/CAL-03)
|
||||
- [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] 02-05-PLAN.md — UX completion: read-only EventDetailPopover + ColorLegend + nav/toolbar + skeleton/empty/error states + human visual verification
|
||||
|
||||
**Gap-closure waves** *(from 03-REVIEW.md — write path was broken end-to-end; Gate 2 / 03-08 is blocked on these)*
|
||||
|
||||
- [x] 03-09-PLAN.md — Route layer: align zod schema to client title/start/end contract (CR-01) + real OIDC iss/sub→users.id resolution on all 5 handlers (CR-06) [wave 1]
|
||||
- [x] 03-12-PLAN.md — PWA EventForm: edit-mode population + recurrence preselect (WR-03), zone-consistent dates (WR-05), real focus trap (WR-07); PWA-01/02 install assets verified [wave 1]
|
||||
- [x] 03-10-PLAN.md — Worker dispatch: build real VEVENT via buildVeventString + all-day DTEND+1 (CR-02/WR-04), fail closed on bad creds (CR-03), backoff index + randomUUID (WR-01/WR-08) [wave 2, after 03-09]
|
||||
- [x] 03-11-PLAN.md — Outbox durability: durable create-before-delete (CR-04), drain concurrency guard (CR-05), fresh-etag-before-PUT (WR-02) [wave 3, after 03-10]
|
||||
- [x] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1-<sha> via --password-stdin)
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 3: Event Write-Back + PWA Install
|
||||
### Phase 9: Faster Write-Back
|
||||
|
||||
**Goal**: Both members can create, edit, and delete events that are written back to the correct Fastmail calendar, and the app is installable to the iPhone and Android home screens with a guided onboarding flow
|
||||
**Mode:** mvp
|
||||
**Depends on**: Phase 2
|
||||
**Requirements**: CAL-04, CAL-05, CAL-06, CAL-07, PWA-01, PWA-02
|
||||
**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. A member can create a timed or all-day event (including recurring events) in the app and see it appear in the native Fastmail app within the next sync cycle
|
||||
2. A member can edit an existing event's title, time, or description and the change persists correctly in Fastmail
|
||||
3. A member can delete an event and it disappears from all views on the next sync
|
||||
4. On Android, the app shows a browser install prompt and installs to the home screen; on iOS, the app shows a guided "Add to Home Screen" walkthrough with annotated screenshots that a non-technical user can follow independently
|
||||
5. The installed PWA opens full-screen without browser chrome on both iOS and Android
|
||||
6. **(Carried from Phase 1 Gate 2, D-14)** Live Authelia OIDC login works over the public Pangolin URL — including the **iOS standalone-PWA** flow: the wife can install to Home Screen and complete login without the redirect breaking out of standalone mode; sessions persist (AUTH-01/02) and members get distinct stable colors (AUTH-03). Verify per `docs/deployment.md` Gate 2 checklist; this is the first real external deploy (local Newt test rig is sufficient — Unraid prod is optional until go-live).
|
||||
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).
|
||||
|
||||
**Plans**: 12 plans (8 original + 4 gap-closure from 03-REVIEW.md)
|
||||
Plans:
|
||||
**Wave 1**
|
||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||
|
||||
- [x] 03-01-PLAN.md — Foundation: calendarOutbox table + calendarEvents.objectUrl (pushed), vite-plugin-pwa install + legitimacy gate, sync.ts objectUrl, full Wave 0 RED test scaffold
|
||||
- **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.
|
||||
|
||||
**Wave 2** *(blocked on Wave 1)*
|
||||
**Plans**: TBD
|
||||
|
||||
- [x] 03-02-PLAN.md — TDD: VEVENT builder (vevent.ts, D-13 DATE/DATETIME + RRULE) + tsdav write wrappers (write.ts, D-12 broker boundary)
|
||||
- [x] 03-03-PLAN.md — Write API: POST/PATCH/DELETE events + GET sync-status, enqueue-only, D-03 ownership, D-04 edit-as-move pair (CAL-04/05/06/07)
|
||||
### Phase 10: Admin Role & Settings
|
||||
|
||||
**Wave 3** *(blocked on Wave 2)*
|
||||
|
||||
- [x] 03-04-PLAN.md — TDD: outbox worker state machine (D-05/06/07/08 retry/backoff/dead/conflict, edit-as-move ordering) + index.ts wiring
|
||||
- [x] 03-05-PLAN.md — Frontend create/edit slice: write client calls + Zustand keys + EventForm (D-01/02/11) + New Event FAB
|
||||
- [x] 03-07-PLAN.md — PWA install: VitePWA manifest + auth-safe SW denylist + icons + InstallPrompt (iOS walkthrough + Android prompt) (PWA-01/02)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3)*
|
||||
|
||||
- [x] 03-06-PLAN.md — Delete + sync feedback: popover Edit/Delete footer + DeleteConfirmationDialog + SyncStateToast polling (D-06/08/09) (CAL-05/06)
|
||||
|
||||
**Wave 5** *(blocked on Wave 4)*
|
||||
|
||||
- [x] 03-08-PLAN.md — Gate 2 live verification: real Authelia OIDC over Pangolin + iOS standalone login + end-to-end Fastmail write round-trips (success criterion 6, D-14/D-15)
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 4: Shared Lists + Live Sync
|
||||
|
||||
**Goal**: Both members can create and manage shared named lists with real-time co-edit sync — edits by one member appear for the other without any manual refresh
|
||||
**Mode:** mvp
|
||||
**Depends on**: Phase 1
|
||||
**⚠️ Entry gate (D-14):** The **SSE-over-Pangolin smoke test** (deferred from Phase 1 Gate 2, issue #1034) MUST pass before building live sync — hold `/api/sse/heartbeat` open 5+ min through the tunnel without it being cut (see `docs/deployment.md`). If it FAILS: fix Pangolin idle-timeout/buffering, or plan a reconnect/polling fallback into this phase before proceeding. Do not build the live-sync layer on an unverified transport.
|
||||
**Requirements**: LIST-01, LIST-02, LIST-03, LIST-04
|
||||
**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. Either member can create a named list (e.g., "Groceries") and delete a list they no longer need
|
||||
2. Either member can add items to a list, check items off, reorder them by drag-and-drop, and delete individual items
|
||||
3. When one member adds or checks off an item, the other member sees the change appear in the list within a few seconds without refreshing — even if they reconnect after a brief network gap
|
||||
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).
|
||||
|
||||
**Entry gate status (2026-06-08):** CLEARED — SSE-over-Pangolin smoke test PASSED (35 heartbeats over ~6 min, buffering off, no cut). Live sync may be built directly on SSE; polling fallback (D-12) retained as belt-and-suspenders.
|
||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||
|
||||
**Plans**: 7 plans (6 + 1 gap-closure)
|
||||
Plans:
|
||||
**Wave 1**
|
||||
- **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.
|
||||
|
||||
- [x] 04-01-PLAN.md — Foundation + app shell: deps install (+ legitimacy gate), list tables generate+migrate [BLOCKING], API test harness + Wave-0 RED stubs, react-router + BottomTabBar + empty ListsIndex (D-13/D-16/D-17/D-18)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1)*
|
||||
|
||||
- [x] 04-02-PLAN.md — TDD: scoped in-memory fan-out (listEmitter) + getAccessibleListIds access scope — the load-bearing D-04 no-leak primitive (LIST-04)
|
||||
- [x] 04-03-PLAN.md — List CRUD slice: POST/GET/PATCH/DELETE /api/lists with scoped access + auto-share-on-create + ListsIndex/ListCard/CreateListSheet/ListDeleteDialog (LIST-01, D-01/D-02/D-06)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2)*
|
||||
|
||||
- [x] 04-04-PLAN.md — Item CRUD + checked-sink slice: item endpoints + fractional rank + per-field LWW PATCH + ListDetail/ItemRow/AddItemInput + optimistic UI (LIST-02, D-05/D-07/D-08/D-09)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3)*
|
||||
|
||||
- [x] 04-05-PLAN.md — Reorder slice: dnd-kit sortable + generateKeyBetween rank + one-row position PATCH + animate-on-remote (LIST-03, D-13/D-14/D-15)
|
||||
|
||||
**Wave 5** *(blocked on Waves 2 + 4)*
|
||||
|
||||
- [x] 04-06-PLAN.md — Live-sync slice: scoped /api/sse/lists + fan-out triggers + useListSSE bounded-backoff hook + LiveSyncIndicator + polling fallback (LIST-04, D-04/D-10/D-11/D-12)
|
||||
|
||||
**Wave 6** *(gap closure — blocked on Waves 2 + 4)*
|
||||
|
||||
- [x] 04-07-PLAN.md — Gap closure: migrate list_items.rank to COLLATE utf8mb4_bin (LIST-03 drag-to-top) + owner-only guard on PATCH isShared (T-04-08/T-04-05) — two TDD features (LIST-03)
|
||||
**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**: TBD
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 5: Web Push Notifications
|
||||
### Phase 11: Per-Event Reminders
|
||||
|
||||
**Goal**: Both members receive timely Web Push alerts for upcoming events, event changes made by the other member, and list changes — reliably on both iOS and Android
|
||||
**Mode:** mvp
|
||||
**Depends on**: Phase 3, Phase 4
|
||||
**Requirements**: NOTIF-01, NOTIF-02, NOTIF-03
|
||||
**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. A member receives a push notification on their phone approximately 15 minutes before a calendar event starts — delivered to the installed PWA, including on iOS
|
||||
2. When the other member adds or changes a calendar event, the first member receives a push notification with the event title and action described in the payload
|
||||
3. When the other member modifies a shared list (adds, checks off, or deletes an item), the first member receives a push notification identifying the list and the change
|
||||
4. After an extended period of app inactivity, push notifications are still delivered (subscription health-check prevents silent revocation on iOS)
|
||||
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.
|
||||
|
||||
**Plans**: 8 plans (6 waves)
|
||||
Plans:
|
||||
**Wave 1**
|
||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||
|
||||
- [x] 05-01-PLAN.md — Foundation: install web-push + workbox deps (legitimacy gate), generate VAPID keypair, push_subscriptions table + calendar_events.title generate+migrate [BLOCKING], Wave-0 RED scaffolds (D-11/D-12)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1)*
|
||||
|
||||
- [x] 05-02-PLAN.md — TDD: pushDispatcher (VAPID send + dual-format payload + 410/404 prune) (D-11)
|
||||
- [x] 05-03-PLAN.md — TDD: pushCoalescer (per-list/actor debounce, generic copy, self-suppress) (D-01/D-02/D-03)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2)*
|
||||
|
||||
- [x] 05-04-PLAN.md — Subscribe slice (end-to-end): push subscription API + setVapidDetails, generateSW→injectManifest SW migration (push/notificationclick/denylist), usePushSubscription + PushPermissionPrompt (D-08/D-11/D-14)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3)*
|
||||
|
||||
- [x] 05-05-PLAN.md — NOTIF-02 list-change slice: listChangeDispatcher + hook coalescer into mutations, reorder-silent (D-01/D-02/D-03)
|
||||
- [x] 05-06-PLAN.md — TDD: NOTIF-01 reminderScheduler — shared-timed 15-min scan (query-enforced D-05), all-day excl, dedup, empty-set safe (D-05/D-06/D-07)
|
||||
|
||||
**Wave 5** *(blocked on Wave 4)*
|
||||
|
||||
- [x] 05-07-PLAN.md — TDD: NOTIF-03 eventChangeDispatcher + syncCalendar diff/title/onChanges hook (poller + outbox), meaningful-only, actor-suppressed (D-02/D-03/D-04/D-13)
|
||||
|
||||
**Wave 6** *(blocked on Wave 3)*
|
||||
|
||||
- [x] 05-08-PLAN.md — Settings + reliability: master toggle (D-09) + silent re-subscribe (D-10) + PermissionDeniedBanner + avatar→Settings sheet
|
||||
- **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 6: UX Polish
|
||||
### Phase 12: Initial Setup Wizard
|
||||
|
||||
**Goal**: Smooth the rough edges surfaced during live use — clearer all-day events, saner event-form date/recurrence behavior, recurring-series editing, and auth-flow polish — so the app feels slick for the non-technical Apple member (hard UX constraint).
|
||||
**Mode:** mvp
|
||||
**Depends on**: Phase 3 (calendar/event-form polish); Phase 4 for any list-related polish
|
||||
**Requirements**: none (all v1 REQ-IDs complete in Phases 1–5; this is a polish phase tracked against backlog items 999.2/3/6/7/8/9 and locked decisions D-01..D-13)
|
||||
**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. All-day events are visually distinct from timed events at a glance
|
||||
2. The event form keeps a sane duration when the start moves, all-day edits don't grow the event, and a recurrence can be bounded (repeat-until / count)
|
||||
3. A recurring series can be edited as a whole
|
||||
4. A session that expires mid-use redirects cleanly to sign-in instead of hanging on a generic error
|
||||
5. Unauthenticated cold load shows a neutral "signing you in…" splash — no calendar/"sign-in required" flash before Authelia
|
||||
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.
|
||||
|
||||
**Scope** (promoted from backlog, locked at planning): 999.2 (login flash), 999.3 (session-timeout redirect), 999.6 (all-day visual), 999.7 (form end-tracking + all-day-edit off-by-one), 999.8 (recurrence bound), 999.9 (recurring-series edit). 999.4 (reminders) and 999.5 (provider setup) deferred to milestone 1.1 (D-01/D-02).
|
||||
**Pitfalls this phase owns** (from PITFALLS.md):
|
||||
|
||||
**Plans**: 6 plans (2 waves)
|
||||
Plans:
|
||||
**Wave 1** *(parallel — exclusive file ownership)*
|
||||
|
||||
- [x] 06-01-PLAN.md — TDD: duration-preserving end-tracking math (computeNewTimedEnd/computeNewAllDayEnd) in eventDateTime.ts (D-04)
|
||||
- [x] 06-02-PLAN.md — TDD: RRULE UNTIL/COUNT serialization + Zod acceptance + FREQ-persistence regression (vevent/outboxWorker/events route) (D-06/D-07)
|
||||
- [x] 06-03-PLAN.md — TDD: hasRrule on CalendarOccurrence + bounded-expansion lock (expand.ts) (D-06/D-08)
|
||||
- [x] 06-04-PLAN.md — Spinner/pulse: global @keyframes pulse + remove redundant spin redefinition (D-13)
|
||||
- [x] 06-05-PLAN.md — Auth gating slice: SessionExpiredError + AuthSplash + global QueryCache/MutationCache error handler; client.ts type mirrors (D-10/D-11, + D-06/D-08 type carriers)
|
||||
|
||||
**Wave 2** *(blocked on 06-01/02/03/05)*
|
||||
|
||||
- [x] 06-06-PLAN.md — EventForm integration slice: end-tracking wiring + recurrence-bound control + series-edit prompt + all-day pill (D-03/D-04/D-05/D-06/D-07/D-08/D-09/D-12)
|
||||
- **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
|
||||
|
||||
### 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**: TBD
|
||||
**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**: TBD
|
||||
**UI hint**: no
|
||||
|
||||
## Progress
|
||||
|
||||
**Execution Order:**
|
||||
Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6
|
||||
Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is complete. It is serialized here to reduce work-in-progress.
|
||||
|
||||
| Phase | Plans Complete | Status | Completed |
|
||||
|-------|----------------|--------|-----------|
|
||||
| 1. Foundation + Broker Spike | 4/4 | Complete | 2026-06-04 |
|
||||
| 2. Calendar Display | 5/5 | Complete | 2026-06-05 |
|
||||
| 3. Event Write-Back + PWA Install | 12/12 | Complete | 2026-06-07 |
|
||||
| 4. Shared Lists + Live Sync | 6/6 | Complete | 2026-06-09 |
|
||||
| 5. Web Push Notifications | 8/8 | Complete | 2026-06-10 |
|
||||
| 6. UX Polish | 6/6 | Complete | 2026-06-10 |
|
||||
| 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 | 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 | - |
|
||||
| 13. Real Lint Gate (ESLint) | v1.1 | 0/? | Not started | - |
|
||||
| 14. Desktop E2E Coverage | v1.1 | 0/? | Not started | - |
|
||||
|
||||
## Backlog
|
||||
|
||||
@@ -268,7 +275,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple
|
||||
|
||||
**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:
|
||||
|
||||
@@ -288,27 +295,7 @@ Plans:
|
||||
**Requirements:** TBD
|
||||
**Plans:** 0 plans
|
||||
|
||||
Plans:
|
||||
|
||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||
|
||||
### Phase 999.5: First-login provider setup — prompt + instructions to add a Fastmail app password (BACKLOG)
|
||||
|
||||
**Goal:** [Captured for future planning] On a member's first login there is no onboarding to connect their own calendar provider. Today the broker uses a single seeded Fastmail app password (the operator's), so a second member (e.g. the wife) who logs in sees only what that token reaches — she has no way to attach her **own** Fastmail personal calendar (the D-09 per-member app-password model). Add a first-login flow that detects a member has no `member_credentials` row and prompts them to create + paste a Fastmail app password, with clear step-by-step instructions (where to generate it in Fastmail settings, required scope: Calendars/CalDAV, that one app password covers all of that account's calendars). Store it encrypted (APP_PASSWORD_ENCRYPTION_KEY, existing crypto path), then trigger an initial sync so their personal calendar lane populates.
|
||||
|
||||
**Context** (surfaced 2026-06-07, Gate 2 live testing): the wife logged in on her iPhone and added the PWA to her Home Screen, but there is no provider-setup step — so her personal calendar can't be connected. This is the onboarding half of the "each member's personal calendar" v1 requirement.
|
||||
|
||||
**Scope to decide when promoted:**
|
||||
|
||||
- Detect "no credential yet" state server-side (`GET /api/me` exposes a `needsProviderSetup` flag, or a dedicated endpoint) and gate a setup screen in the PWA.
|
||||
- App-password entry UI + validation (test the credential with a CalDAV PROPFIND before saving), encrypted storage, and triggering the first sync.
|
||||
- Non-technical-friendly instructions (the hard UX constraint) — ideally with a direct link to Fastmail's app-password page and a screenshot/walkthrough.
|
||||
- Decide the model: does every member attach their own personal calendar, or do some members only see the shared family calendar? (Open question from D-16.)
|
||||
- Security: never log/echo the app password; member-scoped; T-03-19 style scoping.
|
||||
|
||||
**Severity:** high for true multi-member use — without it the second member has no personal calendar. Tags: phase-03, onboarding, auth, caldav, per-member-credential, D-09.
|
||||
**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:
|
||||
|
||||
@@ -322,6 +309,9 @@ Plans:
|
||||
- **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
|
||||
|
||||
@@ -340,6 +330,9 @@ Plans:
|
||||
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
|
||||
|
||||
@@ -352,6 +345,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.
|
||||
@@ -359,6 +353,9 @@ Plans:
|
||||
**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
|
||||
|
||||
@@ -371,6 +368,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).
|
||||
@@ -378,6 +376,9 @@ Plans:
|
||||
**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
|
||||
|
||||
@@ -390,6 +391,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?).
|
||||
@@ -399,6 +401,56 @@ Plans:
|
||||
**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
|
||||
|
||||
|
||||
+53
-25
@@ -1,42 +1,40 @@
|
||||
---
|
||||
gsd_state_version: 1.0
|
||||
milestone: v1.0
|
||||
milestone_name: milestone
|
||||
status: "v1.0 milestone shipped -- PR #1 (gsd/v1.0-milestone -> main)"
|
||||
stopped_at: "Completed 06-03: hasRrule server-side exposure"
|
||||
last_updated: "2026-06-10T21:23:49.165Z"
|
||||
last_activity: "2026-06-10 -- Shipped v1.0 milestone (all 6 phases) -- Gitea PR #1"
|
||||
milestone: v1.1
|
||||
milestone_name: Operability & Polish
|
||||
status: phase-complete
|
||||
stopped_at: Phase 08 complete — all 4 plans executed, CI-01 + CI-02 delivered, publish job verified green (run #14)
|
||||
last_updated: "2026-06-11T22:00:00.000Z"
|
||||
last_activity: "2026-06-11 -- 08-04 complete; publish job green (run #14, merge commit 98acff8): both image tags pushed (familysync-api:latest + :v1.1-98acff8), PAT masked, --password-stdin confirmed. REGISTRY_PAT naming fix (73eecf7). Phase 8 (Gitea CI) complete — all 6 ROADMAP criteria met."
|
||||
progress:
|
||||
total_phases: 17
|
||||
completed_phases: 5
|
||||
total_plans: 42
|
||||
completed_plans: 39
|
||||
percent: 29
|
||||
total_phases: 16
|
||||
completed_phases: 2
|
||||
total_plans: 8
|
||||
completed_plans: 8
|
||||
percent: 12
|
||||
---
|
||||
|
||||
# Project State
|
||||
|
||||
## Project Reference
|
||||
|
||||
See: .planning/PROJECT.md (updated 2026-06-07)
|
||||
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:** Phase 06 — ux-polish
|
||||
**Current focus:** Phase 08 — gitea-ci
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 06 (ux-polish) — COMPLETE (all 6 plans executed)
|
||||
Plan: 6 of 6
|
||||
Status: v1.0 milestone shipped -- PR #1 (gsd/v1.0-milestone -> main)
|
||||
Last activity: 2026-06-10 -- Shipped v1.0 milestone (all 6 phases) -- Gitea PR #1
|
||||
|
||||
Progress: [█████████░] 89%
|
||||
Phase: 08 (gitea-ci) — COMPLETE
|
||||
Plan: 4 of 4 (08-04 complete)
|
||||
Status: Phase 08 complete — all 4 plans executed, CI-01 + CI-02 delivered
|
||||
Last activity: 2026-06-11 -- Quick task 260611-ozt: split publish into standalone push-only publish.yml (kills orphaned CI / publish (pull_request) pending status, WR-01); release model documented in README + publish.yml. Branch-protection contexts unchanged.
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
**Velocity:**
|
||||
|
||||
- Total plans completed: 17
|
||||
- Total plans completed: 21
|
||||
- Average duration: -
|
||||
- Total execution time: 0 hours
|
||||
|
||||
@@ -46,6 +44,7 @@ Progress: [█████████░] 89%
|
||||
|-------|-------|-------|----------|
|
||||
| 02 | 5 | - | - |
|
||||
| 03 | 12 | - | - |
|
||||
| 07 | 4 | - | - |
|
||||
|
||||
**Recent Trend:**
|
||||
|
||||
@@ -79,6 +78,10 @@ Progress: [█████████░] 89%
|
||||
| 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
|
||||
|
||||
@@ -87,6 +90,15 @@ Progress: [█████████░] 89%
|
||||
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.
|
||||
- D-PAT-NAMING (2026-06-11, 08-04): Gitea runner silently drops secrets with the `GITEA_` prefix (reserved namespace). Secret renamed from GITEA_REGISTRY_PAT → REGISTRY_PAT in both repo secret and ci.yml (commit 73eecf7). Use REGISTRY_PAT in any future registry operations.
|
||||
- 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.
|
||||
@@ -126,19 +138,23 @@ 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
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- **v1.1 roadmap created (2026-06-10):** 6 phases (7–12), continuing v1.0 numbering. 17/17 requirements mapped, no orphans. Promotions: 999.13→Faster Write-Back (CAL-15), 999.10→Admin Role & Settings (ADMIN-01/02/03), 999.11→Setup Wizard (SETUP-01/02/03/04), 999.4→Per-Event Reminders (CAL-13/14 + NOTIF-04/05/06), 999.14→Gitea CI (CI-01/02), 999.12→Mobile Test Harness (TEST-01/02). The v1.1 DB migration (users.is_admin, calendar_events.reminder_lead_minutes, app_config) is folded into the Admin phase (per ARCHITECTURE.md ordering), not a standalone migration phase; the reminders + wizard phases consume it. Backlog 999.5 (self-service onboarding) and 999.1 (provider abstraction) stay deferred — ADMIN-01 covers the admin-managed credential gap in the interim.
|
||||
- **v1.1 roadmap reordered (2026-06-10):** same 6 phases / 17 mappings, new order/numbering. Phase 7 = Mobile Test Harness (was 12), Phase 8 = Gitea CI (was 11), Phase 9 = Faster Write-Back (was 7), Phase 10 = Admin Role & Settings (was 8, carries the DB migration), Phase 11 = Per-Event Reminders (was 10), Phase 12 = Initial Setup Wizard (was 9). **Phase 8 (CI) scope extended:** the PR regression now also runs the Phase 7 mobile harness as a UI-regression step, bringing up the dev stack (API + PWA dev servers + MariaDB service container + DEV_AUTH_BYPASS) in the runner — so **Phase 8 now depends on Phase 7**. New critical path: **7 → 8** (CI consumes the harness); **9** independent; **10 → 11** and **10 → 12** (admin migration precedes reminders + wizard). Parallelizable once prerequisites met: 9 anytime; 11 and 12 in parallel after 10; 7 then 8.
|
||||
|
||||
### Pending Todos
|
||||
|
||||
- ~~**Fix `docs/deployment.md` local-dev command**~~ DONE 2026-06-10 (quick 260610-czd) — added a "Running locally (host-side, no Docker)" subsection with the correct two-terminal command (`set -a; source .env; set +a && DEV_AUTH_BYPASS=true DB_HOST=localhost pnpm --filter @familysync/api dev` + `pnpm --filter @familysync/pwa dev`). `--env-file` deliberately NOT baked into the dev script (root `.env` sets `DB_HOST=mariadb`; auto-load would break host-side dev).
|
||||
- ~~**REQUIREMENTS.md traceability gap**~~ DONE 2026-06-10 (gsd-fast) — added the 6 deferred REQ-IDs to the Traceability table: CAL-09…CAL-12 (v1.x, Deferred), DISP-01/DISP-02 (v2, Deferred). v1 coverage stays 20/20; deferred IDs tracked separately.
|
||||
- **DST spring-forward spot-check (Phase 2)** — recurring/DST is implemented and code-verified (VTIMEZONE before expansion + local display TZ), and operator approved general times; navigating to March 2026 to eyeball the spring-forward transition is a recommended future spot-check.
|
||||
- ~~**Gitea CI — regression on PR to main + Docker build/publish**~~ PROMOTED TO BACKLOG 999.14 (2026-06-10, /gsd-review-backlog) — self-hosted Gitea runner exists but no CI yet. Full regression (lint/typecheck/unit + API integration vs a MariaDB service container + PWA build) gating PRs to `main`, plus build/publish the Docker image to the Gitea registry. Detail retained in pending todo `2026-06-10-gitea-ci-regression-and-docker-publish.md` (backing the backlog entry).
|
||||
- ~~**Gitea CI — regression on PR to main + Docker build/publish**~~ PROMOTED TO BACKLOG 999.14 → now **v1.1 Phase 8** (Gitea CI, CI-01/CI-02). Detail retained in pending todo `2026-06-10-gitea-ci-regression-and-docker-publish.md`. Scope extended in the reorder: CI also runs the Phase 7 mobile harness as a UI-regression step.
|
||||
|
||||
### Blockers/Concerns
|
||||
|
||||
@@ -148,6 +164,8 @@ Recent decisions affecting current work:
|
||||
- Phase 2/3 dev: build behind a documented dev-auth bypass until Gate 2 deploy (D-14).
|
||||
- Phase 5: iOS push subscriptions silently revoked after 3 silent pushes. Subscription health-check and event.waitUntil() are mandatory from day one.
|
||||
- Phase 06 residual device-only items (not drivable in desktop Chromium): (1) PushPermissionPrompt spinner visible only in an installed iOS/standalone PWA — code-confirmed uses global @keyframes spin; spot-check at go-live. (2) iOS-Safari standalone cold-load and Authelia redirect — per 06-VALIDATION.md Manual-Only table; not yet verified. (3) Dev-bypass user (id 1) has no CalDAV credential/calendars; live event-create via the form requires user 2 or a dev-seed fix before go-live testing.
|
||||
- **v1.1 Phase 7 (Mobile Test Harness):** DEV_AUTH_BYPASS user 1 has no CalDAV credential/calendars — the harness verifies layout/flows, not live event-create. Confirm SW-block + bypass strategy before the first test (PITFALLS 14/15). These specs become Phase 8's CI UI-regression step, so structure them to run headlessly against a runner-hosted dev stack.
|
||||
- **v1.1 Phase 8 (Gitea CI):** Unraid Gitea runner Docker-socket/Node/pnpm state unknown — runner-probe is the first CI task (PITFALLS 12); MariaDB 11 readiness uses `healthcheck.sh --connect`, not `mysqladmin ping` (removed in MariaDB 11). NEW: the harness step brings up the API + PWA dev servers in the runner — added startup/readiness races on top of the MariaDB race; the step must wait for **both** dev servers to be ready before launching Playwright. Depends on Phase 7.
|
||||
|
||||
### Quick Tasks Completed
|
||||
|
||||
@@ -163,6 +181,7 @@ Recent decisions affecting current work:
|
||||
| 260610-jlp | Fix broken "How to enable" link in notifications-blocked UI (Phase 5 UAT Test 4) — extracted InstructionSheet into a shared component; SettingsSheet "How to enable" now opens the OS-step instructions instead of just closing the sheet. 187 pwa tests pass, build green | 2026-06-10 | f82837c | Verified | [260610-jlp-fix-broken-how-to-enable-link-in-notific](./quick/260610-jlp-fix-broken-how-to-enable-link-in-notific/) |
|
||||
| 260610-k1z | Persist OIDC session cookie (AUTH-02) — @hono/oidc-auth 1.8.3 sets a session-scoped `oidc-auth` cookie (no maxAge) so it died on PWA/browser close → re-login almost every return (both devices). Added persistSessionCookie middleware re-issuing the cookie with maxAge(=OIDC_AUTH_EXPIRES)+SameSite=Lax, ONLY when a valid session exists (no resurrection guard). NOT an Authelia/refresh issue. 14 auth tests pass | 2026-06-10 | 8343fad | Verified | [260610-k1z-persist-oidc-session-cookie-with-maxage-](./quick/260610-k1z-persist-oidc-session-cookie-with-maxage-/) |
|
||||
| 260610-ka9 | Fix silent Android push (Phase 5 UAT Test 4) — SW showNotification had only {body,tag,data} → Android Chromium/Edge showed them silently. Added icon/badge/renotify:true/vibrate; generalized re-enable instructions to Chrome-or-Edge. iOS unaffected. Build emits sw.js with renotify; 187 pwa tests pass | 2026-06-10 | c864fc4 | Verified | [260610-ka9-fix-silent-android-push-notifications-en](./quick/260610-ka9-fix-silent-android-push-notifications-en/) |
|
||||
| 260611-ozt | Split publish job into standalone .gitea/workflows/publish.yml (on: push→main only, no redundant event-guard if:; MILESTONE env moved with it) and strip it + the push trigger from ci.yml — kills the orphaned `CI / publish (pull_request)` pending status (phase-8 code-review WR-01). name:CI + fast-checks/api/harness job ids held stable so the required branch-protection contexts stay valid. Documented the release model in README "Publishing / Releases" + publish.yml header. Both YAML validated (yq) | 2026-06-11 | 92353e1 | | [260611-ozt-split-publish-job-into-standalone-gitea-](./quick/260611-ozt-split-publish-job-into-standalone-gitea-/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
@@ -173,11 +192,20 @@ Recent decisions affecting current work:
|
||||
| Calendar | Apple Calendar native subscribe URL docs | v1.x | Roadmap |
|
||||
| Calendar | Secondary timezone display toggle | v1.x | Roadmap |
|
||||
| Display | Wall-display / kiosk dashboard | v2 | PROJECT.md |
|
||||
| Reminders | Multiple reminders per event (2× VALARM) | v1.2 | v1.1 REQUIREMENTS.md |
|
||||
| Onboarding | Self-service member app-password setup (999.5) — v1.1 covers admin-managed (ADMIN-01) | Backlog | v1.1 roadmap |
|
||||
| Calendar | Provider abstraction (999.1) — Fastmail as one of several backends | Backlog | v1.1 roadmap |
|
||||
| Setup | Wizard re-run / reconfigure flow after first setup | Backlog | v1.1 REQUIREMENTS.md |
|
||||
| Notifications | **Android event-change push delivery (Phase 5 UAT Test 4)** — confirm member B's Android device receives a non-silent "A updated an event" push after member A edits a shared event. Blocking bugs already fixed + deployed (quick 260610-jlp how-to-enable link, 260610-ka9 silent-notification options); server-side FCM delivery proven (FCM 201). Remaining: on-device confirmation + operator raises the Edge/Android notification-channel importance. See 05-UAT.md Test 4. | Phase 6 verification | 2026-06-10 |
|
||||
| ~~Calendar~~ | ~~Mark shared-family calendar `is_shared=1`~~ **RESOLVED 2026-06-10** — operator created the "FamilySync" calendar on the primary Fastmail account; poller synced it as calendars.id=10 (user 2); ran `UPDATE calendars SET is_shared=1 WHERE id=10`. Shared color lane now populated; Phase 5 reminders now fire on its events. Poller upsert does not touch is_shared, so the flag persists. | ~~Phase 2 (deferred, D-16)~~ DONE | 2026-06-05 → 2026-06-10 |
|
||||
| ~~Calendar~~ | ~~Mark shared-family calendar `is_shared=1`~~ **RESOLVED 2026-06-10** — operator created the "FamilySync" calendar on the primary Fastmail account; poller synced it as calendars.id=10 (user 2); ran `UPDATE calendars SET is_shared=1 WHERE id=10`. Shared color lane now populated; Phase 5 reminders now fire on its events. Poller upsert does not touch is_shared, so the flag persists. (ADMIN-02 in v1.1 Phase 10 replaces this manual step with a UI toggle.) | ~~Phase 2 (deferred, D-16)~~ DONE | 2026-06-05 → 2026-06-10 |
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-06-10T15:20:02.349Z
|
||||
Stopped at: Completed 06-03: hasRrule server-side exposure
|
||||
Resume file: None
|
||||
Last session: 2026-06-11T22:00:00.000Z
|
||||
Stopped at: Phase 08 complete — CI-01 + CI-02 delivered, publish job verified, SUMMARY + VERIFICATION written
|
||||
Resume file: None — start Phase 09 with /gsd-plan-phase 9
|
||||
|
||||
## Operator Next Steps
|
||||
|
||||
- **Phase 8 is complete.** CI pipeline is fully operational on the self-hosted Gitea runner.
|
||||
- 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,
|
||||
"exa_search": false,
|
||||
"git": {
|
||||
"branching_strategy": "milestone",
|
||||
"branching_strategy": "phase",
|
||||
"create_tag": true,
|
||||
"phase_branch_template": "gsd/phase-{phase}-{slug}",
|
||||
"milestone_branch_template": "gsd/{milestone}-{slug}",
|
||||
@@ -90,6 +90,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"graphify": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"auto_update": true
|
||||
}
|
||||
}
|
||||
|
||||
+933
-215
File diff suppressed because it is too large
Load Diff
+66051
-19369
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
||||
# Requirements Archive: v1.0 MVP
|
||||
|
||||
**Archived:** 2026-06-10
|
||||
**Status:** SHIPPED
|
||||
|
||||
For current requirements, see `.planning/REQUIREMENTS.md`.
|
||||
|
||||
---
|
||||
|
||||
# Requirements: FamilySync
|
||||
|
||||
**Defined:** 2026-06-03
|
||||
**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 Requirements
|
||||
|
||||
Requirements for initial release. Each maps to roadmap phases.
|
||||
|
||||
### Authentication & Onboarding
|
||||
|
||||
> **Given:** Authelia is already deployed and both household members already have Authelia accounts. Auth scope is therefore app-side only — register FamilySync as an OIDC confidential client in Authelia and integrate the login flow. No Authelia deployment, no account provisioning.
|
||||
|
||||
- [ ] **AUTH-01**: User can log in through Authelia (OIDC SSO) — no separate FamilySync account or password to create
|
||||
- [ ] **AUTH-02**: User stays logged in across sessions so re-authentication is rare (persistent session)
|
||||
- [ ] **AUTH-03**: Each member maps to a stable identity (OIDC `iss`+`sub`) and is assigned a consistent per-member color
|
||||
|
||||
### Calendar
|
||||
|
||||
- [ ] **CAL-01**: App reads the shared family Fastmail calendar via a CalDAV broker token and caches it locally (ctag polling)
|
||||
- [x] **CAL-02**: User sees a unified, color-coded calendar that aggregates every accessible calendar into one view
|
||||
- [x] **CAL-03**: User can switch between week, month, day, and agenda/list views
|
||||
- [x] **CAL-04**: User can create a timed or all-day event, written back to the correct Fastmail calendar
|
||||
- [x] **CAL-05**: User can edit an existing event
|
||||
- [x] **CAL-06**: User can delete an event
|
||||
- [x] **CAL-07**: User can create a recurring event and see all its occurrences expanded correctly (single-occurrence editing deferred to v1.x)
|
||||
- [ ] **CAL-08**: Each member's personal Fastmail calendar is overlaid into the unified view — *spike-gated in Phase 1*; if cross-account CalDAV sharing proves infeasible, v1 falls back to shared-family-only and this moves to v1.x
|
||||
|
||||
### Lists
|
||||
|
||||
- [x] **LIST-01**: User can create and delete named lists (e.g. Groceries, Gift Ideas)
|
||||
- [x] **LIST-02**: User can add items to a list, check them off, and delete them
|
||||
- [x] **LIST-03**: User can reorder items within a list
|
||||
- [x] **LIST-04**: Both members' list edits appear live for the other member without manual refresh
|
||||
|
||||
### Notifications
|
||||
|
||||
- [x] **NOTIF-01**: User receives a Web Push reminder before an event starts
|
||||
- [x] **NOTIF-02**: User receives a Web Push alert when the other member changes a shared list
|
||||
- [x] **NOTIF-03**: User receives a Web Push alert when an event is added or changed
|
||||
|
||||
### PWA & Install
|
||||
|
||||
- [x] **PWA-01**: App is installable to the Home Screen on iPhone and Android (web manifest + service worker, served over HTTPS)
|
||||
- [x] **PWA-02**: First-time users get a guided "Add to Home Screen" prompt (prerequisite for iOS Web Push)
|
||||
|
||||
## v1.x Requirements
|
||||
|
||||
Deferred to a near-term follow-up release. Tracked but not in the v1 roadmap.
|
||||
|
||||
### Calendar
|
||||
|
||||
- **CAL-09**: User can edit/delete a single occurrence of a recurring event (RECURRENCE-ID / EXDATE)
|
||||
- **CAL-10**: User can apply a "this and following" edit to a recurring series
|
||||
- **CAL-11**: Documentation for subscribing to the Fastmail calendar natively in Apple Calendar via CalDAV (no new code)
|
||||
- **CAL-12**: Secondary-timezone display toggle for travel
|
||||
|
||||
## v2 Requirements
|
||||
|
||||
### Display
|
||||
|
||||
- **DISP-01**: Always-on wall-display / kiosk dashboard view (Skylight-style)
|
||||
- **DISP-02**: Upcoming-events / agenda summary widget tuned for the wall display
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Explicitly excluded. Documented to prevent scope creep. Anti-features sourced from research (`.planning/research/FEATURES.md`).
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| Email features | Members keep existing mail clients; never the product's job |
|
||||
| Self-hosted calendar server (Baikal/Radicale) | Fastmail hosts all calendars via CalDAV; one fewer service |
|
||||
| Vikunja / external task backend | Lists live in MariaDB; cross-ecosystem native task sync is impossible anyway |
|
||||
| React Native / App Store app | PWA delivers app-like UX without publishing overhead |
|
||||
| PostgreSQL | Not in the stack; MariaDB is the database |
|
||||
| Chores / rewards / star system | No children in the household; lists cover any task need |
|
||||
| Meal planning / recipe box | Separate domain, high cost; grocery list covers the coordination need |
|
||||
| Kids / sub-accounts | No children; irrelevant |
|
||||
| AI email-to-event import | Requires email access (out of scope) + LLM backend; privacy risk |
|
||||
| RSVP / invite flows (iTIP/iMIP) | Two people share one calendar; both attend by default |
|
||||
| Event-level comments / photos | Two people can text; adds chat/media storage for ~zero value |
|
||||
| Activity feed / audit log | Obvious with two users |
|
||||
| Multi-household / accounts at scale | One household, two hardcoded Authelia accounts |
|
||||
| Ads / monetization | Self-hosted; no revenue model |
|
||||
| Complex permissions / role tiers | Two equal partners with identical write access |
|
||||
| Offline-first with CRDT conflict resolution | Home WiFi is primary; optimistic updates + retry suffice |
|
||||
| Grocery delivery integration | Third-party dependency; not needed |
|
||||
|
||||
## Traceability
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| AUTH-01 | Phase 1 | Pending |
|
||||
| AUTH-02 | Phase 1 | Pending |
|
||||
| AUTH-03 | Phase 1 | Pending |
|
||||
| CAL-01 | Phase 1 | Pending |
|
||||
| CAL-08 | Phase 1 | Pending |
|
||||
| CAL-02 | Phase 2 | Complete |
|
||||
| CAL-03 | Phase 2 | Complete |
|
||||
| CAL-04 | Phase 3 | Complete |
|
||||
| CAL-05 | Phase 3 | Complete |
|
||||
| CAL-06 | Phase 3 | Complete |
|
||||
| CAL-07 | Phase 3 | Complete |
|
||||
| PWA-01 | Phase 3 | Complete |
|
||||
| PWA-02 | Phase 3 | Complete |
|
||||
| LIST-01 | Phase 4 | Complete |
|
||||
| LIST-02 | Phase 4 | Complete |
|
||||
| LIST-03 | Phase 4 | Complete |
|
||||
| LIST-04 | Phase 4 | Complete |
|
||||
| NOTIF-01 | Phase 5 | Complete |
|
||||
| NOTIF-02 | Phase 5 | Complete |
|
||||
| NOTIF-03 | Phase 5 | Complete |
|
||||
| CAL-09 | v1.x | Deferred |
|
||||
| CAL-10 | v1.x | Deferred |
|
||||
| CAL-11 | v1.x | Deferred |
|
||||
| CAL-12 | v1.x | Deferred |
|
||||
| DISP-01 | v2 | Deferred |
|
||||
| DISP-02 | v2 | Deferred |
|
||||
|
||||
**Coverage:**
|
||||
|
||||
- v1 requirements: 20 total
|
||||
- Mapped to phases: 20
|
||||
- Unmapped: 0 ✓
|
||||
- Deferred (not in v1 scope): 6 — CAL-09…CAL-12 (v1.x), DISP-01/DISP-02 (v2)
|
||||
|
||||
---
|
||||
*Requirements defined: 2026-06-03*
|
||||
*Last updated: 2026-06-10 — added deferred REQ-IDs (CAL-09…CAL-12, DISP-01/02) to traceability table*
|
||||
@@ -0,0 +1,410 @@
|
||||
# Roadmap: FamilySync
|
||||
|
||||
## Overview
|
||||
|
||||
FamilySync is built in five phases, each delivering an end-to-end user-observable capability. Phase 1 is both the foundation and the highest-risk gate: OIDC auth must work and the CalDAV broker must prove it can read personal Fastmail calendars before any calendar UI is built. Phases 2–3 complete the calendar. Phase 4 delivers shared lists with live co-edit sync. Phase 5 wires up Web Push notifications. The dependency chain is strict: each phase is a prerequisite for the next, except the lists track (Phase 4) which is independent of the calendar write path.
|
||||
|
||||
## Phases
|
||||
|
||||
**Phase Numbering:**
|
||||
|
||||
- Integer phases (1, 2, 3): Planned milestone work
|
||||
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
|
||||
|
||||
Decimal phases appear between their surrounding integers in numeric order.
|
||||
|
||||
- [x] **Phase 1: Foundation + Broker Spike** - Auth, Docker scaffold, CalDAV broker read path, and personal-calendar ACL spike (go/no-go gate) (completed 2026-06-04)
|
||||
- [x] **Phase 2: Calendar Display** - Read-only unified color-coded calendar (week/month/day/agenda) built on the confirmed broker (completed 2026-06-05)
|
||||
- [x] **Phase 3: Event Write-Back + PWA Install** - Full event CRUD written back to Fastmail, PWA manifest + service worker, guided iOS install flow (completed 2026-06-07)
|
||||
- [x] **Phase 4: Shared Lists + Live Sync** - Named collaborative lists with item CRUD and real-time SSE co-edit sync (completed 2026-06-09)
|
||||
- [x] **Phase 5: Web Push Notifications** - VAPID push for event reminders, event changes, and list-change alerts (completed 2026-06-10; on-device UAT 1/2/5 PASS, T3 dropped as non-gating, T4 Android event-change push deferred to Phase 6 verification — see 05-UAT.md)
|
||||
- [x] **Phase 6: UX Polish** - All-day visual distinction, event-form date/recurrence behavior, recurring-series edit, and auth-flow smoothing (completed 2026-06-10)
|
||||
|
||||
## Phase Details
|
||||
|
||||
### Phase 1: Foundation + Broker Spike
|
||||
|
||||
**Goal**: The app stack is running, both members can authenticate, and the CalDAV broker can read Fastmail calendars — with a confirmed go/no-go decision on personal-calendar cross-account sharing
|
||||
**Mode:** mvp
|
||||
**Depends on**: Nothing (first phase)
|
||||
**Requirements**: AUTH-01, AUTH-02, AUTH-03, CAL-01, CAL-08
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. Both members can reach the app URL, authenticate through Authelia OIDC, and land on the app home page without entering any Fastmail credentials
|
||||
2. Sessions persist across browser restarts — neither member is asked to log in again on the next visit
|
||||
3. Each member is assigned a stable, distinct display color that does not change between sessions
|
||||
4. The broker successfully fetches and caches at least one event from the shared Fastmail calendar via CalDAV PROPFIND/REPORT
|
||||
5. The personal-calendar ACL spike produces a documented go/no-go decision: either the broker token sees the wife's personal calendar after Fastmail share+accept, or the fallback strategy (shared-family-only or per-member app password) is chosen and recorded
|
||||
|
||||
**Verification status (D-14, 2026-06-04):** Code + **Gate 1** complete. Gate 1 = stack up (`/health` live), CAL-01 proven live (503 real events cached via REPORT), CAL-08 = **GO** (per-member app-password model, see `CAL-08-DECISION.md`). **Gate 2 deferred** — criteria 1/2/3 (live Authelia OIDC login over Pangolin, session persistence, distinct colors in a real browser) and the SSE-over-Pangolin smoke test require the operator's Authelia + Pangolin/Newt infra; tracked in `01-HUMAN-UAT.md` and `docs/deployment.md`. The live AUTH smoke (incl. iOS) is folded into **Phase 3**; the SSE smoke is a hard gate before **Phase 4**. Phases 2–3 develop behind a documented dev-auth bypass.
|
||||
|
||||
**Plans**: 4 plans
|
||||
Plans:
|
||||
|
||||
- [x] 01-01-PLAN.md — Walking skeleton: monorepo scaffold + Docker/MariaDB + Drizzle schema (push) + /health end-to-end slice + Vitest Wave 0 harness
|
||||
- [x] 01-02-PLAN.md — Authelia OIDC slice: stable identity (iss+sub) + auto-assigned member color + /api/me + authenticated PWA shell (AUTH-01/02/03)
|
||||
- [x] 01-03-PLAN.md — CalDAV broker slice: AES-256-GCM credential encryption + tsdav broker + ical.js sync (all-day DATE) + ctag poller + /api/events (CAL-01)
|
||||
- [x] 01-04-PLAN.md — Integration + gate: wire poller/routes, event-proof landing page, CAL-08 spike + go/no-go doc, live Pangolin deploy + SSE smoke test
|
||||
|
||||
### Phase 2: Calendar Display
|
||||
|
||||
**Goal**: Both members can see a unified, color-coded calendar aggregating all accessible Fastmail calendars across day, week, month, and agenda views — read-only, no write-back yet
|
||||
**Mode:** mvp
|
||||
**Depends on**: Phase 1
|
||||
**Requirements**: CAL-02, CAL-03, CAL-07
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. Opening the app shows a color-coded calendar where each member's events appear in their assigned color, with shared events distinguishable from personal events
|
||||
2. The user can switch between day, week, month, and agenda views and all events render correctly in each view
|
||||
3. A recurring event (e.g., weekly meeting) displays all its occurrences correctly in the current view window, including correct behavior across DST boundaries
|
||||
4. All-day events (birthdays, holidays) appear as full-day banners on the correct date with no timezone shift
|
||||
|
||||
**Plans**: 5 plansPlans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 02-01-PLAN.md — Foundation: schema (hasRrule + calendars.isShared, pushed) + dev-auth bypass + PWA vitest/jsdom harness + ICS fixtures + RED test stubs
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [x] 02-02-PLAN.md — Backend slice: expandOccurrences() (VTIMEZONE/DST + all-day + EXDATE) + windowed/joined/zod-validated /api/events + shared-calendar checkpoint (CAL-02/CAL-07)
|
||||
- [x] 02-03-PLAN.md — Frontend foundation: CSS token layer + colorUtils + calendarConfig (firstDayOfWeek 0→7) + hydrateEvents (Temporal/PlainDate guard) + Zustand store + windowed fetchEvents
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
|
||||
- [x] 02-04-PLAN.md — Vertical slice: CalendarShell mounts Schedule-X, renders real windowed Fastmail events color-coded across all four views (CAL-02/CAL-03)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3 completion)*
|
||||
|
||||
- [x] 02-05-PLAN.md — UX completion: read-only EventDetailPopover + ColorLegend + nav/toolbar + skeleton/empty/error states + human visual verification
|
||||
|
||||
**Gap-closure waves** *(from 03-REVIEW.md — write path was broken end-to-end; Gate 2 / 03-08 is blocked on these)*
|
||||
|
||||
- [x] 03-09-PLAN.md — Route layer: align zod schema to client title/start/end contract (CR-01) + real OIDC iss/sub→users.id resolution on all 5 handlers (CR-06) [wave 1]
|
||||
- [x] 03-12-PLAN.md — PWA EventForm: edit-mode population + recurrence preselect (WR-03), zone-consistent dates (WR-05), real focus trap (WR-07); PWA-01/02 install assets verified [wave 1]
|
||||
- [x] 03-10-PLAN.md — Worker dispatch: build real VEVENT via buildVeventString + all-day DTEND+1 (CR-02/WR-04), fail closed on bad creds (CR-03), backoff index + randomUUID (WR-01/WR-08) [wave 2, after 03-09]
|
||||
- [x] 03-11-PLAN.md — Outbox durability: durable create-before-delete (CR-04), drain concurrency guard (CR-05), fresh-etag-before-PUT (WR-02) [wave 3, after 03-10]
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 3: Event Write-Back + PWA Install
|
||||
|
||||
**Goal**: Both members can create, edit, and delete events that are written back to the correct Fastmail calendar, and the app is installable to the iPhone and Android home screens with a guided onboarding flow
|
||||
**Mode:** mvp
|
||||
**Depends on**: Phase 2
|
||||
**Requirements**: CAL-04, CAL-05, CAL-06, CAL-07, PWA-01, PWA-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. A member can create a timed or all-day event (including recurring events) in the app and see it appear in the native Fastmail app within the next sync cycle
|
||||
2. A member can edit an existing event's title, time, or description and the change persists correctly in Fastmail
|
||||
3. A member can delete an event and it disappears from all views on the next sync
|
||||
4. On Android, the app shows a browser install prompt and installs to the home screen; on iOS, the app shows a guided "Add to Home Screen" walkthrough with annotated screenshots that a non-technical user can follow independently
|
||||
5. The installed PWA opens full-screen without browser chrome on both iOS and Android
|
||||
6. **(Carried from Phase 1 Gate 2, D-14)** Live Authelia OIDC login works over the public Pangolin URL — including the **iOS standalone-PWA** flow: the wife can install to Home Screen and complete login without the redirect breaking out of standalone mode; sessions persist (AUTH-01/02) and members get distinct stable colors (AUTH-03). Verify per `docs/deployment.md` Gate 2 checklist; this is the first real external deploy (local Newt test rig is sufficient — Unraid prod is optional until go-live).
|
||||
|
||||
**Plans**: 12 plans (8 original + 4 gap-closure from 03-REVIEW.md)
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 03-01-PLAN.md — Foundation: calendarOutbox table + calendarEvents.objectUrl (pushed), vite-plugin-pwa install + legitimacy gate, sync.ts objectUrl, full Wave 0 RED test scaffold
|
||||
|
||||
**Wave 2** *(blocked on Wave 1)*
|
||||
|
||||
- [x] 03-02-PLAN.md — TDD: VEVENT builder (vevent.ts, D-13 DATE/DATETIME + RRULE) + tsdav write wrappers (write.ts, D-12 broker boundary)
|
||||
- [x] 03-03-PLAN.md — Write API: POST/PATCH/DELETE events + GET sync-status, enqueue-only, D-03 ownership, D-04 edit-as-move pair (CAL-04/05/06/07)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2)*
|
||||
|
||||
- [x] 03-04-PLAN.md — TDD: outbox worker state machine (D-05/06/07/08 retry/backoff/dead/conflict, edit-as-move ordering) + index.ts wiring
|
||||
- [x] 03-05-PLAN.md — Frontend create/edit slice: write client calls + Zustand keys + EventForm (D-01/02/11) + New Event FAB
|
||||
- [x] 03-07-PLAN.md — PWA install: VitePWA manifest + auth-safe SW denylist + icons + InstallPrompt (iOS walkthrough + Android prompt) (PWA-01/02)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3)*
|
||||
|
||||
- [x] 03-06-PLAN.md — Delete + sync feedback: popover Edit/Delete footer + DeleteConfirmationDialog + SyncStateToast polling (D-06/08/09) (CAL-05/06)
|
||||
|
||||
**Wave 5** *(blocked on Wave 4)*
|
||||
|
||||
- [x] 03-08-PLAN.md — Gate 2 live verification: real Authelia OIDC over Pangolin + iOS standalone login + end-to-end Fastmail write round-trips (success criterion 6, D-14/D-15)
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 4: Shared Lists + Live Sync
|
||||
|
||||
**Goal**: Both members can create and manage shared named lists with real-time co-edit sync — edits by one member appear for the other without any manual refresh
|
||||
**Mode:** mvp
|
||||
**Depends on**: Phase 1
|
||||
**⚠️ Entry gate (D-14):** The **SSE-over-Pangolin smoke test** (deferred from Phase 1 Gate 2, issue #1034) MUST pass before building live sync — hold `/api/sse/heartbeat` open 5+ min through the tunnel without it being cut (see `docs/deployment.md`). If it FAILS: fix Pangolin idle-timeout/buffering, or plan a reconnect/polling fallback into this phase before proceeding. Do not build the live-sync layer on an unverified transport.
|
||||
**Requirements**: LIST-01, LIST-02, LIST-03, LIST-04
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. Either member can create a named list (e.g., "Groceries") and delete a list they no longer need
|
||||
2. Either member can add items to a list, check items off, reorder them by drag-and-drop, and delete individual items
|
||||
3. When one member adds or checks off an item, the other member sees the change appear in the list within a few seconds without refreshing — even if they reconnect after a brief network gap
|
||||
|
||||
**Entry gate status (2026-06-08):** CLEARED — SSE-over-Pangolin smoke test PASSED (35 heartbeats over ~6 min, buffering off, no cut). Live sync may be built directly on SSE; polling fallback (D-12) retained as belt-and-suspenders.
|
||||
|
||||
**Plans**: 7 plans (6 + 1 gap-closure)
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 04-01-PLAN.md — Foundation + app shell: deps install (+ legitimacy gate), list tables generate+migrate [BLOCKING], API test harness + Wave-0 RED stubs, react-router + BottomTabBar + empty ListsIndex (D-13/D-16/D-17/D-18)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1)*
|
||||
|
||||
- [x] 04-02-PLAN.md — TDD: scoped in-memory fan-out (listEmitter) + getAccessibleListIds access scope — the load-bearing D-04 no-leak primitive (LIST-04)
|
||||
- [x] 04-03-PLAN.md — List CRUD slice: POST/GET/PATCH/DELETE /api/lists with scoped access + auto-share-on-create + ListsIndex/ListCard/CreateListSheet/ListDeleteDialog (LIST-01, D-01/D-02/D-06)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2)*
|
||||
|
||||
- [x] 04-04-PLAN.md — Item CRUD + checked-sink slice: item endpoints + fractional rank + per-field LWW PATCH + ListDetail/ItemRow/AddItemInput + optimistic UI (LIST-02, D-05/D-07/D-08/D-09)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3)*
|
||||
|
||||
- [x] 04-05-PLAN.md — Reorder slice: dnd-kit sortable + generateKeyBetween rank + one-row position PATCH + animate-on-remote (LIST-03, D-13/D-14/D-15)
|
||||
|
||||
**Wave 5** *(blocked on Waves 2 + 4)*
|
||||
|
||||
- [x] 04-06-PLAN.md — Live-sync slice: scoped /api/sse/lists + fan-out triggers + useListSSE bounded-backoff hook + LiveSyncIndicator + polling fallback (LIST-04, D-04/D-10/D-11/D-12)
|
||||
|
||||
**Wave 6** *(gap closure — blocked on Waves 2 + 4)*
|
||||
|
||||
- [x] 04-07-PLAN.md — Gap closure: migrate list_items.rank to COLLATE utf8mb4_bin (LIST-03 drag-to-top) + owner-only guard on PATCH isShared (T-04-08/T-04-05) — two TDD features (LIST-03)
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 5: Web Push Notifications
|
||||
|
||||
**Goal**: Both members receive timely Web Push alerts for upcoming events, event changes made by the other member, and list changes — reliably on both iOS and Android
|
||||
**Mode:** mvp
|
||||
**Depends on**: Phase 3, Phase 4
|
||||
**Requirements**: NOTIF-01, NOTIF-02, NOTIF-03
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. A member receives a push notification on their phone approximately 15 minutes before a calendar event starts — delivered to the installed PWA, including on iOS
|
||||
2. When the other member adds or changes a calendar event, the first member receives a push notification with the event title and action described in the payload
|
||||
3. When the other member modifies a shared list (adds, checks off, or deletes an item), the first member receives a push notification identifying the list and the change
|
||||
4. After an extended period of app inactivity, push notifications are still delivered (subscription health-check prevents silent revocation on iOS)
|
||||
|
||||
**Plans**: 8 plans (6 waves)
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 05-01-PLAN.md — Foundation: install web-push + workbox deps (legitimacy gate), generate VAPID keypair, push_subscriptions table + calendar_events.title generate+migrate [BLOCKING], Wave-0 RED scaffolds (D-11/D-12)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1)*
|
||||
|
||||
- [x] 05-02-PLAN.md — TDD: pushDispatcher (VAPID send + dual-format payload + 410/404 prune) (D-11)
|
||||
- [x] 05-03-PLAN.md — TDD: pushCoalescer (per-list/actor debounce, generic copy, self-suppress) (D-01/D-02/D-03)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2)*
|
||||
|
||||
- [x] 05-04-PLAN.md — Subscribe slice (end-to-end): push subscription API + setVapidDetails, generateSW→injectManifest SW migration (push/notificationclick/denylist), usePushSubscription + PushPermissionPrompt (D-08/D-11/D-14)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3)*
|
||||
|
||||
- [x] 05-05-PLAN.md — NOTIF-02 list-change slice: listChangeDispatcher + hook coalescer into mutations, reorder-silent (D-01/D-02/D-03)
|
||||
- [x] 05-06-PLAN.md — TDD: NOTIF-01 reminderScheduler — shared-timed 15-min scan (query-enforced D-05), all-day excl, dedup, empty-set safe (D-05/D-06/D-07)
|
||||
|
||||
**Wave 5** *(blocked on Wave 4)*
|
||||
|
||||
- [x] 05-07-PLAN.md — TDD: NOTIF-03 eventChangeDispatcher + syncCalendar diff/title/onChanges hook (poller + outbox), meaningful-only, actor-suppressed (D-02/D-03/D-04/D-13)
|
||||
|
||||
**Wave 6** *(blocked on Wave 3)*
|
||||
|
||||
- [x] 05-08-PLAN.md — Settings + reliability: master toggle (D-09) + silent re-subscribe (D-10) + PermissionDeniedBanner + avatar→Settings sheet
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 6: UX Polish
|
||||
|
||||
**Goal**: Smooth the rough edges surfaced during live use — clearer all-day events, saner event-form date/recurrence behavior, recurring-series editing, and auth-flow polish — so the app feels slick for the non-technical Apple member (hard UX constraint).
|
||||
**Mode:** mvp
|
||||
**Depends on**: Phase 3 (calendar/event-form polish); Phase 4 for any list-related polish
|
||||
**Requirements**: none (all v1 REQ-IDs complete in Phases 1–5; this is a polish phase tracked against backlog items 999.2/3/6/7/8/9 and locked decisions D-01..D-13)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. All-day events are visually distinct from timed events at a glance
|
||||
2. The event form keeps a sane duration when the start moves, all-day edits don't grow the event, and a recurrence can be bounded (repeat-until / count)
|
||||
3. A recurring series can be edited as a whole
|
||||
4. A session that expires mid-use redirects cleanly to sign-in instead of hanging on a generic error
|
||||
5. Unauthenticated cold load shows a neutral "signing you in…" splash — no calendar/"sign-in required" flash before Authelia
|
||||
|
||||
**Scope** (promoted from backlog, locked at planning): 999.2 (login flash), 999.3 (session-timeout redirect), 999.6 (all-day visual), 999.7 (form end-tracking + all-day-edit off-by-one), 999.8 (recurrence bound), 999.9 (recurring-series edit). 999.4 (reminders) and 999.5 (provider setup) deferred to milestone 1.1 (D-01/D-02).
|
||||
|
||||
**Plans**: 6 plans (2 waves)
|
||||
Plans:
|
||||
**Wave 1** *(parallel — exclusive file ownership)*
|
||||
|
||||
- [x] 06-01-PLAN.md — TDD: duration-preserving end-tracking math (computeNewTimedEnd/computeNewAllDayEnd) in eventDateTime.ts (D-04)
|
||||
- [x] 06-02-PLAN.md — TDD: RRULE UNTIL/COUNT serialization + Zod acceptance + FREQ-persistence regression (vevent/outboxWorker/events route) (D-06/D-07)
|
||||
- [x] 06-03-PLAN.md — TDD: hasRrule on CalendarOccurrence + bounded-expansion lock (expand.ts) (D-06/D-08)
|
||||
- [x] 06-04-PLAN.md — Spinner/pulse: global @keyframes pulse + remove redundant spin redefinition (D-13)
|
||||
- [x] 06-05-PLAN.md — Auth gating slice: SessionExpiredError + AuthSplash + global QueryCache/MutationCache error handler; client.ts type mirrors (D-10/D-11, + D-06/D-08 type carriers)
|
||||
|
||||
**Wave 2** *(blocked on 06-01/02/03/05)*
|
||||
|
||||
- [x] 06-06-PLAN.md — EventForm integration slice: end-tracking wiring + recurrence-bound control + series-edit prompt + all-day pill (D-03/D-04/D-05/D-06/D-07/D-08/D-09/D-12)
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
## Progress
|
||||
|
||||
**Execution Order:**
|
||||
Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6
|
||||
Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is complete. It is serialized here to reduce work-in-progress.
|
||||
|
||||
| Phase | Plans Complete | Status | Completed |
|
||||
|-------|----------------|--------|-----------|
|
||||
| 1. Foundation + Broker Spike | 4/4 | Complete | 2026-06-04 |
|
||||
| 2. Calendar Display | 5/5 | Complete | 2026-06-05 |
|
||||
| 3. Event Write-Back + PWA Install | 12/12 | Complete | 2026-06-07 |
|
||||
| 4. Shared Lists + Live Sync | 6/6 | Complete | 2026-06-09 |
|
||||
| 5. Web Push Notifications | 8/8 | Complete | 2026-06-10 |
|
||||
| 6. UX Polish | 6/6 | Complete | 2026-06-10 |
|
||||
|
||||
## 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:
|
||||
|
||||
- [ ] 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
|
||||
|
||||
Plans:
|
||||
|
||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||
|
||||
### Phase 999.5: First-login provider setup — prompt + instructions to add a Fastmail app password (BACKLOG)
|
||||
|
||||
**Goal:** [Captured for future planning] On a member's first login there is no onboarding to connect their own calendar provider. Today the broker uses a single seeded Fastmail app password (the operator's), so a second member (e.g. the wife) who logs in sees only what that token reaches — she has no way to attach her **own** Fastmail personal calendar (the D-09 per-member app-password model). Add a first-login flow that detects a member has no `member_credentials` row and prompts them to create + paste a Fastmail app password, with clear step-by-step instructions (where to generate it in Fastmail settings, required scope: Calendars/CalDAV, that one app password covers all of that account's calendars). Store it encrypted (APP_PASSWORD_ENCRYPTION_KEY, existing crypto path), then trigger an initial sync so their personal calendar lane populates.
|
||||
|
||||
**Context** (surfaced 2026-06-07, Gate 2 live testing): the wife logged in on her iPhone and added the PWA to her Home Screen, but there is no provider-setup step — so her personal calendar can't be connected. This is the onboarding half of the "each member's personal calendar" v1 requirement.
|
||||
|
||||
**Scope to decide when promoted:**
|
||||
|
||||
- Detect "no credential yet" state server-side (`GET /api/me` exposes a `needsProviderSetup` flag, or a dedicated endpoint) and gate a setup screen in the PWA.
|
||||
- App-password entry UI + validation (test the credential with a CalDAV PROPFIND before saving), encrypted storage, and triggering the first sync.
|
||||
- Non-technical-friendly instructions (the hard UX constraint) — ideally with a direct link to Fastmail's app-password page and a screenshot/walkthrough.
|
||||
- Decide the model: does every member attach their own personal calendar, or do some members only see the shared family calendar? (Open question from D-16.)
|
||||
- Security: never log/echo the app password; member-scoped; T-03-19 style scoping.
|
||||
|
||||
**Severity:** high for true multi-member use — without it the second member has no personal calendar. Tags: phase-03, onboarding, auth, caldav, per-member-credential, D-09.
|
||||
**Requirements:** TBD
|
||||
**Plans:** 0 plans
|
||||
|
||||
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.
|
||||
**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.
|
||||
**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.
|
||||
**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.
|
||||
**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.
|
||||
**Requirements:** TBD
|
||||
**Plans:** 0 plans
|
||||
|
||||
Plans:
|
||||
|
||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user