Bump vite from 5.4.19 to 5.4.20 in the npm_and_yarn group across 1 directory - #1
Closed
dependabot[bot] wants to merge 1 commit into
Closed
Bump vite from 5.4.19 to 5.4.20 in the npm_and_yarn group across 1 directory#1dependabot[bot] wants to merge 1 commit into
dependabot[bot] wants to merge 1 commit into
Conversation
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `vite` from 5.4.19 to 5.4.20 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.4.20/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.4.20/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 5.4.20 dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com>
Contributor
|
Important Review skippedBot user detected. To trigger a single review, invoke the You can disable this status message by setting the Comment |
Author
|
Looks like vite is no longer updatable, so this is no longer needed. |
dependabot
Bot
deleted the
dependabot/npm_and_yarn/npm_and_yarn-08d968b73d
branch
September 22, 2025 14:13
jdelgado2002
added a commit
that referenced
this pull request
May 18, 2026
* docs(kiosk-perf): design — fast punch + skip-photo fix Optimistic UI: close camera dialog immediately, show success after PIN verify, run INSERT + upload + last_used_at update in background. Adds maxWidth/quality props to ImageCapture, swaps auth.getUser for auth.getSession, and reads punch status from React Query cache first. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(kiosk-perf): fold frontend design review into spec Address 2 critical and 4 major concerns from the frontend-design-reviewer: - Critical #1: add processing-state re-entry guard for double-tap during the optimistic background window. Keypad/Confirm/Skip disabled, with a belt-and-braces early return in handlePunch. - Critical #2: defer force_reset PIN-change dialog and tip dialog into the per-call onSuccess so they cannot leak open when the INSERT rolls back. - Major #1: expose stopCamera() via ImageCapture.forwardRef so the parent can tear the MediaStream down synchronously before unmounting. - Major #2: gate the 5s punch-status cache short-circuit on !hasQueuedPunches() to keep offline-queue flushes correct. - Major #3: add role="status" + aria-live="polite" to the success Alert. - Major #4: accept user-scalable=no for kiosk surface; document the WCAG trade-off in code + PR description; personal /clock is unaffected. Minor advisories adopted: lower getUserMedia constraints when maxWidth<=480, add silent? toggle on useCreateTimePunch.mutate to avoid stacking a toast on top of the kiosk success Alert. Extends the test matrix and acceptance criteria to cover all the above. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(kiosk-perf): TDD plan — ImageCapture, punchContext, useTimePunches, KioskMode Sequenced as 4 atoms + verify + reviews + ship: 1. ImageCapture forwardRef + maxWidth/quality + lowered getUserMedia ideal 2. punchContext.startPunchContext (eager geolocation, shared promise) 3. useTimePunches: getSession (not getUser), silent flag, fire-and-forget bookkeeping (last_used_at moves out of the hot path) 4. KioskMode: optimistic flow, processing re-entry guard, dialogs deferred to per-call onSuccess with rollback in onError, stopCamera ref call, cache short-circuit gated on !hasQueuedPunches(), role=status on Alert Each task starts with failing tests, then implementation, then green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(kiosk-perf): fast punch + skip-photo fix (optimistic mutate) Punch in/out previously waited synchronously for the storage upload, the INSERT, an `auth.getUser()` round-trip AND a `last_used_at` PATCH before ever showing success. Under a real production network that totalled ~500ms-5s of "spinner staring at the screen" per employee, which doesn't scale to many concurrent clock-ins. Skip photo also failed to close the camera because `resetCameraState` was deferred to the end of the same awaited chain. Changes: - useTimePunches uses `supabase.auth.getSession()` (local cache, 0ms) instead of `getUser()` (50-150ms /auth/v1/user round-trip), and now honours a `silent: true` option so kiosk's own optimistic UI doesn't stack a global toast. - ImageCapture is a forwardRef exposing `stopCamera()` so callers can tear down the MediaStream synchronously, plus `maxWidth`/`quality` props to drop the JPEG to a 480-wide ~30-80KB upload (matches the EmployeeClock profile). - punchContext exports `startPunchContext` — an eager-start helper that shares one in-flight geolocation promise across the camera dialog open and the eventual Confirm/Skip click. Reused for ~10s, then re-armed. - KioskMode: * Camera teardown happens BEFORE any async work in Skip/Confirm — the dialog goes away the moment the employee taps the button. * Confirm captures the photo BEFORE stopping the stream (the old order silently lost the blob when the canvas had no source). * handlePunch is now optimistic: PIN match + status + context are awaited, then the success Alert is shown and processing is released, and the INSERT runs in the background via `createPunch.mutate(..., { onSuccess, onError })`. force_reset and the tip dialog are deferred into `onSuccess` so an offline/rejected punch doesn't trigger them. * `processingRef` covers the React commit gap so a same-tick double-tap on Skip/Confirm can't fire two punches. * Success Alert has `role="status"` + `aria-live="polite"` so screen readers announce the punch result. * `last_used_at` is fire-and-forget — never on the hot path. * Closing the camera dialog via ESC/backdrop now goes through `resetCameraState` so the camera light goes off. * `user-scalable=no` annotated as a WCAG 1.4.4 accept-with-rationale (shared kiosk lock-out hazard). - Tests: * tests/unit/ImageCapture.test.tsx — 6 tests covering stopCamera, getUserMedia constraints by maxWidth, canvas downscaling, quality pass-through to toBlob. * tests/unit/punchContext.test.ts — 17 tests (incl. 3 new for the eager-start sharing/reset semantics). * tests/unit/useTimePunches.test.tsx — 4 tests verifying getSession is used (not getUser) and silent suppresses the toast. * tests/unit/KioskMode.test.tsx — 7 integration tests covering the skip-photo bug fix, eager geolocation, role=status on the Alert, deferred force_reset/tip dialogs, optimistic rollback, and the re-entry guard. Verification: typecheck clean; 3920 unit tests pass; production build green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(kiosk-perf): code-simplifier pass Drop dead and redundant code from the kiosk-perf rewrite: - ImageCapture: remove unused `videoReady` state (declared but never set). - KioskMode: - `handleOfflineQueue` and `queuePunchOffline` no longer thread a `pin` parameter — `queuePunchOffline` never forwarded it, and the `if (offline && pin)` guard was tautological after `validatePinInput`. Both signatures now match what they actually need. - Drop the `submittedPin` snapshot — only callsite was the immediately- following `verifyPinForRestaurant`, so a snapshot was unnecessary. - Hoist repeated `pinMatch!.*` accesses in `onSuccess` into one local `match`, removing four non-null assertions. - Strip three "Helper: ..." comments that described WHAT the function is instead of WHY. Verification: typecheck clean; all 34 kiosk-perf unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(kiosk-perf): apply multi-reviewer findings (sound-logic, perf, security, maint) Address the must-fix findings from the four Phase 7a reviewers: Sound-logic (CRITICAL + MAJOR): - Thread `photoBlob` arg through `handleOfflineQueue` and `queuePunchOffline` so offline-queued punches preserve the captured photo instead of reading the (already-cleared) `capturedPhotoBlob` state. - Project the new clock state into the React Query cache after `releaseLock()` so a rapid re-punch inside the 5s freshness window doesn't see stale "already clocked in" / "no open shift" status. - Replace `void handlePunch(...)` with `.catch(...)` to surface synchronous throws to the operator and re-release the lock instead of swallowing them. - Use `hasQueuedPunches()` live check in `onSuccess` instead of the closed- over `queuedCount` (which could miss punches queued mid-mutation). - Use `pinMatch!` consistently — once the early-return guards have run the match is provably non-null; the `?.` was misleading. - Move `captureFn` to a ref (`captureFnRef`) so the Confirm button reads the latest value at click-time and isn't bitten by stale-closure semantics. - Surface a UI error when the camera hasn't initialised yet, instead of silently punching with no photo. - Reset `errorMessage` when the camera dialog reopens. Performance (CRITICAL + MAJOR): - `useEmployeeTips.submitTip` getUser → getSession (kiosk post-clock-out hot path). - `useUpdateTimePunch`, `useBulkCreateTimePunches`, `useBulkCreateEmployeeTips` getUser → getSession (consistency across the file). - Add module-level `flushing` mutex to `offlineQueue.flushQueuedPunches` so N concurrent `onSuccess` flushes don't double-send the same entries. Security (MAJOR): - Gate camera dialog `onOpenChange(false)` on `!createPunch.isPending` and `!processingRef.current` so an ESC/backdrop tap during the optimistic window can't slip a second punch through against a still-fresh cache. Maintainability (MAJOR + MINOR): - Stabilise the `onCaptureRef` callback via `useCallback` so ImageCapture's internal effect doesn't fire on every KioskMode render (clock tick, PIN keystroke). - Fix stale `isLoading` closure in `ImageCapture.startCamera` setTimeout fallback — use a local `metadataFired` flag instead. - Narrow `catch (error: any)` → `unknown` with `instanceof Error` checks in ImageCapture. - Drop WHAT comments that just restated the next line. Tests: - Add KioskMode integration tests for cache projection and photoBlob threading through the offline queue (now 9 tests, was 7). - Add `tests/unit/offlineQueue.test.ts` (3 tests) covering the flush mutex, photo rehydration, and `isLikelyOffline` semantics. Deferred (separate PR — schema-level work): - Security CRITICAL on `employee_pins_usage_updates` RLS WITH CHECK gap (pre-existing). - Security MAJOR on `created_by` audit trail using cached JWT (would need a DEFAULT `auth.uid()` column-default migration). - Security MAJOR on offline-queued `force_reset` punches bypassing the PIN reset prompt (requires a schema flag on the queue payload). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(kiosk-perf): apply PR #503 review feedback (CodeRabbit + Codex + CodeQL) Critical / Major findings: - offlineQueue.flushQueuedPunches: preserve all not-yet-attempted entries when one send fails. Previous loop only pushed the failed entry then broke, silently dropping 2..n of every multi-entry batch. - KioskMode.handleOfflineQueue: reject the enqueue when employee_id is undefined. A queued entry without employee_id can never flush — the validation re-throws on every retry — so we should surface the error to the user instead of writing a dead row. - KioskMode optimistic rollback: scope the lastResult/statusMessage rollback to the current op-id. If a newer employee's punch has already overwritten the chip, an older late-failing punch must not erase that newer success. Adds `opIdRef` + identity check around the rollback. - KioskMode camera dialog: drop `createPunch.isPending` from the onOpenChange guard so the NEXT employee can still ESC/backdrop out while a PREVIOUS employee's mutation is still flushing in the background. `processingRef.current` already scopes the guard to the current flow. Minor / polish: - offlineQueue.randomId: replace Math.random fallback with crypto.getRandomValues (with a counter-based last-resort), so CodeQL stops flagging it as a security-context randomness issue. - KioskMode: gate skip-photo and confirm-punch console.error calls behind import.meta.env.DEV per project style. - design doc: add `text` language tag to the kiosk-timeline fenced code block to satisfy markdownlint MD040. Tests: - New regression: offlineQueue preserves tail entries when send #2 of 3 fails. Confirms the indexed-loop / slice-append fix. - New regression: KioskMode does NOT roll back a newer employee's success when an older punch errors late. Confirms the opIdRef guard. - Hardened the photoBlob test to use the hoisted addQueuedPunchMock and assert toHaveBeenCalledTimes(1) unconditionally (no more conditional-spy false-pass). - Hardened useCreateTimePunch test to capture and assert the actual insert() row contains created_by = session user id (was only asserting the call fired). 3926 unit tests pass (+2 net). Typecheck clean. Build clean. Lint shows only the pre-existing react-hooks/exhaustive-deps warning on the mutateRef pattern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(kiosk-perf): duck-type photoBlob assertion for CI Blob identity mismatch `expect(arg.photoBlob).toBeInstanceOf(Blob)` passes locally under jsdom but fails in CI: undici-backed jsdom's `fetch(dataUrl).blob()` returns a Blob from a constructor that isn't reference-identical to the test scope's global `Blob`, so `instanceof` is false even though `size`, `type`, and the data are correct. Switched the assertion to check the duck-typed shape (defined, correct MIME type, numeric size > 0) which is what the test actually wants to prove — that the data URL was rehydrated into a blob-shaped payload before being handed to `sendPunch`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(kiosk-perf): clear SonarCloud reliability + coverage gates Reliability: `if (inFlight)` flagged "Expected non-Promise value in a boolean conditional" — Promise truthiness is always true and is rarely the intent. Made it `if (inFlight !== null)` so we're explicitly probing for the cached-singleton sentinel; semantics unchanged. Coverage: added three targeted tests to hit the branches that local jsdom skips: - `startPunchContext` reuse-window expiry (the `setTimeout` that nulls out the cached promise after 10s). - `randomId` `crypto.getRandomValues` fallback (`randomUUID` undefined but `getRandomValues` present). - `randomId` counter fallback (no `crypto` global at all) — uses `Object.defineProperty` because jsdom exposes `crypto` as a getter. 3929 unit tests pass locally, typecheck + lint clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
6 tasks
jdelgado2002
added a commit
that referenced
this pull request
Jul 2, 2026
…assthrough, refetchInterval polling
- Export FocusRestaurantOption type {restaurant_guid, restaurant_name} (design §4.2)
- Add listRestaurantsMutation calling focus-list-restaurants edge fn; handles all
three error shapes (transport reject, HTTP error, Focus-side data.error message)
- Fix triggerManualSync to accept and spread options ({startDate,endDate}) into the
invoke body; was silently dropping custom range (design §8.5 / Frontend critical #1)
- Add __focusRefetchInterval exported helper (polls 8000ms while backfilling; returns
false on done/inactive/error/null; design §8.5 / Frontend major #1)
- Wire refetchInterval into useQuery with the helper; staleTime stays 30000
- 16 new Vitest tests (34 total); zero regressions (5189 passed)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jdelgado2002
added a commit
that referenced
this pull request
Jul 2, 2026
…assthrough, refetchInterval polling
- Export FocusRestaurantOption type {restaurant_guid, restaurant_name} (design §4.2)
- Add listRestaurantsMutation calling focus-list-restaurants edge fn; handles all
three error shapes (transport reject, HTTP error, Focus-side data.error message)
- Fix triggerManualSync to accept and spread options ({startDate,endDate}) into the
invoke body; was silently dropping custom range (design §8.5 / Frontend critical #1)
- Add __focusRefetchInterval exported helper (polls 8000ms while backfilling; returns
false on done/inactive/error/null; design §8.5 / Frontend major #1)
- Wire refetchInterval into useQuery with the helper; staleTime stays 30000
- 16 new Vitest tests (34 total); zero regressions (5189 passed)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jdelgado2002
added a commit
that referenced
this pull request
Jul 2, 2026
…ll (#564) * chore: stop tracking progress.md (gitignored, force-committed in #563) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(focus): design — restaurant picker + server-side background backfill Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(focus): fold Phase 2.5 design-review resolutions (CAS, sync custom-range, budgets, a11y) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(focus): implementation plan — picker + background backfill (TDD, review-hardened) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(focus/A1): add focusListRestaurantsHandler — auth/role/SSRF guards, Focus API call, response shaping Adds the shared handler for the focus-list-restaurants edge function (task A1). - POST body: restaurantId + apiKey + apiSecret + optional environment - Auth: JWT → 401; role owner|manager → 403 (same FOCUS_ALLOWED_ROLES as test-connection) - SSRF: isSafeUrl(baseUrl, FOCUSPOS_HOST_RE) guard before any outbound call - sandboxBaseUrl from deps only — never from request body (§8.6) - Focus-side failures (401/403/404/other) → HTTP 200 {success:false, error:<friendly>} - Network errors → HTTP 200 {success:false} - Success → HTTP 200 {success:true, restaurants:[{restaurant_guid, restaurant_name}]} - Blank/missing restaurant_name defaults to the GUID (§4.1 step 8) - Filters out items without a string restaurant_guid - apiKey/apiSecret never logged on any code path (28-test assertion suite) - 28 Vitest tests: RED → GREEN; full suite 5173 tests passing, zero regressions Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(focus): add focus-list-restaurants edge function + config.toml entry (A2) Thin Deno entry that mirrors focus-save-connection: CORS pre-flight, userClient (JWT forwarded) + serviceClient (service role), passes globalThis.fetch and FOCUS_API_SANDBOX_URL env var as injectable deps. All business logic lives in focusListRestaurantsHandler (A1). config.toml gains [functions.focus-list-restaurants] verify_jwt=false. 5173 Vitest tests pass, zero regressions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(focus/A3): add listRestaurants, FocusRestaurantOption, options passthrough, refetchInterval polling - Export FocusRestaurantOption type {restaurant_guid, restaurant_name} (design §4.2) - Add listRestaurantsMutation calling focus-list-restaurants edge fn; handles all three error shapes (transport reject, HTTP error, Focus-side data.error message) - Fix triggerManualSync to accept and spread options ({startDate,endDate}) into the invoke body; was silently dropping custom range (design §8.5 / Frontend critical #1) - Add __focusRefetchInterval exported helper (polls 8000ms while backfilling; returns false on done/inactive/error/null; design §8.5 / Frontend major #1) - Wire refetchInterval into useQuery with the helper; staleTime stays 30000 - 16 new Vitest tests (34 total); zero regressions (5189 passed) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(pos/focus): replace GUID input with server-fetched restaurant picker in setup wizard Implements design §4.3 + §8.5 (A4): step machine instructions→credentials→select→done. Removes RESTAURANT_GUID_PATTERN and the GUID text input; "Find my restaurant(s)" button calls listRestaurants and advances to a select step with auto-select-when-one and a shadcn Select for multiple results. aria-current="step" moved to role="listitem". Done step copy updated to background 90-day import framing. 18 new Vitest tests (40 total, 5207 suite total, zero regressions). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(focus): pure processBackfillBatch helper + 23 Vitest tests (B1) Budget/day-cap loop, cursor advance, initialSyncDone flag, injectable clock+processDayTransactions — fully tested with zero regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(focus): batch per-check item/payment upserts + processDateRangeTransactions (B2) Replace per-row await loops in upsertItems/upsertPayments with a single array upsert per check (§8.4), collapsing hundreds of sequential DB round-trips per day into two. Add processDateRangeTransactions export (iterates explicit date list, skipUnifiedSalesSync each day, one RPC for the full range) with injectable processDayTransactions for testability. Add DateRangeSyncDeps type. 15 new Vitest tests (41 total in file); full suite 5245 tests, zero regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(focus/B3): small backfill kick via processBackfillBatch + CAS + custom-range + error status - Lynk backfill path delegates to processBackfillBatch({budgetMs:12_000, maxDays:5}) for instant feedback; the durable 5-min cron finishes the rest server-side. - Cursor write uses CAS (§8.1): .eq('sync_cursor', readCursor).select() guards concurrent pg_cron ticks from clobbering each other. - On batch error: persists connection_status='error' + last_error + last_error_at so the frontend polling stops and the operator sees the stall (§8.3). - Response includes backgrounded=true while initial_sync_done=false. - Custom range (body startDate+endDate, Lynk only): validates both required, start≤end, span≤14 days (§8.2 — synchronous, no waitUntil); calls processDateRangeTransactions; returns {daysSynced, status}. - Bad range (span>14, start>end, unparseable, missing one field) → HTTP 400. - Portal path CAS-updated to use the same 3-eq chain for multi-tenant safety (§8.1). - 17 new Vitest tests (44 total in file); full suite 5262 tests, zero regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(focus/B4): focus-backfill-sync function + cron (durable 5-min backfill engine) New focusBackfillSyncHandler.ts: timing-safe Bearer gate, queries only is_active+initial_sync_done=false+api_key IS NOT NULL (Lynk backfill rows), round-robin LIMIT 5, processBackfillBatch per restaurant (maxDays=7, per-restaurant budget from remaining wall time capped at 50s), CAS write (§8.1), error status write on batch failure (§8.3), 2s inter-restaurant sleep, isolated errors continue loop. Returns {processed,errors,elapsedMs}. Thin focus-backfill-sync/index.ts mirrors focus-bulk-sync entry pattern. config.toml: [functions.focus-backfill-sync] verify_jwt=false. Migration 20260702120000_focus_backfill_cron.sql: idempotent unschedule guard, */5 * * * * schedule, current_setting missing_ok GUC for graceful auth on unset keys (§8.6). pgTAP 48_focus_backfill_cron.sql asserts cron job exists on */5 * * * * schedule. 24 Vitest tests; full suite 5286 tests, zero regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(focus/B5): focus-bulk-sync cedes Lynk backfill to 5-min cron (TDD) Add skip guard at the top of the isLynkPath block in processConnection: if (!row.initial_sync_done) return { unchanged cursor/flag } so the 6-h bulk-sync never races the 5-min focus-backfill-sync cron on sync_cursor. Test-first (design §8.7): 4 new Vitest tests assert — • backfilling Lynk row: processDayTransactions NOT called, cursor unchanged • skipped row still counted as processed (no error surfaced) • incremental Lynk row (initial_sync_done=true): still processed (2 calls) • portal backfilling row (no api_key): still processed via processReportDay Also mocks focusTransactionSyncHandler + focusLynkClient at module level so future Lynk-path tests in this file don't require real network calls. Full suite: 5290 tests, zero regressions (5286 before + 4 new). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(focus-sync): B6 — single handleSync call, background toast, passive progress, a11y Design §8.5 / Frontend critical #2 + major #4: FocusSync.tsx: - Remove dead state: syncProgress, totalDaysSynced, syncResult, SyncResult interface - Remove SyncProgressDisplay and SyncResults renders (no progress loop) - handleSync makes ONE call: recent/initial → triggerManualSync(restaurantId); custom → triggerManualSync(restaurantId, {startDate, endDate}) in yyyy-MM-dd - Toast changed to background message: "Running in the background. You can leave this page; it keeps going." SyncComponents.tsx — InitialSyncPendingAlert: - Collapse to single message path (drop hasProgress bifurcation) - New copy: "Importing your last 90 days in the background (N of 90). No need to keep this page open." - Wrap progress count in <span role="status" aria-live="polite" aria-atomic="true"> - Add Progress bar with aria-label="Sync progress" aria-valuemin/max - Remove dead "First sync pending" / "Click Sync Now to continue" branches 15 new Vitest tests (TDD: RED → GREEN); full suite 5305 tests, zero regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style(focus-sync): apply Apple/Notion guidelines to SyncComponents + fix useQuery generic - Replace direct palette colors (green-100/700, text-primary, text-green-600) with semantic tokens (text-foreground, border-border/40, bg-muted/*) - Align typography to design scale: text-[14px]/[13px]/[12px] replacing text-sm/text-xs/text-2xl - Apply canonical card container style (rounded-xl border border-border/40 bg-muted/30) to ConnectionStatus and SyncResults - Apply canonical primary button style to SyncButton (h-9 px-4 rounded-lg bg-foreground) - Add aria-hidden to decorative icons in SyncProgressDisplay and HowSyncingWorksInfo - Fix useFocusConnection useQuery<FocusConnection | null> explicit generic so TypeScript resolves connection type correctly (was resolving to unknown due to refetchInterval cast) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * simplify(focus): hoist todayInTz, import shared constant, extract handlers, unify timestamps Reuse: - focusListRestaurantsHandler: import FOCUS_ALLOWED_ROLES from focusReportClient instead of re-defining an identical Set locally Efficiency: - focusBackfillBatch: hoist todayInTz() call above the loop (timezone + now are constant per call; no need to rebuild Intl.DateTimeFormat on each iteration) - focusBackfillSyncHandler + focusSyncDataHandler: capture a single nowIso constant per update block so last_sync_time, updated_at, and last_error_at are guaranteed to share the same timestamp Simplification: - FocusSetupWizard: replace 'no-restaurants' string sentinel with a dedicated listEmpty boolean state (string overloaded two different meanings) - FocusSetupWizard: extract inline async Sync Now click handler to named handleSyncNow, consistent with handleFindRestaurants/handleSaveAndConnect/handleRetry - SyncComponents: extract capitalize() helper for config.dataLabel (3 call sites) Tests: 5305 passed, 0 regressions; typecheck: 0 errors Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): fold 7b findings — sandbox URL bleed, CAS miss, dead code, UI copy - Thread FOCUS_API_SANDBOX_URL through BackfillSyncDeps / BulkSyncDeps / SyncDataDeps + index.ts entry points (Codex critical: sandbox connections were silently falling back to the production URL — focusApiBaseUrl §6) - Inspect CAS .select() result in focusBackfillSyncHandler and focusSyncDataHandler: skip incrementing processed / return stale data when 0 rows (another tick already won — spec §8.1; security + performance major) - Remove dead Lynk backfill block in focusBulkSyncHandler (lines 214-231); B5 skip guard at line 187 made it unreachable — security/maintainability/ sound-logic major - Fix SyncModeSelector description "max 90 days" → "max 14 days" to match the MAX_CUSTOM_RANGE_DAYS=14 server-side cap (sound-logic major) - Fix useFocusConnection staleTime: 0 (was 30000) so 8s refetchInterval actually fires network requests during backfill (performance minor) - Remove dead serviceClient variable in focus-list-restaurants/index.ts (OCR-rules / maintainability minor) - Merge duplicate focusLynkClient imports in focusBackfillSyncHandler (maintainability minor) - 2 new Vitest tests: CAS miss not counted as processed; sandboxBaseUrl threaded to txConfig.baseUrl; 5307 tests green, 0 TS errors Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(focus-sync): show 'Sync complete' toast title for synchronous custom-range sync Custom-range sync is processed synchronously by the edge function, so 'Import started' was misleading — the sync is already done when the toast appears. Use 'Sync complete' for custom mode, keep 'Import started' for recent/initial background kicks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(lint): remove no-explicit-any in new files and fix block comment parsing error - focus-backfill-sync/index.ts: escape cron schedule comment (*/5 was ending block comment prematurely, causing ESLint parse failure) - focusTransactionSyncHandler.test.ts: replace `as any` casts with typed alternatives (TransactionSupabaseDeps, FetchDatafeedFn, Record<string,unknown>[], unknown[], { error?: string }) — zero no-explicit-any errors in all new files Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(focus-backfill): handle inprogress status in batch, add guard in handleSyncNow, thread staleTime fix - focusBackfillBatch: add 'inprogress' to BackfillBatchResult.status union; first-day inprogress now surfaces without a completed-day status - focusBackfillSyncHandler: thread sandboxBaseUrl to txConfig.baseUrl; skip restaurant when CAS misses (0 rows updated = stale cursor); write error status on batch failure; add missing test coverage for new paths - FocusSetupWizard: add isSyncingNow guard in handleSyncNow to prevent concurrent sync kicks; add finally block to always reset flag - SyncComponents/useFocusConnection: staleTime 0 so 8s refetchInterval fires reliably during backfill (was 30000) - 15 new Vitest tests; full suite 5312 tests, zero regressions Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: retry Supabase Preview after transient 502 infra failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(focus): address PR #564 review findings — security, stability, test coverage Security (CodeRabbit major × 2): - focusDatafeedParser: add processEntities:false to XMLParser to prevent XXE/DoS via crafted DTD entities in Focus datafeed XML (fast-xml-parser v5 option) - focus-backfill-sync/index.ts: fail closed on missing SUPABASE_URL/SERVICE_KEY to prevent Bearer timingSafeEqual('','') pass-through on misconfigured deploy Stability (CodeRabbit minor × 3): - focusBackfillSyncHandler: add .catch() to fire-and-forget error-state write so an unexpected promise rejection is logged rather than silently dropped - focusBulkSyncHandler: persist connection_status='error' + last_error when Lynk incremental sync fails (matches legacy portal path and backfill cron behavior) - focusBulkSyncHandler: bump last_sync_time in catch block so failed connections don't monopolize round-robin ORDER BY last_sync_time ASC NULLS FIRST Error persistence (CodeRabbit minor): - focusSyncDataHandler: persist connection_status='error'+last_error when Lynk incremental processDayTransactions fails; mirrors backfill error path Test coverage (4 new tests, github-code-quality, CodeRabbit nit × 2): - focusListRestaurantsHandler.test.ts: remove unused beforeEach import (lint fix) - focusSyncB6.test.tsx: rename test title to match actual assertions (no toast) - 46_focus_transactions_schema.sql: plan(36→38) + 2 new card_last4 cases (too-short 3-digit and non-digit '12AB') to fully enforce PCI constraint - focusBulkSyncHandler.test.ts: 2 new tests for error persistence + starvation fix - focusSyncDataHandler.test.ts: 2 new tests for incremental error persistence + import focusLynkClient to control fetchDatafeed in error scenario Doc fixes (CodeRabbit nit): - 20260701140000_focus_transactions_cron.sql: fix comment '*/6' → '0 */6' Full suite: 5314 tests pass, 0 TS errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(focus): add no-explicit-any eslint-disable header to mock-heavy test files Matches the repo convention (focusSaveConnectionHandler.test.ts et al.) for Supabase client mocks; removes 60 lint errors introduced when review-fix commits added mock coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(focus-backfill): sync backfilled days into unified_sales (Codex P1) Backfill days are written with skipUnifiedSalesSync:true, but both unified_sales crons only re-sync the last 2 business days — so backfilled history (days 3..90) would never reach unified_sales / P&L. processBackfillBatch now syncs its own processed date range via sync_focus_transactions_to_unified_sales after the loop, and a full 90-day window on completion as a reconciliation backstop. Non-fatal on RPC error (order rows are durably written). +4 tests. Also adds no-explicit-any eslint-disable header to 3 mock-heavy test files (repo convention) to clear 60 lint errors surfaced by the review-fix test coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(focus-bulk-sync): skipped Lynk backfill rows get NO write (CodeRabbit Major, 9d) The B5 skip guard returned row.sync_cursor unchanged, but the persistence path still wrote it back — racing focus-backfill-sync: if the 5-min cron advanced the cursor between bulk-sync's read and this write, bulk-sync regressed it to the stale value (and spuriously bumped last_sync_time, perturbing the round-robin). processConnection now returns skipped:true and the caller writes nothing for skipped rows (still counted as processed). Test updated to assert no write. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
jdelgado2002
added a commit
that referenced
this pull request
Jul 3, 2026
- 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>
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>
jdelgado2002
added a commit
that referenced
this pull request
Jul 4, 2026
* fix(focus): cut datafeed parse CPU ~10x + sync error hygiene
## Root cause of HTTP 546 (edge-worker CPU limit)
The Focus POS Lynk datafeed returns ~4.5 MB of XML per business day.
Roughly 90% of that payload is static menu/config data (Configuration,
MenuItems, Employees sections) that we never read. When a user triggers a
6-day custom-range sync, focus-sync-data was handing 6 × 4.5 MB = 27 MB to
fast-xml-parser in a single edge-function invocation, pushing it past the
Deno worker CPU budget and returning HTTP 546.
## Four fixes
### 1. Parser CPU fix — focusDatafeedParser.ts
Before calling fast-xml-parser, pre-extract only the <Checks>…</Checks>
block using cheap string indexOf scanning (O(n), no regex over 4.5 MB).
The extracted content is re-wrapped as <DailyData><Checks>…</Checks></DailyData>
so all existing object-path code (`doc.DailyData.Checks.Check`) remains
unchanged. If the block is absent (config-only feed for very old dates)
return {checks:[], deletedCheckIds:[]} immediately without calling the
XML parser at all. If extraction produces unparseable XML, fall back to the
full parse (never worse than before). Cuts per-day CPU by ~90%.
### 2. Remove in-worker aggregation RPC — focusTransactionSyncHandler.ts
processDateRangeTransactions was calling sync_focus_transactions_to_unified_sales
for the full range at the end of every custom-range sync. This was redundant
because a 5-minute pg_cron job (focus-transactions-unified-sales-sync) already
runs sync_all_focus_transactions_to_unified_sales() in Postgres and picks up
recently written focus_orders rows automatically. Removing the in-worker RPC
call eliminates the work that pushed the 6-day range over the CPU limit even
after fix #1, and also means partial progress survives a mid-range crash since
each per-day write lands immediately. Each per-day processDayTransactions call
retains skipUnifiedSalesSync:true (unchanged).
### 3. Transient no-blob_url retry — focusLynkClient.ts
Focus occasionally returns a valid 200 OK response from /api/lynk/sync but
with no blob_url in the payload; a retry always succeeds. When fetchDatafeed
receives an OK-shaped response missing blob_url, it now retries the POST once
after a default 1 500 ms delay (injectable via deps.sleep so tests don't
sleep) before returning the parse error. Exactly two POST attempts maximum.
### 4. Error hygiene — focusSyncDataHandler.ts, focusBackfillSyncHandler.ts
connection_status='error' / last_error were never cleared after a subsequent
successful sync, so a transient failure left a scary banner in the UI
indefinitely. Successful sync paths (status ok/empty) now write
connection_status='connected', last_error=null, last_error_at=null alongside
the cursor/timestamp fields. Error paths (status error) are unchanged.
Affects: Lynk backfill batch, Lynk incremental, portal path, and the cron
backfill handler.
## Tests
All existing tests pass unchanged. New tests added:
- focusDatafeedParser: large config wrapper parses identically; config-only
feed (no <Checks>) returns empty without error
- focusTransactionSyncHandler: range path never calls the RPC (even when
skipUnifiedSalesSync=false); single-day also no RPC
- focusLynkClient: first-attempt missing blob_url + second-attempt ok →
success with 2 POSTs; both missing → error after exactly 2 POSTs
- focusSyncDataHandler: successful incremental and backfill write
connection_status=connected + last_error=null
- focusBackfillSyncHandler: successful batch tick writes connection_status=
connected + last_error=null
Full suite: 5485 tests pass (406 files), 0 failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(focus): persist portal-path sync errors to connection_status (CodeRabbit)
The new success-clears-banner logic on the legacy portal path only wrote
connected/null when status!=='error' — a portal sync failure was reported in the
JSON response but never persisted, so a stale 'connected' state could mask a
failing sync. Now symmetric with the Lynk backfill/incremental paths (if/else
writing error state + message on failure). Also drop the unused syncStatus
destructure flagged by code-quality. +1 test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
jdelgado2002
added a commit
that referenced
this pull request
Jul 21, 2026
…/week/month) (#624) * docs(labor): design for financial labor view (dashboard card + /labor page) Distinct from the shipped scheduling SPLH feature (#611): financial framing (labor % of sales, revenue/labor-hr), payroll-grade labor reconciled with P&L, day/week/month, sales-volume busy-hours heatmap, editable target_labor_pct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(labor): fold Phase 2.5 review — real per-day sales, tz, tokens, stacked charts - critical: drop usePnLAnalyticsFromSource (fakes per-day revenue); series revenue from real unified_sales via useSplhData/buildSplhTimeseries - tz window boundaries via getTodayInTimezone - dedicated --labor-over/under/balanced tokens (splh tokens are inverted) - two stacked charts, not dual-axis; propagate capped; default target 22 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(labor): implementation plan (TDD tasks A–G) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(labor-pnl): add laborPnlAnalytics types + classifyBalance Introduces src/lib/laborPnlAnalytics.ts: FinancialPoint, SalesVolumeCell, LaborBalanceWindow, LaborPnlSummary, BalanceState, and LABOR_BALANCE_BAND (default 6pt), plus classifyBalance(laborPct, targetPct, band) — the over/balanced/under classifier that drives the labor financial view's coloring and verdicts (design §3). Guards targetPct<=0 and a null laborPct (no-sales bucket) as 'balanced'. Task A1 of docs/superpowers/plans/2026-07-20-labor-financial-view-plan.md. * feat(labor-pnl): add monthKeyOf + bucketKeyOf, export mondayOf for reuse Adds monthKeyOf(dateStr) (calendar-month YYYY-MM key) and bucketKeyOf (day passthrough / week via reused mondayOf / month via monthKeyOf) to laborPnlAnalytics.ts, ahead of buildFinancialSeries (A3). Exports splhAnalytics.mondayOf so it can be reused rather than duplicated, per design §5. Tests cover day/week/month bucket keys and the Dec->Jan year boundary. * feat(labor-pnl): buildFinancialSeries — outer join + day/week/month bucketing Joins the real daily sales series (buildSplhTimeseries 'day') with the real daily labor series (useLaborCostsFromTimeTracking) on restaurant-local date, then rolls up to day/week/month via the existing bucketKeyOf (design §5). Outer join: a sales-only or labor-only day still surfaces as its own bucket instead of being dropped. laborPct is null (never Infinity) when a bucket's sales <= 0. Tests: day passthrough, week Monday-start aggregation, month calendar aggregation, 0-sales -> null pct, sales-only outer join, and bucket sort across mixed sales-only/labor-only days. * feat(labor-pnl): buildSalesVolumeGrid(cells, estimated) — busy-hours intensity + peak flag Task A4 of the labor financial view plan (design §5/§8). Normalizes each SplhGridCell's totalSales into a 0..1 intensity against the window's max cell (0, never NaN, on an all-zero window), flags peak at/above the prototype's 72% threshold, and passes the window-level `estimated` flag through to every cell. Design deviation note (documented, not silent): SplhGridCell carries no per-cell `estimated` field, and buildSplhGrid derives every cell from either real per-sale hours or the daily-spread fallback — never a mix — so the function takes `estimated` as a second boolean parameter (mirroring SplhHeatmap's existing `estimated` prop) rather than reading it off the cells. SplhGridCell/buildSplhGrid stay unmodified per design §5's "reuse without modification" list. Tests: intensity scaling, peak threshold edges (72% inclusive vs 71.99% exclusive), estimated passthrough (true/false), all-zero window (no NaN), empty cells array. 5 new tests, 26 total in laborPnlAnalytics.test.ts. No regression in splhAnalytics.test.ts (21 tests); typecheck + eslint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(labor-pnl): add summarizeLaborPnl (totals, verdict, over/under windows) Task A5: summarizeLaborPnl(points, targetPct) sums a FinancialPoint[] window into KPI totals + revPerLaborHr, builds a plain-English verdict line (per design §2.1/§2.2) tagged with a tone ('over'|'balanced'| 'under'|'none'), and extracts contiguous over/under bucket runs as LaborBalanceWindow[] staffing callouts. Guards 0-sales (verdictTone 'none', mirroring SplhSummary) and 0-hours (revPerLaborHr null, clause omitted from the verdict) per design §6/§8. 8 new tests (34 total in file); no regression in splhAnalytics.test.ts. * feat(labor-pnl): add bucketKeyComparator + balanceStateClassName helpers A6: extract the locale-aware bucket-key comparator buildFinancialSeries was already using inline (SonarCloud S2871 lesson) into a reusable exported bucketKeyComparator, and add balanceStateClassName (tone -> --labor-* token className) mirroring splhAnalytics.verdictToneClassName for the upcoming Labor cost UI components. 7 new tests in tests/unit/laborPnlAnalytics.test.ts (41 total in file); typecheck + eslint clean; full unit suite (508 files, 6604 tests) green. * feat(labor-pnl): add --labor-over/under/balanced CSS tokens Dedicated financial-balance color scale for the labor P&L view (light + dark), per design doc §7. Deliberately NOT aliased to the existing --splh-lean/slack tokens, whose red/blue semantics are inverted (SPLH red = understaffed; labor red = over budget) and which can appear on-screen simultaneously with this card. - light: --labor-over 0 72% 42%, --labor-under 32 90% 34%, --labor-balanced 142 50% 32% (all ≥4.5:1 contrast vs light bg) - dark: --labor-over 0 72% 62%, --labor-under 32 92% 62%, --labor-balanced 142 50% 58% tests/unit/laborColorTokens.test.ts mirrors the existing splhColorTokens.test.ts pattern (HSL-triplet presence in :root/.dark) plus a regression guard that these tokens are never aliased to --splh-*. * feat(labor-pnl): useLaborPnlCore shared hook (tz, target, restaurant-tz window, daily series) Mirrors useSplhCore's shared-setup pattern for the new financial labor surface: validates restaurant tz, reads target_labor_pct from useStaffingSettings, derives the labor-cost fetch window from getTodayInTimezone (restaurant-local "today", not host/UTC), and composes useSplhData (sales+punches) + useLaborCostsFromTimeTracking (payroll-grade daily labor via calculateActualLaborCost). dailySales is derived via the existing buildSplhTimeseries(sales, sessions, tz, 'day') — real per-day sales, matching design §4/§5. hasData requires both sales AND punches (setup-invite empty state, same semantics as useSplhCore). capped/isLoading/isError/error/refetch combine both source hooks so useLaborPnlSummary/useLaborPnlAnalytics (next tasks) get one surface to build on. Test: tests/unit/useLaborPnlCore.test.ts (8 tests) — dailySales derivation, tz/targetPct passthrough, restaurant-tz window boundaries (Honolulu UTC-vs-local day fixture, mirroring useSplhData's own §5 S-min1 test), loading/hasData guards, capped propagation, combined loading/error, combined refetch. * feat(labor-pnl): useLaborPnlSummary — 4-week dashboard-card hook Wraps useLaborPnlCore + buildFinancialSeries/summarizeLaborPnl into a lightweight summary for the LaborPnlCard dashboard card (design §2.1/§5): period totals (labor % of sales, rev/labor-hr, verdict) plus a daily sparkline, no hourly grid. Mirrors useSplhSummary's shape over useSplhCore. The sparkline is the day-granularity FinancialPoint[] series itself, and summary is summarizeLaborPnl run over that same series, so the KPI totals are always exactly the sum of the sparkline buckets (design §8 reconciliation invariant) rather than a second independently-computed total. Tests (tests/unit/useLaborPnlSummary.test.ts, 5 tests): tz/window passthrough to useSplhData; reconciliation (summary totals == sum of sparkline); null labor%/'none' verdictTone on a no-sales window; no `grid` property exposed; capped/hasData/loading/error/refetch propagation from the core hook. * feat(labor-pnl): useLaborPnlAnalytics — /labor page hook (series, grid, summary, updateTarget) Builds the full labor-P&L dataset for the /labor page (design §2.2/§5): day/week/month `series` via buildFinancialSeries, the busy-hours `grid` (sales volume, via buildSplhGrid -> buildSalesVolumeGrid, estimated-flagged from hasHourlyBreakdown), the period `summary`, and a dirty-checked `updateTarget` wrapping useStaffingSettings.updateSettings (design §7: guards the Enter+blur double-commit). Extends useLaborPnlCore (C1) to also expose the raw `sales`/`sessions` inputs and the target-write path (`updateSettings`/`isSavingTarget`) from its existing useStaffingSettings call, so useLaborPnlAnalytics can build the grid and the target control without a second useSplhData fetch or a second useStaffingSettings mutation instance — mirrors useSplhCore exposing `data`/`sessions` alongside its own derived `grid`. Tests: tests/unit/useLaborPnlAnalytics.test.ts (6 tests) — series+grid+ summary from real sales/labor fixtures; granularity switch rebuilds the series (day vs week bucket counts differ); grid cells flip estimated:true under the daily-spread fallback; updateTarget dirty-check (no-op when unchanged, single write when changed); capped/hasData/loading/error/ refetch passthrough; empty-data shape (empty series, all-zero 7x24 grid). Full unit suite (513 files, 6632 tests) green, no regressions; typecheck + eslint clean on touched files. * feat(labor): LaborBalanceRibbon component — per-bucket balance chip strip Phase 4 task D1: pure flex strip of over/balanced/under chips (one per FinancialPoint), colored via the dedicated --labor-over/under/balanced tokens (never --splh-lean/slack — inverted semantics, design §7). Each chip carries an aria-label naming its bucket and balance state (role="listitem" inside a labeled role="list"); renders nothing for an empty window since the parent chart owns the shared empty state. Will sit under DemandVsStaffingChart's shared x-axis (design §2.2/§7). * feat(labor): SalesVolumeHeatmap — dow×hour busy-hours grid (design D2) Busy-hours sales-volume heatmap for the /labor page: full 7x24 grid trimmed to active hours (any day with sales), role="grid"/row/gridcell with per-cell aria-label naming day/hour/sales/peak, sticky day-of-week column, min-w-10/min-h-10 cells. Colors ramp opacity on the --labor-balanced token by SalesVolumeCell.intensity (design §7's green ramp — distinct read from SplhHeatmap's lean/slack diverging colors); peak cells (>=72% of window max) get a ring outline so the busiest hours are never color-only. "Estimated" badge mirrors SplhHeatmap's daily-spread-fallback note; new "Partial window" badge surfaces useSplhData's capped flag per design §6. Tests: tests/unit/SalesVolumeHeatmap.test.tsx (17 tests) — active-hour trimming, cell aria-label (with/without peak), style ramp (zero-sales bg-muted vs intensity-scaled --labor-balanced), 7 Mon-first day rows, focusable gridcells, estimated/capped badge show-hide, all-zero fallback message. Confirmed RED first (component didn't exist), then GREEN. Full unit suite (515 files, 6654 tests) green, no regressions; tsc --noEmit clean; eslint clean (3 react-refresh/only-export- components warnings — same pattern already accepted on SplhHeatmap.tsx/LaborBalanceRibbon.tsx, not new). * feat(labor): DemandVsStaffingChart — stacked sales area + labor-% line (D3) Two stacked Recharts sharing one x-axis (design §7): net-sales AreaChart on top, LaborBalanceRibbon in between, labor-% LineChart + target ReferenceLine below. Deliberately not a dual-axis single chart — mirrors SplhTimelineChart's single-axis + ReferenceLine precedent to avoid the #611-lesson correlation-implying-scale failure mode. Renders nothing for an empty points array (parent /labor page owns loading/error/empty). Tests: tests/unit/DemandVsStaffingChart.test.tsx (9 tests) — chart-data mapping preserves null laborPct for connectNulls gaps, tooltip formatter, two-chart smoke render + target reference line label, ribbon chip count matches points length, accessible name naming target + granularity view, empty-array no-render. Confirmed RED first, then GREEN. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(labor): EditableLaborTarget component (D4) Labeled number input for target_labor_pct, commit on blur or Enter, guarded by its own dirty check (committedRef) so an Enter-then-blur sequence never double-fires onCommit even against a bare mock. Optimistically updates the displayed value on commit; reverts to the previous value and shows a destructive toast if the write rejects. Re-syncs its displayed value when targetPct changes externally. Design doc §7 (target-edit control) / plan D4. Tests: tests/unit/EditableLaborTarget.test.tsx (8 tests) — labeled accessible input; commit-once on blur when changed; no-op on unchanged blur; Enter+blur fires exactly once; optimistic value; revert+toast on commit failure; external targetPct re-sync; empty/ non-numeric entry reverts without committing. Confirmed RED first (component didn't exist), then GREEN. Full unit suite (517 files, 6671 tests) green, no regressions; tsc --noEmit clean; eslint clean. * feat(labor): LaborVerdict component — tone dot + verdict sentence (D5) Tone-dot-plus-sentence line for the /labor page (design §2.2), mirroring CoverageVerdict's existing dot pattern: a colored dot (--labor-over/under/ balanced tokens, or a neutral dot for the no-sales 'none' tone) next to summarizeLaborPnl's plain-English verdict string. Sentence text stays text-foreground regardless of tone — color lives on the dot only, never the sole signal. Defensively guards a null/undefined summary prop (renders nothing) even though its owning hooks (useLaborPnlSummary/useLaborPnlAnalytics) always produce a valid LaborPnlSummary once mounted. Tests: tests/unit/LaborVerdict.test.tsx (8 tests) — tone->dot-class mapping incl. 'none', dot+sentence render per tone, null/undefined summary guards. Confirmed RED first (component didn't exist), then GREEN. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(labor): LaborPnlCard dashboard card (Phase 4 E1) Composes useLaborPnlSummary for the dashboard "Labor cost" card (design §2.1): hero labor % of sales vs. target_labor_pct, revenue per labor hour, a tone-colored verdict line, a daily labor-% sparkline, and an "Open labor detail" link to /labor. Mirrors LaborEfficiencyCard's loading/error/empty-state structure but uses the dedicated --labor-over/under/balanced tokens (balanceStateClassName) rather than --splh-lean/slack, since the two are semantically inverted (design §7). The sparkline trends laborPct per day (buildLaborSparklineData), preserving null buckets so connectNulls=false gaps no-sales days instead of interpolating — laborPct already is the sales-vs-labor read (laborCost ÷ sales). Tests: tests/unit/LaborPnlCard.test.tsx (12 tests) — sparkline mapping incl. null passthrough; loading/error/empty states by role; hero number + target + verdict; revenue-per-labor-hour line shown/omitted; null-laborPct em dash; over-target tone class on hero + verdict; accessible sparkline image; navigate('/labor') on link click. Confirmed RED first (component didn't exist), then GREEN. Full unit suite (519 files, 6691 tests, 2 skipped) green, no regressions; tsc --noEmit clean; eslint clean (1 react-refresh/only-export-components warning, same accepted pattern as D1-D5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(labor): Labor.tsx page composing useLaborPnlAnalytics Composes useLaborPnlAnalytics (C3) into the /labor page (design §2.2): Day/Week/Month ToggleGroup, LaborVerdict, a 4-tile KPI row (labor % of sales, revenue/labor-hour, net sales, labor $), the DemandVsStaffingChart (D3, with the D1 ribbon), the SalesVolumeHeatmap (D2), auto-generated over/under staffing callouts with a $ estimate, and the EditableLaborTarget (D4). Three states (loading skeleton / inline error+retry / POS-connect empty state) mirroring LaborEfficiencyPanel's structure, plus a no-restaurant-selected guard. Two new local pure helpers cover design gaps left open by the already- committed Phase A lib: `estimateWindowDollars`/`findWindowPoints` compute the callout's "$ estimate" (LaborBalanceWindow only carries labels/ bucketCount, not a dollar figure) from the window's underlying FinancialPoints; `windowRangeLabel` formats single- vs multi-bucket ranges. The Day/Week/Month toggle is treated as this page's period control — no separate prior-period navigator or KPI delta is built, consistent with how LaborPnlCard (E1) already read the same design wording as delta-vs-target (already shown via tone-colored KPIs + verdict) rather than delta-vs-a- previous-window, since no hook in Phase A/C fetches or computes a prior comparable period. Route wiring (/labor in App.tsx) and dashboard mount are separate tasks (F1/F2, not yet done). Tests: tests/unit/Labor.test.tsx (16 tests) — pure helpers (window range label, point slicing, dollar estimate for over/under windows); three states by role; no-restaurant guard; KPI row + verdict + chart + heatmap render with real data; heatmap "estimated" flag derived from grid cells; granularity toggle re-invokes the hook; staffing callouts render with $ estimates; capped propagation to the heatmap; editable target wiring. Full unit suite (520 files, 6707 tests, 2 skipped) green, no regressions; tsc --noEmit clean; eslint clean (3 react-refresh/only-export-components warnings — same pattern already accepted on D1-D5, not new). * feat(labor): register /labor route in App.tsx (task F1) Eager-imports the Labor page beside Payroll and wires <Route path="/labor" element={<ProtectedRoute><Labor /></ProtectedRoute>} /> per design §2.2. Regression test mirrors the existing appNoVercelAnalytics.test.ts source-text-assertion pattern, since App.tsx's provider/router tree isn't practically unit-rendered. * feat(labor): mount collapsible Labor cost section on dashboard (task F2) Adds a "Labor cost" Collapsible section to src/pages/Index.tsx, mounting LaborPnlCard (C2/E1) in the financial cluster right after SalesVsBreakEvenChart — matching the existing <Collapsible>+<h2> idiom used elsewhere on the page. Defaults closed (laborCostOpen=false), per plan task F2, and is placed well away from the existing scheduling Labor Efficiency section further down the page to keep the two mental models (financial vs. scheduling) visually separate, per design §2.1. Test: tests/unit/indexLaborCostSection.test.ts (6 tests) — since Index.tsx isn't practically unit-rendered (large provider/hook tree), follows the established source-text-assertion pattern from appLaborRoute.test.ts: LaborPnlCard import present, laborCostOpen state declared default false, "Labor cost" heading + design-specified subtitle present, LaborPnlCard mounted scoped to selectedRestaurant.restaurant_id, Collapsible wired to laborCostOpen/setLaborCostOpen, and section placed after SalesVsBreakEvenChart with clear separation (>2000 chars) from the Labor Efficiency section. Confirmed RED first (section didn't exist), then GREEN. Full unit suite (522 files, 6716 tests, 2 skipped) green, no regressions; tsc --noEmit clean; eslint clean on touched files (the one pre-existing Index.tsx `any` error and two exhaustive-deps warnings predate this change, confirmed via git stash diff). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(labor): add "Labor" nav link to sidebar Operations group Adds a "Labor" entry (path /labor, Banknote icon) to the Operations navigation group in AppSidebar.nav.ts, right after Payroll, so the new /labor financial-view page (task F1) is reachable from the sidebar for owner/manager/chef and operations_manager roles — the same set of roles that already see Payroll and Reports. Tests: tests/unit/AppSidebar.nav.test.ts — Labor item present in the Operations group directly after Payroll; surfaced for operations_manager (Operations group survives that role's filter); surfaced for owner/manager (full nav). Confirmed RED first (item didn't exist), then GREEN. Full unit suite (522 files, 6719 tests) green, no regressions; tsc --noEmit and eslint clean on touched files. * refactor(labor): reuse balanceStateClassName for staffing-callout tone Labor.tsx was writing text-[hsl(var(--labor-over/under))] inline for the over/under staffing callouts instead of the shared balanceStateClassName helper laborPnlAnalytics.ts already exports (and LaborPnlCard already uses) — single source of truth for the dedicated --labor-* tone tokens per design doc §7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(labor): dedupe tone->bg-class mapping, extract KPI tile/callout loop Phase 6 (simplify) on the labor financial view: - LaborBalanceRibbon's balanceChipClassName and LaborVerdict's verdictDotClassName independently re-implemented the same over/under/balanced -> --labor-* background-token mapping. Extracted a shared laborPnlAnalytics.balanceStateBgClassName (the background counterpart to the already-shared balanceStateClassName) and made both components thin wrappers over it, so the tone->class mapping has one source of truth. Exported function names/behavior unchanged, no test changes needed. - Labor.tsx's 4-tile KPI row was four copy-pasted divs differing only in label/value; extracted a local KpiTile component. The over/under staffing-callout blocks were the same JSX twice differing only in tone; unified into one loop over [over, under] windows. No behavior change: full unit suite (521 files, 6719 tests, 2 skipped) green, typecheck clean, eslint clean (only pre-existing react-refresh/only-export-components warnings, same pattern already accepted on D1-D5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): labor window end anchored to end-of-day — addresses sound-logic useLaborPnlCore's laborCostWindow() built windowEnd at midnight-START of "today" instead of end-of-day. useLaborCostsFromTimeTracking feeds that straight into lookaheadPunchFetchRange(dateFrom, dateTo), which widens only the END of the time_punches fetch by OVERNIGHT_BUFFER_HOURS (18h) — so the fetch silently cut off at 6pm today, dropping every clock-in/out after that (and any shift whose clock_out landed past 6pm read as an incomplete shift and was dropped entirely, not just partially undercounted). Meanwhile dailySales (useSplhData, DATE-column query) has no such cutoff, so "today"'s laborCost/laborHours were undercounted relative to sales, making laborPct artificially low — contradicting design §3's clock-in-through-now requirement. Anchoring windowEnd at 23:59:59.999 instead of 00:00:00 fixes the fetch range; generateDateRange/format() in calculateActualLaborCost and useLaborCostsFromTimeTracking's query-key both normalize to Y-M-D and are unaffected by the time-of-day change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(labor): toggle selects period (today/week/month), intraday Day chart Phase-7 review finding #2: the Day/Week/Month toggle only re-bucketed a fixed 12-week window, so KPIs + verdict were identical across toggles and Day plotted ~84 days. Now the toggle is a period selector: - KPI row + verdict come from the period's payroll-grade daily series - chart shows the period's sub-buckets: Day = hour-of-day intraday (avg-rate labor SHAPE; totals stay payroll-grade), Week = by day, Month = by week - staffing callouts track the chart series (Day = hour ranges) New pure lib: currentPeriodWindow, dateInWindow, buildIntradayFinancialSeries (+ exported extractBalanceWindows, hourOfSale). Full tests updated/added. Finding #1 (labor/sales day-boundary tz mismatch when viewer tz != restaurant tz) accepted as a documented limitation shared with Payroll (design §10.5) — a tz-aware fix belongs in calculateActualLaborCost app-wide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(labor): keep /labor period fresh across midnight; single-round intraday labor Sound-logic re-review folds: - [major] the Day/Week/Month period window froze at mount (useMemo dep was [granularity, tz]); a page left open past midnight kept labelling yesterday as today. Now polls restaurant-tz date once a minute + on focus/visibility, updating state only when the date actually changes. Regression test added. - [minor] buildIntradayFinancialSeries priced labor off pre-rounded hours; now single-rounds off raw hours. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): refresh labor-cost fetch window across midnight; clamp target to [1,100] CodeRabbit findings (both major): - useLaborPnlCore's laborCostWindow froze at mount ([tz, weeks] memo), so after midnight the time_punches fetch stayed anchored to yesterday and dropped today's punches for BOTH the /labor page and the dashboard card. Extracted the today-poller into a shared useTodayInTimezone hook (removes the duplicate poller added to useLaborPnlAnalytics) and threaded todayStr into the fetch window. - EditableLaborTarget only had advisory min/max; a typed -5/0/500 committed garbage to target_labor_pct. Now clamps to [1,100] before commit. Tests: useTodayInTimezone.test.ts (new), clamp cases, all green (6736). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(labor): clearAllMocks in useTodayInTimezone beforeEach — addresses coderabbit (minor) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): heatmap estimated-flag via hourOfSale; Link CTA; doc + artifact cleanup Phase 9d CodeRabbit triage: - [major] hasHourlyBreakdown checked only sold_at, but buildSplhGrid derives the hour from sold_at OR sale_time — a sale_time-only POS had its heatmap falsely labelled 'Estimated'. Now uses hourOfSale(s, tz), matching buildSplhGrid. +test. - [nitpick] LaborPnlCard 'Open labor detail' now a <Link> (native keyboard/ new-tab/middle-click), matching the sibling 'Connect your POS' CTA. Test updated. - [minor] reconciled stale design-doc refs (§8/§9): buildFinancialSeries name, labor%=laborCost/sales, internal reconciliation — dropped usePnLAnalyticsFromSource. - [nitpick] reverted churned dev-tools/ scratch artifacts (phase7-diff.patch, codex-review-output.md) back to origin/main. - [major] EditableLaborTarget clamp already landed in 42903b9 (CodeRabbit confirmed). Full suite green (6737). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(labor): count open shifts through now, date-range picker, prototype visual polish Preview feedback (3 items): 1. $28 labor bug: still-open shifts (currently clocked in) counted zero hours (parseWorkPeriods drops un-clocked-out shifts). New appendOpenShiftClockOuts util closes open shifts at "now" (16h stale guard); wired into the payroll-grade daily cost (useLaborCostsFromTimeTracking throughNow opt-in) and the intraday labor shape. Fixes the KPI $ and the collapsing labor-% line. 2. Time axis + "now": top sales chart now shows hour labels (was hover-only); Day view caps at the current hour ("so far today") instead of running to 9 PM. 3. Date-range picker: preset dropdown (Today / This week / Last week / This month / Last month / Custom start–end) replaces the Day/Week/Month toggle; chart auto-buckets (hour-of-day / day / week); KPIs/verdict/callouts follow the range. Visual polish to match the approved prototype: - "What to do about it" recommendation cards (icon badge + tone + $ impact), from over/under windows + peak bucket + overall vs-target gap. - KPI tiles: accent dots, tone-colored labor-% + vs-target caption, per-tile sparklines (new Sparkline component). Range caption in the header. New/updated tests: openShiftPunches, Sparkline, range resolution + capHour, range-based hook API, findings cards. Full suite green (6760); build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): intraday now-tick, restaurant-tz picker bounds, clamps Sound-logic + CodeRabbit re-review of e832bae: - [critical] intraday sessions memo froze the open-shift 'now' synthesis: React Query structural sharing keeps data.punches reference-stable across content-identical refetches, so the chart's labor line collapsed mid-shift while the KPI (recomputed per queryFn) climbed. New useNowTick minute-ticker is a real dep of the sessions memo → intraday now advances with the clock. - [major] custom date-picker bounds used host-local/UTC math (off-by-one for non-UTC users); now derived from restaurant todayStr via addDaysStr. - [minor] revenue/labor-hr sparkline plotted sales; now sales/laborHours. - [minor] buildSalesVolumeGrid intensity clamped >=0 (refund cells). - [minor] hourOfSale validates sale_time hour is an integer 0..23. Tests: useNowTick, intensity clamp, malformed-hour rejection. Suite green (6763). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): guard useNowTick period; stricter sale_time hour prefix CodeRabbit closure (2 minors): - useNowTick normalizes an invalid periodMs (0/negative/NaN/Infinity) to 60s to avoid a busy interval. - hourOfSale rejects an empty/whitespace hour prefix (Number('') === 0 would otherwise pass ':30' as hour 0) via a /^\d{1,2}$/ check. Tests extended. Suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): add created_at/updated_at to synthetic open-shift punches CodeRabbit (PR bot): TimePunch requires created_at/updated_at; the synthetic clock_out/break_end objects omitted them (a contract gap the array-spread hid from tsc). Stamp both at 'now'. Test factory updated to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(lessons): reuse-hook data-granularity, clock-in-through-now, RQ structural-sharing memo freeze (PR #624) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
jdelgado2002
added a commit
that referenced
this pull request
Jul 26, 2026
…n-chart SPLH slider, arithmetic receipt (#650) * docs(scheduling): design — coverage chart explainer redesign Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(scheduling): fold Phase 2.5 design-review resolutions into coverage chart spec Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(scheduling): implementation plan — coverage chart explainer Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(scheduling): add demand field to HourlyStaffingRecommendation type Type-only change (task 1.1 of coverage-chart-explainer plan). Adds the raw pre-floor demand field so downstream code (staffingCalculator, coverageSummary, coverage chart) can distinguish demand-short from floor-short hours. Population lands in the next task. * feat(staffing): buildHourlyRecommendations computes demand pre-floor Threads raw demand = ceil(avgSales/targetSplh) (0 when sales/targetSplh <= 0) through buildHourlyRecommendations and derives recommendedStaff as max(demand, minStaff) directly, instead of delegating to calculateRecommendedStaff. Behavior-preserving for recommendedStaff; demand is new and lets the coverage chart split demand-short from floor-short later in this feature. Task 1.2 of coverage-chart-explainer plan. * feat(scheduling): thread demand + scheduledMax through CoverageHour summarizeCoverageHours now copies rec.demand into CoverageHour.demand (null when no rec for that hour, back-compat when recs are omitted) and computes scheduledMax as the per-hour maximum headcount alongside the existing conservative scheduled=min. Both are additive fields that lay the groundwork for the demand/floor split in CoverageChart — needed, delta, and all other CoverageHour semantics are unchanged, so buildVerdict, mergeUnderStaffedRange, and summarizeAreaCoverage (demand=null path) are unaffected. TDD: added 4 RED tests (demand copied per hour; demand null with no recs; scheduledMax = max of in-hour samples; scheduledMax = 0 with no coverage), confirmed failing, then GREEN. Full unit suite (7318 tests) + typecheck + lint clean on touched files. * feat(scheduling): coverageChartModel.classifyHour with boundary tests New pure module for coverage-chart presentation helpers. classifyHour(h, minStaff) buckets an hour into crit/floor/spare/ok/nodata: nodata when h.demand is null; otherwise derives needed = max(demand, minStaff) and compares against scheduled. Deliberately independent of h.needed so it stays correct under a hypothetical minStaff (e.g. settings-form preview) that hasn't round-tripped through the recommendation pipeline. TDD: 9 RED tests covering the five buckets plus boundaries (scheduled === demand, demand < minStaff pulling needed up to the floor, zero/zero/zero), confirmed failing (module didn't exist), then GREEN. Typecheck + lint clean on touched files. * feat(scheduling): coverageChartModel impliedLabor + laborConsistentSplh impliedLabor({ wage, splh, targetLaborPct }) -> { pct, overTarget } computes the on-chart SPLH slider's live labor-% readout (pct = wage/splh*100), flagging overTarget once pct clears targetLaborPct by more than a 0.05-point tolerance so a target hit within float noise doesn't flash red. laborConsistentSplh({ wage, targetLaborPct }) = wage / (targetLaborPct/100) gives the SPLH value at which the slider's track notch is drawn — where a manager's own labor goal puts the knob. TDD: 8 RED tests (pct math, overTarget boundary at target+0.05 from both sides, inverse wage/SPLH relationship, targetLaborPct=100 edge), confirmed failing (functions didn't exist), then GREEN. Targeted suite (coverageChartModel + coverageSummary + staffingCalculator, 68 tests), project-wide typecheck, and lint on touched files all clean. * feat(scheduling): coverageChartModel.buildReceipt ledger + asides buildReceipt(h, { minStaff, weekdayKey, wage, lookbackWeeks }) builds the pinned receipt's ordered ledger rows plus contextual asides for a single coverage column, per the design doc's worked example. - nodata (demand === null): no rows, single explanatory aside referencing weekdayKey/lookbackWeeks/scheduled (replaces the old "N / 0" reading). - otherwise: Avg {weekday} sales / ÷ target / = demand / min staff / needed / scheduled / kind-specific closing row (Short on demand | Short on floor | On target | Covered), each row tagged with a tone (default/critical/ warning/positive) for the UI to color. - "÷ target" is the implied target (round(projectedSales/demand)), not the live SPLH slider value — CoverageHour doesn't carry that raw value (only the already-folded demand), and the implied figure keeps the "sales ÷ target = demand" line on the receipt exactly self-consistent (no hidden Math.ceil to explain). Omitted when demand === 0 (a rec exists but avgSales <= 0, so there's nothing to divide). - Asides: implied SPLH at the scheduled count (skipped at scheduled === 0 to avoid a divide-by-zero), the mid-hour scheduledMax->scheduled note (only when they differ), and the floor-only explainer. Reuses classifyHour internally so the receipt's kind can never drift from the chart's own classification. TDD: 7 RED tests added (nodata; crit/floor/ok/spare row sets incl. the design doc's own $503/17-people/$30 example, which also validated the "implied target" derivation against the doc's illustrative numbers; mid-hour aside presence boundary; demand===0 edge omitting the ÷ target row), confirmed failing (function didn't exist), then GREEN. Targeted suite (coverageChartModel + coverageSummary + staffingCalculator + coverageChart component test, 103 tests), project-wide typecheck, and lint (touched files) clean. Full 7300+-test unit suite was invoked but stalled in the sandbox background scheduler without producing output after several minutes (same intermittent issue noted on commit 31e39de/task 2.2); this change only adds one new pure export with zero edits to existing exported behavior, so it was not blocking. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(scheduling): coverageChartModel.chartSummaryLabel rolled-up aria + windows list chartSummaryLabel(hours, minStaff) rolls a day's CoverageHour[] into the chart's accessible summary: an "N short on demand, M at the floor over K hours" string for the sr-only <p>, plus an ordered understaffedWindows list (one entry per crit/floor hour) feeding a sr-only <ul aria-label="Understaffed windows"> — porting CoverageStatusStrip's screen-reader gap enumeration guarantee onto the new chart (design doc, Phase 2.5 resolutions #1-#2). Classifies every hour via classifyHour (not h.delta/h.needed) so counts and windows always agree with the chart/receipt's own minStaff-aware classification, even under a live settings-form minStaff preview. TDD: 4 RED tests (rollup counts across crit/floor/ok/spare/nodata; singular "1 hour" boundary; zero-shorts boundary with empty windows list; per-window label/order/exclusion), confirmed failing (export didn't exist), then GREEN. Full unit suite (584 files, 7346 tests passed, 2 skipped) + project-wide typecheck + lint (touched files) clean. * feat(coverage-chart): replace Area/Delta toggle with single SVG chart Stage 3.1 of the coverage chart explainer redesign. CoverageChart.tsx is now a single SVG chart with a real people y-axis, replacing the old AreaColumn/DeltaColumn/Legend two-view toggle: - Per hour: a scheduled bar (--primary), a solid demand slice (--destructive) when short of raw demand (crit), a dashed/hatched floor slice (--warning) covering the gap up to the minStaff floor (crit/floor), and a hatched nodata ghost via an SVG <pattern> for hours with no sales history (replacing the old misleading "N / 0"). A `floor N` rule line marks minStaff across the plot. - New props: minStaff, selectedStartMin, onSelect, onQuickAdd (type only — its UI affordance is Stage 3.3); `view`/`targetSplh` removed. - Accessibility: role="toolbar" container (not role="img", which would flatten the interactive subtree) with role="option" columns, roving tabIndex (selected=0, rest=-1), ArrowLeft/ArrowRight to move selection, and a pure exported `columnAriaLabel` helper for each column's one-line summary. A sr-only <p> carries the chartSummaryLabel() rollup and a sr-only <ul aria-label="Understaffed windows"> enumerates every crit/floor hour, porting CoverageStatusStrip's screen-reader gap guarantee onto the new chart. - All fills/strokes use hsl(var(--token)) semantic colors only; the floor slice's dashed stroke and the nodata hatch pattern give each state a texture, not just a color. ShiftTimelineTab.tsx gets the minimal compatibility shim needed for the new prop contract: a local selectedStartMin state, minStaff via the existing computeMinStaffFromCrew(min_crew, min_staff), and onQuickAdd={handleGapClick} (reusing the existing gap-click handler). The coverageView state, Area/Delta ToggleGroup, and CoverageStatusStrip render are intentionally left in place — their removal is bundled with the full Stage 5 wiring (sliderTarget, avgWage, receipt panel) per the design doc's "E. ShiftTimelineTab.tsx wiring" section, out of scope here. TDD: tests/unit/coverageChart.test.tsx fully rewritten (25 tests) to target the new contract — column layout/alignment, roving-tabindex keyboard nav, demand/floor SVG slice rendering, the nodata hatch pattern, the floor rule, semantic-color-only assertion, the sr-only summary/list, the four-swatch legend, and columnAriaLabel's per-kind wording. Confirmed RED (23 failed) against the old component, then GREEN. Full unit suite (584 files, 7343 tests, 2 skipped) + project-wide typecheck + lint (touched files) clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(coverage-chart): lock sticky y-axis gutter contract mirroring TimelineLane CoverageChart's y-axis gutter already carried the sticky left-0 z-10 w-[120px] classes from Stage 3.1's SVG rewrite (mirroring TimelineLane's sticky label column, design doc §Design-review resolutions #3), but no test pinned that contract. Add a data-testid to the gutter and cover: the required class set, DOM order (gutter before the scrollable toolbar/plot region), tick rendering scoped to the gutter, and that the plot columns remain aligned to minToPct with the wrapper in place. RED: 3 of 4 new tests failed (data-testid didn't exist) against the existing markup. GREEN: added the data-testid only, no layout changes. Full unit suite (585 files, 7347 tests) + typecheck + lint (touched files) clean. * feat(coverage-chart): one-click hover "+" quick-add on crit/floor columns CoverageChart tracks hovered/focused startMin and reveals a small "+" button over crit/floor columns (only when onQuickAdd is supplied), calling onQuickAdd(startMin) on click without triggering column selection — preserving the one-click quick-add parity with the old CoverageStatusStrip gap-click (design doc resolution #5). Omitting onQuickAdd keeps the affordance hidden entirely (back-compat). TDD: 5 RED tests (hidden without onQuickAdd; reveals + calls onQuickAdd for crit; same for floor; never renders for spare/ok/nodata; hides on mouseleave; accessible label names the hour), confirmed failing, then GREEN. Full unit suite (585 files, 7353 tests passed, 2 skipped) + project-wide typecheck + lint (touched files) clean. * feat(scheduling): SplhSlider sub-component for coverage chart explainer Native range input (25-120, step 5) with a live implied-labor readout (`→ X% labor at $W/hr`) + red/green pill from `impliedLabor`, a notch on the track at `laborConsistentSplh` labeling where the manager's own labor goal sits, a Reset button, and a manager-only Save button gated by a caller-computed `canSave` prop with an `isSaving` pending state. `aria-valuetext` carries the implied labor % alongside the native `aria-valuenow`. Purely presentational — holds no staffing state of its own; `onChange`/`onSave`/`onReset` let the eventual `ShiftTimelineTab` wiring (Stage 5) own the slider/override/save state and re-run the staffing pipeline live. TDD: 11 RED tests (range bounds/value; onChange on drag; readout text; pill wording+styling at both sides of the target+0.05 threshold; notch value/label; Reset click; Save hidden for canSave=false, visible+wired for true; Saving pending state disables the button; aria-valuetext content), confirmed failing (module didn't exist), then GREEN. Full unit suite (586 files, 7364 tests passed, 2 skipped) + project-wide typecheck + lint (touched files) clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(scheduling): CoverageReceipt sub-component (buildReceipt + quick-add + debounced aria-live) New standalone panel rendering buildReceipt's ledger rows/asides for the selected coverage-chart hour, with an "Add shift for this hour" action on crit/floor hours (calls onQuickAdd(startMin); omitted onQuickAdd or an ok/spare/nodata hour hides it, back-compat with the chart's own hover "+"). The visible ledger redraws live on every prop change (including SPLH-slider drag frames once wired in Stage 5), but the aria-live="polite" region only re-announces on commit (window pointerup/keyup) or on an actual hour-selection change, per design doc §Design-review resolutions #4 — screen readers get one coherent sentence per commit, not a stream of per-frame updates. TDD: 11 RED tests (row/value rendering; nodata asides-only path; quick-add present for crit/floor, calling onQuickAdd with startMin; absent for ok/spare/nodata and when onQuickAdd is omitted; aria-live region content; immediate announce on hour-selection change; frozen announce through a same-hour "drag frame" until pointerup/keyup fires), confirmed failing (module didn't exist), then GREEN. Full unit suite (587 files, 7375 tests passed, 2 skipped) + project-wide typecheck + lint (touched files) clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(scheduling): thread sliderTarget preview state into ShiftTimelineTab Adds the SPLH slider's local preview state (design doc §B/§E, plan 5.1): `sliderTarget` (null = follow saved settings) now feeds `useWeekStaffingSuggestions`'s `settingsOverrides` argument via a new pure `resolveSettingsOverrides` helper, replacing the hard-coded `null`. Also destructures `updateSettings`/`isSaving` from the hook and derives `avgWage` (computeAvgHourlyRateCents(employees)/100) alongside the existing `minStaff` derivation — all four feed the SplhSlider/CoverageReceipt wiring landing in the next task; none are rendered yet. TDD: 3 RED tests for resolveSettingsOverrides (null passthrough, override object, 0-is-not-null), confirmed failing (function didn't exist), then GREEN. Full unit suite (588 files, 7378 tests passed, 2 skipped) + project-wide typecheck + lint (touched files, 0 errors) clean. * feat(scheduling): fold coverage panel into scrollable chart + pinned receipt (Stage 5.2) ShiftTimelineTab now renders the coverage panel as a flex row: the scrollable CoverageChart (header folds in CoverageVerdict/CoverageDemandInfo) on the left, and a pinned CoverageReceipt outside the horizontal scroll on the right (stacking below on mobile). Removes the coverageView state, the Area/+/- ToggleGroup, and the CoverageStatusStrip render per design doc §E. Quick-add entry point relocates from CoverageStatusStrip's gap cells to CoverageReceipt's "Add shift for this hour" button (crit/floor hours only), calling the same handleGapClick(startMin) — behavior unchanged, per §D. Adds two pure helpers backing the receipt's default-hour selection: - pickDefaultHour(hours, minStaff): worst crit hour by demand deficit (demand - scheduled), earliest startMin on ties, else first hour, else null for empty input. - weekdayKeyForDay(day): noon-anchored weekday name for the receipt's "Avg {weekday} sales" row, matching the dayStringToDow DST-safety pattern. Updates the one pre-existing overlay test whose click target was the removed CoverageStatusStrip gap cell to go through the relocated CoverageReceipt button instead, adding a `demand` field to its recommendation fixture so the hour classifies as `crit` under the new demand-driven classifyHour pipeline (previously only the legacy needed/delta pipeline saw it as short). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(scheduling): gate SPLH slider Save to owner/manager/operations_manager (Stage 5.3) Mounts <SplhSlider> above the coverage chart in ShiftTimelineTab, wiring live preview + Reset (available to every role) and Save (gated). - Add SPLH_SAVE_ROLES = ['owner', 'manager', 'operations_manager'], mirroring the existing ad-hoc inline predicate already used in TimePunchesManager.tsx and Inventory.tsx (design doc §Design-review resolutions #6 — there's no shared role-gate helper for this style of check yet, so this follows the established convention rather than introducing a new one). - canSaveSplhTarget reads selectedRestaurant?.role via useRestaurantContext() and is passed to SplhSlider's canSave prop, which hides the Save button entirely for non-gated roles while the range input (live preview) and Reset stay unconditionally available. - handleSplhChange/handleSplhSave/handleSplhReset drive a local sliderTarget override; Save calls updateSettings({ target_splh }) then clears the override so subsequent renders read straight off activeSettings again. - targetLaborPct sourced from activeSettings.target_labor_pct, falling back to the useStaffingSettings DEFAULTS value (22) before settings load. - Slider is hidden entirely when there's no target to preview yet (targetSplh === null), matching the design doc's "No demand configured" state. Tests (RED -> GREEN): - New describe block in shiftTimelineTab.test.tsx: Save button shown for owner/manager/operations_manager, hidden (but preview+Reset kept) for staff/chef/kiosk/collaborator_accountant and for no-restaurant-selected, slider drag + Reset round-trip, Save persists via updateSettings, and the slider not rendering at all when activeSettings hasn't loaded. - ShiftTimelineTab now calls useRestaurantContext() directly, so shiftTimelineTab.mobileLayout.test.tsx and shiftTimelineTabOverlay.test.tsx needed a `@/contexts/RestaurantContext` mock (defaulting to 'owner') to avoid the "must be used within a RestaurantProvider" throw. Verified: full unit suite (588 files / 7397 tests, 2 skipped) green, typecheck clean, lint clean on touched files. Note for the eventual PR body (design doc requirement): the gated role set is ['owner', 'manager', 'operations_manager']. * feat(scheduling): reshape coverage loading skeleton to mirror slider/axis/chart/receipt (Stage 5.4) The loading skeleton previously used generic bars unrelated to the new coverage-panel layout (slider panel -> chart -> axis -> lanes, plus a pinned receipt column). Reshaped it to mirror that structure exactly — same lg:w-[320px] lg:shrink-0 receipt column, same top-to-bottom order in the scrollable chart column — so nothing jump-shifts once real data lands, per design doc resolution #9. TDD: added a RED test asserting dedicated skeleton bands (skeleton-splh-slider/-coverage-chart/-timeline-axis/-coverage-receipt), the receipt band living in its own pinned column separate from the chart/axis column, and slider-before-chart-before-axis DOM order; confirmed failing against the old generic skeleton, then GREEN. Full unit suite (589 files, 7398 tests passed, 2 skipped) + project-wide typecheck + lint (touched files, 0 errors) clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(scheduling): delete unused CoverageStatusStrip.tsx (Stage 6.1) Grep confirmed no production importer remained after Stage 5.2 folded its gap-click/sr-only-enumeration duties into CoverageChart — only its own dedicated test file still referenced it. Deleted both the component and tests/unit/coverageStatusStrip.test.tsx, and cleaned up two now-stale comments (coverageSummary.ts's formatCoverageHour doc, timelineComponents .test.tsx's file-header note) that still named it as a live consumer. TDD: confirmed the dedicated test suite green (14 tests) as baseline, deleted the source file (RED — the test file's import failed to resolve), then deleted the obsolete test file itself (GREEN). Full unit suite (588 files, 7384 tests passed, 2 skipped) + project-wide typecheck + lint (touched files) clean. * refactor(coverage-chart): dedupe demand/minStaff floor formula into neededFor() Math.max(demand, minStaff) was reimplemented 6 times across coverageChartModel.ts (classifyHour, buildReceipt, chartSummaryLabel) and CoverageChart.tsx (computePeak, columnAriaLabel, the SVG render loop). Extracted a single exported neededFor(demand, minStaff) helper in coverageChartModel.ts and pointed every call site at it. Pure refactor, no behavior change — full unit suite (587 files, 7384 tests) unchanged. Left staffingCalculator.ts's own Math.max(demand, minStaff) untouched: that's a deliberately independent upstream computation (recommendedStaff), not the presentation-layer "needed" derivation this helper centralizes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): coverage chart wiring — addresses performance, sound-logic, maintainability, codex - Memoize resolveSettingsOverrides(sliderTarget) so useWeekStaffingSuggestions doesn't recompute the whole week's staffing pipeline on every unrelated re-render (e.g. rAF drag-draft frames) while an SPLH preview is active (performance + sound-logic). - Feed CoverageChart's selectedStartMin from the resolved selectedHour (pickDefaultHour fallback) instead of the raw click state, so the chart's roving-tabindex column always agrees with whichever hour the pinned receipt is showing, including before any explicit click (sound-logic). - Fix handleSplhSave to only clear the slider override if it still matches the value just persisted, so a newer in-flight drag isn't silently discarded by an in-flight Save's resolution (sound-logic). - Remove the redundant outer pl-[120px] wrapper around CoverageChart — CoverageChart already renders its own internal w-[120px] y-axis gutter, so the extra wrapper double-offset the chart against the axis ticks/lanes below (codex). AreaCoverageStrips has no internal gutter, so its pl-[120px] wrapper is unaffected and stays. - Delete calculateRecommendedStaff (staffingCalculator.ts) and its dedicated test block — dead code since task 1.2 inlined its logic at the only call site; grep confirms zero remaining production callers (maintainability). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): coverage chart demand/nodata correctness (CodeRabbit iter 2) - coverageSummary.ts: a staffing rec with projectedSales <= 0 has no demand target to derive from (buildHourlyRecommendations floors its own demand to 0 in that case). summarizeCoverageHours was passing that 0 through as a real "0 people needed" target instead of nodata, contradicting the design doc's own nodata definition ("no rec, or projectedSales ≤ 0"). - coverageChartModel.ts: guard impliedLabor/laborConsistentSplh against a 0 splh/targetLaborPct denominator so a misconfigured or pre-bounds-check value degrades to 0 instead of Infinity/NaN in the labor readout. - tests: cover both fixes, add the missing negative-sales case in staffingCalculator.test.ts, and prefix the touched staffing-calculation tests with CRITICAL: per coding guidelines. Fixed a stale test fixture in shiftTimelineTab.test.tsx (Task 2d wiring rec was missing the required `demand` field, which the coverageSummary.ts fix surfaced as NaN render warnings). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(review): use should/when naming for staffingCalculator demand tests (CodeRabbit iter 3) Rename the added demand/floor test descriptions to the "should X when Y" convention flagged by CodeRabbit for consistency and searchability. * fix(scheduling): disambiguate SplhSlider Save button accessible name The new SplhSlider renders a persistent "Save" button whose accessible name collided with the shift-edit popover's own "Save" button, tripping a Playwright strict-mode violation in timeline-edit-create.spec.ts. Add an aria-label ("Save sales per labor hour target" / "Saving sales per labor hour target"); visible text unchanged. Anchor the matching unit assertions to /^save$/i so they target the popover button, not the slider. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(scheduling): e2e coverage-chart explainer — render + SPLH slider Adds tests/e2e/coverage-chart-explainer.spec.ts covering the redesigned coverage panel end-to-end against the real staffing pipeline: - Seeds one $15/hr server + a mid-day shift + ~2 weeks of daily-spread sales so today's weekday has demand (~4/hr) against a lone scheduled server, guaranteeing demand-short (crit) columns. - Asserts the single coverage chart (role="toolbar"), crit columns, the legend's demand-vs-floor split, the on-chart SPLH slider with its live implied-labor readout (default $60 → 25.0% at $15/hr) + over-target pill, the owner-visible Save button, and the pinned arithmetic receipt with its quick-add. - Drives the slider via keyboard End/Home ($120 → 12.5% "On target"; $25 → 60.0% "Over target") to prove the readout + pill update live. Timezone pinned to UTC and wage fixed at exactly $15.00 for deterministic percentages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(scheduling): rework coverage explainer to match the approved mock The shipped coverage panel diverged from the approved interactive prototype — it read as an undifferentiated block chart with a terse status line. Rework the four presentation surfaces to faithfully match the mock, translating its raw hex/serif design language into the app's semantic tokens and typography: - CoverageVerdict: one plain-English, two-clause sentence that distinguishes demand-short ("Sales justify N more people-hours…, worst at TIME") from floor-only ("Another M hours only trip the K-person floor — demand there is already met"), plus a category chips row (short on demand / at the floor only / covered / no sales history). Extend buildVerdict() with the split fields (demandShort*, floorOnly*, worstCrit, covered/nodataHours) that drive it. - CoverageChart: discrete inset bars instead of one abutting block — scheduled fill + solid primary cap, a solid-red demand-short slice, a dashed-amber floor-only cap (texture, not colour alone), a foreground needed tick, and on-bar −N shortfall labels centred in the gap they name. - CoverageReceipt: an "The arithmetic" eyebrow + hour header, the ledger, and the verdict set off below a divider as an emphasised total. - SplhSlider: reframed as "Each labor hour should bring in $X → Y% labor", with the labor-consistent notch and $min/$max scale ends. Semantic tokens only (destructive/warning/primary/success/muted-foreground) — no raw hex. Unit tests updated for the new verdict shape and the relocated dashed floor cap; typecheck, lint, and the 118 coverage unit tests are green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(scheduling): update no-demand verdict copy assertion The CoverageVerdict rework changed the no-demand prompt to "Add staffing targets to see where sales justify more hands." Point the shiftTimelineTab wiring test at the new copy so the Unit Tests CI job goes green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(scheduling): inset the coverage-chart nodata ghost so hatched hours read as separate bars The "no sales history" hatch rect spanned the full column width while every drawn data bar is inset by pad = width * 0.16, so consecutive nodata hours abutted into one continuous hatched block instead of discrete bars. Hoist the pad/bx/bw inset above the nodata branch and apply it to the ghost too, matching the mock's visually-separated bars. Adds a regression test pinning the ghost's inset geometry to the data bars' width. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(scheduling): paginate the coverage sales query so recent days aren't dropped The hourly-sales query in useWeekStaffingSuggestions was an unbounded select, subject to PostgREST's default 1000-row cap. A busy restaurant's multi-week lookback exceeds that, and because rows are ordered by sale_date ascending the truncation silently drops the most-recent days first — including today, the default-selected day — so the coverage chart rendered every hour as "No sales history". Paginate in 1000-row pages (matching the adjacent time-punch query and useSplhData's fetchAllSales) so the full lookback window is fetched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
jdelgado2002
added a commit
that referenced
this pull request
Jul 28, 2026
* docs(spec): PR comment response gate design
Every review finding (bot or human) must carry a visible verdict reply on
the PR: agreed + commit, pushed back + reason, or ignored + reason. CI
audits and blocks; the /dev session authors the replies.
Co-Authored-By: Claude <noreply@anthropic.com>
* docs(spec): fold Phase 2.5 design-review feedback
- Verify 'agreed' replies cite a real PR commit SHA (knownShas passed
into the pure classifier; audit fetches the PR commit list once).
- Decide bot identity by GraphQL actor __typename, with the [bot] suffix
and login list as backstops, so the list can't silently rot.
- Spell out PR-number resolution across the four differing event shapes.
Co-Authored-By: Claude <noreply@anthropic.com>
* docs(plan): PR comment response gate implementation plan
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(pr-triage): verdict vocabulary and reply composition
* feat(pr-triage): parse verdicts from marker and hand-typed replies
* feat(pr-triage): classify findings into answered and unanswered
* feat(pr-triage): render the check-run summary
* feat(pr-triage): audit, list and reply CLI verbs
* ci: block merge while review findings lack a verdict reply
Adds the pr-comment-response check run, triggered on review comments,
reviews, and PR sync/open events. It fetches dev-tools/pr-triage.js from
the base branch (never the PR head) via pull_request_target so forked
PRs still get a status without ever checking out or running PR code,
then runs `audit --pr <N>` and publishes success/failure as a check run
named exactly `pr-comment-response`.
* docs(workflow): require a verdict reply per finding in Phase 9d
Wires dev-tools/pr-triage.js into the /dev workflow: Phase 9d now
replies to every finding with list/reply before classifying, Phase 9e
requires audit --pr <PR> to exit 0, and dev-tools/README.md documents
the three verbs.
* refactor(pr-triage): dedupe finding construction, parallelize independent PR reads
classifyThreads built near-identical finding objects for threads and
reviews inline; factor both into a shared makeFinding helper. In runCli,
fetchPr (GraphQL threads/reviews) and the commits fetch (REST) don't
depend on each other's result, so run them concurrently instead of in
series.
No behavior change — full prTriage.test.ts suite (44 tests) still passes.
* fix(review): wire MIN_SHA_PREFIX into commit regex, drop nested ternary, surface commits-fetch failures
- citesKnownCommit now builds its SHA regex from MIN_SHA_PREFIX instead of
hardcoding {7,40} alongside the unused constant (maintainability, ocr-rules).
- renderSummary's unanswered-row location is now built by a small
formatLocation() helper instead of a nested ternary (ocr-rules).
- runCli's audit path no longer swallows a failed/malformed PR-commits fetch
silently: it now logs a visible warning (which lands in the same
stdout+stderr stream the check-run summary is built from) before falling
back to skipping commit-citation verification, and a test covers the
failure path (sound-logic).
Addresses: maintainability (dev-tools/pr-triage.js:134), ocr-rules
(dev-tools/pr-triage.js:279, :134), sound-logic (dev-tools/pr-triage.js:483).
* fix(review): move github-context expressions into env vars in pr-comment-response workflow
Six ${{ github.* }} expressions were interpolated directly into shell run:
blocks (repository name, event name, default branch, and step outputs).
All values here are fixed-format/numeric with no exploit path today, but
the pattern is the one the yaml script-injection rule exists to prevent
repo-wide, and this is a pull_request_target workflow. Route every value
through env: instead, matching the pattern GitHub's own hardening guide
recommends.
Addresses: ocr-rules (.github/workflows/pr-comment-response.yml:58).
* fix(review): correlate CHANGES_REQUESTED answers per reviewer
Addresses the sound-logic reviewer's critical finding. The review-level
branch marked a CHANGES_REQUESTED review answered if ANY other review
carried a maintainer verdict — so one reply satisfied every reviewer
blocking at once, and a verdict written earlier could pre-answer a later
finding.
- Collapse reviews to each author's latest, mirroring how GitHub decides
whether a reviewer is currently requesting changes.
- Require the answering review to be later AND to name the reviewer.
- Fetch submittedAt so ordering is available.
- Hold marker-form replies to the same rationale floor as hand-typed
ones, so a hand-written marker reply can't pass with 'ok'.
52/52 unit tests pass. Design doc records the amended contract and why
naming-based correlation was preferred.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(review): fail closed, fix hyphen-login crash, answer review-level findings
CodeRabbit Phase 7c fold. The theme across most findings was that the
gate failed OPEN — reporting success on data it could not read.
Critical: mentionsLogin escaped '-' as '\-', an invalid identity escape
outside a character class under the /u flag. Every hyphenated reviewer
login (chatgpt-codex-connector, copilot-pull-request-reviewer) threw and
took the whole audit down. Own tests missed it because the bot-check
short-circuits first; added a regression test.
Fail closed everywhere the PR cannot be fully read:
- GraphQL errors or a missing pullRequest node now throw instead of
returning empty (which had silently passed as 'no findings').
- A failed PR-commits fetch is an error, not a skipped verification.
- A thread whose replies exceed one page refuses to render a verdict.
- Exit 2 publishes a FAILED check, never a pass.
Also:
- Paginate reviews on their own cursor; the old query re-read the same
first 100 on every thread page and dropped anything beyond.
- Add reply --review <login> so a CHANGES_REQUESTED review, which is
blocking but has no thread to nest under, has a supported answer path.
- Reject bare/non-numeric --pr (Number(true) === 1 silently targeted #1).
- Make repo resolution injectable; tests no longer shell out to gh
(suite runtime 4s -> 11ms).
- Escape pipes in summary cells; CodeRabbit badge rows were splitting
the table into bogus columns.
- Prefer backticked SHAs when verifying an 'agreed' citation.
- Re-audit on comment/review edited+deleted so an edited-away verdict
cannot leave a stale green.
- Document the fork limitation honestly and withdraw the design's
promise to list issue-level comments (nothing ever fetched them).
63/63 unit tests pass; real-data smoke test on PR #657 exits 1 with the
five unanswered bot findings.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ci): report gate-not-installed instead of 404ing, stop cancelling runs
Two failures on this PR's own CI, both in the new workflow:
- The auditor is fetched from the DEFAULT branch (deliberately — a PR must
not swap out the script that grades it), but it isn't on main until this
merges, so the fetch 404'd and the job died. It now publishes a NEUTRAL
check saying the gate isn't installed yet. Claiming a pass would be a
lie; failing would block the PR that installs the gate.
- cancel-in-progress cancelled earlier runs on a burst of bot comments,
and a cancelled run shows as a FAILED check — red for a reason having
nothing to do with unanswered findings. The audit takes seconds; let
each event finish.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(review): COMMENTED must not clear a change request; verify review-level commits
Phase 9d fold of 13 inline findings on PR #662.
Two real logic bugs (both Codex P1):
- latestReviewPerAuthor collapsed to the newest review of ANY state, so a
routine follow-up COMMENTED review retired a still-open
CHANGES_REQUESTED. GitHub only clears one on APPROVED or DISMISSED, and
bots post COMMENTED reviews constantly — this silently dropped blocking
findings. Only state-setting reviews now count.
- The review-level path accepted an 'agreed' verdict without verifying the
cited commit, while the thread path required it — making --review the
soft way in. Both paths now verify.
Correctness:
- gh api --paginate emits one JSON array PER PAGE; JSON.parse chokes on the
concatenation. Added --slurp and flatten (CodeRabbit critical, Codex P2).
- cell() escaped | without first escaping \, which CodeQL flagged as
incomplete escaping: a body ending in \ produced a live separator.
- Emoji strip used [✅↩️⏭️], repeating U+FE0F inside one character class.
- The post-reply resolve lookup could throw after the reply was already
public, turning a successful reply into a failure that invites a
duplicate re-run (Codex P2).
Usability:
- reply now rejects --comment and --review together instead of silently
taking one path (Copilot).
- list names the flag that actually works per finding kind; it used to
print '--comment' even for review-level findings (Copilot).
- Documented push-before-reply in the skill, not just the workflow script.
- Dropped the issue_comment trigger: issue-level conversation is out of
scope, so those runs could never change the outcome.
70/70 unit tests pass; typecheck clean.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(review): don't disarm the gate on non-404 fetch errors
Copilot round on PR #662.
- The bootstrap fallback treated ANY failure to fetch the auditor from the
default branch as 'not installed' and published a neutral check. A rate
limit or token problem would therefore silently disarm the gate — the
exact fail-open this check exists to prevent. Only a genuine 404 now
means 'not installed'; anything else fails the job loudly.
- reply validates --comment/--review before composing, so the body is only
built once the target is known.
- README said the check 'blocks merge'; it only blocks once added as a
required status check in branch protection. Stated accurately.
70/70 tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ci): drop concurrency group, document the bot-trigger approval caveat
Two problems observed in this PR's own runs of the new workflow:
- Runs kept showing as 'cancelled' despite cancel-in-progress: false.
GitHub cancels a PREVIOUSLY PENDING run when a newer one joins a
concurrency group, and a burst of bot comments produces exactly that.
Each cancellation surfaces as a FAILED check — red for a reason
unrelated to unanswered findings. Removed the group; the audit takes
seconds, so running every event is the cheaper trade.
- A run triggered by Copilot came back action_required: GitHub held it for
maintainer approval. While that holds, a finding posted after the last
push won't turn the check red on its own. Documented plainly, and added
workflow_dispatch so a maintainer can re-run by hand. pull_request_target
on push (human actor, never gated) and the local Phase 9d audit both
still cover the gap.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(review): valueless --commit crashed with a TypeError
Copilot round 3 on PR #662.
parseArgs gives a valueless flag the boolean true, so `--commit` with no
value reached (commit ?? '').trim() and threw a raw TypeError instead of
the usage error the author needs. Coerced with String() and rejected the
literal 'true'. Same coercion applied to rationale.
Also realigned the design doc's trigger table with the workflow: the
issue_comment trigger was removed two commits ago but the doc still listed
it and described PR-number resolution across four event shapes.
71/71 tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
* docs(lessons): retrospective from the PR comment response gate (#662)
Six lessons on gate design and workflow, the central one being that a
gate which errors must fail CLOSED — three reviewers independently caught
variants of 'could not read the PR' rendering as 'nothing to report'.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(review): reject valueless --comment/--review flags
Copilot round 4 on PR #662. parseArgs gives a valueless flag the boolean
true, so `--comment` with no value would have built a request to
/comments/true/replies, and `--review` with no value a mention of '@true'.
--comment now requires a numeric id and --review a login, with the boolean
itself as the discriminator (the string 'true' passes a login pattern).
74/74 tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ci): drop PR-controlled triggers, add scheduled re-audit and head-race guard
CodeRabbit round 5 — three Major findings on the workflow, all correct.
SECURITY: pull_request_review and pull_request_review_comment run the
workflow file FROM THE PR (verified: a run reported headBranch =
feature/pr-comment-response-gate). A contributor could therefore edit this
YAML in their own PR to publish a passing check and disarm the gate —
fetching pr-triage.js from the default branch protected the script but not
the workflow invoking it. Both triggers removed. They were also landing in
action_required for bot actors, so they were unreliable regardless.
The staleness those triggers covered is now handled by a 30-minute
schedule that re-audits every open non-draft PR. Schedule,
pull_request_target and workflow_dispatch all run the DEFAULT branch's
copy, which no PR can modify. Forks are now audited on the same footing,
since the removed triggers were the only read-only ones.
RACE: head_sha was read only in the publish step, so a synchronize during
the audit would stamp a verdict computed against the old commit onto the
new one. The head is now captured before the audit, re-read after, and a
mismatched result is discarded rather than published.
Also documents workflow_dispatch in the design doc's trigger matrix.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ci): --slurp cannot combine with --jq; fail closed on head-read errors
CodeRabbit/Copilot round 6.
CRITICAL: 'gh api --paginate --slurp --jq' is rejected outright — 'the
--slurp option is not supported with --jq or --template'. The scheduled
branch would have died before selecting any PRs, so the 30-minute
re-audit (the entire staleness mitigation for dropping the review
triggers) would never have run. Verified against the live CLI. Now
paginates to a file and runs jq over it separately.
The two head-SHA reads no longer depend on ambient 'set -e' state, which
was off on the first loop iteration: a failed read left BEFORE empty, the
!= comparison then read as 'the PR advanced', and the loop continued
WITHOUT setting FAILED — a fail-open in the one file that exists to fail
closed. Both reads are now explicitly checked and fail closed.
renderSummary's help snippet showed only the --comment form, so a
review-level blocker came with instructions that cannot resolve it. Both
forms are now shown and the prose no longer calls every answer 'threaded'.
74/74 tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(review): require a strictly later verdict, not same-second
Copilot round 7. Review-level correlation excluded only strictly EARLIER
verdicts, so one sharing the change request's exact submittedAt counted as
an answer. GitHub timestamps resolve to the second, so an equal stamp
cannot be ordered — and 'cannot be ordered' must not read as 'came after',
or a verdict written first pre-answers the finding. Now strictly later.
75/75 tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(review): a leading @mention must not pad a trivial rationale
Copilot round 8, and a hole I opened myself when adding the @mention for
review-level replies: the mention counted toward MIN_RATIONALE_LENGTH, so
'Agreed — @coderabbitai ok' passed the non-trivial-rationale guarantee.
Leading mentions are now stripped before the length is measured, on both
the marker and hand-typed paths — a mention addresses, it does not explain.
77/77 tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
jdelgado2002
added a commit
that referenced
this pull request
Jul 29, 2026
Phase 7a flagged useWeekPublicationStatus as diverging from the corrected RPC, but reviewed it against pre-#671 code (hardcoded ${weekEnd}T23:59:59Z literals). That code no longer exists: #671 already switched the shifts count to full instants, so a viewer in the restaurant's own timezone agrees with restaurant-local bucketing. What remains is non-goal #1 from the #671 design doc — the week boundary is anchored to the viewer's timezone, not the restaurant's. This change does not introduce it, but makes it observable in one more case, so record that plainly rather than deferring silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jdelgado2002
added a commit
that referenced
this pull request
Jul 30, 2026
…ezone (#675) * docs(spec): design for timezone-aware publish/unpublish week bucketing publish_schedule and unpublish_schedule bucket shifts with a bare start_time::date, which resolves against the database session TimeZone (UTC on Supabase) rather than the restaurant's IANA zone. Late-night shifts land on the wrong side of the week boundary in both directions. Design adopts the v_tz pattern already used by get_open_shifts in the same code family, and specifies pgTAP coverage at both week edges. Follow-up to tracked non-goal #2 of docs/superpowers/specs/2026-07-27-publish-week-tz-offbyone-design.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(spec): fold in Phase 2.5 supabase design review Critical finding accepted: publish_schedule/unpublish_schedule have never carried a GRANT/REVOKE, so both still hold the default PUBLIC EXECUTE while being SECURITY DEFINER — an anonymous caller can publish or unpublish any restaurant's week. The migration now also revokes anon and grants EXECUTE to authenticated + service_role, with has_function_privilege assertions. The in-body identity check stays a tracked follow-up. Three minor doc fixes: label the get_open_shifts snippet as a composite of two migrations, name schedule_publications.published_by as the NOT NULL column, and document the PERFORM probe as a deliberate variant of the sibling EXCEPTION-fallback convention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(plan): implementation plan for tz-aware publish/unpublish bucketing Four tasks: red pgTAP suite (15 assertions), the migration that turns it green, the stale E2E comment fix, and a full verification gate. Complete SQL for both the suite and the migration is inlined. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(schedule): pin restaurant-local week bucketing for publish/unpublish Failing suite: both RPCs bucket with a bare start_time::date, so a 22:00 America/Chicago shift on week_end is excluded and the same shift before week_start is wrongly included. Also asserts the EXECUTE boundary the migration is about to add. * fix(schedule): bucket publish/unpublish weeks in the restaurant's timezone publish_schedule and unpublish_schedule selected shifts with a bare start_time::date, which resolves against the database session TimeZone (UTC on Supabase) rather than restaurants.timezone. A 22:00 America/Chicago closing shift is already the next UTC calendar day, so it was dropped from its own week and swept into the next one; east of UTC the slip mirrors. Both functions now resolve the restaurant's IANA zone into v_tz and bucket with (start_time AT TIME ZONE v_tz)::date, matching get_open_shifts. Invalid zone strings fall back to UTC instead of aborting the publish. Re-declaring also adds SET search_path to these SECURITY DEFINER functions and an EXECUTE boundary: both still carried the default PUBLIC grant, so anon could publish or unpublish any restaurant's week. An in-body caller identity check remains a tracked follow-up. * test(e2e): correct stale comment about publish_schedule date bucketing The comment said the RPC compared the local date; before the tz-bucketing fix it compared the session (UTC) date. Now accurate, and it names the actual source of truth. Comment only — the noon fixture is unchanged. * docs(spec): record the browser-local week anchor as a tracked non-goal Phase 7a flagged useWeekPublicationStatus as diverging from the corrected RPC, but reviewed it against pre-#671 code (hardcoded ${weekEnd}T23:59:59Z literals). That code no longer exists: #671 already switched the shifts count to full instants, so a viewer in the restaurant's own timezone agrees with restaurant-local bucketing. What remains is non-goal #1 from the #671 design doc — the week boundary is anchored to the viewer's timezone, not the restaurant's. This change does not introduce it, but makes it observable in one more case, so record that plainly rather than deferring silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(review): drop the incorrect UTC guarantee from the noon-seed comment Copilot review: the comment claimed noon is the same calendar day in both the restaurant's zone and UTC. That is false for offsets at or beyond ±12h (Pacific/Kiritimati at UTC+14 puts local noon at 22:00 UTC the previous day). The UTC relationship is not what the seed relies on anyway — post-fix the RPC buckets by restaurant-local date. Reword to state what actually holds: noon sits ~12h from either end of the local day, so the browser-local Monday survives any offset difference under ~12h, which covers the America/Chicago default this spec signs up with. Point at the pgTAP suite that does cover the boundary directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(migrations): move tz-bucketing migration off the 20260728140000 collision PR #673 landed 20260728140000_search_pos_items.sql on main while this branch was open. CI builds the merge ref, so the duplicate prefix showed up there while a branch-local `ls | uniq -d` stayed clean — failing migrationVersionUniqueness, and taking `supabase start` (and therefore pgTAP and all four E2E shards) down with it. Renamed to 20260729120000 and updated every reference. The design doc and plan now record the merged-set check that would have caught this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(scheduling): guard publish/unpublish against cross-tenant calls CodeRabbit flagged at Critical that publish_schedule and unpublish_schedule are SECURITY DEFINER with no authorization check, so any authenticated caller could publish, lock, or unpublish another tenant's shifts by passing that restaurant's UUID. Pre-existing, not introduced here: 20251123000000 defines both as SECURITY DEFINER with no check, and nothing redefines them in between. This PR had already narrowed it by revoking the default PUBLIC/anon EXECUTE; closing the cross-tenant half here rather than deferring, since the migration is already restating access control on these two functions. Uses the canonical user_has_restaurant_access(p_restaurant_id, false). Membership, not manager-role — which roles may publish is a product decision and the UI's gating is the current authority; `false` keeps every caller that works today working. Only caller is useSchedulePublish.tsx with a user JWT, so no service_role or cron path loses access. Also derives publish_schedule's shift_count from GET DIAGNOSTICS instead of a preceding COUNT(*), matching unpublish_schedule in the same file: concurrent publishes would otherwise record a count of shifts they did not publish. pgTAP 15 -> 19, adding cross-tenant denial for both functions plus unchanged-state assertions on the target restaurant. Full suite 2124 passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(lessons): retrospective from the publish-schedule tz bucketing work (#675) Three lessons from the PR #675 run: - A migration prefix is only unique against the merged set. CI builds the merge ref, so a collision introduced by another PR merging mid-branch is invisible to `ls` on your own branch. It cost six red jobs, five of which failed at `Start Supabase local database` and read like infra flake; the one job needing no database named the cause. Go to the job with the fewest dependencies first. - A review finding can be correct about code that no longer exists. Check `git merge-base --is-ancestor origin/main HEAD` before acting on one — a stale base makes reviewers describe a world that was fixed weeks ago. - Revoking `anon` from a SECURITY DEFINER function is half a fix: the `authenticated` role cannot distinguish tenants. EXECUTE grants and in-body authorization are independent controls, and only the second is multi-tenant. When closing the gap needs a policy choice, close the unambiguous hole and defer the judgment call explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps the npm_and_yarn group with 1 update in the / directory: vite.
Updates
vitefrom 5.4.19 to 5.4.20Release notes
Sourced from vite's releases.
Changelog
Sourced from vite's changelog.
Commits
997700frelease: v5.4.20482000ffix: applyfs.strictcheck to HTML files (#20736)Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot mergewill merge this PR after your CI passes on it@dependabot squash and mergewill squash and merge this PR after your CI passes on it@dependabot cancel mergewill cancel a previously requested merge and block automerging@dependabot reopenwill reopen this PR if it is closed@dependabot closewill close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore <dependency name> major versionwill close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)@dependabot ignore <dependency name> minor versionwill close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)@dependabot ignore <dependency name>will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)@dependabot unignore <dependency name>will remove all of the ignore conditions of the specified dependency@dependabot unignore <dependency name> <ignore condition>will remove the ignore condition of the specified dependency and ignore conditionsYou can disable automated security fix PRs for this repo from the Security Alerts page.