seubert-claims/dev/shared-count-helper-plan-2026-06-02.md
lorentz 01ecfacd04 feat(imageright): finalize two-pass audit engine and task-audit UI polish
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)
2026-07-16 22:52:41 +00:00

8.5 KiB
Raw Permalink Blame History

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 814) ⊂ "2 Weeks Out" (days 821) 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 17)
    • Next week = today+7 ≤ dueDate ≤ today+13 (days 814)
    • (drop or relabel the current "2 Weeks Out = days 821" 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 surfacesdecision 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 709732) 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 3160).
tasks/page.tsx Import taskVisibilityWhere (lines 2353). 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 93177).
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 2641).

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 B1B5 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