Drop the two-subdomain split (ADR 0004): report at /, view defects at /defects, with a shared layout + header nav. Renames the dashboard page to defects.vue and updates plan/README. Transparency over separation now that auth gates everyone, and it removes the Coolify two-domain routing question.
16 KiB
Implementation Plan: Andon — Weak Point Visualisation
Overview
Build the Andon app per DESIGN.md: a Nuxt 3 full-stack Vue app
on one domain (andon.apoena.dev) — / reports a defect, /defects shows the
weak-point view (ADR 0004) — backed by Postgres/Drizzle, gated by Google SSO,
with Web Push to the owner. Team members file Defects about board
Sections; /defects 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, one domain, two routes —
/reports,/defectsviews; one auth layer, one DB, one board-definition module shared by both (F1). Transparency over separation now that auth gates everyone (ADR 0004). - Coolify-managed PostgreSQL + Drizzle (
pgdriver) — 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-utilsfor 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 devserves the app;GET /api/healthreturns{ ok: true }incl. DB ping.pnpm testandpnpm buildrun green on an empty suite.docker compose upbuilds the app and starts Postgres; app connects viaDATABASE_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 buildandpnpm testpass- 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_idis 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 ≤350ms 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 ≤350ms.
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
- ≤350ms 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.comverified email is denied even ifhdclaims 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.anddashboard.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) |
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 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.comuser?