From 3e5f7d364380c0a4d4f6b6e45fe7eb57bdf92a7f Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Thu, 28 May 2026 00:31:17 +0200 Subject: [PATCH] feat(dashboard): red-dot weak-point map on /defects (T7) Overlay each filed defect as a red dot inside its section box with a diagonal date label; cap visible dots and collapse the remainder to a "+N more" using the authoritative per-section counts (T6). Add an `overlay` scoped slot to BoardView (C2) so the dashboard can layer dots without forking the board definition (F1); the reporting view leaves it empty. DotMap groups the bounded feed by section and the /defects page wires both endpoints. --- app/components/BoardView.vue | 3 + app/components/DotMap.vue | 107 +++++++++++++++++++++++++++++++++++ app/pages/defects.vue | 21 ++++++- tasks/todo.md | 2 +- tests/dotmap.nuxt.spec.ts | 59 +++++++++++++++++++ 5 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 app/components/DotMap.vue create mode 100644 tests/dotmap.nuxt.spec.ts diff --git a/app/components/BoardView.vue b/app/components/BoardView.vue index 6c29833..a8cdb13 100644 --- a/app/components/BoardView.vue +++ b/app/components/BoardView.vue @@ -40,6 +40,7 @@ const hoveredId = ref(null); > {{ section.size }} + @@ -104,6 +105,8 @@ const hoveredId = ref(null); cursor: pointer; padding: 0.5rem; box-sizing: border-box; + /* Positioning context for the dashboard's absolutely-placed dots (C5). */ + position: relative; /* ISO 216: every A-series sheet is landscape √2 : 1 (width : height). */ aspect-ratio: 1.41421356 / 1; } diff --git a/app/components/DotMap.vue b/app/components/DotMap.vue new file mode 100644 index 0000000..f15cd7e --- /dev/null +++ b/app/components/DotMap.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/app/pages/defects.vue b/app/pages/defects.vue index 42727e2..5de48fc 100644 --- a/app/pages/defects.vue +++ b/app/pages/defects.vue @@ -1,6 +1,23 @@ diff --git a/tasks/todo.md b/tasks/todo.md index 2da60e9..d5178e5 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -17,7 +17,7 @@ review. ## Phase 3 — See weak points (G2) - [x] **T6** Defects read API: feed + per-section counts (C7) — F6 (M) — _deps: T3_ -- [ ] **T7** Dashboard DotMap (C5) — F4 (M) — _deps: T2, T6_ +- [x] **T7** Dashboard DotMap (C5) — F4 (M) — _deps: T2, T6_ - [ ] **T8** VerbatimModal + Feed (C6) — F5 (M) — _deps: T6, T7_ - [ ] ⛳ **Checkpoint:** dashboard dot map + feed + modal work, ≤350ms @2k, human review diff --git a/tests/dotmap.nuxt.spec.ts b/tests/dotmap.nuxt.spec.ts new file mode 100644 index 0000000..29038f8 --- /dev/null +++ b/tests/dotmap.nuxt.spec.ts @@ -0,0 +1,59 @@ +// @vitest-environment nuxt +import { describe, it, expect } from 'vitest' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import type { VueWrapper } from '@vue/test-utils' +import DotMap from '../app/components/DotMap.vue' + +// The section box the overlay dots are rendered inside (BoardView marks each +// section with `data-section-id`). +const section = (wrapper: VueWrapper, id: string) => + wrapper.find(`[data-section-id="${id}"]`) + +describe('DotMap', () => { + it('renders a red dot with a date label inside the defect’s section', async () => { + const wrapper = await mountSuspended(DotMap, { + props: { + defects: [{ id: 'd1', sectionId: 'macroplan', createdAt: '2026-05-28T10:00:00Z' }], + counts: { macroplan: 1 }, + }, + }) + + const dots = section(wrapper, 'macroplan').findAll('[data-test="dot"]') + expect(dots).toHaveLength(1) + expect(dots[0]!.text()).toContain('28/05') + + // A section with no defects shows no dots. + expect(section(wrapper, 'problem-solving').findAll('[data-test="dot"]')).toHaveLength(0) + }) + + it('caps visible dots and collapses the overflow to "+N more"', async () => { + const cap = 10 + const defects = Array.from({ length: 15 }, (_, i) => ({ + id: `d${i}`, + sectionId: 'feature-kanban', + createdAt: '2026-05-28T10:00:00Z', + })) + + const wrapper = await mountSuspended(DotMap, { + // Feed holds 15 for this section but the true total is 42 (counts). + props: { defects, counts: { 'feature-kanban': 42 }, cap }, + }) + + const box = section(wrapper, 'feature-kanban') + expect(box.findAll('[data-test="dot"]')).toHaveLength(cap) + const more = box.find('[data-test="more"]') + expect(more.exists()).toBe(true) + expect(more.text()).toContain('+32 more') // 42 total − 10 shown + }) + + it('shows no overflow marker when a section is at or below the cap', async () => { + const wrapper = await mountSuspended(DotMap, { + props: { + defects: [{ id: 'd1', sectionId: 'macroplan', createdAt: '2026-05-28T10:00:00Z' }], + counts: { macroplan: 1 }, + }, + }) + + expect(section(wrapper, 'macroplan').find('[data-test="more"]').exists()).toBe(false) + }) +})