Fix queue list silent crash and missing uploaded status

Three fixes for queue list not updating:
1. Safe getBlob fallback - works even if old storage.js is cached
   by the service worker (no Storage.getBlob method)
2. Include 'uploaded' status in active filter - photos briefly in
   uploaded state (between upload and verify) were invisible
3. Wrap all interval refreshes in try/catch so one error doesn't
   silently kill all future updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 16:28:45 -06:00
parent 4e6fec43d3
commit 9047758c38
2 changed files with 29 additions and 19 deletions

View File

@@ -1,9 +1,9 @@
// NextSnap Service Worker // NextSnap Service Worker
// Provides offline-first caching for the app shell // Provides offline-first caching for the app shell
const CACHE_VERSION = 'nextsnap-v17'; const CACHE_VERSION = 'nextsnap-v18';
const APP_SHELL_CACHE = 'nextsnap-shell-v13'; const APP_SHELL_CACHE = 'nextsnap-shell-v14';
const RUNTIME_CACHE = 'nextsnap-runtime-v13'; const RUNTIME_CACHE = 'nextsnap-runtime-v14';
// Assets to cache on install // Assets to cache on install
const APP_SHELL_ASSETS = [ const APP_SHELL_ASSETS = [

View File

@@ -383,6 +383,15 @@
const currentUsername = '{{ username }}'; const currentUsername = '{{ username }}';
let deletePhotoId = null; let deletePhotoId = null;
// Safe blob accessor — works even if old storage.js is cached without getBlob()
function safeGetBlob(photo) {
if (!photo || !photo.blob) return null;
if (typeof Storage.getBlob === 'function') return Storage.getBlob(photo);
if (photo.blob instanceof Blob) return photo.blob;
// ArrayBuffer stored by new storage.js but old storage.js loaded (no getBlob)
try { return new Blob([photo.blob], { type: 'image/jpeg' }); } catch (e) { return null; }
}
// Initialize storage and load queue // Initialize storage and load queue
Storage.init().then(() => { Storage.init().then(() => {
SyncEngine.init(currentUsername); SyncEngine.init(currentUsername);
@@ -398,9 +407,11 @@ async function loadQueue() {
.where('username').equals(currentUsername) .where('username').equals(currentUsername)
.sortBy('timestamp'); .sortBy('timestamp');
// Split into active (pending/uploading) and completed (verified/failed) // Split into active and completed
const activePhotos = photos.filter(p => p.status === 'pending' || p.status === 'uploading'); const activePhotos = photos.filter(p =>
const completedPhotos = photos.filter(p => p.status === 'verified' || p.status === 'failed'); p.status === 'pending' || p.status === 'uploading' || p.status === 'uploaded');
const completedPhotos = photos.filter(p =>
p.status === 'verified' || p.status === 'failed');
if (photos.length === 0) { if (photos.length === 0) {
emptyState.style.display = 'block'; emptyState.style.display = 'block';
@@ -446,7 +457,7 @@ function createQueueItem(photo, isCompleted) {
item.classList.add('completed'); item.classList.add('completed');
} }
if (photo.status === 'uploading') { if (photo.status === 'uploading' || photo.status === 'uploaded') {
item.classList.add('uploading'); item.classList.add('uploading');
} else if (photo.status === 'verified') { } else if (photo.status === 'verified') {
item.classList.add('verified'); item.classList.add('verified');
@@ -457,7 +468,7 @@ function createQueueItem(photo, isCompleted) {
// Create thumbnail (use placeholder if blob was stripped) // Create thumbnail (use placeholder if blob was stripped)
const thumbnail = document.createElement('div'); const thumbnail = document.createElement('div');
thumbnail.className = 'queue-item-thumbnail'; thumbnail.className = 'queue-item-thumbnail';
const photoBlob = Storage.getBlob(photo); const photoBlob = safeGetBlob(photo);
if (photoBlob && photoBlob.size > 0) { if (photoBlob && photoBlob.size > 0) {
const img = document.createElement('img'); const img = document.createElement('img');
img.src = URL.createObjectURL(photoBlob); img.src = URL.createObjectURL(photoBlob);
@@ -558,7 +569,7 @@ async function updateStats() {
const pendingCount = photos.filter(p => p.status === 'pending').length; const pendingCount = photos.filter(p => p.status === 'pending').length;
const uploadingCount = photos.filter(p => p.status === 'uploading').length; const uploadingCount = photos.filter(p => p.status === 'uploading').length;
const totalSize = photos const totalSize = photos
.map(p => Storage.getBlob(p)) .map(p => safeGetBlob(p))
.filter(b => b && b.size) .filter(b => b && b.size)
.reduce((sum, b) => sum + b.size, 0) / 1024 / 1024; .reduce((sum, b) => sum + b.size, 0) / 1024 / 1024;
@@ -621,25 +632,24 @@ function scheduleRefresh() {
if (refreshTimer) clearTimeout(refreshTimer); if (refreshTimer) clearTimeout(refreshTimer);
refreshTimer = setTimeout(() => { refreshTimer = setTimeout(() => {
refreshTimer = null; refreshTimer = null;
loadQueue(); refreshUI();
updateStats();
}, 300); }, 300);
} }
window.addEventListener('photo-updated', scheduleRefresh); window.addEventListener('photo-updated', scheduleRefresh);
// Safe refresh wrapper — prevents silent failures from killing updates
async function refreshUI() {
try { await loadQueue(); } catch (e) { console.error('[QUEUE] loadQueue error:', e); }
try { await updateStats(); } catch (e) { console.error('[QUEUE] updateStats error:', e); }
}
// Poll every 3s while uploads are active, 15s when idle // Poll every 3s while uploads are active, 15s when idle
let hasActiveUploads = true; // assume active until first check let hasActiveUploads = true; // assume active until first check
setInterval(() => { setInterval(() => {
if (hasActiveUploads) { if (hasActiveUploads) refreshUI();
loadQueue();
updateStats();
}
}, 3000); }, 3000);
// Slower fallback poll for idle state // Slower fallback poll for idle state
setInterval(() => { setInterval(refreshUI, 15000);
loadQueue();
updateStats();
}, 15000);
</script> </script>
{% endblock %} {% endblock %}