Files
andon/tasks/plan.md
Julien Calixte e5c002547f refactor(app): serve one domain — /defects route and header nav
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.
2026-05-28 00:24:02 +02:00

16 KiB
Raw Permalink Blame History

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, /defects views; 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 (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 ≤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 (~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
  • ≤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.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?