Skip to content

feat(scheduling): per-area coverage strips + demand explainer in Timeline - #569

Merged
jdelgado2002 merged 14 commits into
feature/timeline-coverage-redesignfrom
feature/timeline-area-coverage
Jul 3, 2026
Merged

feat(scheduling): per-area coverage strips + demand explainer in Timeline#569
jdelgado2002 merged 14 commits into
feature/timeline-coverage-redesignfrom
feature/timeline-area-coverage

Conversation

@jdelgado2002

@jdelgado2002 jdelgado2002 commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Add summarizeAreaCoverage pure helper that groups scheduled headcount per area using existing computeDayCoverage + summarizeCoverageHours (no new data fetching, no per-area demand)
  • Add CoverageDemandInfo popover explaining how "needed" staff is calculated (SPLH formula, Covered / Short vocabulary, link to staffing settings)
  • Add AreaCoverageStrips component: 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-up
  • Update CoverageStatusStrip cells to display have/needed fractions (e.g. 3/5) when demand is present, scheduled-only count when not
  • Wire all pieces into ShiftTimelineTab: CoverageDemandInfo in header, AreaCoverageStrips below aggregate strip when groupBy === 'area'
  • Fix zero-coverage-with-demand early-return bug (fully-unstaffed periods were hidden)
  • Fix px-4 misalignment on coverage panel that shifted chart/strips 16px from axis ticks
  • Fix CoverageDemandInfo SPA navigation (<a href><Link to>)
  • Clamp hour-bucket filter to visible window bounds for defensive correctness

Test plan

  • Unit: npm run test -- tests/unit/coverageSummary.test.ts — 10 tests pass (area grouping, Unassigned bucket, empty shifts, zero-coverage-with-demand, window clamping)
  • Unit: 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)
  • Unit: npm run test -- tests/unit/coverageDemandInfo.test.tsx — 5 tests pass (trigger aria-label, formula text, settings link href, Covered/Short vocabulary, Escape dismissal)
  • Unit: npm run test -- tests/unit/coverageStatusStrip.test.tsx — 8 tests pass (have/needed fraction, N-of-M aria-label, short/covered/no-demand states)
  • Unit: 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)
  • Full suite: npm run test — 5324 pass, 0 new failures
  • TypeScript: npm run typecheck — exit 0
  • Lint: npm run lint — 0 errors in branch-changed files
  • Build: npm run build — exit 0
  • E2E: scheduling-related tests (shift-planner, shift-template-areas, schedule-group-by-area) — 14/14 pass
  • Visual: open Timeline in groupBy=area mode; verify per-area strips appear below aggregate strip aligned with axis ticks; open demand popover to confirm formula text

Design doc

docs/superpowers/specs/2026-07-03-timeline-area-coverage-design.md

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Redesigned the timeline coverage view with a clearer status summary, improved chart display, and an hour-by-hour coverage strip.
    • Added an info popover explaining how staffing targets are calculated.
    • Added per-area coverage strips when viewing the timeline by area.
  • Bug Fixes

    • Improved handling of missing or short coverage so fully unstaffed hours are shown correctly.
    • Made coverage indicators more accessible with clearer labels and screen-reader support.
    • Strengthened error handling for data sync and connection checks.
  • Documentation

    • Added design and implementation plans for the coverage redesign and area-based coverage updates.

jdelgado2002 and others added 11 commits July 3, 2026 00:10
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>
@netlify

netlify Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploy Preview for easyshifthq ready!

Name Link
🔨 Latest commit e5b5265
🔍 Latest deploy log https://app.netlify.com/projects/easyshifthq/deploys/6a476c27a25f0d0008bb97b0
😎 Deploy Preview https://deploy-preview-569--easyshifthq.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 27
Accessibility: 98
Best Practices: 92
SEO: 98
PWA: 90
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
easyshifthq Ready Ready Preview, Comment Jul 3, 2026 8:11am

@supabase

supabase Bot commented Jul 3, 2026

Copy link
Copy Markdown

Updates to Preview Branch (feature/timeline-area-coverage) ↗︎

Deployments Status Updated
Database Fri, 03 Jul 2026 08:11:16 UTC
Services Fri, 03 Jul 2026 08:11:16 UTC
APIs Fri, 03 Jul 2026 08:11:16 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Fri, 03 Jul 2026 08:11:16 UTC
Migrations Fri, 03 Jul 2026 08:11:16 UTC
Seeding Fri, 03 Jul 2026 08:11:16 UTC
Edge Functions Fri, 03 Jul 2026 08:11:47 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6a3d8c4-6c25-44ab-bd1d-cfbff96f7fe9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR redesigns the ShiftTimeline coverage panel: a new coverageSummary library provides hourly summarization, verdict, and per-area coverage helpers, feeding new CoverageVerdict, CoverageChart, CoverageStatusStrip, CoverageDemandInfo, and AreaCoverageStrips components, replacing CoverageCurve/CoverageGapList. Separately, Focus POS integration code adds SSRF sandbox handling, resilient body reads, a type signature fix, and SQL orphan-cleanup migrations.

Changes

Timeline Coverage Panel Redesign

Layer / File(s) Summary
Design docs and plans
docs/superpowers/plans/*, docs/superpowers/specs/*
Adds implementation plans and design specs for the coverage redesign and per-area demand legibility work.
Coverage summary and verdict logic
src/lib/coverageSummary.ts, tests/unit/coverageSummary.test.ts
Adds CoverageHour/CoverageVerdict/AreaCoverage types with summarizeCoverageHours, buildVerdict, and summarizeAreaCoverage, and tests aggregation/verdict/area-grouping behavior.
CoverageVerdict component
src/components/scheduling/ShiftTimeline/CoverageVerdict.tsx, tests/unit/coverageVerdict.test.tsx
Renders plain-language no-demand/met-all/short-staffed messages with a biggest-gap subline.
CoverageChart component
src/components/scheduling/ShiftTimeline/CoverageChart.tsx, tests/unit/coverageChart.test.tsx
Renders area and delta SVG views with shared axes, legend, and accessibility text.
CoverageStatusStrip component
src/components/scheduling/ShiftTimeline/CoverageStatusStrip.tsx, tests/unit/coverageStatusStrip.test.tsx
Renders per-hour have/needed cells with ARIA labeling and a hidden understaffed-windows list.
CoverageDemandInfo and AreaCoverageStrips
src/components/scheduling/ShiftTimeline/CoverageDemandInfo.tsx, src/components/scheduling/ShiftTimeline/AreaCoverageStrips.tsx, tests/unit/coverageDemandInfo.test.tsx, tests/unit/areaCoverageStrips.test.tsx
Adds a demand-explainer popover and per-area scheduled headcount strips.
ShiftTimelineTab wiring and old component removal
src/components/scheduling/ShiftTimeline/ShiftTimelineTab.tsx, src/components/scheduling/ShiftTimeline/CoverageCurve.tsx, src/components/scheduling/ShiftTimeline/CoverageGapList.tsx, tests/unit/shiftTimelineTab.test.tsx, tests/unit/timelineComponents.test.tsx, tests/unit/coverageGapList.test.tsx
Wires new components/state into the panel, removes CoverageCurve/CoverageGapList and their tests, and updates related tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Focus POS Integration Hardening

Layer / File(s) Summary
Sandbox SSRF base-URL handling
supabase/functions/_shared/focusTestConnectionHandler.ts, tests/unit/focusTestConnectionHandler.test.ts
Extends isSafeBase for operator-provided sandbox hosts and wraps secret decryption/base-URL prep in try/catch with persisted error status.
Resilient body reads and ServiceClient signature
supabase/functions/_shared/focusLynkClient.ts, supabase/functions/_shared/focusSaveConnectionHandler.ts
Wraps response .text() reads in try/catch to return network errors instead of throwing, and broadens ServiceClient.select() to accept columns.
Orphaned unified_sales cleanup migrations
supabase/migrations/20260701130000_focus_transactions_unified_sales.sql, supabase/migrations/20260701160000_focus_transactions_security.sql
Adds Step 0 deletion of orphaned unified_sales rows and extends the incremental lookback window to 3 business days.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

  • toyiyo/nimble-pnl#561: Introduces the CoverageCurve/CoverageGapList components and ShiftTimelineTab wiring that this PR removes and replaces.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main Timeline scheduling changes: per-area coverage strips and the demand explainer.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/timeline-area-coverage

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread tests/unit/areaCoverageStrips.test.tsx Fixed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
supabase/migrations/20260701130000_focus_transactions_unified_sales.sql (2)

103-117: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Correlated string-reconstruction subquery may be slow on full re-syncs.

When p_start_date/p_end_date are NULL (all dates), this correlated NOT EXISTS scans every focus row in unified_sales and rebuilds a concatenated string per focus_orders candidate, which cannot use an index on external_order_id. Under the function's statement_timeout = '120s', large restaurants doing a full re-aggregation could time out. Consider matching focus_orders via an indexable key (e.g., extracted focus_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 win

Add coverage for whole-check orphan cleanup
The pgTAP suite covers item-level orphan cleanup, but not the Step 0 path where a removed focus_orders row should delete the matching unified_sales rows 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 value

Coverage panel wiring, alignment, and accessibility look correct.

The pl-[120px] offset consistently aligns CoverageChart/CoverageStatusStrip/AreaCoverageStrips with TimelineAxis, and removing CoverageGapList is safe since CoverageStatusStrip already 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) alongside MIN_PX_PER_HOUR to 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 win

Duplicated 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 in CoverageStatusStrip.tsx (lines 44-111 of that file). Consider extracting a shared CoverageCell presentational 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 win

Add a regression for an area with no shifts.

The current summarizeAreaCoverage coverage 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 win

Add 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. Exporting isSafeBase would 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

📥 Commits

Reviewing files that changed from the base of the PR and between 24df25e and c179180.

📒 Files selected for processing (28)
  • docs/superpowers/plans/2026-07-02-timeline-coverage-redesign.md
  • docs/superpowers/plans/2026-07-03-timeline-area-coverage.md
  • docs/superpowers/specs/2026-07-02-timeline-coverage-redesign-design.md
  • docs/superpowers/specs/2026-07-03-timeline-area-coverage-design.md
  • src/components/scheduling/ShiftTimeline/AreaCoverageStrips.tsx
  • src/components/scheduling/ShiftTimeline/CoverageChart.tsx
  • src/components/scheduling/ShiftTimeline/CoverageCurve.tsx
  • src/components/scheduling/ShiftTimeline/CoverageDemandInfo.tsx
  • src/components/scheduling/ShiftTimeline/CoverageGapList.tsx
  • src/components/scheduling/ShiftTimeline/CoverageStatusStrip.tsx
  • src/components/scheduling/ShiftTimeline/CoverageVerdict.tsx
  • src/components/scheduling/ShiftTimeline/ShiftTimelineTab.tsx
  • src/lib/coverageSummary.ts
  • supabase/functions/_shared/focusLynkClient.ts
  • supabase/functions/_shared/focusSaveConnectionHandler.ts
  • supabase/functions/_shared/focusTestConnectionHandler.ts
  • supabase/migrations/20260701130000_focus_transactions_unified_sales.sql
  • supabase/migrations/20260701160000_focus_transactions_security.sql
  • tests/unit/areaCoverageStrips.test.tsx
  • tests/unit/coverageChart.test.tsx
  • tests/unit/coverageDemandInfo.test.tsx
  • tests/unit/coverageGapList.test.tsx
  • tests/unit/coverageStatusStrip.test.tsx
  • tests/unit/coverageSummary.test.ts
  • tests/unit/coverageVerdict.test.tsx
  • tests/unit/focusTestConnectionHandler.test.ts
  • tests/unit/shiftTimelineTab.test.tsx
  • tests/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

Comment thread tests/unit/focusTestConnectionHandler.test.ts
…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>
@jdelgado2002

Copy link
Copy Markdown
Member Author

Phase 9d triage complete (commit `677b988d`):

Fixed (bug/correctness):

  • Codex P1 (20260701160000_focus_transactions_security.sql): Step 0 orphan DELETE now captures deleted sale_dates via RETURNING and UNIONs them into the final aggregate_unified_sales_to_daily call — fixing the stale daily_sales/P&L issue for delete-only sync days.
  • github-code-quality: Removed unused within import from areaCoverageStrips.test.tsx.

Fixed (refactor — misleading test title):

  • CodeRabbit [major] (focusTestConnectionHandler.test.ts:196): Renamed test from "rejects a sandbox URL with a different host (SSRF guard)" to "falls back to the production focuspos.com host when no sandboxBaseUrl is configured". The test now accurately describes what it verifies.

New test added:

  • CodeRabbit suggestion (coverageSummary.test.ts): Added regression documenting by-design behaviour that summarizeAreaCoverage omits areas with employees but no shifts in the window. The test title clarifies the intent and deferred zero-shift enhancement.

Declined (nitpick/refactor — not bugs):

  • CodeRabbit nit (ShiftTimelineTab.tsx:266-341): Extract 120px offset into a constant — low value, trivial drift risk, 5 occurrences is not a maintenance problem.
  • CodeRabbit nit (AreaCoverageStrips.tsx:46-68): Extract shared CoverageCell component — valid DRY suggestion; deferred to avoid a mid-PR refactor introducing new component/test scope.
  • CodeRabbit nit (20260701130000_focus_transactions_unified_sales.sql:103-117): Correlated string-reconstruction subquery performance — the function runs within a 120 s timeout; full-date-range syncs are rare; deferred.
  • CodeRabbit nit (20260701130000_focus_transactions_unified_sales.sql:103-117): Add pgTAP test for whole-check orphan cleanup — valid coverage gap; deferred as a follow-up test PR.
  • CodeRabbit trivial (focusTestConnectionHandler.ts:43-55): Export isSafeBase for direct unit testing — refactor out of scope for this PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jdelgado2002
jdelgado2002 changed the base branch from main to feature/timeline-coverage-redesign July 3, 2026 08:07
…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>
@jdelgado2002

Copy link
Copy Markdown
Member Author

Review triage (commit 314f276)

No code changes needed — all three comments are either stale or out of scope:

  • Unused within import (github-code-quality, areaCoverageStrips.test.tsx) — already removed; the file imports only render, screen and npx eslint on it is clean. Stale comment.
  • Recalculate daily totals for delete-only sync (Codex P1, focus_transactions_security.sql) and SSRF test title mismatch (CodeRabbit Major, focusTestConnectionHandler.test.ts) — these are Focus POS files, not this PR's code. I reverted both to the base branch (the migration is already applied and immutable), so this PR makes zero changes to them. They're genuine issues in the merged Focus code and are tracked in a separate Focus data-integrity task, not here.

CI is green (incl. SonarCloud) and the PR is scoped to the coverage panel only.

@jdelgado2002
jdelgado2002 merged commit 77bdbb4 into feature/timeline-coverage-redesign Jul 3, 2026
12 checks passed
@jdelgado2002
jdelgado2002 deleted the feature/timeline-area-coverage branch July 3, 2026 19:36
jdelgado2002 added a commit that referenced this pull request Jul 4, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant