Files
2026-06-18 22:03:56 -04:00

116 lines
9.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 23 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.
---
## Milestone: v1.1 — Operability & Polish
**Shipped:** 2026-06-18
**Phases:** 14 (720) | **Plans:** 57 | **Sessions:** not tracked
### What Was Built
- A self-hosted Gitea CI/CD pipeline: PR-gating lint (real ESLint flat config) / typecheck / MariaDB-backed API integration / a mobile + desktop Playwright regression harness, plus dependency-audit / gitleaks / eslint-plugin-security / dev↔prod image-hygiene gates and a Docker publish on merge.
- In-app operability: role-gated admin (credential rotation, shared-calendar designation, member editor), a validated first-run setup wizard, per-event reminders with a variable-lead scheduler, auto timezone detection, and ~12s event write-back.
- A first-class local-auth (no-OIDC) mode coexisting with Authelia OIDC, removing the hard dependency on a deployed Authelia.
### What Worked
- **Backlog → phase promotion pipeline:** most of v1.1 (999.4/10/11/12/13/14/15/16) was captured as backlog during v1.0, then promoted cleanly into scoped phases — the deferred-idea capture paid off directly.
- **Runner-probe-first for self-hosted CI (PITFALL 12):** probing `node`/`pnpm`/Docker/registry access on the Gitea runner *before* authoring any test/build steps surfaced every fork answer (Docker-executor, `ubuntu-latest`, artifact-fork, `REGISTRY_PAT` naming) up front and avoided blind CI iteration.
- **Zero-dependency in-process solutions:** the EventEmitter outbox-drain signal (CAL-15) hit the latency goal with no new infra; the project later removed Redis entirely as unused.
- **TDD discipline on the admin/auth chain** (Phases 10/11/12/19) kept the role boundary and credential-handling correct, with route-level 403/423/409 guards asserted in tests.
### What Was Inefficient
- **Dev user can't exercise calendar features end-to-end:** `DEV_AUTH_BYPASS` user 1 has no `member_credentials`/calendars, so per-event reminders (Phase 11) could only be verified via tests + a route-mocked smoke, not hands-on by the operator (→ backlog 999.19). Recurring dev-testability friction.
- **Gitea-specific quirks cost cycles:** secrets with the `GITEA_` prefix are silently dropped (→ `REGISTRY_PAT`); `actions/upload-artifact@v4` is broken on Gitea (needs the `ChristopherHX` fork); `actions/cache@v4` timed out; skipped jobs may not emit a commit-status (drove the always-running `gate` aggregate). None are documented as GitHub-incompatible up front.
- **Scope grew mid-milestone:** the milestone planned as 717 but accreted 18/19/20 via `/gsd-phase`, and the ROADMAP header wasn't kept in sync — the phase-detail sections for 1820 ended up appended after the Backlog. Keep the roadmap header + section ordering current when inserting late phases.
### Patterns Established
- **Runner-probe-first** for any new self-hosted-CI capability — never author steps against an unprobed runner.
- **Always-running `gate` aggregate** (`if: always()`, passes on success-or-skipped) is the only safe required-check surface when path-filtering jobs — never mark a path-filtered job itself required (deadlock).
- **In-process EventEmitter over Redis** for single-process work (the outbox drain); reserve external infra for genuinely cross-process needs.
- **Local auth is a first-class mode**, not a fallback — identity stays OIDC-`iss+sub` (never email); a local user is *linked* to an OIDC identity via an explicit claim flow (D-10/D-19).
- **Confine dev-only affordances at build + boot:** bake `NODE_ENV=production` into the prod image and refuse-to-boot if `DEV_AUTH_BYPASS` is set — defense-in-depth beyond the runtime guard.
### Key Lessons
1. Capturing deferred ideas as structured backlog entries during one milestone makes the next milestone's roadmap nearly write-itself — invest in the capture.
2. Self-hosted GitHub-Actions-compatible runners are *not* drop-in GitHub — probe the runtime, the action ecosystem (forks), and the status/secret semantics before designing the pipeline.
3. Dev-environment testability is a feature: if the dev user can't exercise the real flows, every feature regresses to test-only verification and the operator can't UAT — fix the dev seed/provider story early (999.19).
### Cost Observations
- Model mix: not tracked
- Sessions: not tracked
- Notable: 14 phases shipped in ~8 days (2026-06-10 → 2026-06-18) with heavy parallelization across independent tracks (CI chain vs admin chain vs polish) once the harness landed.
---
## Cross-Milestone Trends
### 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 |
| v1.1 | n/a | 14 | Self-hosted Gitea CI/CD as the merge gate; per-phase branch + PR shipping; backlog→phase promotion pipeline; parallel independent tracks |
### Cumulative Quality
| Milestone | Tests | Coverage | Zero-Dep Additions |
| --------- | ------------------------------------- | ------------ | ------------------------------------------- |
| v1.0 | PWA 191 + API broker/events 114 green | not measured | n/a |
| v1.1 | PWA ~249 + API ~347 green | not measured | `outboxTrigger.ts` EventEmitter (CAL-15); Redis later removed entirely as unused |
### Top Lessons (Verified Across Milestones)
1. **Capture deferred ideas as structured backlog during the milestone** — v1.1's roadmap came almost entirely from v1.0-era backlog entries.
2. **iOS-Safari standalone / on-device push stays a human gate** across both milestones — automated harnesses (desktop + mobile-emulated) cover layout/flows, never the device-only behavior.
3. **`setInterval` + in-process signals over external schedulers/brokers** for this single-process app — node-cron silently no-ops (v1.0), Redis went unused (v1.1).