Files
andon/tasks/plan.md
Julien Calixte 405b929f66 ci: Coolify autodeploy from main; migrate-on-boot and split dev/prod compose
- Add Nitro plugin to apply Drizzle migrations on server boot (skips when
  DATABASE_URL is unset, e.g. during build).
- Split compose: docker-compose.dev.yml (hot-reload + Postgres) vs
  docker-compose.yml (production/self-host, env-driven, Coolify-deployable).
- Add .dockerignore; parameterise compose env; document the autodeploy
  decision (Coolify watches main, no CI workflow needed) in ADR 0003 and
  plan.md T12.
2026-05-27 23:13:06 +02:00

360 lines
16 KiB
Markdown
Raw 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.
# Implementation Plan: Andon — Weak Point Visualisation
## Overview
Build the Andon app per [DESIGN.md](../DESIGN.md): a Nuxt 3 full-stack Vue app
serving two domains (`andon.apoena.dev` reporting, `dashboard.andon.apoena.dev`
viewing), backed by SQLite/Drizzle, gated by Google SSO, with Web Push to the
owner. Team members file **Defects** about board **Sections**; the dashboard
renders a red-dot **Weak Point** map. Build order follows the House of Quality
weights: the board definition (F1) is the spine, then frictionless filing
(F2/F3), then the weak-point map (F4/F5), then auth (F9), notifications (F7/F8),
and deployment/durability (F10).
## Architecture Decisions
- **Single Nuxt 3 app, two domains** — host-based routing selects the reporting
vs dashboard view; one auth layer, one DB, one board-definition module shared
by both (DESIGN §3, F1). Avoids drift between the apps.
- **Coolify-managed PostgreSQL + Drizzle (`pg` driver)** — automatic backups and
no single-instance deploy constraint; chosen over SQLite once deploying on
Coolify (DESIGN T5, F10, ADR 0003). Local dev runs Postgres via docker-compose.
- **Reporter is resolved through one `getReporter()` seam** — in early phases it
returns a dev identity; the real Google session is swapped in at Task 9. This
keeps the filing slice buildable before OAuth credentials exist, with minimal
rework (ADR 0002).
- **Package manager: pnpm; tests: Vitest + `@nuxt/test-utils` for unit/component,
Playwright for end-to-end.** Adjust if the team standard differs.
- **Defect is append-only, Section-level** — no lifecycle, no exact-spot pin
(DESIGN T2/T7, ADR 0001).
## Dependency Graph
```
T1 Scaffold (Nuxt + tooling + Docker dev)
├── T2 Board-definition (C1) + BoardView (C2) ── spine, F1
│ ├── T5 Defect filing (C3 form) ── F2
│ ├── T7 DotMap (C5) ── F4
│ └── T8 VerbatimModal + Feed (C6) ── F5
├── T3 DB schema + migrations (C8)
│ ├── T4 Projects API + autocomplete (C4) ── F3 ─┐
│ ├── T5 Defect filing API + form ──────┤ (needs T2, T3, T4)
│ ├── T6 Defects read API (feed + counts) ── F6 │
│ └── T10 Push subscription store ──────┘
├── T6 ──→ T7, T8
├── T9 Google OAuth + domain middleware (C9) ── F9, cross-cutting
│ └── replaces getReporter() stub from T5
├── T10 Web Push subscribe + SW (C10) ──→ T11 Send push on file (F7)
└── T12 Docker image + compose + Coolify volume (C11) ── F10, depends on all
```
Bottom-up: foundation (T1, T3) → spine (T2) → filing slice (T4, T5) → weak-point
slice (T6T8) → auth (T9) → notifications (T10, T11) → ship (T12).
## Task List
### Phase 1 — Foundation & board spine
#### Task 1: Scaffold the Nuxt 3 app with tooling and Dockerised dev
**Description:** Create the Nuxt 3 + TypeScript project, configure ESLint,
Vitest + `@nuxt/test-utils`, Drizzle (`pg` driver), and a dev Dockerfile +
compose (app + Postgres service) so the app runs locally the same way it will on
Coolify. Add a `/api/health` route that also checks the DB connection.
**Acceptance criteria:**
- [ ] `pnpm dev` serves the app; `GET /api/health` returns `{ ok: true }` incl. DB ping.
- [ ] `pnpm test` and `pnpm build` run green on an empty suite.
- [ ] `docker compose up` builds the app and starts Postgres; app connects via `DATABASE_URL`.
**Verification:**
- [ ] Build succeeds: `pnpm build`
- [ ] Tests run: `pnpm test`
- [ ] Manual: open dev server, hit `/api/health`
**Dependencies:** None
**Files likely touched:** `package.json`, `nuxt.config.ts`, `eslint.config.mjs`, `vitest.config.ts`, `server/api/health.get.ts`, `Dockerfile.dev`, `docker-compose.yml`
**Estimated scope:** M
#### Task 2: Board-definition module (C1) + BoardView component (C2)
**Description:** Encode the canonical board (4 Blocks, their Sections with stable
IDs, sizes, grid placement) in one TS module, and render it as the ASCII-styled
DOM grid: bordered monospace section boxes that highlight on hover and emit the
section ID on click. Both views import this single module (F1).
**Acceptance criteria:**
- [ ] All Blocks/Sections from DESIGN render from the one definition module.
- [ ] Hovering a Section highlights it; clicking emits its stable ID.
- [ ] Snapshot test pins the rendered board so the two apps can't drift.
**Verification:**
- [ ] Component test: `pnpm test -- board`
- [ ] Snapshot matches: `pnpm test -- --grep board-snapshot`
- [ ] Manual: hover/click each Section, confirm correct ID in a handler
**Dependencies:** T1
**Files likely touched:** `app/board/definition.ts`, `app/components/BoardView.vue`, `tests/board.spec.ts`
**Estimated scope:** M
### Checkpoint: Foundation
- [ ] `pnpm build` and `pnpm test` pass
- [ ] Board renders and is interactive in the browser
- [ ] Review with human before proceeding
### Phase 2 — File a defect (Goal G1)
#### Task 3: Database schema and migrations (C8)
**Description:** Define Drizzle (Postgres) schema for `projects`, `defects`
(section_id, project_id, verbatim, reporter_email, created_at), and
`push_subscriptions`; generate and apply the migration; expose a typed DB client.
**Acceptance criteria:**
- [ ] Migration creates the three tables with the columns above.
- [ ] A repository helper can insert and read a defect against a test Postgres DB.
- [ ] `defects.section_id` is a plain string (no FK) referencing C1 IDs (ADR 0001).
**Verification:**
- [ ] Migration test: `pnpm test -- db`
- [ ] Manual: run migration against the dev Postgres, inspect schema
**Dependencies:** T1
**Files likely touched:** `server/db/schema.ts`, `server/db/client.ts`, `drizzle.config.ts`, `server/db/migrations/*`, `tests/db.spec.ts`
**Estimated scope:** S
#### Task 4: Projects API + ProjectAutocomplete (C4) — F3
**Description:** `GET /api/projects` (list) and `POST /api/projects`
(create, case-insensitive dedupe) plus a creatable autocomplete component that
lists existing projects and persists a new one inline.
**Acceptance criteria:**
- [ ] Listing returns the shared global project list.
- [ ] Creating "Acme" then "acme" yields one project (deduped).
- [ ] New project becomes selectable without reload.
**Verification:**
- [ ] API + dedupe test: `pnpm test -- projects`
- [ ] Manual: type a new project, submit, re-open select and see it
**Dependencies:** T3
**Files likely touched:** `server/api/projects.get.ts`, `server/api/projects.post.ts`, `app/components/ProjectAutocomplete.vue`, `tests/projects.spec.ts`
**Estimated scope:** M
#### Task 5: File a defect — POST /api/defects + DefectForm (C3) — F2
**Description:** Wire a BoardView Section click to open a modal with the
ProjectAutocomplete + a Verbatim textarea; submit persists a defect. Reporter is
resolved via a `getReporter()` seam returning a dev identity for now; timestamp
auto. Target ≤3 interactions / ≤15s.
**Acceptance criteria:**
- [ ] Click Section → modal → pick project + type verbatim → submit persists a row.
- [ ] Filing takes ≤3 interactions after landing on the board.
- [ ] Verbatim and project are required; section_id comes from the click.
**Verification:**
- [ ] Slice test: `pnpm test -- defect-filing`
- [ ] E2E: `pnpm e2e -- file-defect`
- [ ] Manual: file a defect, confirm DB row with correct section/project/verbatim/dev-reporter
**Dependencies:** T2, T3, T4
**Files likely touched:** `server/api/defects.post.ts`, `server/utils/getReporter.ts`, `app/components/DefectForm.vue`, `app/pages/index.vue` (andon view), `tests/defect-filing.spec.ts`
**Estimated scope:** M
### Checkpoint: Filing works
- [ ] A defect can be filed end-to-end (dev auth) and is persisted
- [ ] All tests pass, build clean
- [ ] Review with human before proceeding
### Phase 3 — See weak points (Goal G2)
#### Task 6: Defects read API — feed + per-section counts (C7) — F6
**Description:** `GET /api/defects` returning a reverse-chronological feed
(bounded slice) and `GET /api/defects/counts` returning per-section counts;
`GET /api/defects?section=ID` for one section's history (lazy, for the modal).
Index `(section_id, created_at)`. Target ≤1s at ~2k rows.
**Acceptance criteria:**
- [ ] Feed returns newest-first, bounded.
- [ ] Counts endpoint returns `{ sectionId: n }` for all sections.
- [ ] With 2,000 seeded defects, dashboard queries return in ≤1s.
**Verification:**
- [ ] API test: `pnpm test -- defects-read`
- [ ] Perf check: seed 2k rows, time the endpoints
- [ ] Manual: hit each endpoint, eyeball ordering and counts
**Dependencies:** T3
**Files likely touched:** `server/api/defects.get.ts`, `server/api/defects/counts.get.ts`, `server/db/seed.ts`, `tests/defects-read.spec.ts`
**Estimated scope:** M
#### Task 7: Dashboard DotMap (C5) — F4
**Description:** On the dashboard view, overlay each defect as a red dot inside
its Section box with a diagonal date label; cap visible dots (~812) and collapse
overflow to a "+N more" marker. Reuses BoardView (C2) + counts/feed from T6.
**Acceptance criteria:**
- [ ] Dots render inside the correct Section with diagonal date labels.
- [ ] A Section over the cap shows the cap + "+N more".
- [ ] Dashboard view loads at `dashboard.` host (or dev equivalent).
**Verification:**
- [ ] Component test: `pnpm test -- dotmap`
- [ ] Manual: seed a hot Section past the cap, confirm "+N more"
**Dependencies:** T2, T6
**Files likely touched:** `app/components/DotMap.vue`, `app/pages/dashboard.vue`, `tests/dotmap.spec.ts`
**Estimated scope:** M
#### Task 8: VerbatimModal + reverse-chronological Feed (C6) — F5
**Description:** Clicking a Section/dot opens a modal listing that Section's
defects (date, project, reporter, verbatim) without leaving the board; a feed
list beside the board shows all defects newest-first.
**Acceptance criteria:**
- [ ] Click Section/dot → modal lists that Section's defects; board stays visible.
- [ ] Feed shows all defects newest-first with date/project/reporter.
- [ ] Closing the modal returns to the board unchanged.
**Verification:**
- [ ] Component test: `pnpm test -- verbatim-modal`
- [ ] E2E: `pnpm e2e -- dashboard-view`
- [ ] Manual: open/close modal on a populated Section
**Dependencies:** T6, T7
**Files likely touched:** `app/components/VerbatimModal.vue`, `app/components/DefectFeed.vue`, `app/pages/dashboard.vue`, `tests/verbatim-modal.spec.ts`
**Estimated scope:** M
### Checkpoint: Weak points visible
- [ ] Dashboard shows the dot map, feed, and modal end-to-end
- [ ] ≤1s render with 2k seeded defects
- [ ] Review with human before proceeding
### Phase 4 — Authentication (Goal G4)
#### Task 9: Google OAuth + session + domain-check middleware (C9) — F9
**Description:** Add Google OAuth/OIDC login, a session, and default-deny server
middleware gating every route; verify the email domain server-side (`hd` not
trusted alone). Swap `getReporter()` to read the real session identity so defects
record the true reporter.
**Acceptance criteria:**
- [ ] Unauthenticated requests are redirected to Google login.
- [ ] A non-`@theodo.com` verified email is denied even if `hd` claims otherwise.
- [ ] Newly filed defects record the authenticated reporter's email.
**Verification:**
- [ ] Auth test: `pnpm test -- auth` (unauth denied, wrong-domain denied)
- [ ] E2E: `pnpm e2e -- login`
- [ ] Manual: log in with a Theodo account, file a defect, confirm reporter
**Dependencies:** T1 (retrofits T5)
**Files likely touched:** `server/middleware/auth.ts`, `server/api/auth/*`, `server/utils/getReporter.ts`, `app/pages/login.vue`, `tests/auth.spec.ts`
**Estimated scope:** M
### Checkpoint: Gated & attributable
- [ ] Both views require `@theodo.com`; reporters are real
- [ ] Review with human before proceeding
### Phase 5 — Notifications (Goal G3)
#### Task 10: Web Push subscription + service worker (C10) — F8
**Description:** Generate VAPID keys; add a service worker and a "Enable
notifications" flow that subscribes the owner's device and stores the
subscription; support multiple devices; prune on HTTP 410.
**Acceptance criteria:**
- [ ] Owner can grant permission and a subscription row is stored.
- [ ] A second device adds a second subscription (both kept).
- [ ] A 410 from the push service prunes the stale subscription.
**Verification:**
- [ ] Store test: `pnpm test -- push-subscriptions`
- [ ] Manual: subscribe on the Pixel, confirm row; revoke, confirm prune
**Dependencies:** T3, T9
**Files likely touched:** `app/sw.ts`, `app/components/EnableNotifications.vue`, `server/api/subscriptions.post.ts`, `server/api/subscriptions.delete.ts`, `tests/push-subscriptions.spec.ts`
**Estimated scope:** M
#### Task 11: Send Web Push on defect filed — F7
**Description:** After the file-defect response returns (fire-and-forget, so
filing stays fast), send a VAPID Web Push to all stored subscriptions with the
section, project, and a verbatim snippet; log failures (no retry queue in v1).
**Acceptance criteria:**
- [ ] Filing a defect delivers a push to subscribed devices in ≤10s.
- [ ] The file-defect response time is unaffected by push sending.
- [ ] Push failures are logged and don't fail the request.
**Verification:**
- [ ] Send test: `pnpm test -- push-send` (mocked sender, fired post-response)
- [ ] Manual: file a defect, time the push on the Pixel
**Dependencies:** T5, T10
**Files likely touched:** `server/utils/sendPush.ts`, `server/api/defects.post.ts`, `tests/push-send.spec.ts`
**Estimated scope:** S
### Checkpoint: Owner notified
- [ ] End-to-end: file → push arrives; filing latency unaffected
- [ ] Review with human before proceeding
### Phase 6 — Ship (F10)
#### Task 12: Production image + compose + Coolify volume & domains (C11)
**Description:** Production Dockerfile; provision Coolify-managed Postgres with
scheduled backups; configure the two domains and TLS on Coolify; wire
env/secrets (`DATABASE_URL`, Google client, VAPID, session secret); run the
migration on boot. Coolify autodeploys on push to `main`, with `main`
protected by PR review + CI.
**Acceptance criteria:**
- [ ] After a redeploy, previously filed defects are still present (0 data loss).
- [ ] Both `andon.` and `dashboard.` resolve over HTTPS to the right views.
- [ ] Migrations run automatically on container start; Postgres backups scheduled.
**Verification:**
- [ ] Durability: deploy, file a defect, redeploy, confirm it persists
- [ ] Manual: both subdomains load, login works in prod
- [ ] Build: production image builds and boots
**Dependencies:** T1T11
**Files likely touched:** `Dockerfile`, `docker-compose.yml`, `.env.example`, `server/plugins/migrate.ts`, deployment notes in `README.md`
**Estimated scope:** M
### Checkpoint: Complete
- [ ] All acceptance criteria met across G1G4
- [ ] Critical performance budget (DESIGN §7) spot-checked
- [ ] Ready for review
## Risks and Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| Data lost on redeploy / disk failure | High | Coolify-managed Postgres with scheduled backups; redeploy durability test (T12, ADR 0003) |
| Autodeploy from `main` ships every merge | Med | Protect `main` with required PR review + CI; migrations-on-boot + healthcheck catch failures; Coolify keeps rollback (T12) |
| Google `hd` claim spoofed by a custom client | High | Server-side verified-email-domain recheck (T9) |
| Google OAuth credentials/redirect URIs unavailable | Med | `getReporter()` seam lets Phases 13 proceed with dev auth (T5, T9) |
| Dot map unreadable on hot Sections | Med | Visible-dot cap + "+N more" (T7) |
| Web Push subscription lost when browser storage cleared | Low | Multi-device store + easy re-subscribe (T10) |
| Two-domain routing in one Nuxt app | Med | Host-based routing decided up front; verify in T1/T7 |
## Open Questions
- **Google OAuth app:** who provisions the client ID/secret and authorized
redirect URIs for both subdomains? Needed before T9.
- **Package manager / test tooling:** is pnpm + Vitest + Playwright the team
standard, or should this match an existing Theodo template?
- **Coolify routing:** does Coolify terminate TLS and route both subdomains to
one service, or do we need two service definitions pointing at the same image?
- **Notification recipients:** v1 pushes to the owner only — is "owner" a single
account, or any subscribed `@theodo.com` user?