docs: add phased implementation plan and task checklist
This commit is contained in:
358
tasks/plan.md
Normal file
358
tasks/plan.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# 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 (T6–T8) → 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 (~8–12) 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. Deploy from a tagged release/manual trigger, not auto-`main`.
|
||||
|
||||
**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:** T1–T11
|
||||
**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 G1–G4
|
||||
- [ ] 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) |
|
||||
| Auto-deploy from `main` ships every merge | Med | Deploy from tagged release / manual trigger only (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 1–3 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?
|
||||
Reference in New Issue
Block a user