Skip to content

Claude/field stay scalability lgfh15 - #508

Merged
smj1860 merged 11 commits into
mainfrom
claude/field-stay-scalability-lgfh15
Jul 26, 2026
Merged

Claude/field stay scalability lgfh15#508
smj1860 merged 11 commits into
mainfrom
claude/field-stay-scalability-lgfh15

Conversation

@smj1860

@smj1860 smj1860 commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Improved booking source display for OwnerRez (labels, colors, and filtering).
    • Added automated daily cleanup of older notifications.
  • Bug Fixes
    • Updated Crew Feedback to show submission dates instead of creation dates.
    • Added clearer “temporarily rate-limited” messaging during OAuth/connect flows.
    • Improved Kroger request handling with consistent rate-limit and retry behavior.
  • Maintenance
    • Hostaway integration remains unavailable during rollout work.
    • Added stronger safeguards to keep database schema and app types in sync.

claude added 9 commits July 25, 2026 19:21
The notifications table (in-app bell events, added 2026-07-15) had no
retention job and grew forever — every other append-heavy table already
has one. New cron-notifications-retention runs daily at 9:30am CT
(15 min after dailyGuestPiiRetention, continuing the retention-cron
stagger) and enforces:

  - read rows (read_at IS NOT NULL) older than 90 days: deleted
  - all rows, read or unread, older than 180 days: deleted

Deletes run in bounded batches (select up to 500 ids, delete by id,
repeat, max 20 batches per policy per run) — never one unbounded
DELETE — so a large backlog degrades to "finish tomorrow" rather than
one giant transaction. Both steps are pure deletes and safe for Inngest
to replay. Service client created inside step.run() only, with the
{ system: 'inngest:notifications-retention' } context. Pure cron
trigger like the sibling retention crons — no new FieldStayEvents
entry needed. Log lines carry row counts only, never notification
content. Registered in the single serve() call in the Inngest route.

Unit test mirrors the queue-based .from() mock convention of
cron-comms-retention.test.ts: policy filters, bounded id-batch deletes,
batch-ceiling behavior, cutoff date math, and error-throw-for-retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp
Adds the database-side broadcast infrastructure for the crew PWA
realtime redesign (Option B: broadcast signal + delta pull), per
docs/CREW_SYNC_V2_PHASES.md section 2:

- notify_crew_sync(uuid[], text): shared realtime.send() helper on
  topic 'crew:{user_id}', event 'sync', payload {entity} only (signal,
  never row data). Per-user exception-safe so a broadcast failure can
  never break the write that triggered it.
- Statement-level AFTER triggers with transition tables (one broadcast
  per statement, not per row): turnover_assignments (INS/UPD/DEL) and
  turnovers (UPD) -> 'turnovers'; checklist_instances and
  checklist_instance_items (INS/UPD) -> 'checklists'; work_orders
  (INS/UPD/DEL) -> 'work_orders'. UPDATE triggers notify old AND new
  assignees so reassignment signals both sides.
- All functions SECURITY DEFINER with pinned empty search_path;
  EXECUTE revoked from PUBLIC/anon/authenticated on the helper AND on
  all five trigger functions (the latter added beyond the spec SQL:
  Supabase security advisors flagged the trigger functions as
  anon/authenticated-executable via PostgREST RPC, lints 0028/0029 --
  confirmed cleared on re-run).
- RLS policy on realtime.messages authorizing each authenticated user
  to receive broadcasts only on their own 'crew:{auth.uid()}' topic
  (negative subscription test verified: foreign topic -> Unauthorized).

Applied to production (vpmznjktllhmmbfnxuvk) and e2e
(syhthijeqlnltufdawyb). Touch tests verified correct realtime.messages
rows for all three entities; scratch client received its own-topic
broadcast and was rejected from a foreign topic. No client subscribes
to these topics until Phase 3, so this ships with zero user-facing
behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp
…Phase 4)

Failed outbox mutations previously re-pushed on every drain trigger
(online event, every enqueueMutation), hammering the server during an
outage. Now a push failure sets nextAttemptAt on the mutation row:
Date.now() + min(2^(retryCount-1) * 5s, 5min) scaled by a uniform
0.5-1.5x jitter factor, so delays grow 5s -> 10s -> 20s ... capped at
5 minutes and a fleet of crew devices recovering from the same outage
doesn't retry in lockstep.

- lib/dexie/schema.ts: Dexie version(8); MutationRow gains
  nextAttemptAt?: number (epoch ms). Non-indexed - the drain scans in
  insertion order - so index strings are unchanged; the full store map
  is repeated as a complete snapshot per the file's v1-v3 pattern.
- lib/dexie/syncService.ts processOutbox(): a head mutation still
  inside its backoff window stops the drain entirely (never
  skip-and-continue - later mutations against the same record must not
  jump ahead; "not due yet" joins the existing stop-on-first-error
  semantics) and schedules a single one-shot resume timer on the
  SyncEngine instance (previous timer cleared first). Success deletes
  the row, clearing nextAttemptAt with it. The MAX_RETRIES=5
  dead-letter (failed: true) classification is preserved exactly.
- unit/dexie/sync-outbox-backoff.test.ts (new, with additive
  fake-dexie.ts extensions: add/update/delete/orderBy on fakeTable,
  mutations table, update in the supabase chain): backoff growth, cap,
  and jitter bounds; drain stops at a not-yet-due head touching
  nothing behind it and resumes via the timer; due mutation retries
  and clears nextAttemptAt on success; permanent-failure path
  unchanged and excluded from later drains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp
Adds the crew PWA's broadcast-signal client path per
docs/CREW_SYNC_V2_PHASES.md section 3, gated on
NEXT_PUBLIC_CREW_SYNC_V2 so it ships dormant — the flag defaults off
and the existing three postgres_changes channels + generation-token
machinery in lib/dexie/context.tsx are untouched (pure additions: one
early-return in run() when the flag is on, new teardown lines appended
after the existing ones, no modified lines in the v1 path).

Flag on: one private broadcast channel (crew:{userId}), subscribed
after supabase.realtime.setAuth() and re-authed on TOKEN_REFRESHED;
signals route through a per-entity 1s trailing debounce with in-flight
serialization (exactly one queued follow-up, never stacked); each
entity resolves to a full-scope pull so cursor advancement stays safe
per the doc's cursor invariants. Safety poll every 5 minutes (also the
only freshness path for property_assets, which has no trigger by
design) plus resyncs on mount/online/visibilitychange/(re)subscribe.
Reconnect on CHANNEL_ERROR/TIMED_OUT/CLOSED after 5s base + uniform
[0,30s] jitter to avoid a rejoin thundering herd.

The per-entity debouncer, entity->action mapping, and reconnect-jitter
math are extracted into lib/dexie/sync/signals.ts as pure/injectable
functions, unit-tested with fake timers in unit/dexie/sync-signals.test.ts
and unit/dexie/sync-reconnect-jitter.test.ts (burst coalescing,
in-flight serialization with a single queued follow-up, unknown
entities ignored, each entity invoking its own sync fn, jitter always
within [5s, 35s]). Documents NEXT_PUBLIC_CREW_SYNC_V2 in .env.example.

Deviation: the doc's Phase 1 field-soak precondition is waived per the
project owner — pre-launch, no field crew traffic exists yet to soak
against.

Manual smoke (doc 3d): the disposable e2e crew user/turnover were
seeded and torn down on the syhthijeqlnltufdawyb project, and the app
was built+started against that project with the flag on. The
browser-driven half of the smoke (Playwright/Chromium hitting
Supabase auth through the sandbox's required HTTPS proxy) could not be
completed — confirmed via a minimal repro that an unrelated bare
page.goto() to Supabase's own endpoint fails the same way through
Chromium's network stack over this proxy, while curl through the
identical proxy succeeds, ruling out a TLS/cert issue and pointing at
a sandbox/Chromium proxy-tunneling limitation rather than anything in
this change. Phase 2's already-completed pure-Node scratch-client test
independently proved the wire-level mechanism this phase's UI consumes
(private channel subscribe via realtime.setAuth(), broadcast delivery,
cross-user-topic rejection). Full verification pass (tsc, lint,
vitest, check:ui-classes) is green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp
Closes the last item from PR #505's db-invariants job: check 4 (types vs.
live schema) was deferred. Adds public.db_type_shape_report() (mirrors
db_invariant_report()'s SECURITY DEFINER/service-role-only pattern) and
scripts/check-type-drift.mjs, which diffs it against a mechanical parse of
types/database.ts — every enum's labels, every table's presence, and
column presence for every table wired into Database.public.Tables. Wired
into the existing db-invariants CI job as a second step.

First real run against both projects surfaced three genuine drift
incidents beyond the wo_status one that motivated this check:

- wo_source was missing 'vacancy_gap_suggestion', which
  advanceScheduleAfterCompletion() already branches on
  (20260725201000_add_vacancy_gap_suggestion_to_wo_source.sql).
- crew_feedback's timestamp column was renamed created_at -> submitted_at
  on both projects by an earlier drift-capture migration, but
  support-inbox/page.tsx and its client still queried/displayed the old
  name — a live "column does not exist" bug in the support inbox feedback
  list, now fixed alongside the stale CrewFeedback interface.
- inventory_count_drafts never actually had the reviewed_at/reviewed_by
  columns approveInventoryCount()/rejectInventoryCount() write to (an
  earlier migration defined them but no-op'd against an
  already-existing table) — every PM approve/reject of a pending count
  was failing. Added the columns for real, with their FK covering index
  (20260725201500_add_reviewed_columns_to_inventory_count_drafts.sql).

Also reconciled a long tail of types/database.ts fields that had drifted
from the live schema (BookingSource.ownerrez, SupportMessageRole.human,
Organization/Property/Booking/Turnover/WorkOrder/MaintenanceSchedule
columns, TurnoverAssignment/PushSubscription/SupportConversation/
SupportMessage/InventoryCountDraft(Item)/InventoryTemplateItem shapes,
new OrgSmsTemplate interface) and wired a dozen previously-unmapped but
already-interfaced tables into Database.public.Tables. Intentional
mismatches (the deprecated work_orders.assigned_crew_id, join-only
relationship fields, DB-internal-only tables like platform_admins) are
allowlisted in the new script, same shrink-only ratchet as
SERVICE_ROLE_ONLY_TABLES.

Migrations applied to both vpmznjktllhmmbfnxuvk and syhthijeqlnltufdawyb.
get_advisors on production shows no new findings from db_type_shape_report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp
Product decision: Hostaway must not be reachable at all right now, but
must stay easy to re-enable later. The connect UI/server-actions were
already disabled in 4c239e3 (revenue-posting gap), but the two real
chokepoints were still live — this closes them:

- lib/integrations/registry.ts: comment out the hostawayProvider import
  and its map entry, so getProvider('hostaway') now throws (webhook
  route at app/api/webhooks/[provider]/route.ts already 404s on that,
  and disconnectIntegration's revoke path already no-ops on failure)
- app/api/inngest/route.ts: comment out the hostawayInitialSync import
  and its serve() array entry, so the job can never be invoked by
  Inngest even if something (a stray manual trigger, a resurrected
  send()) tried to fire integration/hostaway.sync.requested

Both lib/integrations/providers/hostaway.ts and
lib/inngest/functions/hostaway/initial-sync.ts are left functionally
untouched — only a top-of-file comment was added pointing at exactly
what's commented out and where, so re-enabling is a matter of
uncommenting four spots rather than reconstructing anything.

Verified no test asserts on registry contents or the Inngest functions
array (all mock the registry module wholesale), so the existing
Hostaway-specific tests still validate the adapter/job in isolation
unchanged. ops/page.tsx and lib/support/account-tools.ts's Hostaway
mentions are pre-existing generic/example references unrelated to the
connect flow — left alone so support tooling still surfaces any
historical connection state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp
… spend gate

The daily nudge budget check in lib/sms/telnyx.ts already fails closed on a
Redis error (shipped in 268beb5) — this only adds reportError alongside the
existing console.error so the outage surfaces in Sentry, not just logs, and
documents the fail-closed/fail-open split in CLAUDE.md's SMS section per
docs/SCALABILITY_TIERS_REMAINING.md section 4. Audited every other Redis-
backed limiter in the codebase (lib/rate-limit.ts consumers, proxy.ts,
OwnerRez/Hospitable API budgets) — none of them gate real spend, so none
needed a fail-open -> fail-closed change; see session report for the full
classification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp
Cart automation fanning out across orgs shares one app-level Kroger
client credential (one Vercel deployment IP/token), same class of
problem as OwnerRez/Hospitable's shared-IP budgets — but Kroger had no
limiter at all before this, so a busy cart-build tick could burn the
whole platform's daily quota and 429 every other org silently.

lib/rate-limit.ts gains four endpoint-class Upstash sliding-window
limiters, each a fixed shared identifier (not per-org): products
(9,000/day, 90% headroom off Kroger's confirmed 10,000/day) and
locations (1,440/day, off the confirmed 1,600/day) are sourced from
developer.kroger.com; cart and auth/identity limits aren't published
there, so those two use clearly-commented conservative defaults.

lib/kroger/client.ts routes every outbound call through a new
krogerFetch() wrapper (same shape as hospitableFetch in
providers/hospitable.ts): checks the relevant limiter first, reacts to
a real 429 by parsing Retry-After, and fails open on a Redis error
since this is an abuse/quota limiter, not a spend-budget one. Both
throw the existing shared RateLimitError.

Inside Inngest steps (build-shopping-cart.ts, kroger-connected.ts,
integration-token-refresh-handler.ts) a RateLimitError now propagates
so Inngest's own backoff retries the step — build-shopping-cart.ts's
get-customer-token step previously swallowed every error, including a
rate limit, into a silent list-only fallback; it now rethrows
RateLimitError specifically before that fallback. The two OAuth
callback routes run outside any Inngest step, so a rate limit there
gets a distinct rate_limited reason on /connect/error instead of the
generic token_exchange_failed/restart-connect-flow path, since neither
route has a retry mechanism to lean on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp
Hostaway deliberately disabled (not built) per product decision;
Kroger rate limiter, fail-closed spend budgets, and drift check all
shipped. Only Crew Sync v2 Phase 5 remains open.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp
@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
fieldstay Ready Ready Preview, Comment Jul 26, 2026 2:34am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c6671467-2b4a-4559-8094-f519b05c9356

📥 Commits

Reviewing files that changed from the base of the PR and between 81e6da8 and 5c88867.

📒 Files selected for processing (11)
  • lib/dexie/context.tsx
  • lib/dexie/sync/signals.ts
  • lib/dexie/syncService.ts
  • lib/guidebook/offer.ts
  • lib/kroger/client.ts
  • lib/rate-limit.ts
  • lib/sms/telnyx.ts
  • scripts/check-type-drift.mjs
  • supabase/migrations/20260725200500_db_type_shape_report.sql
  • supabase/migrations/20260726130000_guidebook_offer_redemptions_booking_id_index.sql
  • types/database.ts

📝 Walkthrough

Walkthrough

Crew Sync v2 adds opt-in private Realtime broadcasts with debounced entity pulls, reconnect handling, safety resyncs, and teardown. The PR also adds schema drift CI checks, outbox retry backoff, Kroger rate limiting, notification retention, Hostaway disabling, UI/type alignment, and SMS failure reporting.

Changes

Sync, schema, and reliability changes

Layer / File(s) Summary
Crew Sync broadcast synchronization
supabase/migrations/..., lib/dexie/context.tsx, lib/dexie/sync/*, unit/dexie/*, .env.example
Adds private broadcast triggers, opt-in client synchronization, debouncing, reconnect jitter, safety resyncs, and lifecycle cleanup.
Dexie outbox retry scheduling
lib/dexie/schema.ts, lib/dexie/syncService.ts, unit/dexie/*
Adds persisted retry timestamps, exponential backoff, head-of-queue gating, retry timers, and tests.
Database type drift gate
scripts/check-type-drift.mjs, supabase/migrations/*db_type_shape_report*, types/database.ts, .github/workflows/ci.yml
Compares live enum, table, and column shapes with TypeScript declarations and runs the check in CI.
Kroger rate-limit handling
lib/rate-limit.ts, lib/kroger/client.ts, lib/inngest/functions/build-shopping-cart.ts, app/api/integrations/..., app/connect/..., unit/*
Adds endpoint budgets, centralized request handling, retryable rate-limit errors, and related tests.
Retention and Hostaway registration
lib/inngest/functions/cron/*, app/api/inngest/route.ts, lib/integrations/*, docs/*
Registers bounded notification cleanup and disables Hostaway registration points.
UI, schema, and operational updates
app/(dashboard)/*, types/database.ts, lib/sms/telnyx.ts, lib/guidebook/offer.ts, supabase/migrations/*
Adds OwnerRez presentation, changes feedback timestamps, expands schema declarations, reports SMS budget failures, formats offers through helpers, and adds a booking index.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Database
  participant Realtime
  participant DexieProvider
  participant SyncSignalHandler
  participant DeltaPull
  Database->>Realtime: Broadcast entity signal
  Realtime->>DexieProvider: Deliver private crew topic event
  DexieProvider->>SyncSignalHandler: Route entity payload
  SyncSignalHandler->>DeltaPull: Debounce and execute entity pull
  DeltaPull->>DexieProvider: Update local Dexie state
Loading

Possibly related PRs

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too vague and branch-like to clearly describe the main change in the pull request. Use a concise, specific title that names the primary change, such as the main feature or subsystem updated.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/field-stay-scalability-lgfh15

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.

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

Caution

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

⚠️ Outside diff range comments (1)
.github/workflows/ci.yml (1)

66-147: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

No permissions: block — jobs inherit whatever default GITHUB_TOKEN scope the org/repo has set.

Neither the workflow top level nor the e2e/db-invariants jobs declare permissions:, so "these default permissions are excessively permissive; for example, they include read/write access to all issues, PRs, and repository contents" if the repo/org hasn't hardened its default. None of these jobs (build checks, Playwright e2e, DB invariant checks) need write access.

+permissions:
+  contents: read
+
 name: ...
 jobs:
   checks:
🤖 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 @.github/workflows/ci.yml around lines 66 - 147, Add an explicit
least-privilege permissions declaration at the workflow level, setting
GITHUB_TOKEN access to read-only repository contents and disabling unnecessary
scopes. Ensure the build, e2e, and db-invariants jobs inherit these restricted
permissions without granting write access to issues, pull requests, contents, or
other resources.

Source: Linters/SAST tools

🧹 Nitpick comments (3)
scripts/check-type-drift.mjs (1)

161-169: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a timeout to the schema-report fetch.

No timeout is set on the RPC call; a hung connection would stall this CI step for the full job/runner timeout rather than failing fast.

🔧 Proposed fix
 const res = await fetch(new URL('/rest/v1/rpc/db_type_shape_report', url), {
   method: 'POST',
   headers: {
     apikey: key,
     authorization: `Bearer ${key}`,
     'content-type': 'application/json',
   },
   body: '{}',
+  signal: AbortSignal.timeout(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 `@scripts/check-type-drift.mjs` around lines 161 - 169, Configure a finite
timeout for the RPC request in the schema-report fetch, using the existing fetch
options around the db_type_shape_report call. Ensure a hung connection aborts
promptly and causes the CI step to fail rather than waiting for the runner
timeout.
supabase/migrations/20260725201500_add_reviewed_columns_to_inventory_count_drafts.sql (1)

19-27: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Split into zero-downtime migrations to avoid blocking locks.

Adding the FK constraint here takes a table scan + SHARE ROW EXCLUSIVE lock, and the plain CREATE INDEX blocks writes during the build. Supabase applies each migration file inside a transaction, and "CONCURRENTLY cannot run inside a transaction block -- Supabase migrations run each file in a transaction, so use a separate" migration file for the index — a NOT VALID/VALIDATE CONSTRAINT split alone won't unblock CONCURRENTLY.

🔧 Proposed split
 ALTER TABLE public.inventory_count_drafts
   ADD COLUMN IF NOT EXISTS reviewed_at timestamptz,
-  ADD COLUMN IF NOT EXISTS reviewed_by uuid REFERENCES auth.users(id) ON DELETE SET NULL;
-
--- Covering index for the new FK, matching this repo's convention for every
--- other *_by/*_user_id → auth.users(id) column (see
--- idx_work_orders_completion_verified_by, idx_work_order_updates_updated_by_user_id).
-CREATE INDEX IF NOT EXISTS idx_inventory_count_drafts_reviewed_by
-  ON public.inventory_count_drafts (reviewed_by);
+  ADD COLUMN IF NOT EXISTS reviewed_by uuid,
+  ADD CONSTRAINT inventory_count_drafts_reviewed_by_fkey
+    FOREIGN KEY (reviewed_by) REFERENCES auth.users(id) ON DELETE SET NULL NOT VALID;
+
+ALTER TABLE public.inventory_count_drafts
+  VALIDATE CONSTRAINT inventory_count_drafts_reviewed_by_fkey;

Then, in a separate migration file (CONCURRENTLY cannot share a transaction with any other statement):

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_inventory_count_drafts_reviewed_by
  ON public.inventory_count_drafts (reviewed_by);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@supabase/migrations/20260725201500_add_reviewed_columns_to_inventory_count_drafts.sql`
around lines 19 - 27, Split the migration so the ALTER TABLE in the current
migration adds reviewed_at and reviewed_by without creating the foreign key or
index. Add the reviewed_by foreign key in a separate migration using the
repository’s zero-downtime constraint approach, and create
idx_inventory_count_drafts_reviewed_by with CREATE INDEX CONCURRENTLY in its own
migration file containing no other statements.

Source: Linters/SAST tools

unit/dexie/sync-outbox-backoff.test.ts (1)

87-172: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing coverage for the retryCount ≥ 3 skip-and-continue path.

All four scenarios here cover the "stop and reschedule" branch (break) and the dead-letter branch, but none seed a mutation at retryCount: 2 (so the next failure hits the newRetryCount >= 3 skip-and-continue branch) to verify a wake-up is still scheduled once the drain otherwise idles. This gap is what let the missing scheduleRetry() call in that branch (see lib/dexie/syncService.ts lines 113-124) go undetected.

🤖 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 `@unit/dexie/sync-outbox-backoff.test.ts` around lines 87 - 172, Add a test in
the “processOutbox — retry backoff” suite that seeds a mutation with retryCount:
2, forces its next push to fail, and verifies the retryCount ≥ 3
skip-and-continue path schedules exactly one wake-up timer when the drain
becomes idle. Use the existing seedMutation, makeFakeSupabase, processOutbox,
mutationRow, and fake-timer assertions, while preserving the dead-letter
behavior covered by the existing test.
🤖 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 `@app/`(dashboard)/support-inbox/support-inbox-client.tsx:
- Line 382: Update the feedback date rendering in the support-inbox client to
use an explicit locale and timezone with toLocaleDateString, ensuring SSR and
hydration produce identical output while preserving the existing submitted_at
value.

In `@lib/dexie/context.tsx`:
- Around line 282-420: Reduce nesting in the v2 synchronization helpers by
extracting the innermost callbacks from scheduleV2Reconnect and runV2 into named
helpers declared alongside scheduleV2Reconnect. Specifically, move the
resubscribe failure handler, the checklists signal action, and the
TOKEN_REFRESHED setAuth failure handler out of their enclosing callbacks while
preserving their existing behavior and captured values, so nesting depth stays
within four levels.

In `@lib/dexie/syncService.ts`:
- Around line 113-124: Update processOutbox in lib/dexie/syncService.ts at lines
113-124 to call scheduleRetry(nextAttemptAt) before continuing from the
newRetryCount >= 3 branch. Add a test case in
unit/dexie/sync-outbox-backoff.test.ts at lines 87-172 that seeds retryCount: 2,
triggers the skip-and-continue path, and verifies a timer is scheduled and can
wake processing to reach the row.

In `@lib/inngest/functions/build-shopping-cart.ts`:
- Around line 149-156: The RateLimitError branch in the shopping-cart workflow
currently rethrows directly, discarding Kroger’s retry delay. Wrap the error in
Inngest’s RetryAfterError using RateLimitError.retryAfter converted from seconds
to milliseconds, and apply this consistently to Kroger-backed step.run paths
such as product search and cart-add.

In `@lib/inngest/functions/cron/notifications-retention.ts`:
- Line 74: Update the cron trigger object for the notifications retention
function to explicitly use the America/Chicago timezone and run at 9:30 AM local
time, preserving the stated schedule across CST and CDT. Ensure the trigger uses
the correct Inngest cron configuration key and schedule value.

In `@lib/kroger/client.ts`:
- Around line 65-67: Update the 429 handling around Retry-After in the client
response flow to support both non-negative numeric seconds and RFC HTTP-date
values. Parse numeric seconds first; when invalid, parse the date and calculate
the remaining delay in seconds, ensuring the resulting delay is valid and
non-negative. Use the 60-second default only when neither representation is
valid, then pass the resolved delay to RateLimitError.

In `@unit/inngest/cron-notifications-retention.test.ts`:
- Around line 24-26: Replace the untyped chain in the mocked Supabase setup
within the from mock with a small PromiseLike fluent-chain interface describing
the methods used by the test. Type the chain with that interface, remove the
explicit any eslint suppression, and preserve the existing mocked method
behavior.

---

Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 66-147: Add an explicit least-privilege permissions declaration at
the workflow level, setting GITHUB_TOKEN access to read-only repository contents
and disabling unnecessary scopes. Ensure the build, e2e, and db-invariants jobs
inherit these restricted permissions without granting write access to issues,
pull requests, contents, or other resources.

---

Nitpick comments:
In `@scripts/check-type-drift.mjs`:
- Around line 161-169: Configure a finite timeout for the RPC request in the
schema-report fetch, using the existing fetch options around the
db_type_shape_report call. Ensure a hung connection aborts promptly and causes
the CI step to fail rather than waiting for the runner timeout.

In
`@supabase/migrations/20260725201500_add_reviewed_columns_to_inventory_count_drafts.sql`:
- Around line 19-27: Split the migration so the ALTER TABLE in the current
migration adds reviewed_at and reviewed_by without creating the foreign key or
index. Add the reviewed_by foreign key in a separate migration using the
repository’s zero-downtime constraint approach, and create
idx_inventory_count_drafts_reviewed_by with CREATE INDEX CONCURRENTLY in its own
migration file containing no other statements.

In `@unit/dexie/sync-outbox-backoff.test.ts`:
- Around line 87-172: Add a test in the “processOutbox — retry backoff” suite
that seeds a mutation with retryCount: 2, forces its next push to fail, and
verifies the retryCount ≥ 3 skip-and-continue path schedules exactly one wake-up
timer when the drain becomes idle. Use the existing seedMutation,
makeFakeSupabase, processOutbox, mutationRow, and fake-timer assertions, while
preserving the dead-letter behavior covered by the existing test.
🪄 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 Plus

Run ID: 72f145a8-dbc1-4a2a-afb8-85e847ad4d09

📥 Commits

Reviewing files that changed from the base of the PR and between de4ce7e and 81e6da8.

📒 Files selected for processing (39)
  • .env.example
  • .github/workflows/ci.yml
  • CLAUDE.md
  • app/(dashboard)/bookings/bookings-calendar.tsx
  • app/(dashboard)/bookings/bookings-client.tsx
  • app/(dashboard)/support-inbox/page.tsx
  • app/(dashboard)/support-inbox/support-inbox-client.tsx
  • app/api/inngest/route.ts
  • app/api/integrations/[provider]/callback/route.ts
  • app/connect/error/page.tsx
  • app/connect/finish/route.ts
  • docs/SCALABILITY_TIERS_REMAINING.md
  • lib/dexie/context.tsx
  • lib/dexie/schema.ts
  • lib/dexie/sync/signals.ts
  • lib/dexie/syncService.ts
  • lib/inngest/functions/build-shopping-cart.ts
  • lib/inngest/functions/cron/notifications-retention.ts
  • lib/inngest/functions/hostaway/initial-sync.ts
  • lib/integrations/providers/hostaway.ts
  • lib/integrations/registry.ts
  • lib/kroger/client.ts
  • lib/rate-limit.ts
  • lib/sms/telnyx.ts
  • scripts/check-type-drift.mjs
  • supabase/migrations/20260725191358_crew_sync_broadcast_triggers.sql
  • supabase/migrations/20260725200500_db_type_shape_report.sql
  • supabase/migrations/20260725201000_add_vacancy_gap_suggestion_to_wo_source.sql
  • supabase/migrations/20260725201500_add_reviewed_columns_to_inventory_count_drafts.sql
  • types/database.ts
  • unit/dexie/fake-dexie.ts
  • unit/dexie/sync-outbox-backoff.test.ts
  • unit/dexie/sync-reconnect-jitter.test.ts
  • unit/dexie/sync-signals.test.ts
  • unit/inngest/build-shopping-cart.test.ts
  • unit/inngest/cron-notifications-retention.test.ts
  • unit/lib/kroger-client-rate-limit.test.ts
  • unit/route-handlers/integrations-callback.test.ts
  • unit/sms/send-sms-gate.test.ts

</span>
<span style={{ fontSize: '11px', color: 'var(--text-muted)', flexShrink: 0 }}>
{new Date(f.created_at).toLocaleDateString()}
{new Date(f.submitted_at).toLocaleDateString()}

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

Make the feedback date deterministic across SSR and hydration.

toLocaleDateString() uses the server and browser locale/timezone defaults, so this can display different dates and trigger a hydration mismatch. Pass an explicit locale/timezone (or format after mount).

Proposed fix
-                {new Date(f.submitted_at).toLocaleDateString()}
+                {new Date(f.submitted_at).toLocaleDateString('en-US', { timeZone: 'UTC' })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{new Date(f.submitted_at).toLocaleDateString()}
{new Date(f.submitted_at).toLocaleDateString('en-US', { timeZone: 'UTC' })}
🧰 Tools
🪛 React Doctor (0.7.6)

[error] 382-382: This can cause a hydration mismatch because toLocaleDateString() formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.

Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.

(no-locale-format-in-render)

🤖 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 `@app/`(dashboard)/support-inbox/support-inbox-client.tsx at line 382, Update
the feedback date rendering in the support-inbox client to use an explicit
locale and timezone with toLocaleDateString, ensuring SSR and hydration produce
identical output while preserving the existing submitted_at value.

Source: Linters/SAST tools

Comment thread lib/dexie/context.tsx
Comment thread lib/dexie/syncService.ts
Comment on lines +113 to 124
const nextAttemptAt = computeNextAttemptAt(newRetryCount, Date.now())
await db.mutations.update(id, { retryCount: newRetryCount, nextAttemptAt })
// Only block the queue on transient failures, not permanent ones.
// If we've retried >= 3 times, skip this mutation and continue
// draining so later mutations (which may be independent) still go through.
if (newRetryCount >= 3) continue
// Stop draining on first/second failure so later mutations against
// the same record aren't applied out of order.
// the same record aren't applied out of order; wake up again when
// the backoff window elapses.
this.scheduleRetry(nextAttemptAt)
break
}

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 | 🟠 Major | ⚡ Quick win

Skip-and-continue retries can get stranded, and no test catches it. processOutbox()'s "skip and continue" branch (retryCount >= 3) sets nextAttemptAt but never schedules a wake-up timer, unlike the sibling break branch — and the test suite never exercises that exact path to reveal the gap.

  • lib/dexie/syncService.ts#L113-L124: call this.scheduleRetry(nextAttemptAt) before continue in the newRetryCount >= 3 branch, matching the break branch below it.
  • unit/dexie/sync-outbox-backoff.test.ts#L87-L172: add a case seeding a mutation at retryCount: 2 so the next failure hits the skip-and-continue branch, then assert a timer is still scheduled (vi.getTimerCount() and/or advanceTimersByTimeAsync reaching the row) once the drain otherwise completes.
📍 Affects 2 files
  • lib/dexie/syncService.ts#L113-L124 (this comment)
  • unit/dexie/sync-outbox-backoff.test.ts#L87-L172
🤖 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 `@lib/dexie/syncService.ts` around lines 113 - 124, Update processOutbox in
lib/dexie/syncService.ts at lines 113-124 to call scheduleRetry(nextAttemptAt)
before continuing from the newRetryCount >= 3 branch. Add a test case in
unit/dexie/sync-outbox-backoff.test.ts at lines 87-172 that seeds retryCount: 2,
triggers the skip-and-continue path, and verifies a timer is scheduled and can
wake processing to reach the row.

Comment on lines +149 to +156
if (err instanceof RateLimitError) {
// Kroger's own API quota (or our proactive guard in front of it,
// see lib/kroger/client.ts's krogerFetch) is exhausted. Rethrow
// so Inngest retries this step with backoff instead of silently
// degrading to the list-only fallback below — that would mask a
// transient condition as a permanent one and skip a retry that
// would likely succeed once the window resets.
throw err

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
fd -a 'build-shopping-cart\.ts|kroger/client\.ts|lib/inngest/functions' . | sed 's#^\./##'

echo "== build-shopping-cart outline =="
ast-grep outline lib/inngest/functions/build-shopping-cart.ts --view expanded || true

echo "== relevant build-shopping-cart sections =="
nl -ba lib/inngest/functions/build-shopping-cart.ts | sed -n '1,260p'

echo "== kroger fetch references =="
rg -n "krogerFetch|RateLimitError|RetryAfterError|retryAfter|buildShoppingCart|function buildShoppingCart|retries:" lib package.json || true

echo "== package inngest versions =="
if [ -f package.json ]; then jq '.dependencies.inngest, .devDependencies.inngest' package.json; fi

Repository: smj1860/fieldstay

Length of output: 521


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | grep -E '(^lib/inngest/functions/build-shopping-cart\.ts$|kroger/client\.ts$|package\.json$|tsconfig\.json$)' || true

echo "== build-shopping-cart relevant contents =="
awk '{printf "%6d\t%s\n", NR, $0}' lib/inngest/functions/build-shopping-cart.ts | sed -n '1,240p'

echo "== kroger/client relevant contents =="
awk '{printf "%6d\t%s\n", NR, $0}' lib/kroger/client.ts | sed -n '1,220p'

echo "== inngest references =="
rg -n "RateLimitError|RetryAfterError|retryAfter|krogerFetch|buildShoppingCart|retries:" lib lib/inngest/functions lib/kroger package.json || true

echo "== package inngest versions =="
if [ -f package.json ]; then
  python3 - <<'PY' | sed -n '1,40p'
import json
p=json.load(open('package.json'))
print('dependencies:', p.get('dependencies',{}).get('inngest'))
print('devDependencies:', p.get('devDependencies',{}).get('inngest'))
PY
fi

Repository: smj1860/fieldstay

Length of output: 47254


Preserve Kroger’s Retry-After for Inngest retries.

RateLimitError only has a seconds-based retryAfter; plain rethrows let Inngest retry with its default backoff and can exhaust function retries before Kroger’s quota resets. Have Kroger’s error path throw Inngest’s RetryAfterError with retryAfter * 1000 ms, or handle Kroger rate limits by sleeping step.sleep(..., ...s) before retrying in each Kroger-backed step.run, including product search and cart-add.

🤖 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 `@lib/inngest/functions/build-shopping-cart.ts` around lines 149 - 156, The
RateLimitError branch in the shopping-cart workflow currently rethrows directly,
discarding Kroger’s retry delay. Wrap the error in Inngest’s RetryAfterError
using RateLimitError.retryAfter converted from seconds to milliseconds, and
apply this consistently to Kroger-backed step.run paths such as product search
and cart-add.

name: 'Cron: Notifications Retention Purge',
retries: 1,
},
{ cron: '30 14 * * *' }, // 15 min after dailyGuestPiiRetention

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'notifications-retention\.ts$' . || true

echo "== file excerpt if present =="
file="$(fd 'notifications-retention\.ts$' . | head -n1 || true)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '1,130p' "$file" | cat -n
fi

echo "== inngest cron config refs =="
rg -n "notifications-retention|dailyGuestPiiRetention|America/Chicago|timezone|TZ=|\\(cron:" --glob '!node_modules' --glob '!dist' --glob '!build' . || true

echo "== package versions =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
  [ -f "$f" ] && echo "--- $f ---" && (sed -n '1,160p' "$f"; true)
done

Repository: smj1860/fieldstay

Length of output: 34056


🌐 Web query:

Inngest v3 TypeScript cron timezone-prefixed expressions TZ America/Chicago

💡 Result:

In Inngest, you can specify a timezone for cron triggers by adding a TZ prefix to your cron expression [1][2][3]. To use the America/Chicago timezone, you should use the prefix TZ=America/Chicago [3][4]. You can implement this in your TypeScript function using the cron helper or by passing a trigger object [2][5]. Example using the cron helper: import { inngest, cron } from "inngest"; inngest.createFunction( { id: "my-scheduled-job", triggers: [cron("TZ=America/Chicago 0 9 * * 1-5")], }, async ({ step }) => { // Runs at 9:00 AM CT on weekdays }); Example using a trigger object: inngest.createFunction( { id: "my-scheduled-job", triggers: [{ cron: "TZ=America/Chicago 0 9 * * 1-5" }], }, async ({ step }) => { // Runs at 9:00 AM CT on weekdays }); Important considerations: - Daylight Saving Time (DST): Inngest's cron behavior follows the underlying cron library and does not apply special DST correction [3][6]. Schedules near transition times (such as 2:00 AM in many US regions) can behave unexpectedly, potentially running zero, one, or two times depending on the transition [3][6]. To ensure consistent execution timing, Inngest recommends avoiding transition-hour schedules or preferring UTC when possible [3][6].

Citations:


Preserve the stated 9:30 AM Central schedule.

30 14 * * * runs at 9:30 AM during CDT but 8:30 AM during CST. Use chron: 'TZ=America/Chicago 30 9 * * *' with Inngest’s cron trigger object, or correct the stated schedule.

🤖 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 `@lib/inngest/functions/cron/notifications-retention.ts` at line 74, Update the
cron trigger object for the notifications retention function to explicitly use
the America/Chicago timezone and run at 9:30 AM local time, preserving the
stated schedule across CST and CDT. Ensure the trigger uses the correct Inngest
cron configuration key and schedule value.

Comment thread lib/kroger/client.ts
Comment on lines +65 to +67
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60', 10)
throw new RateLimitError(retryAfter)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)lib/kroger/client\.ts$|(^|/)kroger' || true

echo
echo "== file excerpt =="
if [ -f lib/kroger/client.ts ]; then
  nl -ba lib/kroger/client.ts | sed -n '1,130p'
fi

echo
echo "== usages of RateLimitError / krogerFetch =="
rg -n "RateLimitError|krogerFetch|Retry-After" lib pkg ts app worker || true

echo
echo "== behavioral probe: current parseInt behavior for Retry-After forms =="
node - <<'JS'
function currentRetryAfter(resHeader) {
  const retryAfter = parseInt(resHeader ?? '60', 10)
  return retryAfter
}
for (const value of ['60', '0', '61', 'Thu, 01 Jan 2026 00:00:01 GMT', '20260701T000000Z', '']) {
  console.log(JSON.stringify(value), "=>", currentRetryAfter(value))
}
JS

Repository: smj1860/fieldstay

Length of output: 477


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file excerpt =="
if [ -f lib/kroger/client.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' lib/kroger/client.ts | sed -n '1,140p'
fi

echo
echo "== usages of RateLimitError / krogerFetch / Retry-After =="
rg -n "RateLimitError|krogerFetch|Retry-After" lib pkg ts app worker || true

echo
echo "== behavioral probe: current parseInt behavior for Retry-After forms =="
node - <<'JS'
function currentRetryAfter(resHeader) {
  const retryAfter = parseInt(resHeader ?? '60', 10)
  return retryAfter
}
for (const value of ['60', '0', '61', 'Thu, 01 Jan 2026 00:00:01 GMT', '20260701T000000Z', '']) {
  console.log(JSON.stringify(value), "=>", currentRetryAfter(value))
}
JS

Repository: smj1860/fieldstay

Length of output: 12466


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== RateLimitError definition =="
awk '{printf "%6d\t%s\n", NR, $0}' lib/integrations/types.ts | sed -n '260,335p'

echo
echo "== Kroger caller exception handling excerpt =="
awk '{printf "%6d\t%s\n", NR, $0}' lib/inngest/functions/build-shopping-cart.ts | sed -n '138,165p'

echo
echo "== Kroger-specific rate limit tests if present =="
if [ -f unit/lib/kroger-client-rate-limit.test.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' unit/lib/kroger-client-rate-limit.test.ts | sed -n '1,220p'
else
  echo "unit/lib/kroger-client-rate-limit.test.ts not found"
fi

echo
echo "== exact krogerFetch Retry-After usages =="
awk '{printf "%6d\t%s\n", NR, $0}' lib/kroger/client.ts | sed -n '60,75p'

Repository: smj1860/fieldstay

Length of output: 12140


Handle HTTP-date Retry-After values.

parseInt() returns NaN for RFC-compliant date-form Retry-After values, so the Kroger RateLimitError can carry an invalid retry delay. Parse non-negative seconds first, then fall back to Date.parse()/remaining seconds; default to 60 only when neither is valid.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 66-66: Prefer Number.parseInt over parseInt.

See more on https://sonarcloud.io/project/issues?id=smj1860_fieldstay&issues=AZ-cCsfKBaRu4znBsYKS&open=AZ-cCsfKBaRu4znBsYKS&pullRequest=508

🤖 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 `@lib/kroger/client.ts` around lines 65 - 67, Update the 429 handling around
Retry-After in the client response flow to support both non-negative numeric
seconds and RFC HTTP-date values. Parse numeric seconds first; when invalid,
parse the date and calculate the remaining delay in seconds, ensuring the
resulting delay is valid and non-negative. Use the 60-second default only when
neither representation is valid, then pass the resolved delay to RateLimitError.

Source: Linters/SAST tools

Comment on lines +24 to +26
const from = vi.fn((table: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const chain: any = {}

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

Replace the untyped fluent mock.

Define a small PromiseLike chain interface for the mocked Supabase methods instead of const chain: any.

As per coding guidelines, **/*.{ts,tsx} says “Do not use any, as any, or @ts-ignore.”

🤖 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 `@unit/inngest/cron-notifications-retention.test.ts` around lines 24 - 26,
Replace the untyped chain in the mocked Supabase setup within the from mock with
a small PromiseLike fluent-chain interface describing the methods used by the
test. Type the chain with that interface, remove the explicit any eslint
suppression, and preserve the existing mocked method behavior.

Source: Coding guidelines

SonarCloud (13 findings on this branch):
- lib/dexie/context.tsx: extract 3 deeply-nested closures (scheduleV2Reconnect's
  retry, runV2's checklist sync, the v2 auth-state-change handler) into named
  sibling functions to bring nesting depth back under the project's limit
- lib/dexie/sync/signals.ts, lib/dexie/syncService.ts: NOSONAR the two
  Math.random() jitter sites (already eslint-disable-justified as timing
  jitter, not id/token generation)
- lib/kroger/client.ts: Number.parseInt over parseInt
- lib/sms/telnyx.ts: extract formatOfferPrice/formatPercentageOffer/
  formatFixedAmountOffer to remove the nested ternaries and bring
  formatOffer's cognitive complexity back under 15
- scripts/check-type-drift.mjs: NOSONAR the three regex findings — this
  script only ever parses our own committed types/database.ts, never
  attacker-controlled input, so ReDoS isn't a real risk here
- supabase/migrations/20260725200500_db_type_shape_report.sql: dedupe the
  3x-repeated 'public' literal via a target_schema CTE; verified
  byte-identical md5 output on both projects before/after re-applying

CI gate gaps surfaced by PR #508's own db-invariants job, both from PR #507
(guidebook v2) never having its migration pushed to the E2E project:
- Applied 20260726100000_guidebook_v2_foundation.sql and
  20260726120000_guidebook_property_photos_storage_policies.sql to E2E
  (already live on production) — clears the type-drift findings for
  guidebook_offer_redemptions and hero_photo_storage_path
- New migration: covering index for guidebook_offer_redemptions.booking_id,
  the one FK column that migration's own indexes didn't cover — clears the
  FK-coverage invariant finding that surfaced once the table existed on E2E

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp

smj1860 commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

Flagging: 4 e2e failures are pre-existing, not caused by this PR

The e2e check on this PR fails on the same 4 specs across every run:

  • e2e/specs/03-bookings.spec.ts:44getByText(/Booking added/i) not visible within 20000ms
  • e2e/specs/23-booking-validation.spec.ts:70getByText(/Booking added/i) not visible within 20000ms
  • e2e/specs/24-vendor-compliance-block.spec.ts:133getByRole('dialog') still visible after 10000ms (expected closed)
  • e2e/specs/26-turnover-crew-assignment.spec.ts:80getByRole('button', { name: /^Upcoming/ }) not visible within 8000ms

All four are the same shape: a server mutation completes, but its effect doesn't show up in the UI within the test's timeout. 61 other specs pass every run.

This is not a regression from this PR. Confirmed by:

  1. Directly benchmarking the new Crew Sync v2 Phase 2 broadcast triggers (notify_crew_sync() on the E2E project) — 2.91ms for 2 calls, and every join those trigger functions use (turnover_assignments.turnover_id, crew_members.user_id, work_orders.assigned_crew_member_id, etc.) is properly indexed.
  2. Ruling out concurrent DB load from my own migration work — the failure reproduced identically on a run where no concurrent DB activity was happening.
  3. The conclusive evidence: the identical 4 failures, at the identical lines, with identical timeouts, occur on main at commit de4ce7e (the PR Claude/media kit guidebook redesign q27b25 #507 merge) — a commit from over 6 hours before any of this PR's Crew Sync v2 Phase 2-4 code existed.

This is a chronic, pre-existing environmental flake in the shared E2E Supabase project (03-bookings.spec.ts's own comments already documented this exact failure mode as a known risk under "sustained E2E-project DB load"). It currently affects main and would block any PR, not just this one. Tracking it separately rather than blocking this PR's Sonar/type-drift fixes on it.

Merging with this pre-existing failure acknowledged.


Generated by Claude Code

…lability-lgfh15

# Conflicts:
#	lib/sms/telnyx.ts
@sonarqubecloud

Copy link
Copy Markdown

@smj1860
smj1860 merged commit b7da3a2 into main Jul 26, 2026
5 of 8 checks passed
smj1860 pushed a commit that referenced this pull request Jul 26, 2026
Both docs still showed Phases 2-4 as not-done even though they merged via
PR #508 on 2026-07-26 — verified all three are live on main (broadcast
trigger migration applied to both Supabase projects, client cutover flag
in lib/dexie/context.tsx, outbox backoff in syncService.ts). Only Phase 5
(rollout: two-device acceptance test, production flag flip, soak,
old-code deletion) remains open.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNNY4XjLYRHZ3w4RHocLp
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.

2 participants