feat: add offline interview capability#607
feat: add offline interview capability#607jthrilly wants to merge 11 commits intonew-form-systemfrom
Conversation
jthrilly
commented
Feb 3, 2026
- Add service worker with Serwist for offline caching of dashboard and interview pages using NetworkFirst strategy
- Add IndexedDB storage (Dexie) for offline interviews and cached protocols
- Add Start Interview buttons to Protocols and Participants tables
- Add protocol caching/downloading for offline use
- Add sync status indicator in navigation bar
- Add offline error boundary and indicator for interview sessions
- Add offline settings section in dashboard settings
- Add ENABLE_SW=true env variable for testing service worker in development
- Add documentation for testing service worker changes
- Add service worker with Serwist for offline caching of dashboard and interview pages using NetworkFirst strategy - Add IndexedDB storage (Dexie) for offline interviews and cached protocols - Add Start Interview buttons to Protocols and Participants tables - Add protocol caching/downloading for offline use - Add sync status indicator in navigation bar - Add offline error boundary and indicator for interview sessions - Add offline settings section in dashboard settings - Add ENABLE_SW=true env variable for testing service worker in development - Add documentation for testing service worker changes
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🎭 Playwright E2E Test Report❌ Tests failed. View the full report here: |
🎭 Playwright E2E Test Report❌ Tests failed. View the full report here: |
- Fix UnorderedList import to use named export - Remove unused scroll handler from EgoForm
15bc840 to
db09a42
Compare
🎭 Playwright E2E Test Report❌ Tests failed. View the full report here: |
Cross-origin requests (e.g., to UploadThing file storage) were being intercepted by the service worker and failing due to CORS. Now they pass through directly to the network using NetworkOnly strategy. The offline system stores protocol assets in IndexedDB, not the service worker cache, so this doesn't affect offline functionality.
🎭 Playwright E2E Test Report❌ Tests failed. View the full report here: |
🎭 Playwright E2E Test Report❌ Tests failed. View the full report here: |
The previous NetworkOnly approach still went through Serwist's fetch handling which caused CORS issues with UploadThing. Now we add a fetch event listener BEFORE Serwist that intercepts cross-origin requests and does a direct fetch, completely bypassing Serwist.
🎭 Playwright E2E Test Report❌ Tests failed. View the full report here: |
- Remove defaultCache to prevent caching login/auth pages - Scope static asset caching to dashboard/interview paths only - Add public/sw.js to .gitignore (generated file) - Add webpack watchOptions to ignore sw.js in dev mode - Fixes infinite recompile loop in development - Fixes inability to login if localStorage is cleared but SW is registered
The proper solution is to disable Serwist in development mode (already configured via the disable option). Only use ENABLE_SW=true when specifically testing offline/PWA functionality.
🎭 Playwright E2E Test Report❌ Tests failed. View the full report here: |
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive offline interview capability to Fresco, enabling users to conduct interviews without network connectivity. The implementation includes a service worker for caching, IndexedDB for local storage, offline-first UI components, and automatic data synchronization when connectivity is restored.
Changes:
- Added service worker with Serwist for offline page caching and asset management
- Implemented IndexedDB-based storage (Dexie) for offline interviews, cached protocols, and sync queue management
- Added offline-capable UI components including indicators, status badges, dialogs, and error boundaries
- Integrated offline functionality into protocols/participants tables with "Start Interview" and "Enable Offline" actions
Reviewed changes
Copilot reviewed 72 out of 75 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/pwa/sw.ts | Service worker implementing NetworkFirst, StaleWhileRevalidate, and CacheFirst strategies for offline support |
| lib/offline/db.ts | IndexedDB schema using Dexie for storing offline interviews, cached protocols, assets, and sync queue |
| lib/offline/syncManager.ts | Manages synchronization of offline interviews with retry logic and conflict detection |
| lib/offline/assetDownloadManager.ts | Handles downloading and caching protocol assets with progress tracking |
| components/offline/* | UI components for offline mode including indicators, badges, dialogs, and error boundaries |
| app/dashboard/*/_components/*Table/ActionsDropdown.tsx | Integration of "Start Interview" and "Enable Offline" actions in protocols/participants tables |
| tests/e2e/specs/offline/*.spec.ts | Comprehensive E2E tests for offline functionality including conflict resolution |
| package.json | Added dependencies: @serwist/next, dexie, dexie-react-hooks, fake-indexeddb |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| useEffect(() => { | ||
| if (progress.status !== 'downloading') { | ||
| setDownloadSpeed(null); | ||
| return; | ||
| } | ||
|
|
||
| const now = Date.now(); | ||
| const timeDiff = (now - lastTime) / 1000; | ||
| const bytesDiff = progress.downloadedBytes - lastBytes; | ||
|
|
||
| if (timeDiff > 0 && bytesDiff > 0) { | ||
| const speed = bytesDiff / timeDiff; | ||
| setDownloadSpeed(speed); | ||
| setLastBytes(progress.downloadedBytes); | ||
| setLastTime(now); | ||
| } | ||
| }, [progress.downloadedBytes, progress.status, lastBytes, lastTime]); |
There was a problem hiding this comment.
Potential memory leak in download speed tracking. The effect at lines 34-50 updates state based on progress changes but doesn't clean up when the component unmounts. If the component unmounts while a download is in progress, the state updates will continue to be attempted. Consider adding a cleanup function or using a ref to track mounted state.
| const [scrollProgress] = useState(0); | ||
| const [showScrollStatus] = useFlipflop(true, 7000, false); |
There was a problem hiding this comment.
Unused state variables detected. The variables scrollProgress and showScrollStatus are declared but never used after the handleScroll function was removed. Consider removing these unused state declarations to clean up the code.
| @@ -0,0 +1,40 @@ | |||
| export function registerServiceWorkerIfEnabled(): void { | |||
| // CRITICAL: Synchronous check BEFORE any async operations | |||
| if (!localStorage.getItem('offlineModeEnabled')) return; | |||
There was a problem hiding this comment.
The service worker registration logic should verify that offlineModeEnabled is actually stored before attempting registration. The current code only checks if the key exists in localStorage, but doesn't validate the value. Consider changing line 3 to: if (localStorage.getItem('offlineModeEnabled') !== 'true') return; to ensure the service worker only registers when explicitly enabled.
| const isInterview = | ||
| window.location.pathname.startsWith('/interview/'); |
There was a problem hiding this comment.
Race condition in service worker update detection. The check on line 18 for whether the user is on an interview page uses window.location.pathname, which may not be accurate if the URL changes between the time the event fires and when the check executes. Consider storing the pathname in a variable at the start of the event handler to avoid potential race conditions.
| const isValidDate = !isNaN(date.getTime()); | ||
| const localisedDate = isValidDate | ||
| ? new Intl.DateTimeFormat(navigator.language, dateOptions).format(date) | ||
| : 'Unknown'; | ||
|
|
||
| const [timeAgo, setTimeAgo] = useState<string>(''); | ||
|
|
||
| useEffect(() => { | ||
| if (!isValidDate) { | ||
| setTimeAgo('Unknown'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Invalid date handling issue. The TimeAgo component now checks for invalid dates, but if an invalid date is detected, it returns "Unknown" for both the tooltip and the display text. This could be confusing to users. Consider adding more context about why the date is invalid, or handle this at the data source level to ensure only valid dates are passed to the component.
| async checkStorageQuota(): Promise<{ | ||
| available: number; | ||
| used: number; | ||
| total: number; | ||
| percentUsed: number; | ||
| }> { | ||
| if (!navigator.storage?.estimate) { | ||
| return { available: 0, used: 0, total: 0, percentUsed: 0 }; |
There was a problem hiding this comment.
Missing error handling for storage quota check. The checkStorageQuota function returns default values when navigator.storage?.estimate is unavailable, but this could lead to incorrect decisions about whether to download protocols. Consider explicitly indicating when storage quota information is unavailable, or returning an error state that calling code can handle appropriately.
| resumeDownload(): void { | ||
| throw new Error( | ||
| 'Resume not implemented - please restart the download from the beginning', | ||
| ); | ||
| } |
There was a problem hiding this comment.
The resumeDownload function throws an error indicating the feature is not implemented. Either implement resume functionality or remove this method entirely, as exposing an unimplemented public method can confuse API consumers and lead to runtime errors. If resume is planned for the future, consider marking it as deprecated or private.
🎭 Playwright E2E Test Report❌ Tests failed. View the full report here: |
🎭 Playwright E2E Test Report❌ Tests failed. View the full report here: |