28 Commits

Author SHA1 Message Date
shawn ec7478b287 Add machine ID search bar to EXIF Photos Map with highlight zoom
- Search input + Find/Clear buttons above the map
- Partial match search against all marker machine_ids
- Matched markers get amber pulsing highlight icon (22px)
- Auto-zoom to fit matched markers
- Clear button restores original view
- Store _exifGpsPhotos globally for search
- Store machineId on each marker at creation time
- Store default icon reference for proper restore on clear
- CSS: animated amber pulse ring around highlighted markers
2026-05-26 17:41:39 -04:00
shawn bbc5b41868 feat: add GPS-only toggle on import screen
Adds a toggle switch in the summary bar to quickly hide non-GPS
photos from the gallery grid. More discoverable than filter chips.

- Toggle switch with clean CSS animation (sliding pill)
- Renders inline in summary grid next to GPS/no-GPS counts
- Overrides filter chips when active (GPS-only supremacy)
- Persists across photo additions (DOM state, not reset)
- Gallery, detail card, and upload functions all respect it
2026-05-25 21:24:15 -04:00
shawn e3d8481f4f Fix GPS filter: decouple chip state from event.target, sync on re-render
- Extracted updateFilterChips() helper to decouple chip visual state from
  the brittle event.target dependency in setFilter()
- Call updateFilterChips() inside renderGallery() so chip visual state
  always matches currentFilter when gallery re-renders
- Reset chip classes in resetAll() to prevent stale active state from
  persisting across sessions
- Fixes a bug where resetAll() set currentFilter='all' but left a
  previous chip (e.g. '📍 GPS') visually active
2026-05-25 21:23:04 -04:00
shawn 5488f579e7 fix: inline edit now displays correct asset location info on Enter
saveMachineIdInline used wrong field names when rendering the
location info div after a successful assignment:
  'location_name', 'building', 'zone' — none of which exist in
  the asset dict returned by lookup_machine_id().

Replaced with the correct fields matching lookupPrevAsset():
  building_name, floor, room, address, make, model

Now pressing Enter on an inline machine ID edit correctly shows
the asset's building, floor, room, address, and make/model.
2026-05-25 21:21:06 -04:00
shawn a243980c1b fix: inline edit now always looks up & shows asset info
Three changes:
1. renderPrevCard: always render prevAsset/prevMachineInfo divs
   (not conditional on GPS coordinates)
2. loadPreviousPhotos: call lookupPrevAsset for ALL photos with
   machine_id, not just those with GPS coords
3. saveMachineIdInline: dynamically create asset info divs on
   the card if they don't exist yet (defensive fallback)

Previously, manually entering a machine ID on a card without GPS
would silently succeed but never show the asset name/location
because the display elements only existed on GPS-equipped cards.
2026-05-25 21:20:16 -04:00
shawn 22c2acf0da fix: badge shows 'matched'/'no match' instead of machine ID 2026-05-25 21:14:38 -04:00
shawn 6c54912cfc feat: inline machine ID editing in Previous Photos
Tap any machine ID in the Previous Photos list to edit it inline.
- Replaces static text with editable input on click/tap
- Enter/blur saves via /api/assign-machine-id
- Esc restores original value
- Shows ✏️ indicator on hover
- Saves new ID to DB, refreshes asset info and badge instantly
2026-05-25 20:55:07 -04:00
shawn db45b386fa fix: reset flow now fetches photo before deleting, shows preview + manual entry
- resetPhoto() now downloads the photo blob before calling the reset endpoint,
  then re-shows it in the detail view with preview and manual entry
- resetAndReupload() same treatment, with fallback to server fetch
- Added Reset button to ALL server results (not just duplicates)
- Preview in Previous Photos cards with lightbox (prev-thumb + openLightbox)
2026-05-25 20:48:20 -04:00
shawn 23f479dd0c feat: add full-screen photo viewer with zoom + prev-photo thumbnails
Lightbox implementation was already present from prior commit
(04ac2fd). This commit adds the missing piece:

- Add thumbnail images to previously-processed photo cards
  rendered by renderPrevCard(), linked to /api/photos/{id}/file
- Clicking the thumbnail opens the full-screen lightbox with zoom
- Add .prev-thumb CSS for consistent square aspect-ratio display

Acceptance Criteria:
- Tap detail preview image -> full-screen overlay (already done)
- Photo fills screen with proper aspect ratio (already done)
- Pinch/scroll zoom + double-tap toggle (already done)
- Close button + swipe-down-to-dismiss (already done)
- Also works for previous photos section thumbnails (NOW DONE)
2026-05-25 20:38:50 -04:00
shawn 04ac2fd7c7 fix: show manual entry when OCR digits found but lookup fails
- Always show manual entry field regardless of OCR result or GPS presence
- Context-sensitive label: different text when OCR found digits vs none
- Add Assign button to manual lookup result (POST /api/assign-machine-id)
- Add Push GPS button when GPS available after assign
- Store _lastPhotoId / _lastPhotoGps for manual entry functions
- Add lightbox: full-screen photo viewer with pinch/scroll/double-tap zoom
2026-05-25 20:35:45 -04:00
shawn 9512819e3e Add AGENTS.md for AI agent context 2026-05-25 20:18:58 -04:00
shawn ac7a8b1553 chore: add .gitignore (uploads, *.db, certs) 2026-05-25 20:14:53 -04:00
shawn 5cda952072 fix: single-photo analyze now looks up 5+ digit matches in canteen DB instead of showing warning
When OCR finds 5+ digits (match_5plus) but no 5-6 pattern, the frontend
now extracts the first 5 digits as the candidate machine ID, shows it,
and calls lookupAsset() to check the canteen DB — same behavior as the
5dash6 path. Eliminates the confusing '(no 5-6 pattern)' message.
2026-05-25 20:14:49 -04:00
shawn 3c3c06bd56 Update FEATURE_PLAN.md: mark all 9 features as done 2026-05-25 19:32:06 -04:00
shawn b94f2d19f8 E2E Smoke Test: 16 tests covering all new endpoints
Tests: root serving, export (CSV/KML/clipboard), GPS proximity,
sessions CRUD, assign-machine-id, bulk-assign, sw.js, index.html
2026-05-25 19:31:36 -04:00
shawn 7c8eb458ad Offline Queue: IndexedDB PhotoQueue, offline routing, SW caching + sync
- PhotoQueue class with IndexedDB store (queued/uploading/done/failed)
- uploadSelected() and startBulkProcess() check navigator.onLine and queue offline
- Online event auto-drains queue via /api/bulk-process with exponential backoff
- Offline banner shows queued count, hides when empty
- Service Worker: cache-first static, network-first API, background sync listener
- Exponential backoff: 2s → 5s → 15s, then mark failed after 3 retries
2026-05-25 19:28:50 -04:00
shawn 80619eb993 Frontend: Export UI, Inline Edit, Proximity, Drift, Walk-Path, Batch, Sessions, Heatmap 2026-05-25 19:12:58 -04:00
shawn e11ac3c6f2 Backend: Export CSV/KML/clipboard, Assign Machine ID, GPS Proximity, Session Management, Batch Assign 2026-05-25 19:08:07 -04:00
shawn fa894183fc Add FEATURE_PLAN.md — full spec for 11 features (Offline Queue, Export, Inline Edit, Proximity, Walk-Path, Batch Assign, Sessions, Drift Check, Heatmap) 2026-05-25 18:55:20 -04:00
shawn d617b7b9da Fix API key loading (strip whitespace, auto-load .env), add reset endpoint, improve maps + machine info display
- server.py now auto-loads .env file directly (robust key loading)
- Strip whitespace from all env var values
- Add POST /api/photos/{id}/reset endpoint to delete + allow re-upload
- Frontend: Reset button on prev-photo-cards + duplicate banners
- Improved mini-map rendering with better popups and zoom control
- Machine info panel shows building/floor/room/address/model details
- Added resetAndReupload() for in-place re-processing after reset
2026-05-25 18:43:47 -04:00
shawn f1707d9e6e chore: gitignore *.db and .env, remove tracked photos.db 2026-05-25 17:42:24 -04:00
shawn a8b1e694b0 feat: persistence, rerun OCR, Google Gemini, sticker mode, manual entry
- SQLite DB (photos.db) — persists all processed photo records
- Dedup by SHA256 hash — same file upload returns duplicate: true
- /api/photos — list previously processed photos
- /api/photos/{id} — get single record
- /api/photos/{id}/file — serve saved image
- /api/photos/{id}/reprocess — re-run OCR with different engine/model
- Google Gemini OCR engine (gemini-2.5-flash, free tier) alongside
  OpenCode Go LLM and Tesseract
- Sticker mode — specialized LLM/Google prompt for green/orange/yellow
  equipment stickers with 2D barcode + machine ID
- Manual machine ID entry — when GPS exists but OCR fails, show text
  input for manual lookup
- Frontend: Previous Photos section, Re-run OCR per photo, duplicate
  badges, engine dropdown, sticker toggle
2026-05-25 17:42:10 -04:00
shawn 70d8374ca6 feat: batch LLM OCR — multiple images in one API call + downscaling
- Add run_ocr_llm_batch() — sends N images in a single vision API call
  with structured JSON prompt, up to 20 images per batch
- Add _resize_for_llm() — downscales images to 1600px max dimension
  before sending to LLM, reducing per-image token cost
- Update bulk_process() to pre-read all files and batch-OCR in one call
- Graceful fallback: if batch JSON parsing fails, retries individually
- Frontend shows llm_batch engine badge

Without batch: N photos = N API calls (each with full prompt overhead)
With batch: N photos = ceil(N/20) API calls + image downscaling savings
2026-05-25 17:26:10 -04:00
shawn 3a67070063 Add LLM OCR engine via OpenCode Go + mimo-v2-omni
Adds optional LLM-based OCR as an alternative to Tesseract for reading
machine IDs from photos.

Backend (server.py):
- New run_ocr_llm() function calls OpenCode Go API (mimo-v2-omni model)
- Auto-falls back to Tesseract if API key missing or call fails
- Endpoints /api/analyze and /api/bulk-process accept ?ocr_engine=llm
  query param (default: tesseract) and ?ocr_model for model override
- Configurable via env vars: OPENCODE_GO_API_KEY, LLM_OCR_MODEL
- Requires User-Agent: Hermes-Agent/1.0 header for OpenCode Go API

Frontend (static/index.html):
- Toggle checkbox 'Use LLM OCR' in the UI
- OCR engine badge shown in results (llm vs tesseract + model name)
- getOcrParams() helper appends ?ocr_engine=llm to API calls

Infrastructure:
- .gitignore for uploads/ directory

Closes: #2
2026-05-25 17:12:37 -04:00
shawn 0052c59f81 Fix iOS hang when selecting many photos
Root cause: FileReader.readAsDataURL() loads the entire file into memory
as a base64 string. With 50+ iPhone photos (3-12 MB each), this exceeds
iOS Safari's per-tab memory limit and freezes the tab.

Changes:
- Replace readAsDataURL with URL.createObjectURL(file) — zero-copy file
  reference, no memory bloat (static/index.html)
- Reduce batch size from 4 to 2 — gentler on memory-constrained devices
- Add URL.revokeObjectURL() on reset — prevent blob URL leaks
- Add HEIC/HEIF support to server.py — iPhone format compatibility

Closes #1
2026-05-25 16:20:04 -04:00
shawn c1516471ac Bulk OCR + GPS push: process multiple GPS photos, match assets, push coordinates to DB with Leaflet map 2026-05-25 00:55:35 -04:00
shawn 6adaf97958 Add asset lookup from canteen DB — auto-match OCR machine IDs to asset records 2026-05-25 00:43:44 -04:00
shawn b079c9bf8f EXIF scanner: multi-photo gallery with GPS detection 2026-05-25 00:32:16 -04:00