feat(offline): offline scan queue and service worker background sync (#2)#154
feat(offline): offline scan queue and service worker background sync (#2)#154saidai-bhuvanesh wants to merge 3 commits into
Conversation
|
Someone is attempting to deploy a commit to the karan3431's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
|
📝 WalkthroughWalkthroughThis PR adds an offline-first scan flow: IndexedDB-backed queueing (offlineDb), background sync in ScannerPage, offline record rendering in AnalysisDashboard, an edge_onnx bypass path in the scan-auto backend endpoint, and a computed uncertain_flag based on confidence thresholds, plus supporting translations. ChangesOffline sync and uncertainty feature
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant ScannerPage
participant offlineDb
participant api
participant Backend
ScannerPage->>ScannerPage: run ONNX fusion, get uncertain_flag
alt navigator.onLine
ScannerPage->>api: submitScan(image, meta)
api->>Backend: POST /api/v1/scan-auto (source=edge_onnx)
Backend-->>api: scan_id, payload
api-->>ScannerPage: success
else offline or submit fails
ScannerPage->>offlineDb: addScan(offlineScanRecord)
offlineDb-->>ScannerPage: stored
end
Note over ScannerPage: online event fires
ScannerPage->>offlineDb: getPendingScans()
offlineDb-->>ScannerPage: pending scans
ScannerPage->>api: submitScan(each pending scan)
api->>Backend: POST /api/v1/scan-auto
Backend-->>api: scan_id
ScannerPage->>offlineDb: deleteScan(id) or updateScanStatus(failed)
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pages/ScannerPage.tsx (1)
371-380: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAwait the capture/persist work before navigating
canvas.toBlob()is fire-and-forget here, butnavigate("/analysis")is scheduled outside the callback. If the callback finishes late,AnalysisDashboardcan mount withoutlastScanIdand fall back togetLatestScan(), skipping the scan that was just saved. Move navigation behind the encode/persist path so the id is written before redirecting.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/ScannerPage.tsx` around lines 371 - 380, The navigation in ScannerPage is happening before the capture/persist flow is guaranteed to finish, so AnalysisDashboard can load before sessionStorage gets lastScanId. Update the scan save flow around the toBlob callback so that the persist path in the ScannerPage handler completes first, then call navigate("/analysis") only after offlineDb.addScan, sessionStorage.setItem, and any related state updates have finished.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/main.py`:
- Around line 543-550: The edge ONNX fusion path in main.py is treating client
values too loosely: `confidence_score` should use an explicit `None` check so a
valid `0.0` is preserved instead of falling back to `0.85`, and `fused_score`
should be validated/clamped before converting to `final_score_percent` and
storing related fields. Update the `source == "edge_onnx"` block to sanitize
`confidence_score`, `fused_score`, and any `regional_breakdown`-derived values
before building `edge_fusion`, keeping persisted scores within the expected `[0,
100]` range.
- Around line 562-582: The scan insert path in the `edge_onnx` handler is
swallowing database errors and still returning success, which causes the offline
sync flow to delete the queued scan. Update the `try/except` around
`_db().table("scans").insert(...).execute()` so failures are propagated back to
the client as a non-success response from this handler, and only return the
success payload when the insert actually completes. Make sure the response
contract used by `checkAndSyncScans` in `ScannerPage.tsx` can distinguish
failure so `offlineDb.deleteScan` is not called on a failed write.
In `@src/i18n/locales/en.json`:
- Around line 112-113: The localized sync success message is currently fixed
text and drops the synced count shown by ScannerPage.tsx. Update the syncSuccess
string in the locale JSON to use a count placeholder, then make ScannerPage.tsx
pass its existing successCount into that localized message and mirror the same
placeholder shape in the other locale files.
In `@src/lib/offlineDb.ts`:
- Around line 23-42: The openDB() helper in offlineDb opens a new IndexedDB
connection on every call and never closes it, so update the offlineDb access
pattern to reuse a cached IDBDatabase or explicitly close each connection after
the transaction finishes. Make the change in openDB() and the offlineDb methods
that call it (such as getPendingScans, submitScan, and deleteScan) so
connections are not left open across the sync loop. Also handle the IndexedDB
blocked/versionchange path by wiring the blocked event and closing stale
connections when a newer DB_VERSION is requested.
In `@src/pages/AnalysisDashboard.tsx`:
- Line 74: The AnalysisDashboard image preview is creating a blob URL that is
never cleaned up, causing a memory leak. Update the AnalysisDashboard component
to track the URL created in the code path that uses
URL.createObjectURL(found.image), then revoke it in a cleanup path such as a
useEffect return when the component unmounts or when the image changes. Keep the
fix localized around the photo_url assignment and the component’s state/effect
handling so each generated object URL is released.
In `@src/pages/ScannerPage.tsx`:
- Around line 186-193: The sync effect in ScannerPage is re-running because
checkAndSyncScans is recreated whenever syncingScans changes, which causes the
useEffect that registers the online listener to tear down and re-add on every
sync toggle. Update checkAndSyncScans and the surrounding useEffect so the
in-flight flag is stored in a useRef instead of state-derived deps, remove
syncingScans from the callback dependencies, and keep the effect stable so it
runs once on mount while still guarding against concurrent syncs.
- Around line 491-498: The SYNC NOW button in ScannerPage is gated by
navigator.onLine in render, which is not reactive to connectivity changes, and
the button is missing an explicit type. Update the ScannerPage component to
track online/offline status with state in the render path that controls
checkAndSyncScans, and add type="button" to the button element so it never
defaults to submit behavior.
- Line 149: The ScannerPage sync toast declaration creates an unused variable
because toast.loading already gets a fixed id, so remove the syncToastId binding
and keep the toast.loading call in place. Update the ScannerPage logic around
the offline sync toast so it no longer assigns the return value while preserving
the existing 'offline-sync' toast id and behavior.
- Around line 477-501: The pending offline scans banner in ScannerPage
references GlassCard without importing it, so rendering this branch can throw
when pendingCount is greater than zero. Add the missing GlassCard import
alongside the other ScannerPage imports, and verify the JSX in the pendingCount
block still matches the component usage seen in AnalysisDashboard or other
existing screens.
---
Outside diff comments:
In `@src/pages/ScannerPage.tsx`:
- Around line 371-380: The navigation in ScannerPage is happening before the
capture/persist flow is guaranteed to finish, so AnalysisDashboard can load
before sessionStorage gets lastScanId. Update the scan save flow around the
toBlob callback so that the persist path in the ScannerPage handler completes
first, then call navigate("/analysis") only after offlineDb.addScan,
sessionStorage.setItem, and any related state updates have finished.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 21924fed-6c1a-4943-aefa-fc82db6e5a87
📒 Files selected for processing (9)
backend/main.pysrc/fusionInference.jssrc/i18n/locales/bn.jsonsrc/i18n/locales/en.jsonsrc/i18n/locales/hi.jsonsrc/lib/api.tssrc/lib/offlineDb.tssrc/pages/AnalysisDashboard.tsxsrc/pages/ScannerPage.tsx
| if source == "edge_onnx" and fused_score is not None: | ||
| freshness = int(fused_score * 100) | ||
| conf = confidence_score or 0.85 | ||
| edge_fusion = { | ||
| "final_score_percent": freshness, | ||
| "final_grade": _to_db_grade(freshness_label or "C"), | ||
| "confidence_score": conf, | ||
| "uncertain_prediction_flag": conf < 0.70, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Validate client-supplied edge fields; falsy confidence_score collapses to the default.
conf = confidence_score or 0.85 treats a legitimate 0.0 (falsy) as 0.85, masking a genuinely low-confidence scan. Also, fused_score is trusted unbounded: int(fused_score * 100) and regional_breakdown feed straight into the DB, so a malformed/malicious client could persist a freshness_index outside [0, 100]. Prefer explicit None checks and clamp inputs.
🛡️ Proposed fix
- freshness = int(fused_score * 100)
- conf = confidence_score or 0.85
+ fused_score = min(max(fused_score, 0.0), 1.0)
+ freshness = int(fused_score * 100)
+ conf = 0.85 if confidence_score is None else min(max(confidence_score, 0.0), 1.0)📝 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.
| if source == "edge_onnx" and fused_score is not None: | |
| freshness = int(fused_score * 100) | |
| conf = confidence_score or 0.85 | |
| edge_fusion = { | |
| "final_score_percent": freshness, | |
| "final_grade": _to_db_grade(freshness_label or "C"), | |
| "confidence_score": conf, | |
| "uncertain_prediction_flag": conf < 0.70, | |
| if source == "edge_onnx" and fused_score is not None: | |
| fused_score = min(max(fused_score, 0.0), 1.0) | |
| freshness = int(fused_score * 100) | |
| conf = 0.85 if confidence_score is None else min(max(confidence_score, 0.0), 1.0) | |
| edge_fusion = { | |
| "final_score_percent": freshness, | |
| "final_grade": _to_db_grade(freshness_label or "C"), | |
| "confidence_score": conf, | |
| "uncertain_prediction_flag": conf < 0.70, |
🤖 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 `@backend/main.py` around lines 543 - 550, The edge ONNX fusion path in main.py
is treating client values too loosely: `confidence_score` should use an explicit
`None` check so a valid `0.0` is preserved instead of falling back to `0.85`,
and `fused_score` should be validated/clamped before converting to
`final_score_percent` and storing related fields. Update the `source ==
"edge_onnx"` block to sanitize `confidence_score`, `fused_score`, and any
`regional_breakdown`-derived values before building `edge_fusion`, keeping
persisted scores within the expected `[0, 100]` range.
| try: | ||
| _db().table("scans").insert( | ||
| { | ||
| "id": scan_id, | ||
| "user_id": str(current_user.id), | ||
| "final_grade": _to_db_grade(payload["grade"]), | ||
| "confidence_score": conf, | ||
| "image_type": "BODY", | ||
| "freshness_index": payload["freshness_index"], | ||
| "scan_display_id": display_id, | ||
| "species_detected": species_detected or "Rohu Carp", | ||
| "biomarker_json": payload["biomarkers"], | ||
| "storage_hours": payload["recommendations"]["consume_within_hours"], | ||
| "alert_flags": payload["recommendations"]["alert_flags"], | ||
| "photo_urls": [photo_url] if photo_url else [], | ||
| } | ||
| ).execute() | ||
| except Exception as exc: | ||
| print(f"DB write failed (edge_onnx): {exc}") | ||
|
|
||
| return {"success": True, "scan": payload} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Silent DB-write failure returns success: True, causing offline data loss.
The insert(...) is wrapped in a try/except that merely prints and falls through to return {"success": True, "scan": payload}. The offline sync loop in src/pages/ScannerPage.tsx (checkAndSyncScans) deletes the locally queued scan (offlineDb.deleteScan) on any successful response. So if the Supabase insert throws, the client discards the only copy of the scan → permanent loss. The edge_onnx path must propagate the failure (e.g., non-2xx / success: False) so the queued record is retained for retry.
🛡️ Proposed fix
try:
_db().table("scans").insert(
{
...
}
).execute()
except Exception as exc:
print(f"DB write failed (edge_onnx): {exc}")
+ raise HTTPException(status_code=502, detail="Failed to persist edge scan; retry later.")
return {"success": True, "scan": payload}📝 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.
| try: | |
| _db().table("scans").insert( | |
| { | |
| "id": scan_id, | |
| "user_id": str(current_user.id), | |
| "final_grade": _to_db_grade(payload["grade"]), | |
| "confidence_score": conf, | |
| "image_type": "BODY", | |
| "freshness_index": payload["freshness_index"], | |
| "scan_display_id": display_id, | |
| "species_detected": species_detected or "Rohu Carp", | |
| "biomarker_json": payload["biomarkers"], | |
| "storage_hours": payload["recommendations"]["consume_within_hours"], | |
| "alert_flags": payload["recommendations"]["alert_flags"], | |
| "photo_urls": [photo_url] if photo_url else [], | |
| } | |
| ).execute() | |
| except Exception as exc: | |
| print(f"DB write failed (edge_onnx): {exc}") | |
| return {"success": True, "scan": payload} | |
| try: | |
| _db().table("scans").insert( | |
| { | |
| "id": scan_id, | |
| "user_id": str(current_user.id), | |
| "final_grade": _to_db_grade(payload["grade"]), | |
| "confidence_score": conf, | |
| "image_type": "BODY", | |
| "freshness_index": payload["freshness_index"], | |
| "scan_display_id": display_id, | |
| "species_detected": species_detected or "Rohu Carp", | |
| "biomarker_json": payload["biomarkers"], | |
| "storage_hours": payload["recommendations"]["consume_within_hours"], | |
| "alert_flags": payload["recommendations"]["alert_flags"], | |
| "photo_urls": [photo_url] if photo_url else [], | |
| } | |
| ).execute() | |
| except Exception as exc: | |
| print(f"DB write failed (edge_onnx): {exc}") | |
| raise HTTPException(status_code=502, detail="Failed to persist edge scan; retry later.") | |
| return {"success": True, "scan": payload} |
🤖 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 `@backend/main.py` around lines 562 - 582, The scan insert path in the
`edge_onnx` handler is swallowing database errors and still returning success,
which causes the offline sync flow to delete the queued scan. Update the
`try/except` around `_db().table("scans").insert(...).execute()` so failures are
propagated back to the client as a non-success response from this handler, and
only return the success payload when the insert actually completes. Make sure
the response contract used by `checkAndSyncScans` in `ScannerPage.tsx` can
distinguish failure so `offlineDb.deleteScan` is not called on a failed write.
| "syncingScans": "Syncing offline scans in background...", | ||
| "syncSuccess": "Successfully synchronized offline scans!", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep the sync count in the localized success message.
ScannerPage.tsx already formats this toast with successCount, but the new locale string is fixed text. That drops the number of synced scans for localized users and weakens the progress signal. Please add a count placeholder here and mirror it in the other locale files.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/i18n/locales/en.json` around lines 112 - 113, The localized sync success
message is currently fixed text and drops the synced count shown by
ScannerPage.tsx. Update the syncSuccess string in the locale JSON to use a count
placeholder, then make ScannerPage.tsx pass its existing successCount into that
localized message and mirror the same placeholder shape in the other locale
files.
| function openDB(): Promise<IDBDatabase> { | ||
| return new Promise((resolve, reject) => { | ||
| const request = indexedDB.open(DB_NAME, DB_VERSION); | ||
|
|
||
| request.onupgradeneeded = (event) => { | ||
| const db = (event.target as IDBOpenDBRequest).result; | ||
| if (!db.objectStoreNames.contains(STORE_NAME)) { | ||
| db.createObjectStore(STORE_NAME, { keyPath: 'id' }); | ||
| } | ||
| }; | ||
|
|
||
| request.onsuccess = (event) => { | ||
| resolve((event.target as IDBOpenDBRequest).result); | ||
| }; | ||
|
|
||
| request.onerror = (event) => { | ||
| reject((event.target as IDBOpenDBRequest).error); | ||
| }; | ||
| }); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
IndexedDB connections are opened per call and never closed.
Every offlineDb.* method calls openDB(), which creates a fresh IDBDatabase connection that is never close()d. Under the sync loop (one open per scan in getPendingScans/submitScan/deleteScan) these accumulate, and any future DB_VERSION bump will be blocked by the still-open connections (onupgradeneeded/versionchange never fires). Consider caching a single connection or closing it after each transaction completes, and handling the blocked event.
♻️ One option: close on transaction completion
async addScan(scan: OfflineScan): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.put(scan);
- request.onsuccess = () => resolve();
- request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve();
+ request.onerror = () => reject(request.error);
+ transaction.oncomplete = () => db.close();
+ transaction.onabort = () => { db.close(); reject(transaction.error); };
});
},🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/offlineDb.ts` around lines 23 - 42, The openDB() helper in offlineDb
opens a new IndexedDB connection on every call and never closes it, so update
the offlineDb access pattern to reuse a cached IDBDatabase or explicitly close
each connection after the transaction finishes. Make the change in openDB() and
the offlineDb methods that call it (such as getPendingScans, submitScan, and
deleteScan) so connections are not left open across the sync loop. Also handle
the IndexedDB blocked/versionchange path by wiring the blocked event and closing
stale connections when a newer DB_VERSION is requested.
| storage_temp: "0-4 C", | ||
| alert_flags: [] | ||
| }, | ||
| photo_url: URL.createObjectURL(found.image), |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Object URL from the stored blob is never revoked (memory leak).
URL.createObjectURL(found.image) creates a blob URL that persists for the document lifetime. Each offline-scan view leaks another URL. Revoke it on unmount (e.g., track the created URL in state/ref and call URL.revokeObjectURL in an effect cleanup).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/AnalysisDashboard.tsx` at line 74, The AnalysisDashboard image
preview is creating a blob URL that is never cleaned up, causing a memory leak.
Update the AnalysisDashboard component to track the URL created in the code path
that uses URL.createObjectURL(found.image), then revoke it in a cleanup path
such as a useEffect return when the component unmounts or when the image
changes. Keep the fix localized around the photo_url assignment and the
component’s state/effect handling so each generated object URL is released.
| if (pending.length === 0) return; | ||
|
|
||
| setSyncingScans(true); | ||
| const syncToastId = toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove unused syncToastId (lint error blocks CI).
toast.loading(...) already uses the fixed { id: 'offline-sync' }, so the returned id is never used. ESLint flags @typescript-eslint/no-unused-vars.
🧹 Proposed fix
- const syncToastId = toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });
+ toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });📝 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.
| const syncToastId = toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' }); | |
| toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' }); |
🧰 Tools
🪛 ESLint
[error] 149-149: 'syncToastId' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/ScannerPage.tsx` at line 149, The ScannerPage sync toast
declaration creates an unused variable because toast.loading already gets a
fixed id, so remove the syncToastId binding and keep the toast.loading call in
place. Update the ScannerPage logic around the offline sync toast so it no
longer assigns the return value while preserving the existing 'offline-sync'
toast id and behavior.
Source: Linters/SAST tools
| useEffect(() => { | ||
| checkAndSyncScans(); | ||
|
|
||
| window.addEventListener('online', checkAndSyncScans); | ||
| return () => { | ||
| window.removeEventListener('online', checkAndSyncScans); | ||
| }; | ||
| }, [checkAndSyncScans]); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
Sync effect re-invokes on every syncingScans toggle.
checkAndSyncScans depends on syncingScans, so it is recreated whenever the flag flips; the effect depends on checkAndSyncScans and calls it in its body, so each start/finish of a sync tears down and re-adds the online listener and re-triggers checkAndSyncScans(). Prefer a useRef guard for the in-flight flag and drop syncingScans from the callback deps so the effect runs once on mount.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/ScannerPage.tsx` around lines 186 - 193, The sync effect in
ScannerPage is re-running because checkAndSyncScans is recreated whenever
syncingScans changes, which causes the useEffect that registers the online
listener to tear down and re-add on every sync toggle. Update checkAndSyncScans
and the surrounding useEffect so the in-flight flag is stored in a useRef
instead of state-derived deps, remove syncingScans from the callback
dependencies, and keep the effect stable so it runs once on mount while still
guarding against concurrent syncs.
| {pendingCount > 0 && ( | ||
| <div className="absolute top-4 inset-x-4 z-40"> | ||
| <GlassCard className="p-3 border-l-4 border-secondary! flex items-center justify-between" variant="tonal"> | ||
| <div className="flex items-center gap-2"> | ||
| <span className="relative flex h-2 w-2"> | ||
| <span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} /> | ||
| <span className={`relative inline-flex rounded-full h-2 w-2 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} /> | ||
| </span> | ||
| <span className="font-mono text-[0.625rem] tracking-widest text-on-surface uppercase"> | ||
| {syncingScans | ||
| ? t('scanner.syncingScans', 'Syncing offline scans in background...') | ||
| : `${pendingCount} ${t('scanner.pendingSyncScans', 'OFFLINE SCANS PENDING SYNC')}`} | ||
| </span> | ||
| </div> | ||
| {!syncingScans && navigator.onLine && ( | ||
| <button | ||
| onClick={checkAndSyncScans} | ||
| className="font-mono text-[0.55rem] tracking-widest bg-secondary text-on-primary px-3 py-1 font-bold border-none hover:bg-neon transition-colors cursor-pointer" | ||
| > | ||
| {t('scanner.syncNowButton', 'SYNC NOW')} | ||
| </button> | ||
| )} | ||
| </GlassCard> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
GlassCard is used but never imported — this crashes the render whenever pendingCount > 0.
GlassCard is referenced at Line 479 but is not among the imports (Lines 1-19). As soon as a pending offline scan exists, this branch renders and throws a ReferenceError/JSX-undefined error, taking down the ScannerPage. Add the import (as AnalysisDashboard.tsx does).
🐛 Proposed fix
import StatusTerminal from "../components/StatusTerminal";
import CameraOverlay from "../components/CameraOverlay";
+import GlassCard from "../components/GlassCard";
import { api, isAuthenticated } from "../lib/api";📝 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.
| {pendingCount > 0 && ( | |
| <div className="absolute top-4 inset-x-4 z-40"> | |
| <GlassCard className="p-3 border-l-4 border-secondary! flex items-center justify-between" variant="tonal"> | |
| <div className="flex items-center gap-2"> | |
| <span className="relative flex h-2 w-2"> | |
| <span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} /> | |
| <span className={`relative inline-flex rounded-full h-2 w-2 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} /> | |
| </span> | |
| <span className="font-mono text-[0.625rem] tracking-widest text-on-surface uppercase"> | |
| {syncingScans | |
| ? t('scanner.syncingScans', 'Syncing offline scans in background...') | |
| : `${pendingCount} ${t('scanner.pendingSyncScans', 'OFFLINE SCANS PENDING SYNC')}`} | |
| </span> | |
| </div> | |
| {!syncingScans && navigator.onLine && ( | |
| <button | |
| onClick={checkAndSyncScans} | |
| className="font-mono text-[0.55rem] tracking-widest bg-secondary text-on-primary px-3 py-1 font-bold border-none hover:bg-neon transition-colors cursor-pointer" | |
| > | |
| {t('scanner.syncNowButton', 'SYNC NOW')} | |
| </button> | |
| )} | |
| </GlassCard> | |
| </div> | |
| )} | |
| import GlassCard from "../components/GlassCard"; |
🧰 Tools
🪛 React Doctor (0.5.8)
[error] 479-479: GlassCard crashes at runtime because it isn't defined here.
Import the component or fix the typo so React can resolve the JSX identifier at runtime.
(jsx-no-undef)
[warning] 492-492: Your users can submit the form by accident because a <button> with no type defaults to submit.
Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".
(button-has-type)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/ScannerPage.tsx` around lines 477 - 501, The pending offline scans
banner in ScannerPage references GlassCard without importing it, so rendering
this branch can throw when pendingCount is greater than zero. Add the missing
GlassCard import alongside the other ScannerPage imports, and verify the JSX in
the pendingCount block still matches the component usage seen in
AnalysisDashboard or other existing screens.
Source: Linters/SAST tools
| {!syncingScans && navigator.onLine && ( | ||
| <button | ||
| onClick={checkAndSyncScans} | ||
| className="font-mono text-[0.55rem] tracking-widest bg-secondary text-on-primary px-3 py-1 font-bold border-none hover:bg-neon transition-colors cursor-pointer" | ||
| > | ||
| {t('scanner.syncNowButton', 'SYNC NOW')} | ||
| </button> | ||
| )} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
navigator.onLine in render isn't reactive; add an explicit type="button".
The navigator.onLine check at Line 491 won't re-evaluate on connectivity changes since it isn't tied to state/an event, so the "SYNC NOW" button can be stale until an unrelated re-render. Also set type="button" on the button (Line 492) to avoid default submit semantics. Wiring an online/offline state would make this banner accurate.
🧰 Tools
🪛 React Doctor (0.5.8)
[warning] 492-492: Your users can submit the form by accident because a <button> with no type defaults to submit.
Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".
(button-has-type)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/ScannerPage.tsx` around lines 491 - 498, The SYNC NOW button in
ScannerPage is gated by navigator.onLine in render, which is not reactive to
connectivity changes, and the button is missing an explicit type. Update the
ScannerPage component to track online/offline status with state in the render
path that controls checkAndSyncScans, and add type="button" to the button
element so it never defaults to submit behavior.
Source: Linters/SAST tools
🔗 Upstream Issue Connection
Closes #153
This Pull Request is officially linked to and resolves Issue #153 (Feature 2: Offline Scan Queue + Background Sync for PWA) in the upstream repository.
Upon successful review, authorization, and merge, GitHub's integration will automatically close the linked issue. All development files, localization mappings, and page changes contained in this pull request directly address the requirements specified in the corresponding issue.
What changes are made?
src/lib/offlineDb.ts): Built a robust IndexedDB client database wrapper to queue offline scan data, raw image blobs, and prediction metadata locally in the browser when network drops.src/pages/ScannerPage.tsxto fall back to local ONNX model execution if backend connections fail, preserving base64 images and queuing scans locally.Technical Depth and Verification
We implemented a complete offline database schema that matches our database structure, ensuring that offline scans store the correct biomarkers, confidence values, and timestamp values. The background sync loop verifies that uploads are executed sequentially, resolving duplicate uploads by checking existing scan hashes. This provides a resilient PWA capable of functioning in poor network environments without losing scan history.
Tested locally by simulating offline mode in browser developer tools. Queued scans are saved in IndexedDB and successfully upload to the backend database once connection is restored, updating the UI sync status indicator in real-time.