Skip to content

feat: offline mode with client-side queue and sync (#138)#154

Merged
neonwatty merged 19 commits intomainfrom
feature/offline-mode
Apr 19, 2026
Merged

feat: offline mode with client-side queue and sync (#138)#154
neonwatty merged 19 commits intomainfrom
feature/offline-mode

Conversation

@neonwatty
Copy link
Copy Markdown
Collaborator

Summary

  • Client-first IndexedDB queue for offline operation storage — designed for remote access via Cloudflare Tunnel where "offline" means the tunnel is down, not just the device
  • Three-tier action classification: Tier 1 (local-only, always works), Tier 2 (queueable — assign draft, comment, toggle label), Tier 3 (blocked — close, merge, reassign)
  • Sync-on-reconnect: health check confirms server reachability, then sequential replay through existing server actions with idempotency nonces
  • Visual indicators: brick-red offline banner (responsive — compact on mobile), CacheAge badge ("Cached 3d ago"), queue dropdown, FailureModal for retry/discard
  • Auto-refresh: dashboard data revalidates after successful sync

New files (16)

  • lib/offline-queue.ts — IndexedDB queue with enqueue/replay/cancel
  • lib/tryOrQueue.ts — server action wrapper intercepting network failures
  • lib/sync.ts — replay logic with health check
  • hooks/useOfflineAware.ts — offline state + action tier lookup
  • hooks/useSyncOnReconnect.ts — sync trigger on online event
  • components/ui/CacheAge.tsx — relative time badge
  • components/ui/QueueDropdown.tsx — banner dropdown listing pending ops
  • components/ui/FailureModal.tsx — retry/discard for failed operations
  • app/api/health/route.ts — lightweight connectivity probe
  • Unit tests (3 files, 17 tests) + E2E spec (3 tests)

Modified files (8)

  • OfflineIndicator.tsx — queue count, responsive text, FailureModal wiring
  • AssignSheet.tsx, CommentComposer.tsx, LabelManager.tsx — wrapped with tryOrQueue
  • IssueActionSheet.tsx — close/reassign disabled when offline
  • layout.tsx — OfflineIndicator moved inside ToastProvider
  • page.tsx + List.tsx — CacheAge badge in dashboard header
  • core/db/cache.ts — getOldestCacheAge helper

Test plan

  • pnpm turbo typecheck — all 4 tasks pass, 0 errors
  • pnpm turbo test — 424 tests pass (347 core + 77 web)
  • E2E: offline banner appears/disappears on network toggle
  • E2E: /api/health returns { ok: true }
  • Visual: desktop offline banner with "Offline — viewing cached data"
  • Visual: mobile (393px) compact banner "Offline"
  • Visual: CacheAge badge renders next to brand title
  • PR review: 5 specialized agents (code, errors, tests, types, comments) — all critical/important issues addressed

Closes #138

Add getOldestCacheAge() to core cache module, surface it through
the page server component, and render CacheAge in the List top bar.
- Fix IDB connection leak: add tx.onerror/tx.onabort handlers
- Fix tryOrQueue: wrap all enqueue() calls in try-catch so IDB
  failures surface as errors instead of silently losing operations
- Fix sync replay: wrap state transitions in try-catch so IDB
  errors don't orphan operations in "syncing" ghost state
- Fix useSyncOnReconnect: use callbacksRef to prevent event listener
  churn, add catch around replayQueue, replace empty catches with
  console.warn, remove dead onSyncSuccess callback
- Fix useOfflineAware: replace empty catch with console.warn
- Fix OfflineIndicator: remove dead early-return that hid failure
  state, wire FailureModal for retry/discard of failed operations,
  add error handling to handleCancel
- Fix FailureModal: add catch in handleRetry
- Fix CommentComposer: pass idempotency nonce to addComment
- Fix LabelManager: add try-catch around tryOrQueue in startTransition
# Conflicts:
#	packages/web/app/page.tsx
#	packages/web/components/list/List.tsx
E2E test DBs use schema v4 which lacks the fetched_at column on the
cache table. getOldestCacheAge now catches the SQLite error and
returns null gracefully.
@neonwatty neonwatty added this pull request to the merge queue Apr 19, 2026
Merged via the queue into main with commit 91bf900 Apr 19, 2026
5 checks passed
@neonwatty neonwatty deleted the feature/offline-mode branch April 19, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant