Commit Graph

22 Commits

Author SHA1 Message Date
36a53301a7 Show offline mode UI: hide online-only nav items, show offline label
When offline, hide Files and Admin from the bottom nav and display
"Offline Mode" next to the status dot. The SW offline fallback page
also only shows Capture and Queue nav items. Online-only pages
(browser, admin, reviewer) are never cached and go straight to the
offline fallback when the network is unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 23:00:34 -06:00
e88308a622 Pre-cache capture and queue pages for offline support
After the SW activates, fetch /capture and /queue in the background
so they are in the runtime cache and available offline immediately
without requiring manual navigation first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:23:32 -06:00
37f417eb5b Fix SW scope to intercept page navigations for offline support
The SW was served from /static/sw.js with default scope /static/,
so it only intercepted static asset requests. Page navigations to
/capture, /queue, /browser were not handled by the SW at all —
Safari showed its native offline error.

Fix: serve SW from /sw.js route with Service-Worker-Allowed: / header
and register with scope: /. Now the SW intercepts all navigations and
serves the offline fallback page when the network is unavailable.

Also remove auth-protected page routes from precache (they would cache
the login redirect). Pages are cached via network-first on visit instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:16:07 -06:00
ac241fae2e Precache page routes for offline support
Add /capture, /queue, /browser to SW precache so these pages work
offline without needing a prior visit. Capture and queue are fully
functional offline since they use local IndexedDB.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:07:14 -06:00
793238b562 Add offline fallback page to service worker
Safari intercepts bare 503 responses and shows its native "not connected"
error. Return a styled HTML offline page with status 200 so the SW handles
it instead of Safari.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:03:31 -06:00
06e90bbe4e Hide transient upload errors from queue UI
Transient network errors on first upload attempt showed ugly
'Network error: TypeError: Load failed' messages in the queue.
These are normal on iOS Safari and auto-resolve on retry.

- Clean up sync error messages (friendly text, no raw error types)
- Only show error messages in queue after 3+ retries or failed status
- Only show retry counter after 3+ retries
- First couple of retries are silent - just shows Pending status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 21:11:21 -06:00
5f354bb5fc Fix queue list crash: empty-state element destroyed by innerHTML
loadQueue() used getElementById('empty-state') on each call, but the
first successful render wiped it with innerHTML=''. Every subsequent
call got null and threw TypeError on .style access, silently killing
all future list refreshes. Counters kept working because updateStats()
runs independently.

Fix: recreate the empty state as innerHTML string instead of
referencing a DOM element that gets destroyed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 21:02:06 -06:00
70562e1d2b Fix stale queue page: switch pages to network-first in service worker
The service worker was using cache-first for ALL non-API routes,
including page routes like /queue, /capture, /browser. This meant
the browser kept serving old cached HTML with old inline JS even
after deploys, which is why the queue file list never updated
(old JS was running, silently crashing on API mismatches).

Now only /static/ assets use cache-first (they're versioned via
SW cache bumps). All pages and API calls use network-first with
cache as offline fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 20:57:54 -06:00
78c3c54a26 Fix admin panel 412: add OCS-APIRequest header to all OCS methods
Nextcloud requires the OCS-APIRequest: true header on all OCS API
calls as a CSRF protection measure. Without it, the server returns
412 Precondition Failed. The check_admin() method had the header
but all other OCS methods (get_users, create_user, enable/disable/
delete_user) were missing it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 20:51:33 -06:00
9047758c38 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>
2026-02-07 16:28:45 -06:00
4e6fec43d3 Fix queue list not updating: restore fast polling alongside events
The previous change extended the poll from 5s to 30s relying on
photo-updated events, but the service worker may still serve the old
storage.js without event dispatch, leaving the list stale.

Now polls every 3s while there are active uploads (pending/uploading),
15s fallback when idle, plus event-based refresh as a bonus.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:13:38 -06:00
3f0b0ea2e2 Live-update queue list when upload status changes
Storage.updatePhoto() now fires a 'photo-updated' CustomEvent so the
queue page refreshes immediately (300ms debounce) when the sync engine
changes a photo's status, instead of waiting for the 5s poll.

Also reduces background poll to 30s (just a fallback now), and revokes
stale ObjectURLs on each rebuild to prevent memory leaks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:07:53 -06:00
5105b42c46 Prevent blob eviction: store as ArrayBuffer + request persistence
iOS Safari evicts Blob file-backed data from IndexedDB under memory
pressure, causing upload POSTs to throw 'Load failed' without ever
reaching the server. Two-pronged fix:

1. Store photos as ArrayBuffer (inline bytes) instead of Blob (file
   reference) in IndexedDB — ArrayBuffers are not subject to eviction
2. Request navigator.storage.persist() to signal the browser not to
   evict our storage under pressure

Also adds Storage.getBlob() helper for converting stored ArrayBuffer
back to Blob at upload/display time, with backward compat for any
existing Blob-stored photos.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:01:07 -06:00
1da3eea7b8 Fix upload stuck pending: validate blob readability before upload
iOS Safari can evict blob data from IndexedDB while keeping the Blob
reference intact (instanceof/size checks pass but actual data read
fails). This caused upload POST to throw 'Load failed' client-side,
never reaching the server, burning retries endlessly.

Changes:
- Add validateBlobReadable() that reads first byte to verify data access
- If blob is unreadable, mark as 'failed' immediately with clear message
- Re-read photo from IndexedDB before upload for fresh blob reference
- Capture detailed error name/type in catch for better diagnostics
- Bump SW cache versions to v14/v10 to force new code delivery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 15:56:06 -06:00
bc748d05ac Fix uploads stuck pending due to page navigation killing sync
The sync engine was adding per-photo backoff delays (2-60s) inside
the processing loop, during which the user would navigate to another
page (browser -> queue -> browser), killing the sync mid-wait. The
upload POST never fired because the delay ran out the clock.

Changes:
- Remove per-photo backoff delay from processQueue loop — uploads
  start immediately on page load without blocking
- Move retry scheduling to after the full queue pass: if any uploads
  failed, schedule a new triggerSync() after 15s instead of blocking
  inline
- Keep the duplicate check on retries (fast, catches interrupted
  uploads that actually succeeded server-side)
- uploadPhoto now returns true/false so processQueue can track
  failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 15:48:44 -06:00
4491531acb Fix upload failures on iOS Safari (load failed)
iOS Safari kills fetch requests when the app goes to background or
the connection drops during large uploads, producing 'load failed'.
The sync engine was burning through all 5 retries instantly with no
delay, so a transient failure became permanent.

Changes:
- Add AbortController timeout (120s) on upload fetch
- Add exponential backoff between retries (2s, 5s, 10s...60s)
- Increase max retries from 5 to 15 for flaky mobile networks
- Remove 10MB resize step that was re-compressing photos already
  sized at capture time, avoiding extra memory pressure on iOS
- Log photo size with each upload attempt for easier debugging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 15:31:54 -06:00
0eef9bf2f3 Fix gallery rename applying to wrong files
After renaming a file, loadDirectory() was called which rebuilt the
galleryImages array in new alphabetical order. The gallery index
didn't change, so it now pointed to a different file — subsequent
renames hit the wrong file.

Fix: only update local state during the gallery session, defer the
file list refresh to when the gallery closes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 15:27:26 -06:00
32801852cb Add milliseconds to photo filenames to prevent overwrites
Filenames now include milliseconds (e.g. kamaji_20260207_143022347.jpg)
so rapid consecutive captures don't produce identical names and
overwrite each other on Nextcloud.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 05:17:52 -06:00
c4a6239f46 Improve JPEG output quality
- Live camera: 0.92 -> 0.97 quality
- Library upload: 0.92 -> 0.97 starting quality
- Size limit: 10MB -> 20MB before compression kicks in
- Gentler quality steps: drops by 0.03 instead of 0.1
- Higher quality floor: 0.50 instead of 0.30
- Gentler downscaling: 0.85x instead of 0.8x per step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 05:03:23 -06:00
0f1a4f82a9 Add upload from photo library button on capture screen
Adds an "Upload from Library" button below the camera button
that opens the device photo picker (no capture attribute) with
multi-select support. Selected photos are converted to JPEG and
queued for upload, with a progress counter during processing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 05:00:40 -06:00
e37cf878d0 Replace file-input capture with live camera viewfinder
The previous approach used setTimeout to re-trigger the file input
after each capture, which iOS Safari blocks because it loses the
user gesture context.

Now uses getUserMedia for a fullscreen camera viewfinder that stays
open between shots. Shutter button captures frames from the video
stream, saves to IndexedDB, and the camera remains open until the
user taps close. Includes flash feedback, session counter, and
camera flip. Falls back to single-shot file input if getUserMedia
is unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 04:56:16 -06:00
cad4118f72 Add NextSnap PWA with photo gallery viewer and continuous capture
Offline-first photo capture app for Nextcloud with:
- Camera capture with continuous mode (auto-reopens after each photo)
- File browser with fullscreen image gallery, swipe navigation, and rename
- Upload queue with background sync engine
- Admin panel for Nextcloud user management
- Service worker for offline-first caching (v13)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 04:53:13 -06:00