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)
8.5 KiB
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.
- "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. - 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.) - 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.
- Overdue =
- 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.
- 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).
- 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()— fixedAmerica/New_York"today" and week edges.taskVisibilityWhere(opts)— the canonical Prismawhere(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 thewhereforoverdue | 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 viabucketWhere. Authz mirrors/api/tasks(Admin/Manager may passuserId).- 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:
- Manager team-page overdue for user X == that user's task-view Overdue card == SQL truth.
- Dashboard top "Overdue" == sum of per-member overdue rows.
- No single user's bucket exceeds the team aggregate for that bucket.
- "This week" and "next week" are disjoint; no task appears in both.
- 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)
- Branch
fix/shared-task-counts; implement §4; run §5-A/D on the branch. - Open PR; self-review +
/code-review. - In the window:
docker compose -f /opt/stacks/horizon/docker-compose.yml up -d --build. - Post-deploy: run §5-B/C; attach screenshots.
- 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