feat(scheduling): per-area coverage strips + demand explainer in Timeline - #569
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds `summarizeAreaCoverage` pure helper to `src/lib/coverageSummary.ts` that groups shifts by employee area, calls `computeDayCoverage` per area, and returns per-area `CoverageHour[]` with demand=null (scheduled-only). Adds `AreaCoverage` export type. Unassigned employees bucketed under the `UNASSIGNED_LABEL` key, areas sorted alphabetically with Unassigned last. Companion tests in `tests/unit/coverageSummary.test.ts` exercise area grouping, the Unassigned bucket, and the empty-shifts early-exit; marked CRITICAL as required by the plan. Tests pass under TZ=UTC and TZ=Asia/Tokyo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nent Adds CoverageDemandInfo.tsx: a shadcn Popover trigger that explains how the "needed" staffing count is derived (projected sales ÷ SPLH, capped at minimum crew). Includes a link to /settings for adjusting targets. Fully keyboard-accessible (focusable trigger, Escape closes popover). Five unit tests cover: trigger aria-label, formula text, settings link href, Covered/Short vocabulary, and Escape-key dismissal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eadcount strips Adds AreaCoverageStrips.tsx that renders one compact row per area (area label + per-hour scheduled headcount cells) when the Timeline is grouped by Area. Cells are neutral (no red/green) since per-area demand is not yet available. A footnote states demand targets are whole-location. Returns null when areas=[]. Paired with 6 unit tests covering: empty guard, area name labels, CRITICAL role="img" aria-labels (area + hour + count), visible headcount text, the demand footnote, and the Unassigned bucket label. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ells
Cells now display `scheduled/needed` (e.g. `3/5`) instead of `✓` / `−N` so
managers can see the exact comparison at a glance without inferring from color
alone. No-demand cells fall back to the bare scheduled count. aria-labels
updated to expose the fraction ("3 of 5, short 2") for screen readers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…line Wire CoverageDemandInfo + AreaCoverageStrips into ShiftTimelineTab: - Add CoverageDemandInfo popover to the coverage panel header (next to verdict) - Compute areaCoverage via useMemo + summarizeAreaCoverage; active only when groupBy === 'area' - Render AreaCoverageStrips (pl-[120px] aligned) below CoverageStatusStrip when grouped by area - 4 new CRITICAL tests: demand trigger, per-area rows, strips hidden on position groupBy, footnote Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CoverageDemandInfo: move Lucide icon import after shadcn imports (matches CLAUDE.md import order: shadcn #2, icons #3) - CoverageStatusStrip: align cell typography with AreaCoverageStrips (hour label text-[9px], value text-[11px]); widen cell gap to gap-[3px] for visual consistency across both strip components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tation, simplify label lookup - DeltaView now receives `deltaPeak` as a prop from CoverageChart (already computed there for Axes) instead of recomputing it from the same `hours` array — eliminates the duplicate Math.max + spread. - Remove dead `isOver` variable in DeltaView; the zero-delta branch is now expressed as the clearer `h.delta === 0` guard. - Drop redundant `as number` casts in buildVerdict's worst-finding loop (shortHours is already filtered to delta < 0, so non-null is guaranteed). - CoverageStatusStrip builds a `labelByStartMin` Map once so formatHour is called a single time per hour instead of twice (once for the visual strip, once for the sr-only list). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- coverageSummary: emit scheduled=0 hours when demand is configured but no shifts are scheduled, so buildVerdict reports hasDemand:true and the full shortfall count instead of silently hiding fully-unstaffed periods - ShiftTimelineTab: remove px-4 from coverage panel wrapper so pl-[120px] chart/strip children align with TimelineAxis ticks and shift lanes below - CoverageDemandInfo: replace <a href> with <Link to> (React Router 6 SPA navigation — prevents full-page reload); wrap test in MemoryRouter - CoverageStatusStrip: guard hasDemand on both delta !== null AND needed !== null to prevent h.needed! assertion on inconsistent upstream state - CoverageChart: wrap in React.memo to skip O(H) SVG path recomputes on setActiveShift re-renders; clamp surplus label above MARGIN_TOP+8 to prevent clipping above SVG viewBox on max-height positive bars - coverageSummary.test: add regression tests for zero-coverage-with-demand fix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the day window is not hour-aligned, `firstHourStart` can start before `window.startMin`, causing coverage samples outside the visible range to bleed into the first bucket's scheduled count. Clamp the filter to `[max(start, window.startMin), min(start+HOUR, window.endMin))` so only samples inside the actual window are counted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n AreaCoverageStrips Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
✅ Deploy Preview for easyshifthq ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Updates to Preview Branch (feature/timeline-area-coverage) ↗︎
Tasks are run on every commit but only new migration files are pushed.
View logs for this Workflow Run ↗︎. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR redesigns the ShiftTimeline coverage panel: a new ChangesTimeline Coverage Panel Redesign
Estimated code review effort: 4 (Complex) | ~60 minutes Focus POS Integration Hardening
Estimated code review effort: 3 (Moderate) | ~30 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c179180529
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| -- any focus_orders row in the date range (e.g. voided checks deleted by | ||
| -- the sync handler). Must run before the per-check loop so the daily | ||
| -- re-aggregation reflects the correct final state. | ||
| DELETE FROM public.unified_sales us |
There was a problem hiding this comment.
Recalculate daily totals for delete-only syncs
When this orphan pass deletes unified_sales rows for a voided/removed Focus check, those rows are gone without leaving any row whose synced_at >= v_sync_start. The later aggregation in this same function still derives touched dates only from remaining/upserted rows with fresh synced_at (lines 277-284), so a business date where the only change is this delete keeps stale daily_sales/P&L totals that still include the deleted check. Track the deleted sale_dates (for example via RETURNING) or aggregate all processed dates in the requested range.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (6)
supabase/migrations/20260701130000_focus_transactions_unified_sales.sql (2)
103-117: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffCorrelated string-reconstruction subquery may be slow on full re-syncs.
When
p_start_date/p_end_dateare NULL (all dates), this correlatedNOT EXISTSscans everyfocusrow inunified_salesand rebuilds a concatenated string perfocus_orderscandidate, which cannot use an index onexternal_order_id. Under the function'sstatement_timeout = '120s', large restaurants doing a full re-aggregation could time out. Consider matchingfocus_ordersvia an indexable key (e.g., extractedfocus_check_id+business_date) rather than string concatenation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@supabase/migrations/20260701130000_focus_transactions_unified_sales.sql` around lines 103 - 117, The DELETE in the focus unified sales migration uses a correlated NOT EXISTS with string reconstruction, which is inefficient for full re-syncs. Update the logic in the focus cleanup path to match `focus_orders` using indexable columns such as `focus_check_id` and `business_date` instead of rebuilding `external_order_id`. Keep the surrounding `p_restaurant_id`, `v_store_id`, and date-range filters in place, but change the `NOT EXISTS` predicate so the planner can use an index and avoid per-row concatenation.
103-117: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAdd coverage for whole-check orphan cleanup
The pgTAP suite covers item-level orphan cleanup, but not the Step 0 path where a removedfocus_ordersrow should delete the matchingunified_salesrows for that check. Add a case that deletes a check and asserts those rows are removed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@supabase/migrations/20260701130000_focus_transactions_unified_sales.sql` around lines 103 - 117, Add a pgTAP test for the whole-check orphan cleanup path in the focus unified sales logic. Extend the existing coverage around the migration’s delete/orphan behavior so that removing a row from focus_orders causes the matching unified_sales rows for that same check to be deleted as well. Use the migration’s cleanup logic and identifiers like focus_orders and unified_sales to locate the relevant test area, and assert the expected rows are gone after deleting the check.Sources: Coding guidelines, Learnings
src/components/scheduling/ShiftTimeline/ShiftTimelineTab.tsx (1)
266-341: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCoverage panel wiring, alignment, and accessibility look correct.
The
pl-[120px]offset consistently alignsCoverageChart/CoverageStatusStrip/AreaCoverageStripswithTimelineAxis, and removingCoverageGapListis safe sinceCoverageStatusStripalready renders an equivalent sr-only understaffed list.One nit: the
120px offset is now duplicated across 5 spots in this file (three new: Lines 296, 301, 307; two pre-existing: Lines 314, 321). Consider extracting a shared constant (e.g.,LABEL_COLUMN_WIDTH_PX) alongsideMIN_PX_PER_HOURto avoid future drift if the label column width ever changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/scheduling/ShiftTimeline/ShiftTimelineTab.tsx` around lines 266 - 341, The 120px label-column offset is duplicated in several places in ShiftTimelineTab, which risks drift if the layout changes. Extract the repeated value into a shared constant near the existing sizing constants in ShiftTimelineTab (for example alongside MIN_PX_PER_HOUR), then replace the hardcoded pl-[120px] and left-[120px] usages in the CoverageChart, CoverageStatusStrip, AreaCoverageStrips, TimelineAxis, and NowIndicator layout blocks with that constant-driven value.src/components/scheduling/ShiftTimeline/AreaCoverageStrips.tsx (1)
46-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated per-hour cell markup with
CoverageStatusStrip.This cell block (
role="img", same className tokens, same two-span layout for label/count) closely mirrors the cell rendering inCoverageStatusStrip.tsx(lines 44-111 of that file). Consider extracting a sharedCoverageCellpresentational component (accepting label, value, and color-class props) to avoid maintaining two near-identical markup blocks in sync.♻️ Suggested shared cell component
+// src/components/scheduling/ShiftTimeline/CoverageCell.tsx +interface CoverageCellProps { + readonly label: string; + readonly value: string; + readonly ariaLabel: string; + readonly colorClassName?: string; +} + +export function CoverageCell({ label, value, ariaLabel, colorClassName }: CoverageCellProps) { + return ( + <div + role="img" + aria-label={ariaLabel} + title={ariaLabel} + className={cn( + 'flex min-w-0 flex-1 flex-col items-center gap-0.5 rounded py-1 px-0.5', + colorClassName ?? 'bg-muted/50 text-muted-foreground', + )} + > + <span className="text-[9px] font-medium leading-none">{label}</span> + <span className="text-[11px] leading-none tabular-nums">{value}</span> + </div> + ); +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/scheduling/ShiftTimeline/AreaCoverageStrips.tsx` around lines 46 - 68, The per-hour cell markup in the area coverage timeline duplicates the same presentational structure used by CoverageStatusStrip, so extract a shared CoverageCell component to keep both views in sync. Update AreaCoverageStrips and the matching CoverageStatusStrip cell rendering to use that shared component, passing the label, scheduled/value text, aria/title text, and any style class props needed while preserving the existing layout and accessibility attributes.tests/unit/coverageSummary.test.ts (1)
47-84: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd a regression for an area with no shifts.
The current
summarizeAreaCoveragecoverage still passes if an unscheduled brand disappears entirely. Locking in that zero-row case here would catch the bug above.♻️ Proposed test
+ it('CRITICAL: preserves areas with no shifts as zero-scheduled rows', () => { + const res = summarizeAreaCoverage( + [shiftFor('s1', 'a', '2026-07-11T15:00:00Z', '2026-07-11T18:00:00Z')], + [emp('a', 'Cold Stone'), emp('b', "Wetzel's")], + '2026-07-11', + 'America/Chicago', + win, + ); + + expect(res.map((r) => r.area)).toContain("Wetzel's"); + expect(res.find((r) => r.area === "Wetzel's")?.hours.every((h) => h.scheduled === 0)).toBe(true); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/coverageSummary.test.ts` around lines 47 - 84, Add a regression test in summarizeAreaCoverage for an area that has employees but no shifts in the window, and assert that the area still appears with zero scheduled coverage rather than disappearing. Reuse the existing summarizeAreaCoverage, emp, and shiftFor setup, but include at least one employee mapped to a brand/area with no matching shift and verify the returned rows contain that area with scheduled values of 0 across the relevant hours.supabase/functions/_shared/focusTestConnectionHandler.ts (1)
43-55: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winAdd a direct test for the sandbox URL fallback path. The handler only ever passes through the configured
sandboxBaseUrl, so the current test never exercises the exact-match fallback or the malformed-URL branch. ExportingisSafeBasewould make those cases testable.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@supabase/functions/_shared/focusTestConnectionHandler.ts` around lines 43 - 55, The sandbox URL fallback logic in isSafeBase is currently not directly testable from the handler path, so add tests that target the fallback and malformed-URL branches explicitly. Expose isSafeBase for unit testing (or otherwise make it accessible) and write a direct test that verifies a safe URL is accepted only when it exactly matches the sandboxBaseUrl hostname, plus a test that confirms malformed sandboxBaseUrl values return false.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/unit/focusTestConnectionHandler.test.ts`:
- Around line 196-210: The test name for handleTestConnection does not match
what it actually verifies, because it never creates a mismatched sandbox host
and only checks the normal 200 response path. Update the test in
focusTestConnectionHandler.test.ts to either target the real SSRF guard by
testing isSafeBase directly with a malformed or mismatched URL, or rename the
case so it clearly describes the production-fallback behavior when
sandboxBaseUrl is undefined. Use the existing handleTestConnection, makeDeps,
and focusApiBaseUrl flow to keep the assertion aligned with the scenario being
exercised.
---
Nitpick comments:
In `@src/components/scheduling/ShiftTimeline/AreaCoverageStrips.tsx`:
- Around line 46-68: The per-hour cell markup in the area coverage timeline
duplicates the same presentational structure used by CoverageStatusStrip, so
extract a shared CoverageCell component to keep both views in sync. Update
AreaCoverageStrips and the matching CoverageStatusStrip cell rendering to use
that shared component, passing the label, scheduled/value text, aria/title text,
and any style class props needed while preserving the existing layout and
accessibility attributes.
In `@src/components/scheduling/ShiftTimeline/ShiftTimelineTab.tsx`:
- Around line 266-341: The 120px label-column offset is duplicated in several
places in ShiftTimelineTab, which risks drift if the layout changes. Extract the
repeated value into a shared constant near the existing sizing constants in
ShiftTimelineTab (for example alongside MIN_PX_PER_HOUR), then replace the
hardcoded pl-[120px] and left-[120px] usages in the CoverageChart,
CoverageStatusStrip, AreaCoverageStrips, TimelineAxis, and NowIndicator layout
blocks with that constant-driven value.
In `@supabase/functions/_shared/focusTestConnectionHandler.ts`:
- Around line 43-55: The sandbox URL fallback logic in isSafeBase is currently
not directly testable from the handler path, so add tests that target the
fallback and malformed-URL branches explicitly. Expose isSafeBase for unit
testing (or otherwise make it accessible) and write a direct test that verifies
a safe URL is accepted only when it exactly matches the sandboxBaseUrl hostname,
plus a test that confirms malformed sandboxBaseUrl values return false.
In `@supabase/migrations/20260701130000_focus_transactions_unified_sales.sql`:
- Around line 103-117: The DELETE in the focus unified sales migration uses a
correlated NOT EXISTS with string reconstruction, which is inefficient for full
re-syncs. Update the logic in the focus cleanup path to match `focus_orders`
using indexable columns such as `focus_check_id` and `business_date` instead of
rebuilding `external_order_id`. Keep the surrounding `p_restaurant_id`,
`v_store_id`, and date-range filters in place, but change the `NOT EXISTS`
predicate so the planner can use an index and avoid per-row concatenation.
- Around line 103-117: Add a pgTAP test for the whole-check orphan cleanup path
in the focus unified sales logic. Extend the existing coverage around the
migration’s delete/orphan behavior so that removing a row from focus_orders
causes the matching unified_sales rows for that same check to be deleted as
well. Use the migration’s cleanup logic and identifiers like focus_orders and
unified_sales to locate the relevant test area, and assert the expected rows are
gone after deleting the check.
In `@tests/unit/coverageSummary.test.ts`:
- Around line 47-84: Add a regression test in summarizeAreaCoverage for an area
that has employees but no shifts in the window, and assert that the area still
appears with zero scheduled coverage rather than disappearing. Reuse the
existing summarizeAreaCoverage, emp, and shiftFor setup, but include at least
one employee mapped to a brand/area with no matching shift and verify the
returned rows contain that area with scheduled values of 0 across the relevant
hours.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5e885fb9-b4db-4f12-8f14-3eae2571a9a2
📒 Files selected for processing (28)
docs/superpowers/plans/2026-07-02-timeline-coverage-redesign.mddocs/superpowers/plans/2026-07-03-timeline-area-coverage.mddocs/superpowers/specs/2026-07-02-timeline-coverage-redesign-design.mddocs/superpowers/specs/2026-07-03-timeline-area-coverage-design.mdsrc/components/scheduling/ShiftTimeline/AreaCoverageStrips.tsxsrc/components/scheduling/ShiftTimeline/CoverageChart.tsxsrc/components/scheduling/ShiftTimeline/CoverageCurve.tsxsrc/components/scheduling/ShiftTimeline/CoverageDemandInfo.tsxsrc/components/scheduling/ShiftTimeline/CoverageGapList.tsxsrc/components/scheduling/ShiftTimeline/CoverageStatusStrip.tsxsrc/components/scheduling/ShiftTimeline/CoverageVerdict.tsxsrc/components/scheduling/ShiftTimeline/ShiftTimelineTab.tsxsrc/lib/coverageSummary.tssupabase/functions/_shared/focusLynkClient.tssupabase/functions/_shared/focusSaveConnectionHandler.tssupabase/functions/_shared/focusTestConnectionHandler.tssupabase/migrations/20260701130000_focus_transactions_unified_sales.sqlsupabase/migrations/20260701160000_focus_transactions_security.sqltests/unit/areaCoverageStrips.test.tsxtests/unit/coverageChart.test.tsxtests/unit/coverageDemandInfo.test.tsxtests/unit/coverageGapList.test.tsxtests/unit/coverageStatusStrip.test.tsxtests/unit/coverageSummary.test.tstests/unit/coverageVerdict.test.tsxtests/unit/focusTestConnectionHandler.test.tstests/unit/shiftTimelineTab.test.tsxtests/unit/timelineComponents.test.tsx
💤 Files with no reviewable changes (3)
- src/components/scheduling/ShiftTimeline/CoverageCurve.tsx
- tests/unit/coverageGapList.test.tsx
- src/components/scheduling/ShiftTimeline/CoverageGapList.tsx
…ation + test cleanups - SQL: Step 0 orphan DELETE now captures deleted sale_dates via RETURNING into v_deleted_dates; final aggregate_unified_sales_to_daily call UNIONs them in so days where the only change is a voided-check deletion are still re-aggregated (Codex P1 — stale daily_sales/P&L fix). - test: remove unused `within` import from areaCoverageStrips.test.tsx (github-code-quality bot nit — lint cleanliness). - test: rename misleading focusTestConnectionHandler test from "rejects a sandbox URL with a different host (SSRF guard)" to "falls back to production host when no sandboxBaseUrl is configured" — title now matches what it actually verifies (CodeRabbit major). - test: add coverageSummary regression documenting by-design behaviour that summarizeAreaCoverage omits areas with employees but no shifts (CodeRabbit trivial suggestion clarified). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Phase 9d triage complete (commit `677b988d`): Fixed (bug/correctness):
Fixed (refactor — misleading test title):
New test added:
Declined (nitpick/refactor — not bugs):
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…om area-coverage PR - Revert focus_transactions_security.sql (an already-applied, immutable migration) and focusTestConnectionHandler.test.ts to the base — Focus fixes belong in their own task, not this coverage PR. - Remove committed progress.md (ephemeral) and the 9d-triage artifact; gitignore progress.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review triage (commit 314f276)No code changes needed — all three comments are either stale or out of scope:
CI is green (incl. SonarCloud) and the PR is scoped to the coverage panel only. |
77bdbb4
into
feature/timeline-coverage-redesign
…sales ÷ SPLH (#574) * docs(scheduling): design + plan for coverage panel redesign Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(scheduling): pure hourly coverage summary + verdict Add summarizeCoverageHours and buildVerdict pure functions with full unit tests (5 specs covering min-aggregation, demand alignment, null-demand path, metAll, worst-hour selection). TZ-agnostic (minute-offset inputs). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scheduling): add CoverageVerdict component with 3-state verdict display Renders a status dot + sentence above the coverage chart: - No demand → neutral dot + prompt to add targets - All met → green dot + "Meeting demand all day." - Short → red dot + short-staffed count + worst-hour subline ("Biggest gap: 5 PM — short 3") Accepts an optional formatHour() override for testability. 5 tests cover all three states, headline, and subline presence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scheduling): CoverageChart — area + delta SVG views with shortfall wedges Implements CoverageChart.tsx (replaces CoverageCurve) and its test suite (12 tests, TDD red→green). Area view: proper viewBox (no preserveAspectRatio=none), y-axis gridlines + labels, stepped scheduled area, dashed needed line with direct end-label, red shortfall wedges between scheduled and needed (data-shortfall), worst-hour deficit label, and a legend row. Delta view: one diverging bar per hour (data-bar=short|covered|no-demand) from a zero baseline with signed labels; red for short, emerald for over. Both views carry role=img + <title>/<desc> for accessibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scheduling): per-hour status strip with aria-labels and sr-only gap list Adds CoverageStatusStrip — a row of one cell per hour colored green (covered) or red (short), each with an aria-label ("5 PM, short 2") so color is never the only cue. A visually-hidden <ul aria-label="Understaffed windows"> enumerates short hours for screen readers, folding in the former CoverageGapList guarantee. 6 unit tests cover: short labels, covered labels, no-demand, empty array → null, and no understaffed list when fully covered. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scheduling): redesign coverage panel — verdict, shortfall chart, +/- bars, status strip Wire CoverageVerdict, CoverageChart (area + delta views), and CoverageStatusStrip into ShiftTimelineTab, driven by a single hourly summarizeCoverageHours memo. Adds a Chart|+/- bars view toggle (ToggleGroup). Removes CoverageCurve and CoverageGapList (gap accessibility is now folded into CoverageStatusStrip's sr-only list). Deletes coverageGapList.test.tsx; new wiring tests added to shiftTimelineTab.test.tsx. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update progress.md — task 5 completed (f4ecb77) * fix(ui): replace emerald-* literals with semantic success token CoverageVerdict, CoverageStatusStrip, and CoverageChart used bg-emerald-500 / fill-emerald-500 / text-emerald-700 direct color literals in violation of the no-direct-colors rule. Replaced with bg-success / fill-success / text-success which resolves to the same hue via --success CSS variable and respects dark-mode theming. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: record Phase 5 UI review completion in progress.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(scheduling): deduplicate hour formatter and x-axis labels in coverage panel - Extract `formatCoverageHour` into `coverageSummary.ts` as a single canonical export, replacing three near-identical copies of `defaultFormatHour`/ `formatHourLabel` scattered across CoverageVerdict, CoverageStatusStrip, and CoverageChart. The chart variant also had a subtle inconsistency (no space before AM/PM) which is now resolved. - Lift the x-axis hour-label JSX block out of both branches of `Axes` into a shared `xAxisLabels` constant — the block was copy-pasted verbatim for area and delta views. - Simplify `computePeak` from a manual `for` loop with two `if` branches to a one-liner `reduce` + `Math.max`. All 40 tests still pass; typecheck and lint are clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update progress.md for Phase 6 (Simplify) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(review): address all major findings from Phase 7b multi-reviewer pass - test: remove deleted CoverageCurve import/tests from timelineComponents.test.tsx (codex/major — was breaking CI on module resolution) - fix(coverage): demand fallback ?? null instead of ?? 0 so off-peak hours are treated as no-target rather than silently covered (sound-logic/major) - fix(delta-view): DeltaView and Axes use deltaPeak (max abs delta) for bar and axis scale rather than headcount peak; bars now fill the chart correctly when scheduled values far exceed deltas (sound-logic/major) - fix(delta-view): delta===0 renders a 2px success tick at zero-line so "exactly met" is visually distinct from "no bar" (sound-logic/major) - fix(delta-view): cap barH and clamp labelY so text never clips viewBox - fix(style): replace 3 nested ternaries with if/else blocks (ocr-rules/major) - fix(style): replace constant inline style on dashed legend line with Tailwind arbitrary-value classes (ocr-rules/minor) - fix(key): use stable startMin as React key for x-axis SVG text (ocr-rules/minor) Tests: 58/58 passing; typecheck clean Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): address all major findings from Phase 7c CodeRabbit pass Fixes 10 actionable findings from the CodeRabbit review: Security / SSRF: - focusTestConnectionHandler: widen isSafeBase() to also allowlist the operator-configured sandbox host (FOCUS_API_SANDBOX_URL); previously, any non-focuspos.com sandbox URL caused the SSRF check to reject the request even though the host comes from a trusted env var. - focusTestConnectionHandler: wrap decrypt/focusApiBaseUrl/btoa in try/catch and validate environment value; unhandled throws bypassed writeStatus and left connection_status stale. Data integrity: - SQL migrations (unified_sales): add Step 0 check-level orphan DELETE before the per-check loop, so unified_sales rows for checks deleted from focus_orders (voided checks) are cleaned up. Applies to both 20260701130000 and 20260701160000 (re-create migrations). - CoverageChart: clamp no-demand bar height to halfH - 2 in delta view; without clamp, a large scheduled headcount + small deltaPeak caused the neutral bar to overflow the plot viewBox. Reliability: - focusTransactionSyncHandler: isolate per-check upsert errors with try/catch so a single malformed check no longer aborts the entire day's sync and prevents the unified_sales RPC from running. - focusLynkClient: guard both syncRes.text() and blobRes.text() reads in try/catch to honour the "never throws" JSDoc contract; a timeout mid-download caused an uncaught AbortError. Type safety: - focusSaveConnectionHandler: add optional columns param to ServiceClient.select() interface (TS2554 compile error). Comments / housekeeping: - SQL migration comments: align "last 2 business days" to "last 3 business days" (CURRENT_DATE - interval '3 days' is the actual value). - Cron comment: correct */6 to 0 */6 * * * (every 6 hours, not minutes). - Test descriptions: prefix PII-skip and financial tests with CRITICAL:. - Untrack progress.md (already in .gitignore; leaked local file paths). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(review): address CodeRabbit findings on coverage panel - CoverageChart delta view: scale no-demand bars by headcount peak, not deltaPeak (which collapsed to 1 and pegged every bar to max height). (Major) - CoverageChart: drop internal L/R plot margins so hour columns align with the TimelineAxis ticks and shift bars; y-labels move inside, 'Needed' named in legend. (Minor) - CoverageStatusStrip: add role=img so each cell's aria-label is honored. (Minor) - coverageSummary.test.ts: CRITICAL: prefix on core-logic tests per path instructions. (Major) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(scheduling): design + plan for per-area coverage + demand explainer Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(scheduling): summarizeAreaCoverage — per-area scheduled coverage Adds `summarizeAreaCoverage` pure helper to `src/lib/coverageSummary.ts` that groups shifts by employee area, calls `computeDayCoverage` per area, and returns per-area `CoverageHour[]` with demand=null (scheduled-only). Adds `AreaCoverage` export type. Unassigned employees bucketed under the `UNASSIGNED_LABEL` key, areas sorted alphabetically with Unassigned last. Companion tests in `tests/unit/coverageSummary.test.ts` exercise area grouping, the Unassigned bucket, and the empty-shifts early-exit; marked CRITICAL as required by the plan. Tests pass under TZ=UTC and TZ=Asia/Tokyo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scheduling): CoverageDemandInfo — demand explainer popover component Adds CoverageDemandInfo.tsx: a shadcn Popover trigger that explains how the "needed" staffing count is derived (projected sales ÷ SPLH, capped at minimum crew). Includes a link to /settings for adjusting targets. Fully keyboard-accessible (focusable trigger, Escape closes popover). Five unit tests cover: trigger aria-label, formula text, settings link href, Covered/Short vocabulary, and Escape-key dismissal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scheduling): AreaCoverageStrips component — per-area scheduled headcount strips Adds AreaCoverageStrips.tsx that renders one compact row per area (area label + per-hour scheduled headcount cells) when the Timeline is grouped by Area. Cells are neutral (no red/green) since per-area demand is not yet available. A footnote states demand targets are whole-location. Returns null when areas=[]. Paired with 6 unit tests covering: empty guard, area name labels, CRITICAL role="img" aria-labels (area + hour + count), visible headcount text, the demand footnote, and the Unassigned bucket label. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scheduling): CoverageStatusStrip shows have/needed fraction in cells Cells now display `scheduled/needed` (e.g. `3/5`) instead of `✓` / `−N` so managers can see the exact comparison at a glance without inferring from color alone. No-demand cells fall back to the bare scheduled count. aria-labels updated to expose the fraction ("3 of 5, short 2") for screen readers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scheduling): per-area coverage strips + demand explainer in Timeline Wire CoverageDemandInfo + AreaCoverageStrips into ShiftTimelineTab: - Add CoverageDemandInfo popover to the coverage panel header (next to verdict) - Compute areaCoverage via useMemo + summarizeAreaCoverage; active only when groupBy === 'area' - Render AreaCoverageStrips (pl-[120px] aligned) below CoverageStatusStrip when grouped by area - 4 new CRITICAL tests: demand trigger, per-area rows, strips hidden on position groupBy, footnote Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style(timeline): UI review fixes — import order + typography consistency - CoverageDemandInfo: move Lucide icon import after shadcn imports (matches CLAUDE.md import order: shadcn #2, icons #3) - CoverageStatusStrip: align cell typography with AreaCoverageStrips (hour label text-[9px], value text-[11px]); widen cell gap to gap-[3px] for visual consistency across both strip components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(coverage): remove dead variable, deduplicate deltaPeak computation, simplify label lookup - DeltaView now receives `deltaPeak` as a prop from CoverageChart (already computed there for Axes) instead of recomputing it from the same `hours` array — eliminates the duplicate Math.max + spread. - Remove dead `isOver` variable in DeltaView; the zero-delta branch is now expressed as the clearer `h.delta === 0` guard. - Drop redundant `as number` casts in buildVerdict's worst-finding loop (shortHours is already filtered to delta < 0, so non-null is guaranteed). - CoverageStatusStrip builds a `labelByStartMin` Map once so formatHour is called a single time per hour instead of twice (once for the visual strip, once for the sr-only list). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(review): address critical/major multi-reviewer findings - coverageSummary: emit scheduled=0 hours when demand is configured but no shifts are scheduled, so buildVerdict reports hasDemand:true and the full shortfall count instead of silently hiding fully-unstaffed periods - ShiftTimelineTab: remove px-4 from coverage panel wrapper so pl-[120px] chart/strip children align with TimelineAxis ticks and shift lanes below - CoverageDemandInfo: replace <a href> with <Link to> (React Router 6 SPA navigation — prevents full-page reload); wrap test in MemoryRouter - CoverageStatusStrip: guard hasDemand on both delta !== null AND needed !== null to prevent h.needed! assertion on inconsistent upstream state - CoverageChart: wrap in React.memo to skip O(H) SVG path recomputes on setActiveShift re-renders; clamp surplus label above MARGIN_TOP+8 to prevent clipping above SVG viewBox on max-height positive bars - coverageSummary.test: add regression tests for zero-coverage-with-demand fix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(coverage): clamp hour-bucket filter to visible window bounds When the day window is not hour-aligned, `firstHourStart` can start before `window.startMin`, causing coverage samples outside the visible range to bleed into the first bucket's scheduled count. Clamp the filter to `[max(start, window.startMin), min(start+HOUR, window.endMin))` so only samples inside the actual window are counted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(coverage): correct footnote copy from 'per-brand' to 'per-area' in AreaCoverageStrips Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(review): triage PR #569 comments — Codex P1 delete-date re-aggregation + test cleanups - SQL: Step 0 orphan DELETE now captures deleted sale_dates via RETURNING into v_deleted_dates; final aggregate_unified_sales_to_daily call UNIONs them in so days where the only change is a voided-check deletion are still re-aggregated (Codex P1 — stale daily_sales/P&L fix). - test: remove unused `within` import from areaCoverageStrips.test.tsx (github-code-quality bot nit — lint cleanliness). - test: rename misleading focusTestConnectionHandler test from "rejects a sandbox URL with a different host (SSRF guard)" to "falls back to production host when no sandboxBaseUrl is configured" — title now matches what it actually verifies (CodeRabbit major). - test: add coverageSummary regression documenting by-design behaviour that summarizeAreaCoverage omits areas with employees but no shifts (CodeRabbit trivial suggestion clarified). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(9d): write triage artifact + update progress for PR #569 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(scheduling): drop out-of-scope Focus edits + ephemeral files from area-coverage PR - Revert focus_transactions_security.sql (an already-applied, immutable migration) and focusTestConnectionHandler.test.ts to the base — Focus fixes belong in their own task, not this coverage PR. - Remove committed progress.md (ephemeral) and the 9d-triage artifact; gitignore progress.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(scheduling): design + plan — chart grid alignment, hover insight, #569 recovery Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(scheduling): thread projectedSales/laborPct into CoverageHour Extend CoverageHour interface with projectedSales and laborPct (null when no recommendation available). summarizeCoverageHours gains an optional recs parameter; when provided each hour entry is enriched from the matching HourlyStaffingRecommendation by clock hour. Backward compatible — all existing callers unchanged. TDD: 2 CRITICAL tests added (red→green); 11 prior tests remain green; coverageChart, areaCoverageStrips, coverageStatusStrip suites unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(scheduling): update CoverageHour fixtures to include projectedSales/laborPct All object-literal CoverageHour fixtures in coverageChart, areaCoverageStrips, and coverageStatusStrip test suites now carry the two fields added in task 1a (projectedSales: null, laborPct: null). Keeps fixture shapes in sync with the interface so future strict-type test runners don't flag missing properties. All 60 coverage-related tests pass; typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scheduling): grid-aligned HTML column CoverageChart (task 2a) Replace fixed-viewBox SVG CoverageChart with per-hour absolutely-positioned HTML columns using the shared minToPct scale (same as TimelineBar/TimelineAxis). Columns align exactly with the hour grid at every viewport width, including horizontal scroll. Update test selectors from SVG path checks to data-hour-col / data-shortfall / data-bar HTML attribute selectors; add minToPct positioning assertions confirming 0% / 25% left/width for a 10:00-14:00 window. All 59 coverage-related tests green; typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update progress.md for task 2a completion * feat(scheduling): tooltip shell + ShiftTimelineTab wiring for coverage chart (task 2b) Add keyboard-focusable columns (tabIndex=0, aria-label) and shadcn Tooltip shell (TooltipProvider + Tooltip/TooltipTrigger/TooltipContent) to each hour column in CoverageChart. Wire ShiftTimelineTab to pass minToPct and targetSplh to CoverageChart and thread dayRecommendations into summarizeCoverageHours so projectedSales/laborPct flow through to the chart. All 61 coverage-related tests green; typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update progress.md for task 2b completion * fix(scheduling): forwardRef on AreaColumn/DeltaColumn for correct TooltipTrigger asChild wiring Radix UI's Slot (used by TooltipTrigger asChild) must be able to forward a ref to the child element. AreaColumn and DeltaColumn were plain function components, causing React to warn "Function components cannot be given refs". Convert both to forwardRef so the tooltip trigger can wire the ref properly and the warning is silenced. Add two RED→GREEN tests in coverageChart.test.tsx (accessibility suite) that spy on console.error and assert no forwardRef warning fires for either view — ensuring this regression cannot return. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update progress.md for task 2c completion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(scheduling): add call-site wiring tests for ShiftTimelineTab (Task 2d) Adds three tests in a new 'call-site wiring' describe block to verify that ShiftTimelineTab correctly threads dayRecommendations, activeSettings target_splh, and minToPct to the appropriate downstream functions: - Confirms useWeekStaffingSuggestions is called with restaurantId + weekDays - Confirms CoverageChart columns render (data-hour-col) when recommendations exist - Confirms columns carry style.left (minToPct wired) Refactors the vi.mock to use a named mockFn variable so individual tests can override the return value via mockReturnValue, enabling per-test fixture injection without duplicating the full module mock. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update progress.md for Task 2d completion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update progress.md for Task 2e verification (chart+tab suites all green) All 66 coverage-related tests pass; typecheck clean; no lint errors in modified files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scheduling): per-hour tooltip — scheduled/needed + projected sales ÷ SPLH Implements Task 3a (TDD RED→GREEN): - Exports `buildHourTooltip(h, targetSplh)` pure helper that returns tooltip lines: time range, scheduled/needed counts, projected sales (when rec present), ÷ SPLH math (when targetSplh and sales), verdict (Short N/Covered/Right on target/ No demand target). - Updates `buildColumnAriaLabel` to join all lines so keyboard users get the same information as hover users. - Wires `buildHourTooltip` into `<TooltipContent>` in both area and delta views, replacing the bare ariaLabel string with a multi-line <p> layout. - Sets `delayDuration={0}` on `TooltipProvider` for instant tooltip open. - Adds 13 new tests: 10 `buildHourTooltip` unit tests (CRITICAL-prefixed for all branches: short, covered, spare, right-on-target, no-demand, sales omitted, SPLH omitted) + 2 aria-label integration tests + 1 full TooltipContent wiring contract test; all 28 coverageChart tests pass, 79 total across all 6 coverage suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update progress.md for Task 3a completion * chore: update progress.md for Task 3b completion Task 3b (implement buildHourTooltip and wire TooltipContent) was completed as part of the Task 3a RED→GREEN cycle in commit a2e6504. This progress entry documents the implementation details and confirms all 79 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update progress.md for Task 3c completion (tooltip test+lint verification) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update progress.md for Task 4a full verification All verification checks pass: - typecheck clean, lint 0 errors on branch-modified files - 79 coverage-related tests pass under TZ=UTC - 5374 total vitest tests pass (5 focus* files fail due to pre-existing missing fast-xml-parser on main — not introduced by this branch) - recovered #569 suites (areaCoverageStrips 6, coverageDemandInfo 5, coverageStatusStrip 8, shiftTimelineTab 19) all pass - production build succeeds in 50.65s Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * simplify(coverage-chart): unify duplicate map branches and remove trivial aliases Three targeted cleanups with no behavior change: - CoverageChart: merge identical area/delta `hours.map()` branches into one, switching only the inner column component — removes ~35 lines of duplicated tooltip + aria-label logic. - CoverageChart: remove `buildColumnAriaLabel` one-liner wrapper (was just `buildHourTooltip(h, t).join(', ')`); inline at the single call site. - CoverageChart: remove `shortfallBottomPct` alias for `scheduledPct` in AreaColumn — the two were always equal. - coverageSummary: replace ternary `rec ? rec.x : null` with optional-chaining `rec?.x ?? null` for idiomatic null propagation. All 79 coverage-related tests pass; typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update progress.md for Phase 6 simplify Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): timeline chart — addresses codex + sound-logic + ocr-rules findings - ShiftTimelineTab: remove lanes===0 early-return that suppressed the coverage panel on fully-unstaffed days; empty-shift message is now rendered inline below the coverage panel so demand shortfalls are visible even when no lanes exist (codex finding, severity=major) - buildHourTooltip: guard targetSplh > 0 before dividing projectedSales to prevent 'Infinity needed' appearing in tooltip when target is zero (sound-logic finding, severity=minor) - DeltaColumn: extract shared outer wrapper div out of three early-return branches into a single render path, switching only innerContent; eliminates ~20 lines of duplicated ref/data-hour-col/tabIndex/aria-label/className/style markup (ocr-rules finding, severity=major) All 79 coverage-related tests pass; typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(progress): record phase 7b findings and fixes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(verify): phase 8 — all checks pass (unit 5374, e2e 23, typecheck clean, build green) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: record Phase 9a — PR #574 opened * chore(ci): record Phase 9b CI green — all checks pass on PR #574 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(timeline): address Codex + github-code-quality review comments - Remove unused `screen` import from coverageChart.test.tsx (#1 github-code-quality) - Fix delta bar height: barPct was applied inside a flex-1 half-column, so max bars only reached 24% of chart height; switch to absolute positioning so barPct is relative to the full column height as intended (#4 Codex P2) - Fix SPLH tooltip contradiction: tooltip line 4 recomputed needed via Math.round while line 2 showed h.needed (Math.ceil + minimum crew); now both lines use h.needed to stay consistent (#2 Codex P2) - Also fix no-demand bar in delta view — same absolute-height issue Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(9d): update progress.md for Phase 9d review-comment triage Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: untrack ephemeral progress.md from PR; record stacked-PR-loss lesson Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
summarizeAreaCoveragepure helper that groups scheduled headcount perareausing existingcomputeDayCoverage+summarizeCoverageHours(no new data fetching, no per-area demand)CoverageDemandInfopopover explaining how "needed" staff is calculated (SPLH formula, Covered / Short vocabulary, link to staffing settings)AreaCoverageStripscomponent: per-area scheduled headcount row for each brand/area, neutral cells (no red/green since no per-area demand), with footnote noting per-area targets are a follow-upCoverageStatusStripcells to displayhave/neededfractions (e.g.3/5) when demand is present, scheduled-only count when notShiftTimelineTab:CoverageDemandInfoin header,AreaCoverageStripsbelow aggregate strip whengroupBy === 'area'px-4misalignment on coverage panel that shifted chart/strips 16px from axis ticksCoverageDemandInfoSPA navigation (<a href>→<Link to>)Test plan
npm run test -- tests/unit/coverageSummary.test.ts— 10 tests pass (area grouping, Unassigned bucket, empty shifts, zero-coverage-with-demand, window clamping)npm run test -- tests/unit/areaCoverageStrips.test.tsx— 6 tests pass (null guard, area labels, role="img" aria-labels, headcount text, demand footnote, Unassigned bucket)npm run test -- tests/unit/coverageDemandInfo.test.tsx— 5 tests pass (trigger aria-label, formula text, settings link href, Covered/Short vocabulary, Escape dismissal)npm run test -- tests/unit/coverageStatusStrip.test.tsx— 8 tests pass (have/needed fraction, N-of-M aria-label, short/covered/no-demand states)npm run test -- tests/unit/shiftTimelineTab.test.tsx— 16 tests pass (demand trigger present, per-area group rows, strips hidden when groupBy=position, footnote text)npm run test— 5324 pass, 0 new failuresnpm run typecheck— exit 0npm run lint— 0 errors in branch-changed filesnpm run build— exit 0groupBy=areamode; verify per-area strips appear below aggregate strip aligned with axis ticks; open demand popover to confirm formula textDesign doc
docs/superpowers/specs/2026-07-03-timeline-area-coverage-design.md🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation