Skip to content

Storage resilience: fix cross-account/data-loss bugs, sync essays and templates, add realtime + backup#249

Merged
NesiciCoding merged 5 commits into
mainfrom
claude/bold-bartik-8a4ed0
Jul 3, 2026
Merged

Storage resilience: fix cross-account/data-loss bugs, sync essays and templates, add realtime + backup#249
NesiciCoding merged 5 commits into
mainfrom
claude/bold-bartik-8a4ed0

Conversation

@NesiciCoding

@NesiciCoding NesiciCoding commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Full audit-and-fix pass on the persistence/sync layer, done in three phases. Also repositions the project from "offline-first" to Supabase-primary, offline-capable (reduced capabilities) — self-hostable with full functionality throughout CLAUDE.md/README.md, since that's the actual intended architecture.

Phase 1 — correctness fixes

  • Cross-device visibility bug: essayTemplates/gradingTasks were fetched on hydrate but never merged (missing from mergeStoreData's COLLECTIONS) — silently dropped, invisible on other devices. Fixed + regression tests.
  • Cross-account data bleed (most severe): the pending-sync queue wasn't scoped to a user. If teacher A queued offline edits and teacher B logged in on the same browser, A's edits could get pushed into B's account on the next flush. StorageSync.guardOwnerSwitch() now wipes local data (storage.ts#clearLocalData) before flushing when a different user's uid is detected.
  • Pending-queue writes that fail due to a full localStorage quota were silently dropped — now surfaced via the existing quota-exceeded handler, and the queue is capped at 500 entries (drop-oldest).

Phase 2 — bring essays/templates into the sync pattern

  • New tables essay_batch_assignments / essay_offline_submissions (migration 045) for local class-assignment tracking and offline-code-import submissions. Deliberately not merged into the existing essay_assignments table, which is a different single-row-per-teacherKey mechanism backing the share-link/DB-submission flow — doing so would have let one student's row silently overwrite another's.
  • userTemplates ("save as template") now syncs via Supabase (migration 046) instead of being localStorage-only; Dashboard/RubricBuilder route through AppContext instead of calling storage.ts directly.
  • Found and fixed a related bug this surfaced: the pending-queue dedup key only read payload.id/.guid, which would've collapsed every offline edit to a composite-key entity (like the new essay-batch-assignment) into a single queue slot, losing all but the last one.
  • Sign-out now wipes local data once the pending queue is confirmed empty (shared classroom-device hygiene); otherwise it warns instead of risking data loss. New toast.signout_pending_writes key across all 5 locales.

Phase 3 — resilience

  • Realtime sync (migration 047): StorageSync subscribes to Postgres change events on every synced table (RLS-scoped per user), debounced into the existing hydrate() + mergeStoreData() pipeline rather than a new payload-decoding path for ~20 table shapes.
  • Orphaned media GC: IndexedDB recording blobs are pruned after every sync — previously a speaking session deleted on another device left its local blob behind forever, since nothing else triggers deleteBlob.
  • Nightly backup (migration 048 + nightly-backup edge function): for deployments with no server-side pg_dump access (Supabase Cloud, or Supabase self-hosted separately from this repo's own docker-compose.yml, which has no functions runtime). scripts/backup.sh only works against this repo's bundled compose stack.

Compatibility check with PR #248

Confirmed no real conflicts — migration numbers were bumped to avoid colliding with #248's 044_test_assignments.sql, and the touched-file overlaps (AppContext.tsx, StorageSync.ts, SupabaseAdapter.ts, locale files) are all purely additive at different insertion points.

Test plan

  • npm run typecheck — clean
  • npm run lint — clean (only pre-existing warnings)
  • npm test — 2112/2112 pass
  • Manually verified in-browser: save-as-template → Dashboard "My Templates" card → delete, round-tripping through localStorage correctly with no console errors
  • supabase db reset to verify migrations 045–048 apply cleanly — not run, no Docker daemon available in this sandbox; please verify locally before merging
  • Realtime subscriptions, the nightly-backup edge function, and the cross-account wipe path are untestable without a live Supabase project — please smoke-test manually before relying on them in production

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added saved rubric templates that persist for reuse across sessions and devices.
    • Added nightly cloud backups for supported deployments.
  • Bug Fixes
    • Improved offline and sign-out handling to preserve unsynced changes and warn when pending sync exists.
    • Improved sync reliability for templates, essay assignments/submissions, and media cleanup.
  • Documentation
    • Updated setup/product wording to reflect Supabase-primary persistence with reduced offline capabilities when no backend is configured.

…bugs, sync essays and templates, add realtime + backup

Reframes the storage model from "offline-first" to "Supabase-primary, offline-capable
with reduced capabilities, self-hostable with full functionality" throughout
CLAUDE.md/README, and fixes the correctness gaps that audit surfaced.

Phase 1 — correctness fixes:
- Merge essayTemplates/gradingTasks in mergeStoreData (were fetched on hydrate but
  silently dropped, invisible across devices).
- Fix cross-account data bleed: a different user signing in on the same browser could
  have their pending-sync queue flushed into the previous user's account. Local data is
  now wiped on owner switch (storage.ts clearLocalData + StorageSync.guardOwnerSwitch).
- Surface pending-queue quota errors instead of silently dropping writes; cap the queue.

Phase 2 — bring essays/templates into the sync pattern:
- New tables (essay_batch_assignments, essay_offline_submissions) for local
  batch-assignment tracking and offline-import submissions — kept separate from the
  existing single-row-per-teacherKey essay_assignments table used by the share-link
  mechanism, to avoid students silently overwriting each other's rows.
- userTemplates ("save as template") now syncs via Supabase instead of being
  localStorage-only; Dashboard/RubricBuilder now route through AppContext instead of
  calling storage.ts directly.
- Fixed a related bug: the pending-queue dedup key only read payload.id/.guid, which
  would have collapsed all offline edits to a composite-key entity into one slot.
- Sign-out now wipes local data once the pending queue is confirmed empty (shared
  classroom devices), otherwise warns instead of risking data loss.

Phase 3 — resilience:
- Realtime sync: StorageSync subscribes to Postgres changes on every synced table,
  debounced into the existing hydrate()+mergeStoreData() pipeline rather than a new
  payload-decoding path.
- Orphaned IndexedDB recording blobs are pruned after every sync (a session deleted on
  another device previously left its local blob behind forever).
- nightly-backup edge function + export_owner_backup() SQL function for deployments
  with no server-side pg_dump access (Supabase Cloud, or Supabase self-hosted
  separately from this repo's own docker-compose.yml).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 50 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 19a0a439-3a60-48db-9b2e-27a76f4a67fd

📥 Commits

Reviewing files that changed from the base of the PR and between 212c118 and 61a6cc8.

📒 Files selected for processing (1)
  • supabase/functions/nightly-backup/index.ts
📝 Walkthrough

Walkthrough

This PR updates RubricMaker to treat Supabase as the primary persistence path when configured, with offline fallback behavior, new syncable collections, realtime refresh, safer sign-out handling, and a nightly backup export flow. It also updates docs, locales, tests, and database migrations to match.

Changes

Storage, sync, templates, and backups

Layer / File(s) Summary
Docs and locale wording
CLAUDE.md, README.md, src/pages/LandingPage.tsx, src/pages/GradeStudent.tsx, src/locales/*.json
Rewords persistence/offline copy, adds nightly backup documentation, and adds the new pending-writes sign-out toast strings.
Pending queue and local cleanup
src/store/storage.ts, src/store/storage.test.ts, src/services/mediaStore.ts, src/services/__tests__/mediaStore.test.ts
Adds queue capping, quota handling, local-data clearing, and orphaned blob pruning with matching tests.
Merge rules for new collections
src/utils/syncMerge.ts, src/utils/syncMerge.test.ts
Extends merge handling and pending-id resolution for essay and user-template collections.
Supabase sync adapters and realtime storage
src/services/database/SupabaseAdapter.ts, src/services/database/StorageSync.ts
Adds anonymous-session tracking, CRUD support for new tables, realtime subscription refreshes, and expanded hydrate/push paths.
AppContext template actions and sign-out safety
src/context/AppContext.tsx
Adds user-template actions, updates offline persistence and hydration behavior, prunes media after flush, and preserves local data when pending writes exist.
Template UI wiring
src/pages/Dashboard.tsx, src/pages/RubricBuilder.tsx, src/pages/__tests__/pages.deepcoverage.test.tsx
Switches template UI to AppContext-provided actions and updates the page test mock.
Sync tables and realtime publication
supabase/migrations/045_*.sql, 046_*.sql, 047_*.sql
Adds the new sync tables, owner-scoped policies, and realtime publication entries.
Nightly backup export and storage
supabase/migrations/048_*.sql, supabase/functions/nightly-backup/index.ts
Adds the owner backup export function, backup bucket policy, and the nightly backup edge function.

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

Sequence Diagram(s)

sequenceDiagram
  participant SupabaseAdapter
  participant StorageSyncService
  participant AppContext
  participant Supabase
  StorageSyncService->>Supabase: subscribe to postgres_changes
  Supabase-->>StorageSyncService: change event
  StorageSyncService->>StorageSyncService: debounce refresh
  StorageSyncService->>AppContext: notifyReconnect()
  AppContext->>SupabaseAdapter: hydrate and push merged store
Loading
sequenceDiagram
  participant Cron
  participant nightly-backup
  participant Postgres
  participant StorageBucket
  Cron->>nightly-backup: bearer-authenticated trigger
  nightly-backup->>Postgres: list admin/teacher profiles
  loop each profile
    nightly-backup->>Postgres: export_owner_backup(target_owner)
    Postgres-->>nightly-backup: jsonb snapshot
    nightly-backup->>StorageBucket: upload {userId}/{timestamp}.json
    nightly-backup->>StorageBucket: pruneOldBackups(userId)
  end
Loading

Possibly related PRs

Poem

A rabbit hops through sync and light,
Templates tucked in folders tight,
Backups bloom by moonlit time,
Queues stay capped in tidy rhyme,
Burrow safe, offline or online. 🐇

🚥 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 accurately captures the main changes: storage resilience fixes, essay/template sync, realtime sync, and backup support.
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 claude/bold-bartik-8a4ed0

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.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 71.4% (🎯 65%) 7198 / 10080
🔵 Statements 69.63% (🎯 65%) 8201 / 11777
🔵 Functions 62.99% (🎯 60%) 2564 / 4070
🔵 Branches 61.83% (🎯 58%) 5965 / 9646
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/context/AppContext.tsx 55.17% 37.5% 48.99% 56.88% 160, 211-230, 276-292, 342, 380, 383, 395, 398, 431, 473, 481, 494, 501-507, 525-647, 652, 840-872, 886, 913-920, 977-978, 988-1059, 1067, 1071-1075, 1076-1077, 1083-1089, 1105-1131, 1144-1184, 1214-1215, 1246-1247, 1266, 1324, 1438, 1450-1453, 1457, 1460, 1463, 1466, 1469, 1473, 1477, 1482, 1486-1487, 1490-1491, 1494-1495, 1498-1499, 1504, 1507, 1513-1533, 1540-1541, 1545, 1549-1553, 1559, 1563-1570, 1574, 1577, 1579, 1583, 1587, 1591, 1594, 1595, 1597, 1601, 1604, 1605, 1606, 1607, 1611-1612, 1616-1617, 1622-1624, 1630, 1634, 1638, 1642-1655, 1661-1675, 1686, 1688, 1691, 1694, 1697, 1698, 1700, 1704, 1709-1720, 1847
src/pages/Dashboard.tsx 70.9% 56.62% 60.97% 76.08% 35, 36, 37, 91, 101, 114, 116, 132, 140-147, 159-268, 341, 353-374, 418-522
src/pages/GradeStudent.tsx 45.67% 38.37% 32.91% 47.78% 69, 70, 80-83, 101-124, 176, 233, 235, 250, 260, 264-276, 287, 289, 292, 306, 319, 334, 341, 352-358, 370-373, 376, 383-411, 421-423, 431-432, 437-442, 448, 454-456, 463-467, 474-482, 488-493, 500, 505-507, 513, 515, 516, 517, 522-523, 530-531, 540-542, 552-783, 840-847, 922-1042, 1206-1336, 1372-1376, 1406-1472, 1506-1819, 1968
src/pages/LandingPage.tsx 69.69% 82.75% 84.61% 68.75% 180-187, 343, 372
src/pages/RubricBuilder.tsx 63.66% 61.5% 58.05% 64.76% 174-181, 219-227, 266-285, 379, 416-420, 432, 448-449, 456, 463, 468-469, 517-539, 551, 565-578, 593, 599-631, 684-701, 746, 827-828, 862-899, 963, 1068-1070, 1136-1139, 1193-1234, 1315-1333, 1374-1393, 1516, 1587-1597, 1685-1688, 1760-1778, 1850-1856, 1891, 1927-1936, 2048-2056, 2131-2300, 2435-2729, 2818, 2840, 2873, 2889-2950, 2965, 3017-3056, 3093-3108, 3131-3184, 3198, 3214-3272, 3350, 3376, 3383, 3393-3398, 3511-3517, 3540-3554, 3627, 3630, 3667-3669, 3699, 3707-3712, 3730, 3740, 3754, 3847-3854, 3975-4035, 4099, 4125-4135, 4212-4276, 4303
src/store/storage.ts 87.32% 77.02% 87.5% 85.56% 463, 467, 471, 475, 479, 489, 493, 497, 501, 505, 509, 513, 518, 522, 526, 533-535, 539, 543, 547, 554-559, 563, 570-572, 576, 593-597, 678, 716
src/utils/syncMerge.ts 93.75% 100% 85% 93.02% 123, 142, 165-172
Generated in workflow #726 for commit 61a6cc8 by the Vitest Coverage Report Action

…4ed0

# Conflicts:
#	src/context/AppContext.tsx
#	src/services/database/StorageSync.ts
#	src/services/database/SupabaseAdapter.ts
@NesiciCoding

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Caution

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

⚠️ Outside diff range comments (1)
CLAUDE.md (1)

33-48: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Tech-stack table still says "localStorage primary; Supabase optional sync".

Line 41 wasn't updated alongside the new "Storage rule (Supabase-primary, offline-capable)" section (lines 62-70), so the doc now contradicts itself on the core architectural positioning this PR is meant to fix.

📝 Proposed fix
-| Persistence | `localStorage` primary; Supabase optional sync |
+| Persistence | Supabase primary when configured; `localStorage` offline fallback |
🤖 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 `@CLAUDE.md` around lines 33 - 48, Update the Tech stack table in CLAUDE.md so
it matches the new storage architecture; the current “Persistence” entry still
says “localStorage primary; Supabase optional sync,” which conflicts with the
“Storage rule (Supabase-primary, offline-capable)” section. Change the
persistence description to reflect Supabase as the primary source of truth with
offline-capable localStorage support, and keep the wording consistent with the
storage rule section so the document does not contradict itself.
🤖 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 `@src/context/AppContext.tsx`:
- Around line 617-627: The SAVE_USER_TEMPLATE reducer in AppContext is capping
the authoritative userTemplates state to 20 items, which makes the
diff(prev.userTemplates, state.userTemplates, 'userTemplate', ...) effect
interpret evicted entries as deletions and sync them to Supabase. Remove the
.slice(0, 20) cap from the state update in SAVE_USER_TEMPLATE so the sync
source-of-truth is preserved, and if a 20-item limit is still needed, apply it
only in presentation code or local-only storage helpers like saveUserTemplates
rather than in the reducer state used by the sync logic.

In `@src/pages/LandingPage.tsx`:
- Around line 284-288: The landing page still hardcodes user-facing English
text, and the new checklist label in LandingPage.tsx continues that pattern.
Update the checklist labels in LandingPage and the surrounding JSX to use
useTranslation instead of inline strings, so all visible copy is sourced from
localized keys rather than hardcoded English.

In `@src/services/database/StorageSync.ts`:
- Around line 256-263: `startRealtimeSync()` can return early when
`this.realtimeChannel` is already set, so an owner change through `configure()`
may leave the old subscription active. Update
`StorageSync.configure()`/`guardOwnerSwitch()` to clear or stop the existing
realtime channel before starting a new owner, or make `startRealtimeSync()`
track the current UID and replace stale channels so the new `sync:${uid}`
subscription is always created.

In `@src/store/storage.ts`:
- Around line 630-633: Remove the stray “ponytail:” fragment from the comment
near MAX_PENDING_OPS in storage.ts so the documentation reads as intentional
prose; update the nearby comment text in the storage logic around the pending
ops cap without changing the behavior of MAX_PENDING_OPS or the related
deduping/eviction explanation.
- Around line 648-654: The pending-sync cap in storage.queue handling silently
discards the oldest entry in the MAX_PENDING_OPS path without notifying the
user, unlike the quota-exceeded flow. Update the queue eviction logic in
storage.ts around the pending-sync queue branch so it invokes the same
user-facing notification path used by quotaExceededHandler when an item is
dropped, and keep the existing console.warn for diagnostics. Use the
pending-sync queue handling and quotaExceededHandler symbols to locate the
change.

In `@supabase/functions/nightly-backup/index.ts`:
- Around line 1-3: The header comment for nightly-backup is inconsistent with
the README about self-hosted scheduling. Update the top-of-file comment in the
nightly-backup Edge Function to match the deployment guidance in the README, so
it states that official self-hosted deployments can also schedule this function
instead of steering operators only to scripts/backup.sh. Keep the note aligned
with the Edge Function entry point and avoid suggesting the function is
Cloud-only.
- Around line 69-72: The prune path in pruneOldBackups currently ignores storage
list/remove failures, which can make backup retention silently fail while the
caller still reports success. Update pruneOldBackups to propagate or return a
warning when admin.storage.from('backups').list or remove fails, and make the
backup flow in index.ts use that result so the response reflects pruning errors
instead of swallowing them.
- Around line 15-24: The nightly-backup handler currently defaults
SUPABASE_SERVICE_ROLE_KEY and SUPABASE_URL to empty strings, which can let an
empty Bearer token through and then create an invalid admin client. Update the
auth/initialization flow in index.ts so the request is rejected with a server
error before calling createClient whenever either required env var is missing,
and keep the Authorization check in the same early-validation path.
- Around line 27-30: The profile lookup in nightly-backup is still doing a
single unbounded scan, so it can stop at the Supabase row limit and miss later
admin/teacher records. Update the profiles query in the backup flow to page
through results using .range() in a loop, accumulate all matching rows, and only
then continue into the export loop. Keep the change localized around the
admin.from('profiles').select('id').in(...) logic so the backup job processes
every page before exporting.

In `@supabase/migrations/045_essay_local_tracking_sync.sql`:
- Around line 20-27: Add a short safety comment near the index statements in the
migration noting that these are newly created empty tables, so the indexes are
intentionally created without CONCURRENTLY to remain compatible with
transaction-wrapped Supabase migrations. Keep the note close to the
essay_batch_assignments and essay_offline_submissions index definitions so
future readers understand why SQLFluff’s suggestion is not applied.

In `@supabase/migrations/046_user_templates_sync.sql`:
- Line 10: Document in the 046_user_templates_sync migration that the
user_templates_owner_id_idx creation is intentionally non-concurrent and must
remain inside the transaction-wrapped migration flow. Add a brief rationale near
the create index statement explaining that Supabase migrations should stay
transaction-safe, so CREATE INDEX CONCURRENTLY must not be used here. Reference
the user_templates_owner_id_idx index creation on public.user_templates so
future edits don’t “fix” it into a transaction-incompatible form.

In `@supabase/migrations/047_enable_realtime.sql`:
- Around line 7-28: The supabase_realtime publication update is not idempotent
because `ALTER PUBLICATION ... ADD TABLE` will fail if a table is already
included. Update the migration logic around the `ALTER PUBLICATION
supabase_realtime` statement to guard each table in the list with a
`pg_publication_tables` membership check before adding it, or use an equivalent
per-table conditional approach. Keep the fix localized to this publication
update so retries or manual reruns do not abort.

In `@supabase/migrations/048_nightly_backup.sql`:
- Around line 67-73: The nightly backup logic in the backup JSON builder is only
exporting `essay_assignments` and `essay_submissions` rows, but
`essay_submissions` depends on Storage object paths and will not be restorable
if bucket contents are lost. Update the backup flow in the migration’s backup
function to either include the referenced Storage objects in the artifact or
clearly mark this backup as metadata-only by adding explicit
documentation/comments and naming. Use the `essay_submissions` export block and
the surrounding backup assembly as the location to implement the chosen
approach.

---

Outside diff comments:
In `@CLAUDE.md`:
- Around line 33-48: Update the Tech stack table in CLAUDE.md so it matches the
new storage architecture; the current “Persistence” entry still says
“localStorage primary; Supabase optional sync,” which conflicts with the
“Storage rule (Supabase-primary, offline-capable)” section. Change the
persistence description to reflect Supabase as the primary source of truth with
offline-capable localStorage support, and keep the wording consistent with the
storage rule section so the document does not contradict itself.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 35cb9b7d-9f4d-4f3e-96db-1164da385ad9

📥 Commits

Reviewing files that changed from the base of the PR and between 3335a77 and e6e1053.

📒 Files selected for processing (26)
  • CLAUDE.md
  • README.md
  • src/context/AppContext.tsx
  • src/locales/de.json
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/fr.json
  • src/locales/nl.json
  • src/pages/Dashboard.tsx
  • src/pages/GradeStudent.tsx
  • src/pages/LandingPage.tsx
  • src/pages/RubricBuilder.tsx
  • src/pages/__tests__/pages.deepcoverage.test.tsx
  • src/services/__tests__/mediaStore.test.ts
  • src/services/database/StorageSync.ts
  • src/services/database/SupabaseAdapter.ts
  • src/services/mediaStore.ts
  • src/store/storage.test.ts
  • src/store/storage.ts
  • src/utils/syncMerge.test.ts
  • src/utils/syncMerge.ts
  • supabase/functions/nightly-backup/index.ts
  • supabase/migrations/045_essay_local_tracking_sync.sql
  • supabase/migrations/046_user_templates_sync.sql
  • supabase/migrations/047_enable_realtime.sql
  • supabase/migrations/048_nightly_backup.sql

Comment thread src/context/AppContext.tsx
Comment thread src/pages/LandingPage.tsx
Comment thread src/services/database/StorageSync.ts
Comment thread src/store/storage.ts
Comment thread src/store/storage.ts
Comment thread supabase/functions/nightly-backup/index.ts Outdated
Comment thread supabase/migrations/045_essay_local_tracking_sync.sql
Comment thread supabase/migrations/046_user_templates_sync.sql
Comment thread supabase/migrations/047_enable_realtime.sql Outdated
Comment thread supabase/migrations/048_nightly_backup.sql
- Critical: SAVE_USER_TEMPLATE's .slice(0, 20) cap evicted entries from the
  reducer's synced state array, which the delta-sync diff() effect reads as a
  delete and pushes to Supabase — saving a 21st template silently deleted the
  oldest one from the cloud and every other device. Cap removed.
- Major: StorageSync.startRealtimeSync() no-ops when a channel already exists,
  so configure() called twice without an intervening disconnect()/signOut()
  (e.g. an owner switch) left the realtime subscription scoped to the previous
  user's uid. Now stops any existing channel before guardOwnerSwitch() runs.
- Major: nightly-backup edge function defaulted SUPABASE_URL/SERVICE_ROLE_KEY to
  '', letting an empty bearer token pass the auth check if the env vars were
  unset. Now fails closed with a 500 before checking auth.
- Major: nightly-backup's profile scan was unbounded — a school with 1000+
  teacher/admin accounts would silently lose backup coverage past the API's
  default row cap. Now pages through with .range().
- Minor: pending-sync queue eviction at MAX_PENDING_OPS only logged a
  console.warn; now also fires the same quotaExceededHandler used for quota
  errors, since a dropped queue entry is the same kind of data loss.
- Minor: nightly-backup's pruneOldBackups() swallowed list/remove errors,
  letting retention drift while still reporting success; now throws so the
  caller's existing per-profile error handling picks it up.
- Minor: nightly-backup's header comment said self-hosted should use
  scripts/backup.sh instead, contradicting the README (which documents the
  official self-hosted stack using this function too, since scripts/backup.sh
  is hardwired to this repo's own docker-compose.yml).
- Major: 047's `ALTER PUBLICATION ... ADD TABLE` has no IF NOT EXISTS form and
  errors (not a no-op) on a table that's already a member, so a retry would
  abort the migration. Wrapped in a per-table pg_publication_tables guard.
- Trivial: removed a stray "ponytail:" fragment from a storage.ts comment
  (an AI-assistant-session marker with no meaning to other tools/readers).
- Trivial: documented why 045/046's indexes intentionally skip CONCURRENTLY
  (Supabase migrations run in a transaction, which disallows it; both tables
  are newly created in the same migration, so there's no data to lock).
- Documented (not fixed — flagged "heavy lift" by the reviewer, and out of
  scope for a lightweight per-user DB snapshot): 048's backup covers table rows
  only, not the files referenced by Storage-path columns (essay submissions,
  recordings) — noted in both the migration and the README.
- Not changed: LandingPage.tsx's hardcoded "Works offline" string — the
  reviewer flagged this as low-value on its own, since the entire page is
  already unlocalized; not worth an isolated fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread supabase/functions/nightly-backup/index.ts Fixed
…ion fallback

E2E — Supabase Integration caught a real regression: guardOwnerSwitch() (added
this PR) was wiping local data on a plain page reload, deleting data an offline
grading test had just seeded before the app could flush/use it.

Root cause: SupabaseAdapter.connect()'s pre-existing session-restore has a
fallback — if client.auth.getSession() returns null (e.g. a transient race
right after a fresh client is created on page load, before Supabase-js has
finished restoring the persisted session from storage), it silently signs in
anonymously instead, producing a brand new uid. Before this PR that was mostly
harmless. With guardOwnerSwitch() now wiping local data whenever the uid
doesn't match the last-known owner, that same fallback became destructive: a
spurious anonymous fallback looks identical to a genuine account switch.

Fix: track whether the current session is anonymous (SupabaseAdapter.
isAnonymousSession()) and skip the owner-switch check entirely when it is —
never wipe, and don't overwrite the stored owner, so a later reconnect with
the real session still compares against the correct last-known owner. This
doesn't reopen the original cross-account-bleed gap the guard exists for:
that scenario is always a real authenticated login (OTP/magic link/password),
never anonymous.

Also (from remaining CI feedback):
- CodeQL: nightly-backup's profile-listing failure returned the raw exception
  message in the HTTP response; now logs it server-side and returns a generic
  message instead.
- CodeRabbit (stale review pass against an earlier commit): CLAUDE.md's tech
  stack table still said "localStorage primary; Supabase optional sync",
  contradicting the "Storage rule (Supabase-primary, offline-capable)" section
  from earlier in this PR.

Co-Authored-By: Claude Sonnet 5 <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.

Actionable comments posted: 1

🤖 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 `@supabase/functions/nightly-backup/index.ts`:
- Around line 76-84: The profile pagination in the backup sweep is unstable
because the query in nightly-backup’s profile loop uses .range() without a
deterministic sort. Update the profiles query in the backup iteration to include
an explicit .order('id') before .range(from, from + PROFILE_PAGE_SIZE - 1), so
paging stays consistent across batches and the loop in the nightly-backup
function does not skip or duplicate profiles.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ff2fdf03-fc8c-4f14-ad62-3a371cb5b7b3

📥 Commits

Reviewing files that changed from the base of the PR and between e6e1053 and 212c118.

📒 Files selected for processing (11)
  • CLAUDE.md
  • README.md
  • src/context/AppContext.tsx
  • src/services/database/StorageSync.ts
  • src/services/database/SupabaseAdapter.ts
  • src/store/storage.ts
  • supabase/functions/nightly-backup/index.ts
  • supabase/migrations/045_essay_local_tracking_sync.sql
  • supabase/migrations/046_user_templates_sync.sql
  • supabase/migrations/047_enable_realtime.sql
  • supabase/migrations/048_nightly_backup.sql

Comment thread supabase/functions/nightly-backup/index.ts
.range() without an explicit .order() has no guaranteed row ordering across
successive requests (PostgREST/Postgres row order is otherwise unspecified),
so paginating without one could skip or duplicate profiles across pages.
Added .order('id') before .range().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@NesiciCoding NesiciCoding merged commit 0a81b37 into main Jul 3, 2026
8 checks passed
@NesiciCoding NesiciCoding deleted the claude/bold-bartik-8a4ed0 branch July 3, 2026 06:25
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