Uncommitted work-in-progress from 2026-07-08, carried forward: - audit-engine: refine two-pass metadata + content-inspection audit model - audit-matching, content-inspector, shape-audit-spec: supporting matching and spec updates for the two-pass model - task-audit route/panel: UI and API polish - add @napi-rs/canvas dependency, Dockerfile/next.config adjustments for it - test updates and additions (audit-engine, audit-decision-logic, run-task-audit) - add loose dev notes (clone runbook, issues implementation, overdue filter bug report, shared count helper plan)
118 lines
8.5 KiB
Markdown
118 lines
8.5 KiB
Markdown
# Plan — Shared Task-Count Engine (single source of truth)
|
||
|
||
**Author:** Claude · **Date:** 2026-06-02 · **Status:** Draft for review
|
||
**Deploy window:** after hours, this week (TBD date)
|
||
|
||
---
|
||
|
||
## 1. Why
|
||
|
||
The overnight fix (`b1b0dfa`) resolved the headline bugs — Christine Gove now shows **258** overdue (was 0), Jeanne Strong **133** (was 21), and the "drops to 8 when switching users" glitch is gone. Verified live 2026-06-02.
|
||
|
||
But the numbers still don't reconcile across screens, because **each surface computes "overdue / this week / next week" with its own formula.** Verified live today:
|
||
|
||
| Symptom (live) | Numbers | Cause |
|
||
|---|---|---|
|
||
| Same person, different per screen | Christine 257 (team page) vs 258 (task view); Jeanne 131 vs 133 | "overdue" boundary = midnight on one screen, current moment on the other |
|
||
| Overlapping buckets | "1 Week Out" (days 8–14) ⊂ "2 Weeks Out" (days 8–21) | bucket windows overlap; same tasks counted in both |
|
||
| Dashboard self-contradiction | top "Overdue" = 519, sum of per-person overdue = 493 (Δ26) | aggregate counts a different population than the per-member rows |
|
||
| One person > whole team | Christine "This Week" = 90 > team "Due This Week" = 81 | different week definition + scope (all-clients vs Shape-only) |
|
||
|
||
There are currently **6 independent implementations** of these counts:
|
||
|
||
| # | File | Scope | "overdue" boundary | "now" basis |
|
||
|---|---|---|---|---|
|
||
| 1 | `app/(dashboard)/tasks/page.tsx` (SSR initial) | all clients | n/a (returns rows) | — |
|
||
| 2 | `app/(dashboard)/tasks/page-client.tsx:709-732` (cards) | loaded rows (cap 1000) | `dueDate < now` (current time) | **browser** |
|
||
| 3 | `app/api/tasks/route.ts` (switcher/bulk) | all clients | n/a (returns rows) | — |
|
||
| 4 | `app/api/dashboard/workload/route.ts:93-121` | **Shape only** | `dueDate < today` (midnight) | **server** |
|
||
| 5 | `app/(dashboard)/manager/page.tsx` + `components/manager/team-members-by-department.tsx:108` | **Shape only** | `dueDate < today` (midnight) | server-render, browser-eval |
|
||
| 6 | `app/api/metrics/route.ts:26-41` | all clients | `dueDate < todayStart` | server |
|
||
|
||
## 2. Goal
|
||
|
||
One shared module that owns **(a)** the task-visibility filter, **(b)** the "now"/week-boundary math, and **(c)** the bucket definitions. Every surface calls it. Numbers reconcile by construction.
|
||
|
||
---
|
||
|
||
## 3. Decisions to lock before coding
|
||
|
||
These are product decisions, not code details. Recommended defaults in **bold**; please confirm or override.
|
||
|
||
1. **"Overdue" boundary** — count a task overdue when `dueDate < start of today`. **Recommend: start-of-day** (a task due *today* is "due today," not "overdue"). Apply identically everywhere. Resolves the 257-vs-258 and 131-vs-133 drift.
|
||
2. **Time zone** — all day/week math computed in **`America/New_York`** (Seubert's business zone), server-side, never browser-local. Resolves off-by-a-day at midnight and the browser-vs-server split. (Today's cards use the viewer's browser clock — a user in a different zone sees different buckets.)
|
||
3. **Bucket definitions** — non-overlapping, contiguous:
|
||
- **Overdue** = `dueDate < today`
|
||
- **This week** = `today ≤ dueDate ≤ today+6` (days 1–7)
|
||
- **Next week** = `today+7 ≤ dueDate ≤ today+13` (days 8–14)
|
||
- (drop or relabel the current "2 Weeks Out = days 8–21" card, which overlaps)
|
||
- All exclude terminal statuses (COMPLETED/CANCELLED/NA) and dead-policy tasks; open tasks are never aged out.
|
||
4. **Scope on manager/dashboard surfaces** — *decision needed.* Today the team dashboard and team page count **Shape clients only**, while a user's own task list counts **all clients**. For users with non-Shape work these will never match. **Recommend: make the per-user task view also Shape-scoped** so "what the manager sees" == "what the employee sees," OR add an explicit "Shape only" toggle. Flagging for your call — this is the one behavior change end-users might notice.
|
||
5. **Aggregate vs sum** — the dashboard "Overdue" total should equal the sum of the per-person rows shown beneath it. **Recommend:** define the aggregate as "overdue tasks assigned to listed active team members," so top number == sum of rows (no orphan/unassigned tasks silently inflating the header).
|
||
6. **Counts come from the server, not the browser** — cards must request counts from an endpoint, not compute them from a capped 1,000-row array. Fixes the latent truncation bug (Christine has 1,719 open tasks; "this week"/"next week" can silently undercount once a user exceeds 1,000).
|
||
|
||
---
|
||
|
||
## 4. Implementation
|
||
|
||
### 4.1 New shared module — `src/lib/task-buckets.ts`
|
||
- `businessNow()` / `startOfBusinessDay()` — fixed `America/New_York` "today" and week edges.
|
||
- `taskVisibilityWhere(opts)` — the canonical Prisma `where` (dead-policy exclusion, expired-group rule that never hides open tasks, terminal-recency rule). Single definition; surfaces 1/3 import it instead of inlining.
|
||
- `bucketWhere(bucket, base)` — returns the `where` for `overdue | thisWeek | nextWeek | dueToday`, built from the agreed boundaries.
|
||
- `BUCKETS` — ordered, non-overlapping definitions + labels, consumed by the cards.
|
||
- Unit tests (Vitest/Jest) covering boundary cases: due exactly at midnight, DST transitions, terminal-but-recent, open-but-old, dead policy, expired group.
|
||
|
||
### 4.2 New counts endpoint — `src/app/api/tasks/counts/route.ts`
|
||
- `GET ?userId=…&scope=shape|all` → `{ overdue, thisWeek, nextWeek }` computed server-side via `bucketWhere`. Authz mirrors `/api/tasks` (Admin/Manager may pass `userId`).
|
||
- Cards call this; they stop deriving counts from the loaded task array.
|
||
|
||
### 4.3 Refactor each surface to call the shared code
|
||
| File | Change |
|
||
|---|---|
|
||
| `tasks/page-client.tsx` | Replace `now`/`week*End` consts and the `overdue/dueToday/due1Week/due2Weeks` filters (lines 709–732) with values from the counts endpoint; remove the overlapping "2 Weeks Out" card or relabel per decision #3. |
|
||
| `api/tasks/route.ts` | Import `taskVisibilityWhere` instead of the inline AND-block (lines 31–60). |
|
||
| `tasks/page.tsx` | Import `taskVisibilityWhere` (lines 23–53). Keep `take` but raise/paginate, or rely on counts endpoint for totals. |
|
||
| `api/dashboard/workload/route.ts` | Use `bucketWhere`/boundaries; align scope + aggregate-vs-sum per decisions #4/#5 (lines 93–177). |
|
||
| `manager/page.tsx` + `team-members-by-department.tsx` | Use shared boundaries/`startOfBusinessDay` instead of local `today` (line 108). |
|
||
| `api/metrics/route.ts` | Use shared overdue definition (lines 26–41). |
|
||
|
||
No DB schema change. No migration. No data is modified — only what's displayed.
|
||
|
||
---
|
||
|
||
## 5. Verification (before & after, gated by evidence)
|
||
|
||
**A. SQL ground truth** (read-only, against `horizon-db`) — compute overdue/thisWeek/nextWeek per the agreed definitions for a fixed sample: Christine Gove, Jeanne Strong, Mimi Rawlings, Luke Billman, + one all-clients user. Record expected numbers.
|
||
|
||
**B. Reconciliation assertions** — must all hold post-deploy:
|
||
1. Manager team-page overdue for user X **==** that user's task-view Overdue card **==** SQL truth.
|
||
2. Dashboard top "Overdue" **==** sum of per-member overdue rows.
|
||
3. No single user's bucket exceeds the team aggregate for that bucket.
|
||
4. "This week" and "next week" are disjoint; no task appears in both.
|
||
5. A user with >1,000 tasks (Christine) has correct thisWeek/nextWeek (truncation gone).
|
||
|
||
**C. Live Playwright walkthrough** (as today): manager team page → switcher for each sample user → dashboard KPIs; capture screenshots; confirm B1–B5 on screen.
|
||
|
||
**D. `npx tsc --noEmit`** clean; unit tests green.
|
||
|
||
---
|
||
|
||
## 6. Rollout (after-hours, this week)
|
||
|
||
1. Branch `fix/shared-task-counts`; implement §4; run §5-A/D on the branch.
|
||
2. Open PR; self-review + `/code-review`.
|
||
3. In the window: `docker compose -f /opt/stacks/horizon/docker-compose.yml up -d --build`.
|
||
4. Post-deploy: run §5-B/C; attach screenshots.
|
||
5. **Rollback:** revert the merge commit and rebuild (no schema/data change, so rollback is a clean redeploy of the prior image).
|
||
|
||
**User-facing note for the window:** "Brief maintenance — task counts will be momentarily unavailable. No tasks change; only how totals are displayed is being made consistent across screens."
|
||
|
||
---
|
||
|
||
## 7. Open items needing your sign-off
|
||
- Decision #1 (overdue = start of day) ✅ recommend
|
||
- Decision #2 (America/New_York) ✅ recommend
|
||
- Decision #3 (drop "2 Weeks Out" overlap) ✅ recommend
|
||
- **Decision #4 (Shape-only vs all-clients on user task view)** ⬅ needs your call — only item with visible behavior change
|
||
- Decision #5 (aggregate == sum of rows) ✅ recommend
|
||
- Decision #6 (server-side counts) ✅ recommend
|