Skip to content

feat(focus): restaurant picker + server-side 90-day background backfill - #564

Merged
jdelgado2002 merged 25 commits into
mainfrom
feat/focus-focuslink-datafeed
Jul 2, 2026
Merged

feat(focus): restaurant picker + server-side 90-day background backfill#564
jdelgado2002 merged 25 commits into
mainfrom
feat/focus-focuslink-datafeed

Conversation

@jdelgado2002

@jdelgado2002 jdelgado2002 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Increment A — Restaurant picker: replace the hand-typed GUID field in the setup wizard with a server-fetched restaurant list. Adds focus-list-restaurants edge function, focusListRestaurantsHandler.ts, listRestaurants mutation, and a step-by-step picker UI (credentials → select → done). Auto-selects single results; shows a labeled <Select> for multiple; falls back to "(name unavailable)" for nameless entries. Eliminates the UX dead-end that blocked non-technical operators.
  • Increment B — Durable background backfill: adds a dedicated focus-backfill-sync edge function on a 5-minute pg_cron, replacing the old single-day-per-click model. Lynk backfill now advances sync_cursor up to 7 days per 5-min tick with a wall-clock budget (80 s). focus-bulk-sync cedes Lynk backfilling to the new cron (skip guard). Manual "Sync Now" kicks a small 5-day batch and returns immediately. Custom date-range sync (≤14 days) is synchronous. Progress is polled passively via refetchInterval (8 s while backfilling → stops on done/inactive/error).
  • CAS on all cursor writes: .eq('sync_cursor', readCursor) prevents concurrent-tick clobbering across cron and manual paths. CAS misses produce zero retries (accepted trade-off).
  • Batch upserts: per-check item/payment upsert loops replaced with single array upserts; skipUnifiedSalesSync per-day during initial backfill with one unified-sales RPC at range end.
  • Sandbox URL fixed: FOCUS_API_SANDBOX_URL now threaded to all three handler dep objects and their index.ts entry points — previously silently fell back to the prod URL.
  • UI/a11y cleanup: SyncComponents aligned to Apple/Notion design scale; semantic color tokens throughout; aria-live="polite" progress region; <Progress> with aria-label/valuemin/max.
  • 5310 unit tests, 1497 pgTAP tests, 0 TS errors, Vite build green; 3 pre-existing E2E failures on payroll/scheduling specs not touched by this branch.

Test plan

  • Unit tests: npm run test — 5310 pass, 0 fail
  • TypeScript: npm run typecheck — 0 errors
  • Build: npm run build — produces dist/ without errors
  • DB tests: npm run test:db — 1497/1497 pass; focus-backfill-sync cron job confirmed in cron.job on */5 * * * *
  • E2E: npm run test:e2e — 144–146 pass, 3 pre-existing failures (payroll/scheduling, unrelated to this branch)
  • Live smoke: setup wizard "Find my restaurant(s)" returns real restaurant list; Save & Connect stores correct GUID; manual Sync Now returns immediately; backfill advances passively in the UI without keeping the tab open
  • Sandbox URL: confirm FOCUS_API_SANDBOX_URL env var is wired in staging edge function config

Design doc

docs/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.md

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a restaurant picker during Focus setup, including restaurant search, auto-select for single matches, and clearer connection steps.
    • Added background initial sync/backfill with automatic polling and updated progress messaging.
    • Added support for custom date-range syncs.
  • Bug Fixes

    • Improved sync reliability and error handling for restaurant setup and transaction syncing.
    • Updated status and progress displays to better reflect long-running imports and completed syncs.

@netlify

netlify Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploy Preview for easyshifthq ready!

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

QR Code

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

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

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
easyshifthq Ready Ready Preview, Comment Jul 2, 2026 11:16pm

@supabase

supabase Bot commented Jul 2, 2026

Copy link
Copy Markdown

Updates to Preview Branch (feat/focus-focuslink-datafeed) ↗︎

Deployments Status Updated
Database Thu, 02 Jul 2026 23:16:00 UTC
Services Thu, 02 Jul 2026 23:16:00 UTC
APIs Thu, 02 Jul 2026 23:16:00 UTC

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

Tasks Status Updated
Configurations Thu, 02 Jul 2026 23:16:01 UTC
Migrations Thu, 02 Jul 2026 23:16:01 UTC
Seeding Thu, 02 Jul 2026 23:16:01 UTC
Edge Functions Thu, 02 Jul 2026 23:16:31 UTC

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

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@jdelgado2002, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e105f3c-a8c3-43c2-bcea-e33bfa5cba27

📥 Commits

Reviewing files that changed from the base of the PR and between f8dc402 and 7ef5215.

📒 Files selected for processing (4)
  • supabase/functions/_shared/focusBackfillBatch.ts
  • supabase/functions/_shared/focusBulkSyncHandler.ts
  • tests/unit/focusBackfillBatch.test.ts
  • tests/unit/focusBulkSyncHandler.test.ts
📝 Walkthrough

Walkthrough

This PR adds a Focus POS restaurant picker flow and server-side background backfill. A new focus-list-restaurants edge function authenticates and lists Focus restaurants via SSRF-guarded API calls; FocusSetupWizard gains a credentials→select→done picker flow. A shared processBackfillBatch helper drives day-sliced backfill with budgets/CAS cursor writes, used by a new focus-backfill-sync cron and integrated into focus-sync-data/focus-bulk-sync. focusTransactionSyncHandler batches item/payment upserts and supports date-range syncing. Frontend sync UI moves to passive polling with simplified toasts. Includes docs, migrations, and extensive tests.

Changes

Focus picker and background sync

Layer / File(s) Summary
Planning docs and design specs
docs/superpowers/plans/2026-07-02-..., docs/superpowers/specs/2026-07-02-...
Adds implementation plan and binding design spec documents for the picker + background backfill effort.
Restaurant listing edge function
supabase/functions/_shared/focusListRestaurantsHandler.ts, supabase/functions/focus-list-restaurants/index.ts, supabase/config.toml, tests/unit/focusListRestaurantsHandler.test.ts
Adds handleListRestaurants with auth/role checks, SSRF-guarded Focus API call, error shaping, and the corresponding edge function entrypoint and config.
Picker hook and wizard UI
src/hooks/useFocusConnection.tsx, src/components/pos/FocusSetupWizard.tsx, tests/unit/focusSetupWizard.test.tsx
Adds listRestaurants/__focusRefetchInterval to the hook and rewrites FocusSetupWizard into an API-key/secret → restaurant selection → done flow.
Backfill batching helper
supabase/functions/_shared/focusBackfillBatch.ts, tests/unit/focusBackfillBatch.test.ts
Adds processBackfillBatch, a pure day-sliced cursor advancement helper with budget/cap/error semantics.
Transaction sync batching and date ranges
supabase/functions/_shared/focusTransactionSyncHandler.ts, tests/unit/focusTransactionSyncHandler.test.ts
Batches item/payment upserts per check and adds processDateRangeTransactions for explicit date-range syncing.
Sync-data and bulk-sync integration
supabase/functions/_shared/focusSyncDataHandler.ts, supabase/functions/_shared/focusBulkSyncHandler.ts, supabase/functions/focus-sync-data/index.ts, supabase/functions/focus-bulk-sync/index.ts, tests/unit/focusSyncDataHandler.test.ts, tests/unit/focusBulkSyncHandler.test.ts
Adds Lynk custom-range/backfill/incremental branching with CAS-filtered cursor persistence; makes focus-bulk-sync skip Lynk backfill and persist error state.
Backfill cron
supabase/functions/_shared/focusBackfillSyncHandler.ts, supabase/functions/focus-backfill-sync/index.ts, supabase/migrations/20260702120000_focus_backfill_cron.sql, supabase/tests/48_focus_backfill_cron.sql, tests/unit/focusBackfillSyncHandler.test.ts
Adds a bearer-gated cron handler processing Lynk backfilling connections with wall-budget limits, CAS writes, and the scheduling migration.
Frontend sync UX
src/components/FocusSync.tsx, src/components/pos/SyncComponents.tsx, tests/unit/focusSyncB6.test.tsx
Simplifies FocusSync to a single background sync call and updates InitialSyncPendingAlert/buttons/results for passive progress polling.
Misc hardening and schema tests
supabase/functions/_shared/focusDatafeedParser.ts, supabase/migrations/20260701140000_..., supabase/tests/46_focus_transactions_schema.sql
Disables XML entity processing, fixes a cron comment, and expands card_last4 pgTAP assertions.
Hook polling/listing tests
tests/unit/useFocusConnection.test.tsx
Adds tests for date-range passthrough, restaurant option shape, listRestaurants, and refetch interval gating.

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

Sequence Diagram(s)

sequenceDiagram
  participant Wizard as FocusSetupWizard
  participant Hook as useFocusConnection
  participant EdgeFn as focus-list-restaurants
  participant Handler as handleListRestaurants
  participant Focus as Focus POS API

  Wizard->>Hook: listRestaurants(restaurantId, apiKey, apiSecret, environment)
  Hook->>EdgeFn: invoke focus-list-restaurants
  EdgeFn->>Handler: handleListRestaurants(req, deps)
  Handler->>Handler: auth.getUser() + role check
  Handler->>Handler: SSRF guard on base URL
  Handler->>Focus: GET /api/restaurants (Basic auth)
  Focus-->>Handler: restaurants JSON or error
  Handler-->>EdgeFn: 200 {success, restaurants|error}
  EdgeFn-->>Hook: response
  Hook-->>Wizard: FocusRestaurantOption[]
  Wizard->>Wizard: auto-select or render picker
Loading
sequenceDiagram
  participant Cron as pg_cron
  participant EdgeFn as focus-backfill-sync
  participant Handler as handleBackfillSync
  participant Batch as processBackfillBatch
  participant DB as focus_connections

  Cron->>EdgeFn: net.http_post every 5 min
  EdgeFn->>Handler: handleBackfillSync(req, deps)
  Handler->>DB: select backfilling Lynk connections
  loop each connection within budget
    Handler->>Batch: processBackfillBatch(config, opts)
    Batch-->>Handler: {syncCursor, status, daysProcessed}
    Handler->>DB: CAS update (id, restaurant_id, sync_cursor)
  end
  Handler-->>EdgeFn: {processed, errors, elapsedMs}
Loading

Possibly related PRs

  • toyiyo/nimble-pnl#557: Extends the same Focus POS sync surface (focus-sync-data/focus-bulk-sync orchestration and shared handlers) established in that PR.
  • toyiyo/nimble-pnl#563: Builds directly on the Lynk datafeed→transaction sync foundation (focusTransactionSyncHandler, unified_sales wiring) from that PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: the restaurant picker and the server-side 90-day background backfill.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/focus-focuslink-datafeed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Comment thread tests/unit/focusListRestaurantsHandler.test.ts Fixed

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7937f16f65

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +158 to +163
const result: TransactionSyncResult = await deps.processDayTransactions(
{ supabase: deps.supabase, fetchDatafeed: deps.fetchDatafeed },
config,
targetDate,
{ skipUnifiedSalesSync: true },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Sync backfilled days into unified_sales

When initial backfill runs through either focus-sync-data or the new 5-minute cron, every day is handed to processDayTransactions with skipUnifiedSalesSync: true here. Neither caller invokes sync_focus_transactions_to_unified_sales for the processed range afterward, and the 6-hour safety-net RPC only resyncs the last few days, so once the cursor advances the older backfilled orders/items/payments never reach unified_sales or daily aggregates and won't appear in P&L unless manually resynced. Run the range RPC for each successful batch before advancing or marking the backfill done.

Useful? React with 👍 / 👎.

Comment on lines +278 to +284
for (const voidedCheckId of deletedCheckIds) {
const { error: delError } = await deps.supabase
.from('focus_orders')
.delete()
.eq('restaurant_id', config.restaurantId)
.eq('business_date', businessDate)
.eq('focus_check_id', voidedCheckId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove unified_sales rows for deleted checks

When a later datafeed contains a DeleteRecord for a previously synced check, this path deletes the focus_orders row before the unified-sales RPC runs. The RPC cleanup only iterates existing focus_orders, so the deleted check has no iteration that can remove its existing focus-* sale/tip/discount rows or reaggregate the day, leaving voided revenue in reports. Delete the matching unified_sales rows and refresh aggregates as part of handling the DeleteRecord.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md (2)

165-178: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the custom-range flow with the binding resolution.

This section still instructs implementers to background custom ranges with scheduleBackground/waitUntil, but §8.2 explicitly makes custom range synchronous. Keeping both instructions in the same plan will send the implementation in opposite directions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md`
around lines 165 - 178, The custom-range guidance is inconsistent with the
binding resolution because it still tells implementers to use
scheduleBackground/waitUntil even though custom range handling is meant to be
synchronous. Update the plan section that describes the custom-range flow so it
no longer references backgrounding for custom ranges and instead aligns with the
synchronous behavior already defined in the custom-range rules, keeping the
instructions consistent with the surrounding focus-picker and background-sync
plan.

191-194: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the backfill batch cap consistent.

maxDays: 30 here conflicts with §8.3’s maxDays: 7 for cron backfill. Please pick one limit and use it consistently across the plan and tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md`
around lines 191 - 194, Make the backfill batch cap consistent across the plan
and its tests: the current maxDays value conflicts with the cron backfill limit
described in the plan’s backfill section. Update the backfill configuration in
the plan so the same maxDays limit is used everywhere, and align any test
expectations or references that mention the alternate value. Use the backfill
cron section and related test descriptions as the source of truth when updating
the maxDays setting.
supabase/functions/_shared/focusSyncDataHandler.ts (1)

102-138: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add the missing CAS chain level here

update(...).eq(...).eq(...).eq('sync_cursor', ...) needs a 4-step chain, matching focusBackfillSyncHandler.ts. CasEq3 currently exposes only select(), so the helper's type surface doesn't match the actual call shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/_shared/focusSyncDataHandler.ts` around lines 102 - 138,
The CAS helper types in focusSyncDataHandler are missing one step in the update
chain, so the declared surface does not match the actual
.update(...).eq(...).eq(...).eq('sync_cursor', ...) usage. Update the chain
types around CasUpdateResult, CasEq1, CasEq2, and CasEq3 so the helper supports
the 4-step CAS flow used by the sync logic, matching the shape already used in
focusBackfillSyncHandler and allowing select() only after the extra eq() level.
🧹 Nitpick comments (8)
src/hooks/useFocusConnection.tsx (1)

111-117: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use staleTime: 30_000 here (src/hooks/useFocusConnection.tsx:111-117)

refetchInterval already runs independently of staleTime, so staleTime: 0 just adds extra refetches on focus/remount. A 30s stale window keeps the 8s backfill poll and matches the hook guidelines.

Suggested change
-    staleTime: 0,
+    staleTime: 30_000,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useFocusConnection.tsx` around lines 111 - 117, Update
useFocusConnection’s useQuery options so the query stays fresh for 30 seconds
instead of forcing immediate staleness; in useFocusConnection.tsx, change the
staleTime setting in the hook’s query config to 30_000 while keeping
refetchInterval unchanged. This reduces extra focus/remount refetches without
affecting the backfill polling behavior.

Source: Coding guidelines

tests/unit/focusTransactionSyncHandler.test.ts (1)

166-535: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider prefixing critical-path test descriptions with CRITICAL:.

This suite covers financial transaction persistence, batched upserts, and unified_sales sync — tests like "returns { status: 'error' } when focus_orders upsert fails" or the RPC-params tests are good candidates. As per coding guidelines, tests/unit/**/*.test.{ts,tsx}: "Prefix critical business logic tests with 'CRITICAL:' in test description."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/focusTransactionSyncHandler.test.ts` around lines 166 - 535,
Prefix the critical-path test cases in processDayTransactions with CRITICAL: in
their description strings, especially the ones covering focus_orders upsert
failure, items/payments batch upserts, unified_sales RPC params, and
voided-check deletes. Update the relevant it(...) titles in
focusTransactionSyncHandler.test.ts so they clearly identify
financial/business-critical behavior while leaving the test logic and assertions
unchanged.

Source: Coding guidelines

supabase/functions/_shared/focusSyncDataHandler.ts (2)

438-485: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Incremental sync failures aren't persisted, unlike backfill failures.

The backfill branch persists connection_status='error'/last_error when batchResult.status === 'error' (Lines 402-406), but the incremental branch here only refreshes timestamps regardless of status. A recurring incremental failure (Lynk auth issue, license expiry, etc.) won't surface via connection_status/last_error for the frontend/ops to notice.

♻️ Proposed fix — persist error state on incremental failures too
       const nowIso = now.toISOString();
+      const incUpdatePayload: Record<string, unknown> = {
+        last_sync_time: nowIso,
+        updated_at: nowIso,
+      };
+      if (status === 'error') {
+        incUpdatePayload.connection_status = 'error';
+        incUpdatePayload.last_error = r1.status === 'error' ? r1.error : r2.error;
+        incUpdatePayload.last_error_at = nowIso;
+      }
       const { error: casIncErr } = await deps.serviceClient
         .from('focus_connections')
-        .update({
-          last_sync_time: nowIso,
-          updated_at: nowIso,
-        })
+        .update(incUpdatePayload)
         .eq('id', connRow.id)
         .eq('restaurant_id', restaurantId)
         .eq('sync_cursor', readCursor)
         .select();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/_shared/focusSyncDataHandler.ts` around lines 438 - 485,
The incremental path in focusSyncDataHandler should persist failures the same
way the backfill branch does: after processDayTransactions returns r1/r2, if
status is error, update focus_connections via deps.serviceClient to set
connection_status to error and last_error from the failure result before
returning. Keep the existing timestamp refresh for success/empty cases, and
preserve the current CAS behavior around the sync_cursor/readCursor update.

297-316: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

decrypt() call unwrapped in try/catch.

Unlike the legacy path's decrypt (wrapped to catch FocusAuthError), this Lynk-path decrypt at Line 305 isn't guarded. A decrypt failure (e.g. corrupted ciphertext or key rotation) will propagate uncaught out of handleSyncData instead of returning a clean JSON error, potentially violating the edge-function convention of consistent error responses.

As per coding guidelines, supabase/functions/**/*.ts should "Use try-catch and return consistent JSON error responses in Edge Functions."

♻️ Proposed fix
     const encSvc = await getEncryptionService();
-    const apiSecret = await encSvc.decrypt(connRow.api_secret_encrypted);
+    let apiSecret: string;
+    try {
+      apiSecret = await encSvc.decrypt(connRow.api_secret_encrypted);
+    } catch {
+      return jsonError(500, 'Failed to decrypt Focus POS API credentials');
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/_shared/focusSyncDataHandler.ts` around lines 297 - 316,
The Lynk-path secret decryption in focusSyncDataHandler’s handleSyncData flow is
not protected, so a decrypt failure can escape instead of returning a consistent
Edge Function JSON error. Wrap the
getEncryptionService()/encSvc.decrypt(connRow.api_secret_encrypted) block in
try/catch, handle the failure the same way the legacy path handles
FocusAuthError, and return a jsonError response with an appropriate
status/message so the Lynk path matches the existing error-response convention.

Source: Coding guidelines

supabase/functions/_shared/focusLynkClient.ts (1)

229-254: 🩺 Stability & Availability | 🔵 Trivial

Per-day network latency can exceed the caller's backfill budget.

Each fetchDatafeed call can take up to ~60s worst case (30s POST + 30s blob GET), but processBackfillBatch only checks budgetMs before starting a day (not during). With budgetMs set to 12s (manual sync) or ~50s per connection (cron), one slow Focus/Azure response can make a single invocation run far longer than its nominal budget, risking edge-function execution-time limits being hit mid-write.

Consider tightening TIMEOUT_MS for this use case or sharing a single deadline-based AbortController across both requests bounded by the caller's remaining budget, so worst-case latency per day stays proportional to the allotted slice.

Also applies to: 325-343

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/_shared/focusLynkClient.ts` around lines 229 - 254, The
fetchDatafeed path in focusLynkClient should not use a fixed per-request timeout
that can exceed the caller’s remaining backfill budget. Update the POST
/api/lynk/sync flow and the related blob GET path it feeds (the same network
pattern referenced by fetchDatafeed) to use a shared deadline-based
AbortController or otherwise derive timeouts from the remaining budget passed
through processBackfillBatch, so each day’s work stays within the caller’s
slice. Keep the existing fetch/deps.fetch structure and syncUrl handling, but
make TIMEOUT_MS or request cancellation budget-aware rather than independently
allowing ~30s per request.
supabase/functions/_shared/focusBulkSyncHandler.ts (2)

386-393: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Failed connections never get last_sync_time bumped, risking round-robin starvation.

On any processConnection throw (now including the new Lynk incomplete-credentials/sync-failure paths), the catch block only logs and records the error — it never updates last_sync_time. Since rows are selected ORDER BY last_sync_time ASC NULLS FIRST LIMIT 5, a persistently failing connection will keep being re-selected ahead of healthy connections on every 6h run, starving the round-robin. focusBackfillSyncHandler.ts already solves this exact problem with a best-effort fire-and-forget last_sync_time write in its catch block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/_shared/focusBulkSyncHandler.ts` around lines 386 - 393,
The error path in focusBulkSyncHandler’s processConnection catch block is not
advancing last_sync_time, so failing connections keep getting picked first by
the ORDER BY last_sync_time ASC NULLS FIRST query. Update the catch block in
focusBulkSyncHandler to perform the same best-effort fire-and-forget
last_sync_time bump used by focusBackfillSyncHandler after logging/pushing the
error, while keeping the existing error handling and result.errors behavior
intact.

196-231: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Lynk sync failures don't persist connection_status, unlike the legacy path and the sibling backfill cron.

When Lynk credentials are incomplete (Line 197) or processDayTransactions fails for both recent days (Lines 228-231), processConnection just throws. The legacy path persists connection_status: 'error' on FocusAuthError (Lines 241-249), and focusBackfillSyncHandler.ts explicitly writes connection_status='error' + last_error on Lynk backfill failures. Here, the failure is only logged/pushed into the response errors[] array — the focus_connections row still reflects a healthy state, so the UI polling connection_status (per PR objectives) won't surface repeated incremental-sync failures to the user.

♻️ Suggested fix: persist error state for Lynk incremental failures
     const failed = results.find((r) => r.status === 'error');
     if (failed?.status === 'error') {
-      throw new Error(failed.error ?? 'Focus transaction incremental sync failed');
+      const message = failed.error ?? 'Focus transaction incremental sync failed';
+      await deps.serviceClient
+        .from('focus_connections')
+        .update({
+          connection_status: 'error',
+          last_error: message,
+          updated_at: new Date().toISOString(),
+        })
+        .eq('id', row.id)
+        .eq('restaurant_id', row.restaurant_id);
+      throw new Error(message);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/_shared/focusBulkSyncHandler.ts` around lines 196 - 231,
The Lynk incremental sync path in processConnection currently throws on missing
credentials or when processDayTransactions fails, but it never updates the
focus_connections row to an error state. Update this branch to persist
connection_status: 'error' and a meaningful last_error before
returning/throwing, mirroring the legacy FocusAuthError handling and the
focusBackfillSyncHandler failure path. Use the existing symbols
processConnection, processDayTransactions, and the focus_connections update
logic so UI polling on connection_status reflects incremental-sync failures.
tests/unit/focusSyncDataHandler.test.ts (1)

631-631: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace repeated as any casts with a properly-typed fixture.

Static analysis flags no-explicit-any at all these call sites where MOCK_CONNECTION_LYNK_BACKFILL/MOCK_CONNECTION_LYNK_INCREMENTAL are cast to any when passed to serviceClientOpts.connection. This suggests the MockConnection type used by makeServiceClientMock doesn't yet include the Lynk fields (api_key, api_secret_encrypted, environment). Extending that type once would eliminate all ~17 casts.

Also applies to: 644-644, 655-655, 676-676, 687-687, 701-701, 728-728, 746-746, 769-769, 784-784, 799-799, 821-821, 835-835, 849-849, 861-861, 873-873, 886-886

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/focusSyncDataHandler.test.ts` at line 631, The repeated `as any`
casts on `serviceClientOpts.connection` indicate the shared mock type is missing
the Lynk-specific fields. Update the `MockConnection` type used by
`makeServiceClientMock` to include the `api_key`, `api_secret_encrypted`, and
`environment` properties so `MOCK_CONNECTION_LYNK_BACKFILL` and
`MOCK_CONNECTION_LYNK_INCREMENTAL` can be passed without casts. Then remove all
the `as any` call sites in `focusSyncDataHandler.test.ts` and keep the fixtures
strongly typed.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md`:
- Around line 7-8: The live verification step currently hard-codes a
machine-specific absolute path, which leaks a private filesystem layout and
breaks portability. Update the verification note in this plan to use the same
repo-relative or env-var-based form used elsewhere in the document, and keep the
`FOCUS_API_KEY`/`FOCUS_API_SECRET`/`FOCUS_RESTAURANT_GUID` guidance intact
without exposing any values.

In
`@docs/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.md`:
- Around line 191-194: The cron backfill cap is inconsistent between this
section and the later binding resolution, so align the `processBackfillBatch`
limit with the final `maxDays` choice. Update the spec text near
`processBackfillBatch` to use the same cron value as §8.3, and make sure any
related references to `BUDGET_MS`, per-restaurant budget, and the cron batch
behavior in the surrounding design stay consistent.
- Around line 212-214: Update the Connection query in the relevant React Query
hook so polling stops as soon as a connection enters error, even if it is still
is_active and initial_sync_done is false. Adjust the conditional refetchInterval
logic to also check the connection status before returning 8000, and otherwise
return false; keep staleTime unchanged. Use the existing query setup and data
fields in the Connection query function/hook to locate the change.
- Around line 169-178: The custom-range flow still uses the obsolete
backgrounding path, which conflicts with the updated design and makes the
behavior ambiguous. Remove the scheduleBackground/waitUntil wording from the
custom range section and update the flow to be synchronous instead. Keep the
range-processing logic centered on processDateRangeTransactions and the
custom-range handling in focusTransactionSyncHandler or focusBackfillBatch, but
describe it as executing inline and returning the normal response path rather
than backgrounded:true.

In `@package.json`:
- Line 98: Upgrade the fast-xml-parser dependency in package.json from 4.5.6 to
a fixed 5.5.6+ release, since it parses untrusted Focus XML and the current
version is in the affected CVE range. Then verify
supabase/functions/_shared/focusDatafeedParser.ts still keeps DTD/entity
handling disabled when constructing or using the parser, and adjust the parser
configuration there if needed.

In `@supabase/functions/_shared/focusBackfillSyncHandler.ts`:
- Around line 356-376: The best-effort write to focus_connections currently
handles only the resolved error case in the `.then()` callback and can still
produce an unhandled rejection if the promise rejects before resolving. Update
the fire-and-forget update in `focusBackfillSyncHandler` to fail safely by
adding a `.catch()` on the Supabase chain (or wrapping the async write in a
try/catch helper) so network/runtime failures are swallowed and only logged,
preserving the intended never-throw behavior for this error-state write.

In `@supabase/functions/_shared/focusDatafeedParser.ts`:
- Around line 112-116: The XMLParser configuration in focusDatafeedParser should
disable entity processing for this external feed. Update the XMLParser options
in the parser setup to set processEntities to false unless entity/DOCTYPE
support is explicitly required, keeping the existing ignoreAttributes,
parseTagValue, and trimValues behavior unchanged.

In `@supabase/functions/_shared/focusTransactionSyncHandler.ts`:
- Around line 302-319: `processDayTransactions` and
`processDateRangeTransactions` are swallowing
`sync_focus_transactions_to_unified_sales` RPC failures by only calling
`console.warn`, so callers still see success even when `unified_sales` is out of
sync. Update the return shape in these functions to surface the RPC outcome (for
example a warning/failure flag or message) and propagate it from the shared sync
block that calls `deps.supabase.rpc('sync_focus_transactions_to_unified_sales',
...)`. Add tests for both the single-day and range paths to assert the
RPC-failure case is reported to the caller instead of being silently ignored.

In `@supabase/functions/focus-backfill-sync/index.ts`:
- Around line 35-48: The auth check in focus-backfill-sync is fail-open when
SUPABASE_SERVICE_ROLE_KEY is missing because serviceRoleKey becomes an empty
string and handleBackfillSync can accept an empty Bearer token. Change the flow
in the main try block and/or handleBackfillSync so missing
SUPABASE_SERVICE_ROLE_KEY is treated as a misconfiguration and immediately
rejected with a non-2xx response, before createClient or timingSafeEqual is
used. Keep the fix centered around handleBackfillSync, serviceRoleKey, and the
Authorization Bearer parsing so only a real configured secret can pass.

In `@supabase/migrations/20260701130000_focus_transactions_unified_sales.sql`:
- Around line 92-263: The sync logic in the per-check loop fixes item-level
orphans, but it still leaves whole-check rows behind when a `focus_orders` check
is voided or deleted, and the later aggregation still relies on `synced_at >=
v_sync_start`, which misses those removed checks. Add a check-level cleanup in
the same migration before the aggregation step to delete `unified_sales` rows
for `pos_system = 'focus'` whose `external_order_id` no longer matches any
`focus_orders` record in the processed date range, while preserving
`parent_sale_id IS NULL` so split rows stay intact. Then restore the aggregation
behavior to the `focus_daily_reports` approach from
`20260627150000_focus_sync_hardening.sql`, or otherwise ensure deleted dates are
explicitly re-aggregated.

In `@supabase/migrations/20260701140000_focus_transactions_cron.sql`:
- Line 13: The schedule comment is misleading because the cron expression
described by the text does not match the actual interval used by the focus
transactions job. Update the comment near the cron setup in the migration so it
accurately reflects the intended schedule for the focus transactions cron,
keeping it consistent with the cron expression used by the scheduling logic and
avoiding any mention of the wrong frequency.

In `@supabase/migrations/20260701150000_focus_transactions_integrity.sql`:
- Around line 12-24: In the previous migration file
(20260701130000_focus_transactions_unified_sales.sql), the cleanup logic for
unified_sales table (around lines 92-261) only handles pruning rows for checks
that exist in focus_orders. Add an explicit DELETE statement that removes
orphaned unified_sales rows where the corresponding focus_orders record no
longer exists, using the natural key columns (restaurant_id, business_date,
focus_check_id) in a NOT IN or LEFT JOIN condition to identify and delete rows
whose referenced focus_orders have been deleted.

In `@supabase/tests/46_focus_transactions_schema.sql`:
- Around line 124-188: The current checks in the focus transaction schema test
only confirm that SELECT and ALL policies exist on the transaction tables, but
they do not verify the actual policy predicates. Update the assertions around
pg_policies for focus_orders, focus_order_items, and focus_payments to inspect
the policy condition fields (such as qual and with_check) and confirm they match
the migration contract, specifically the user_restaurants membership and role IN
('owner', 'manager') logic.
- Around line 289-297: The card_last4 test only covers an overlong value, so the
PCI boundary in the focus_payments checks may still miss short or non-numeric
fragments. Update the SQL in the focus_transactions_schema test around the
throws_ok cases for focus_payments.card_last4 to add assertions for a too-short
value and a non-digit value, alongside the existing 5-digit case, so the
constraint is validated against all non-4-digit inputs.

In `@tests/unit/focusSyncB6.test.tsx`:
- Around line 203-222: The test name in FocusSync B6 is misleading because it
claims the destructive toast path is covered, but the body only checks the
disabled Sync button and that triggerManualSync is not called. Either rename the
test around renderFocusSync to reflect the disabled-button guard, or adjust the
test to exercise handleSync’s missing-date branch and assert mockToast is called
so the toast behavior is actually covered.

---

Outside diff comments:
In `@docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md`:
- Around line 165-178: The custom-range guidance is inconsistent with the
binding resolution because it still tells implementers to use
scheduleBackground/waitUntil even though custom range handling is meant to be
synchronous. Update the plan section that describes the custom-range flow so it
no longer references backgrounding for custom ranges and instead aligns with the
synchronous behavior already defined in the custom-range rules, keeping the
instructions consistent with the surrounding focus-picker and background-sync
plan.
- Around line 191-194: Make the backfill batch cap consistent across the plan
and its tests: the current maxDays value conflicts with the cron backfill limit
described in the plan’s backfill section. Update the backfill configuration in
the plan so the same maxDays limit is used everywhere, and align any test
expectations or references that mention the alternate value. Use the backfill
cron section and related test descriptions as the source of truth when updating
the maxDays setting.

In `@supabase/functions/_shared/focusSyncDataHandler.ts`:
- Around line 102-138: The CAS helper types in focusSyncDataHandler are missing
one step in the update chain, so the declared surface does not match the actual
.update(...).eq(...).eq(...).eq('sync_cursor', ...) usage. Update the chain
types around CasUpdateResult, CasEq1, CasEq2, and CasEq3 so the helper supports
the 4-step CAS flow used by the sync logic, matching the shape already used in
focusBackfillSyncHandler and allowing select() only after the extra eq() level.

---

Nitpick comments:
In `@src/hooks/useFocusConnection.tsx`:
- Around line 111-117: Update useFocusConnection’s useQuery options so the query
stays fresh for 30 seconds instead of forcing immediate staleness; in
useFocusConnection.tsx, change the staleTime setting in the hook’s query config
to 30_000 while keeping refetchInterval unchanged. This reduces extra
focus/remount refetches without affecting the backfill polling behavior.

In `@supabase/functions/_shared/focusBulkSyncHandler.ts`:
- Around line 386-393: The error path in focusBulkSyncHandler’s
processConnection catch block is not advancing last_sync_time, so failing
connections keep getting picked first by the ORDER BY last_sync_time ASC NULLS
FIRST query. Update the catch block in focusBulkSyncHandler to perform the same
best-effort fire-and-forget last_sync_time bump used by focusBackfillSyncHandler
after logging/pushing the error, while keeping the existing error handling and
result.errors behavior intact.
- Around line 196-231: The Lynk incremental sync path in processConnection
currently throws on missing credentials or when processDayTransactions fails,
but it never updates the focus_connections row to an error state. Update this
branch to persist connection_status: 'error' and a meaningful last_error before
returning/throwing, mirroring the legacy FocusAuthError handling and the
focusBackfillSyncHandler failure path. Use the existing symbols
processConnection, processDayTransactions, and the focus_connections update
logic so UI polling on connection_status reflects incremental-sync failures.

In `@supabase/functions/_shared/focusLynkClient.ts`:
- Around line 229-254: The fetchDatafeed path in focusLynkClient should not use
a fixed per-request timeout that can exceed the caller’s remaining backfill
budget. Update the POST /api/lynk/sync flow and the related blob GET path it
feeds (the same network pattern referenced by fetchDatafeed) to use a shared
deadline-based AbortController or otherwise derive timeouts from the remaining
budget passed through processBackfillBatch, so each day’s work stays within the
caller’s slice. Keep the existing fetch/deps.fetch structure and syncUrl
handling, but make TIMEOUT_MS or request cancellation budget-aware rather than
independently allowing ~30s per request.

In `@supabase/functions/_shared/focusSyncDataHandler.ts`:
- Around line 438-485: The incremental path in focusSyncDataHandler should
persist failures the same way the backfill branch does: after
processDayTransactions returns r1/r2, if status is error, update
focus_connections via deps.serviceClient to set connection_status to error and
last_error from the failure result before returning. Keep the existing timestamp
refresh for success/empty cases, and preserve the current CAS behavior around
the sync_cursor/readCursor update.
- Around line 297-316: The Lynk-path secret decryption in focusSyncDataHandler’s
handleSyncData flow is not protected, so a decrypt failure can escape instead of
returning a consistent Edge Function JSON error. Wrap the
getEncryptionService()/encSvc.decrypt(connRow.api_secret_encrypted) block in
try/catch, handle the failure the same way the legacy path handles
FocusAuthError, and return a jsonError response with an appropriate
status/message so the Lynk path matches the existing error-response convention.

In `@tests/unit/focusSyncDataHandler.test.ts`:
- Line 631: The repeated `as any` casts on `serviceClientOpts.connection`
indicate the shared mock type is missing the Lynk-specific fields. Update the
`MockConnection` type used by `makeServiceClientMock` to include the `api_key`,
`api_secret_encrypted`, and `environment` properties so
`MOCK_CONNECTION_LYNK_BACKFILL` and `MOCK_CONNECTION_LYNK_INCREMENTAL` can be
passed without casts. Then remove all the `as any` call sites in
`focusSyncDataHandler.test.ts` and keep the fixtures strongly typed.

In `@tests/unit/focusTransactionSyncHandler.test.ts`:
- Around line 166-535: Prefix the critical-path test cases in
processDayTransactions with CRITICAL: in their description strings, especially
the ones covering focus_orders upsert failure, items/payments batch upserts,
unified_sales RPC params, and voided-check deletes. Update the relevant it(...)
titles in focusTransactionSyncHandler.test.ts so they clearly identify
financial/business-critical behavior while leaving the test logic and assertions
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0177592d-dfa2-4d47-b386-bd7725b877a2

📥 Commits

Reviewing files that changed from the base of the PR and between 2f128dd and 7937f16.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • public/logos/focus.svg is excluded by !**/*.svg
📒 Files selected for processing (54)
  • docs/superpowers/plans/2026-07-01-focus-pos-transactions-plan.md
  • docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md
  • docs/superpowers/specs/2026-07-01-focus-pos-transactions-design.md
  • docs/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.md
  • package.json
  • src/components/FocusSync.tsx
  • src/components/IntegrationLogo.tsx
  • src/components/pos/FocusSetupWizard.tsx
  • src/components/pos/SyncComponents.tsx
  • src/hooks/useFocusConnection.tsx
  • supabase/config.toml
  • supabase/functions/_shared/focusBackfillBatch.ts
  • supabase/functions/_shared/focusBackfillSyncHandler.ts
  • supabase/functions/_shared/focusBulkSyncHandler.ts
  • supabase/functions/_shared/focusDatafeedParser.ts
  • supabase/functions/_shared/focusListRestaurantsHandler.ts
  • supabase/functions/_shared/focusLynkClient.ts
  • supabase/functions/_shared/focusSaveConnectionHandler.ts
  • supabase/functions/_shared/focusSyncDataHandler.ts
  • supabase/functions/_shared/focusTestConnectionHandler.ts
  • supabase/functions/_shared/focusTransactionSyncHandler.ts
  • supabase/functions/deno.json
  • supabase/functions/focus-backfill-sync/index.ts
  • supabase/functions/focus-bulk-sync/index.ts
  • supabase/functions/focus-list-restaurants/index.ts
  • supabase/functions/focus-save-connection/index.ts
  • supabase/functions/focus-sync-data/index.ts
  • supabase/functions/focus-test-connection/index.ts
  • supabase/migrations/20260630120000_focus_focuslink_api.sql
  • supabase/migrations/20260701120000_focus_transactions.sql
  • supabase/migrations/20260701130000_focus_transactions_unified_sales.sql
  • supabase/migrations/20260701140000_focus_transactions_cron.sql
  • supabase/migrations/20260701150000_focus_transactions_integrity.sql
  • supabase/migrations/20260701160000_focus_transactions_security.sql
  • supabase/migrations/20260702120000_focus_backfill_cron.sql
  • supabase/tests/45_focus_focuslink_api.sql
  • supabase/tests/46_focus_transactions_schema.sql
  • supabase/tests/47_focus_transactions_unified_sales.sql
  • supabase/tests/48_focus_backfill_cron.sql
  • tests/fixtures/focus-datafeed-sample.xml
  • tests/unit/focusBackfillBatch.test.ts
  • tests/unit/focusBackfillSyncHandler.test.ts
  • tests/unit/focusBulkSyncHandler.test.ts
  • tests/unit/focusDatafeedParser.test.ts
  • tests/unit/focusListRestaurantsHandler.test.ts
  • tests/unit/focusLynkClient.test.ts
  • tests/unit/focusPosRegistration.test.tsx
  • tests/unit/focusSaveConnectionHandler.test.ts
  • tests/unit/focusSetupWizard.test.tsx
  • tests/unit/focusSyncB6.test.tsx
  • tests/unit/focusSyncDataHandler.test.ts
  • tests/unit/focusTestConnectionHandler.test.ts
  • tests/unit/focusTransactionSyncHandler.test.ts
  • tests/unit/useFocusConnection.test.tsx

Comment on lines +7 to +8
**Live verify:** `source "/Users/josedelgado/Documents/Cold Stone Setup/focus-creds.env"`
(`FOCUS_API_KEY`/`FOCUS_API_SECRET`/`FOCUS_RESTAURANT_GUID`) — **never echo/print/commit the values**.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a repo-relative verification path.

The hard-coded /Users/.../focus-creds.env only works on one machine and leaks a private filesystem layout into the repo. Please replace it with the env-var/repo-relative form used elsewhere in the doc.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md`
around lines 7 - 8, The live verification step currently hard-codes a
machine-specific absolute path, which leaks a private filesystem layout and
breaks portability. Update the verification note in this plan to use the same
repo-relative or env-var-based form used elsewhere in the document, and keep the
`FOCUS_API_KEY`/`FOCUS_API_SECRET`/`FOCUS_RESTAURANT_GUID` guidance intact
without exposing any values.

Comment on lines +169 to +178
- **Custom range (new)**: if body has `{ startDate, endDate }` (ISO `YYYY-MM-DD`, ≤ 90 days, `start ≤ end`):
run it in the background via an injectable `deps.scheduleBackground(promise)`:
- index.ts passes `(p) => (globalThis as any).EdgeRuntime?.waitUntil?.(p) ?? p`.
- tests pass a collector (`(p)=>{ tasks.push(p) }`) and await it, so the range logic is fully tested.
- Range logic = `processDateRangeTransactions(deps, config, startDate, endDate, { maxDays: 90 })`
(new small helper in `focusTransactionSyncHandler.ts` or `focusBackfillBatch.ts`): iterate the explicit
date list, `processDayTransactions` each, then one `sync_focus_transactions_to_unified_sales(start,end)`.
Respond immediately `{ backgrounded:true, mode:'custom' }`.
- Note: custom-range uses `waitUntil` (not the durable cron), acceptable because ranges are small and
user-retriggerable; the important 90-day backfill is on the durable cron.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the obsolete backgrounding path here.

This section still routes custom range through scheduleBackground/waitUntil, but §8.2 explicitly removes that path and makes custom range synchronous. Leaving both versions in the doc makes the intended behavior ambiguous.

🧰 Tools
🪛 LanguageTool

[style] ~174-~174: This is not the usual sequence for adjectives that have no special emphasis.
Context: ...rtDate, endDate, { maxDays: 90 }) (new small helper infocusTransactionSyncHandler....

(EN_ADJ_ORDER)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.md`
around lines 169 - 178, The custom-range flow still uses the obsolete
backgrounding path, which conflicts with the updated design and makes the
behavior ambiguous. Remove the scheduleBackground/waitUntil wording from the
custom range section and update the flow to be synchronous instead. Keep the
range-processing logic centered on processDateRangeTransactions and the
custom-range handling in focusTransactionSyncHandler or focusBackfillBatch, but
describe it as executing inline and returning the normal response path rather
than backgrounded:true.

Comment on lines +212 to +214
- Connection query: add a **conditional `refetchInterval`** so progress updates while backfilling:
`refetchInterval: (q) => q.state.data && !q.state.data.initial_sync_done && q.state.data.is_active ? 8000 : false`.
Keep `staleTime: 30000`. (No manual caching — pure React Query, per CLAUDE.md.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Stop polling errored connections here too.

This refetch interval keeps firing while is_active and !initial_sync_done, even if the connection has already entered error. That leaves the UI polling a dead sync until the later §8.5 rewrite lands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.md`
around lines 212 - 214, Update the Connection query in the relevant React Query
hook so polling stops as soon as a connection enters error, even if it is still
is_active and initial_sync_done is false. Adjust the conditional refetchInterval
logic to also check the connection status before returning 8000, and otherwise
return false; keep staleTime unchanged. Use the existing query setup and data
fields in the Connection query function/hook to locate the change.

Comment thread package.json
"date-fns": "^3.6.0",
"date-fns-tz": "^3.2.0",
"embla-carousel-react": "^8.6.0",
"fast-xml-parser": "^4.5.6",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n== files ==\n'
git ls-files | rg '(^|/)(package\.json|focusDatafeedParser\.ts|tests/fixtures/focus-datafeed-sample\.xml|fast-xml-parser|xmlparser|xml-parser)' || true

printf '\n== search fast-xml-parser ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'fast-xml-parser|XMLParser|DOCTYPE|entity' .

printf '\n== package.json slice ==\n'
nl -ba package.json | sed -n '85,110p'

Repository: toyiyo/nimble-pnl

Length of output: 50375


🏁 Script executed:

set -euo pipefail

echo '== focusDatafeedParser.ts =='
fd -a 'focusDatafeedParser.ts' . || true

echo '== focus-datafeed-sample.xml =='
fd -a 'focus-datafeed-sample.xml' . || true

echo '== package.json dependency context =='
nl -ba package.json | sed -n '90,105p'

echo '== fast-xml-parser usage search =='
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'fast-xml-parser|XMLParser|doctype|DOCTYPE|entity' .

Repository: toyiyo/nimble-pnl

Length of output: 424


🏁 Script executed:

set -euo pipefail

echo '== focusDatafeedParser.ts matches =='
rg -n -C 4 'XMLParser|fast-xml-parser|doctype|DOCTYPE|entity' supabase/functions/_shared/focusDatafeedParser.ts

echo '== focusDatafeedParser.ts excerpt =='
sed -n '1,220p' supabase/functions/_shared/focusDatafeedParser.ts

echo '== package.json excerpt =='
sed -n '90,105p' package.json

echo '== package-lock fast-xml-parser excerpt =='
rg -n -C 2 '"fast-xml-parser"|node_modules/fast-xml-parser|resolved": ".*fast-xml-parser' package-lock.json

Repository: toyiyo/nimble-pnl

Length of output: 8346


🌐 Web query:

fast-xml-parser 4.5.6 entity shadowing DoS advisory 4.1.3 5.3.5 CVE-2026-33036

💡 Result:

The vulnerability identified as CVE-2026-33036 is an XML entity expansion Denial of Service (DoS) flaw in the fast-xml-parser library [1][2]. It functions as an incomplete fix or bypass for the previous CVE-2026-26278 [3][1]. Vulnerability Summary: Versions 4.0.0-beta.3 through 5.5.5 are affected [1][2]. The issue stems from the replaceEntitiesValue function in OrderedObjParser.js, which only applies expansion limits (such as maxTotalExpansions and maxExpandedLength) to DOCTYPE-defined entities [3][1]. Numeric character references (e.g., &#NNN;, &#xHH;) and standard XML entities are processed via a separate code path that lacks these expansion counters [3][2]. This allows attackers to bypass configured limits by using large quantities of numeric entity references, leading to excessive memory allocation, high CPU consumption, and potential process crashes [3][1]. Related Advisories and Issues: - CVE-2026-33036: Numeric entity expansion bypass (Fixed in 5.5.6) [1][2]. - CVE-2026-26278: Original DoS through entity expansion in DOCTYPE (Affected: 4.1.3 to 4.5.4, 5.0.0 to 5.3.6; Fixed in 4.5.4, 5.3.6) [4][5]. - CVE-2026-25896: Entity encoding bypass via regex injection in DOCTYPE entity names (Affected: 4.1.3 to 5.3.4; Fixed in 4.5.4, 5.3.5) [6][7][8]. - CVE-2026-33349: Improper validation of specified quantity in input when limits are set to 0 (Fixed in 5.5.7) [9]. It is recommended to upgrade to the latest version of fast-xml-parser (at least 5.5.7 or higher) to address these and subsequent related security issues [9].

Citations:


package.json:98 — upgrade fast-xml-parser off 4.5.6. This parser consumes untrusted Focus XML, and 4.5.6 is in the affected range for CVE-2026-33036. Move to a fixed release (5.5.6+) and keep DTD/entity handling disabled in supabase/functions/_shared/focusDatafeedParser.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 98, Upgrade the fast-xml-parser dependency in
package.json from 4.5.6 to a fixed 5.5.6+ release, since it parses untrusted
Focus XML and the current version is in the affected CVE range. Then verify
supabase/functions/_shared/focusDatafeedParser.ts still keeps DTD/entity
handling disabled when constructing or using the parser, and adjust the parser
configuration there if needed.

Comment thread supabase/migrations/20260701140000_focus_transactions_cron.sql Outdated
Comment thread supabase/migrations/20260701150000_focus_transactions_integrity.sql
Comment on lines +124 to +188
-- 20-22: SELECT policy — member can read (all three tables)
-- We verify by checking the policy rows in pg_policies.
-- ─────────────────────────────────────────────────────────────────────
SELECT ok(
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'public'
AND tablename = 'focus_orders'
AND cmd = 'SELECT'
),
'focus_orders has a SELECT policy'
);

SELECT ok(
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'public'
AND tablename = 'focus_order_items'
AND cmd = 'SELECT'
),
'focus_order_items has a SELECT policy'
);

SELECT ok(
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'public'
AND tablename = 'focus_payments'
AND cmd = 'SELECT'
),
'focus_payments has a SELECT policy'
);

-- ─────────────────────────────────────────────────────────────────────
-- 23-25: FOR ALL policy exists on each transaction table
-- ─────────────────────────────────────────────────────────────────────
SELECT ok(
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'public'
AND tablename = 'focus_orders'
AND cmd = 'ALL'
),
'focus_orders has a FOR ALL (owner/manager) policy'
);

SELECT ok(
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'public'
AND tablename = 'focus_order_items'
AND cmd = 'ALL'
),
'focus_order_items has a FOR ALL (owner/manager) policy'
);

SELECT ok(
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'public'
AND tablename = 'focus_payments'
AND cmd = 'ALL'
),
'focus_payments has a FOR ALL (owner/manager) policy'
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Assert the policy predicate, not just its existence.

These checks only prove that a SELECT/ALL policy exists; a permissive policy with the wrong USING clause would still pass. Please verify the user_restaurants membership and role IN ('owner', 'manager') expressions from the migration contract via pg_policies.qual/with_check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/tests/46_focus_transactions_schema.sql` around lines 124 - 188, The
current checks in the focus transaction schema test only confirm that SELECT and
ALL policies exist on the transaction tables, but they do not verify the actual
policy predicates. Update the assertions around pg_policies for focus_orders,
focus_order_items, and focus_payments to inspect the policy condition fields
(such as qual and with_check) and confirm they match the migration contract,
specifically the user_restaurants membership and role IN ('owner', 'manager')
logic.

Comment thread supabase/tests/46_focus_transactions_schema.sql Outdated
Comment thread tests/unit/focusSyncB6.test.tsx
jdelgado2002 added a commit that referenced this pull request Jul 2, 2026
…st 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>
@jdelgado2002

Copy link
Copy Markdown
Member Author

Review triage complete (Phase 9d, commit def81015)

Fixed (commit def8101)

# Reviewer Finding Resolution
1 github-code-quality Unused beforeEach import in focusListRestaurantsHandler.test.ts Removed
2 CodeRabbit major focusDatafeedParser.ts: XMLParser without processEntities: false — XXE/DoS risk Fixed: added processEntities: false
3 CodeRabbit major focus-backfill-sync/index.ts: fail-open Bearer auth on empty env key Fixed: early-return 500 when URL/key absent
4 CodeRabbit minor focusBackfillSyncHandler.ts: fire-and-forget .then() missing .catch() Fixed: added .catch()
5 CodeRabbit nit 20260701140000_focus_transactions_cron.sql: comment says */6 but schedule is 0 */6 Fixed comment
6 CodeRabbit nit focusSyncB6.test.tsx: test title claims toast assertion that never runs Renamed test title to match actual assertions
7 CodeRabbit minor 46_focus_transactions_schema.sql: card_last4 only tests 5-digit case Added 2 cases: 3-digit too-short + non-digit '12AB'
8 CodeRabbit minor focusSyncDataHandler.ts: incremental sync errors not persisted to connection_status Fixed: persist error state + tests
9 CodeRabbit minor focusSyncDataHandler.ts: Lynk decrypt not wrapped in try/catch Fixed: wrapped with jsonError(500)
10 CodeRabbit minor focusBulkSyncHandler.ts: Lynk incremental failures don't persist connection_status Fixed: best-effort error write + tests
11 CodeRabbit minor focusBulkSyncHandler.ts: failed connections never get last_sync_time bumped (starvation) Fixed: best-effort bump in catch block + test

Declined

Codex P1 (focusBackfillBatch.ts:163): Backfilled days never reach unified_sales — 6h cron only covers 3 days.

The backfill design deliberately skips the per-day RPC (CPU budget). The 6-hour sync_all_focus_transactions_to_unified_sales cron calls _impl with a 3-day lookback window — correct for incremental syncs. For historical backfill (days >3 ago), the intent was to widen that window. This is a known trade-off acknowledged in the design. The correct fix is to widen the cron window from 3 days to cover the full 90-day backfill window, or to call the RPC once when initial_sync_done flips — that's a separate schema migration outside this PR's scope. Filed as a follow-up task.

Codex P1 (focusTransactionSyncHandler.ts:284): Voided checks don't clean up unified_sales rows.

The TypeScript layer deletes focus_orders (with ON DELETE CASCADE cleaning up focus_order_items / focus_payments). The SQL _sync_focus_transactions_to_unified_sales_impl function's Step 2 performs an orphan-delete of unified_sales rows whose external_order_id has no matching focus_orders row for the given date. The next incremental sync call after a void will clean up unified_sales. This is covered by design.

CodeRabbit nit (useFocusConnection.tsx): staleTime: 0 → revert to 30_000.

The code comment at line 60 and 81 explains why staleTime: 0 is needed: during active backfill (the polling window), stale data from a previous render would cause refetchInterval ticks to skip the network call, silencing progress updates. Once backfill stops (initial_sync_done=true or connection_status=error), the interval stops anyway. The low staleTime cost is bounded to the backfill window only.

CodeRabbit (focusSyncDataHandler.ts:319): unified_sales RPC failure silently swallowed — still reports success.

Intentional: the data is durably written to focus_orders/items/payments; the 6-hour cron safety-net picks it up. Surfacing this as an error to the caller would cause unnecessary retries and alarm for a non-fatal lag.

CodeRabbit (focusBulkSyncHandler.ts:196): focusBulkSyncHandler.ts:229 decrypt not in try/catch.

The bulk-sync handler wraps the entire processConnection call in a try/catch at the outer loop (line ~387); a decrypt throw would be caught there, logged, added to errors[], and the restaurant skipped safely. No separate inner try/catch needed.

CodeRabbit (focusLynkClient.ts:229): Per-day network latency can exceed backfill budget.

Valid observation; the TIMEOUT_MS per-request is already set at 30s (not 60s) for the POST and 30s for the blob GET. The backfill budget is a soft cap — if Focus is slow, we process fewer days that tick (cursor not advanced on timeout). Wiring a deadline AbortController across requests is an enhancement beyond this PR's scope.

CodeRabbit (focusSyncDataHandler.ts:102): Missing CAS chain level in type surface.

The 4-step chain .update().eq().eq().eq('sync_cursor', ...).select() works at runtime. The CasEq3 interface is narrow but correct: the 3rd .eq() returns an object with only .select(), which is exactly what the code calls. Widening the type would require adding a 4th level (CasEq4) — a refactor with no runtime impact. Deferred.

CodeRabbit (focusTransactionSyncHandler.test.ts): Add CRITICAL: prefix to financial-path tests.

Valid style suggestion but the CRITICAL: prefix convention is not enforced by CI and the tests already have descriptive names. Deferred to a future cleanup pass.

CodeRabbit (plan + spec docs): waitUntil / maxDays: 30 still in planning docs.

The planning docs describe the pre-binding state; §8 binding resolutions supersede them. The implementation is correct (synchronous custom range, maxDays=7 in cron). Fixing historical docs doesn't affect correctness.

CodeRabbit (46_focus_transactions_schema.sql:188): Assert RLS policy predicate, not just existence.

Checking pg_policies.qual for the exact USING expression would be more rigorous, but the migration already tests the policies behaviorally via actual INSERT operations as a restricted role. Adding predicate string matching would be brittle against whitespace changes. Deferred.

jdelgado2002 and others added 18 commits July 2, 2026 17:21
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fill

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…om-range, budgets, a11y)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… review-hardened)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ds, 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>
…ntry (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>
…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>
…cker 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>
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>
…ransactions (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>
…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>
…ckfill 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>
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>
…ive 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>
…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>
…dlers, 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>
…e, 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>
…tom-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>
jdelgado2002 and others added 5 commits July 2, 2026 17:21
…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>
…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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…st 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 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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
supabase/functions/_shared/focusBulkSyncHandler.ts (1)

400-411: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Skip sync_cursor on Lynk backfill rows. This write can clobber a newer cursor from focus-backfill-sync: the Lynk backfill branch returns row.sync_cursor unchanged, so if the cron advances between the read and this update, bulk sync regresses the row back to the stale value. Omit sync_cursor/initial_sync_done from this path or add a CAS on sync_cursor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/_shared/focusBulkSyncHandler.ts` around lines 400 - 411,
The update in focusBulkSyncHandler should not blindly write sync_cursor and
initial_sync_done for Lynk backfill rows, since that can overwrite a newer
cursor from focus-backfill-sync. In the bulk sync persistence path, use the
existing identifiers row.sync_cursor and newSyncCursor to either omit
sync_cursor/initial_sync_done from this update when the Lynk backfill branch is
returning the cursor unchanged, or add a compare-and-swap guard on sync_cursor
before calling .update(...).eq('id', row.id).eq('restaurant_id',
row.restaurant_id).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@supabase/functions/_shared/focusBulkSyncHandler.ts`:
- Around line 400-411: The update in focusBulkSyncHandler should not blindly
write sync_cursor and initial_sync_done for Lynk backfill rows, since that can
overwrite a newer cursor from focus-backfill-sync. In the bulk sync persistence
path, use the existing identifiers row.sync_cursor and newSyncCursor to either
omit sync_cursor/initial_sync_done from this update when the Lynk backfill
branch is returning the cursor unchanged, or add a compare-and-swap guard on
sync_cursor before calling .update(...).eq('id', row.id).eq('restaurant_id',
row.restaurant_id).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8ac4e24f-0bb3-48f0-b770-d1bdb475d573

📥 Commits

Reviewing files that changed from the base of the PR and between 7937f16 and f8dc402.

📒 Files selected for processing (31)
  • docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md
  • docs/superpowers/specs/2026-07-02-focus-picker-and-background-sync-design.md
  • src/components/FocusSync.tsx
  • src/components/pos/FocusSetupWizard.tsx
  • src/components/pos/SyncComponents.tsx
  • src/hooks/useFocusConnection.tsx
  • supabase/config.toml
  • supabase/functions/_shared/focusBackfillBatch.ts
  • supabase/functions/_shared/focusBackfillSyncHandler.ts
  • supabase/functions/_shared/focusBulkSyncHandler.ts
  • supabase/functions/_shared/focusDatafeedParser.ts
  • supabase/functions/_shared/focusListRestaurantsHandler.ts
  • supabase/functions/_shared/focusSyncDataHandler.ts
  • supabase/functions/_shared/focusTransactionSyncHandler.ts
  • supabase/functions/focus-backfill-sync/index.ts
  • supabase/functions/focus-bulk-sync/index.ts
  • supabase/functions/focus-list-restaurants/index.ts
  • supabase/functions/focus-sync-data/index.ts
  • supabase/migrations/20260701140000_focus_transactions_cron.sql
  • supabase/migrations/20260702120000_focus_backfill_cron.sql
  • supabase/tests/46_focus_transactions_schema.sql
  • supabase/tests/48_focus_backfill_cron.sql
  • tests/unit/focusBackfillBatch.test.ts
  • tests/unit/focusBackfillSyncHandler.test.ts
  • tests/unit/focusBulkSyncHandler.test.ts
  • tests/unit/focusListRestaurantsHandler.test.ts
  • tests/unit/focusSetupWizard.test.tsx
  • tests/unit/focusSyncB6.test.tsx
  • tests/unit/focusSyncDataHandler.test.ts
  • tests/unit/focusTransactionSyncHandler.test.ts
  • tests/unit/useFocusConnection.test.tsx
✅ Files skipped from review due to trivial changes (2)
  • supabase/migrations/20260701140000_focus_transactions_cron.sql
  • docs/superpowers/plans/2026-07-02-focus-picker-and-background-sync-plan.md
🚧 Files skipped from review as they are similar to previous changes (23)
  • supabase/tests/48_focus_backfill_cron.sql
  • supabase/functions/focus-sync-data/index.ts
  • supabase/migrations/20260702120000_focus_backfill_cron.sql
  • supabase/functions/focus-bulk-sync/index.ts
  • tests/unit/focusBackfillSyncHandler.test.ts
  • supabase/functions/focus-backfill-sync/index.ts
  • tests/unit/focusSyncB6.test.tsx
  • supabase/config.toml
  • supabase/functions/focus-list-restaurants/index.ts
  • tests/unit/focusSyncDataHandler.test.ts
  • supabase/functions/_shared/focusBackfillSyncHandler.ts
  • src/components/FocusSync.tsx
  • supabase/functions/_shared/focusTransactionSyncHandler.ts
  • src/components/pos/SyncComponents.tsx
  • tests/unit/focusListRestaurantsHandler.test.ts
  • supabase/functions/_shared/focusListRestaurantsHandler.ts
  • tests/unit/focusTransactionSyncHandler.test.ts
  • supabase/functions/_shared/focusBackfillBatch.ts
  • tests/unit/focusBackfillBatch.test.ts
  • supabase/tests/46_focus_transactions_schema.sql
  • src/components/pos/FocusSetupWizard.tsx
  • tests/unit/focusSetupWizard.test.tsx
  • supabase/functions/_shared/focusSyncDataHandler.ts

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>
@jdelgado2002

Copy link
Copy Markdown
Member Author

Review re-triage — post-rebase (commit 07c3bcc3)

PR #563 was squash-merged into main mid-run, so this branch was rebased --onto origin/main (dropping #563's now-redundant originals). The diff is now increment-only (+5.8k/−726, 32 files) and CodeRabbit/Codex re-reviewed the rebased commit. Re-triage below.

✅ Fixed (commit 07c3bcc3)

Finding Resolution
Codex P1focusBackfillBatch.tsSync backfilled days into unified_sales Real bug, fixed. Backfill writes with skipUnifiedSalesSync:true, and both unified_sales crons only re-sync the last 2 business days — so backfilled history (days 3..90) never reached unified_sales/P&L. processBackfillBatch now calls sync_focus_transactions_to_unified_sales for its processed range after the loop, plus a full 90-day window on completion as a reconciliation backstop. Non-fatal on RPC error (order rows are durably written). +4 tests.

❌ Declined — false positive (with evidence)

Finding Reason
Codex P1focusTransactionSyncHandler.ts:284Remove unified_sales rows for deleted checks The RPC's Step 2 orphan-delete removes unified_sales rows via external_item_id NOT IN (SELECT … FROM focus_order_items WHERE business_date = …) (migration 20260701130000, lines 116/205/254). A voided check (its items CASCADE-deleted from focus_order_items) therefore has its sale/tip/discount rows orphan-deleted when the RPC runs for that date. The P1 fix above ensures that RPC now runs for backfilled ranges too.

✅ Already addressed (verified in current code)

  • CodeRabbit Security Majorfocus-backfill-sync/index.ts fail-open Bearer → the entry fails closed: returns 500 if SUPABASE_SERVICE_ROLE_KEY/SUPABASE_URL is empty, before any auth compare.
  • CodeRabbit MinorfocusBackfillSyncHandler.ts fire-and-forget update lacks .catch() → the best-effort error-state write already has a .then(...).catch(...) chain.
  • github-code-qualityfocusListRestaurantsHandler.test.ts:18 unused beforeEach → already removed (import { describe, it, expect, vi }).

❌ Declined — by design (with rationale)

  • CodeRabbit MajorfocusTransactionSyncHandler.ts:319 unified_sales RPC failure swallowed, returns okIncremental path: the 6-hour transaction cron re-syncs the last 2 business days, so a transient RPC failure self-heals while the focus_orders/items/payments rows are durably written. Backfill path: the new completion full-window sync (above) is the backstop. Making it fatal would strand durably-written order rows on a transient RPC blip.

⏭️ Out of scope — already-merged #563 code

CodeRabbit comments on supabase/migrations/20260701130000|140000|150000_*.sql, supabase/tests/46_focus_transactions_schema.sql, focusDatafeedParser.ts, and package.json target code already merged to main via #563 — not in this PR's diff. If any are real, they belong in a follow-up against main, not here.

📝 Nits (read, not actioned)

  • Design/plan doc suggestions (*-design.md, *-plan.md) — these are point-in-time design records; kept as-authored.
  • focusSyncB6.test.tsx:222 test-title wording — the test still validates the behavior via its assertions.

Re-verified locally on main: typecheck 0 · lint (changed files) 0 errors · 5,324 tests pass · build ✓.

…bbit 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>
@jdelgado2002

Copy link
Copy Markdown
Member Author

Follow-up fix (commit 7ef52158)

CodeRabbit MajorfocusBulkSyncHandler.ts Skip sync_cursor on Lynk backfill rowsvalid, fixed. 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 write, bulk-sync regressed it to the stale value (and spuriously bumped last_sync_time). processConnection now returns skipped:true and the caller writes nothing for skipped rows (still counted as processed). Test updated to assert no write. Re-verified: typecheck 0 · 5,324 tests pass.

@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant