Commit Graph

86 Commits

Author SHA1 Message Date
shawn c9c6317900 fix: larger park/resort labels (13px bold), allow 2-line wrap, center-aligned 2026-05-23 00:41:35 -04:00
shawn 0be50e7957 fix: revert chips to emoji, Snack pins use 🍿 fallback (img→emoji), bigger markers 28px 2026-05-23 00:37:10 -04:00
shawn 1bb03eb964 feat: SVG icons for map toolbar chips — flame/crosshair/outline, 13px, with label spans 2026-05-23 00:34:54 -04:00
shawn 384a0b842b fix: simple colored circle pins, no transforms — 22px dot with emoji and white border 2026-05-23 00:33:53 -04:00
shawn 71de462040 fix: teardrop pin markers (smaller, 24px) with category emoji on white dot 2026-05-23 00:31:46 -04:00
shawn 26311e7fe6 feat: text labels on Leaflet.draw toolbar — Area/Box/Circle/Edit/Delete instead of bare icons; revert chip changes 2026-05-23 00:29:56 -04:00
shawn 0401e45ea4 feat: category-based icon pins on map — 🥤🍔🔧 etc instead of blue dots 2026-05-23 00:27:54 -04:00
shawn 796914b089 feat: clearer map control icons with SVG + label spans, replace emoji chips 2026-05-23 00:25:43 -04:00
shawn 4ac930f2bf feat: 15 named Disney resort outlines with individual labels on map 2026-05-23 00:18:47 -04:00
shawn 0f2ead4b5b feat: bolder park outline styling — thicker lines (5px), full opacity, stronger fill, saturated colors 2026-05-23 00:15:08 -04:00
shawn 39b8669b21 Fix park outline coordinates + visibility
- Magic Kingdom: accurate 60-point polygon from OSM way 297428432
- All parks: adjusted boundaries for better alignment
- Increased stroke weight (3px) and opacity for visibility
- Higher fill opacity (0.15)
2026-05-23 00:04:27 -04:00
shawn 5380bf7256 Add Disney park outlines to map
- Polygons for Magic Kingdom, Epcot, Hollywood Studios, Animal Kingdom, Disney Springs
- Resort area outline (dashed) for Pop Century / Art of Animation
- Color-coded to match existing park badge colors
- Labels with park icons at polygon centers
- Toggle chip (🏰 Parks) in map controls to show/hide
- Outlines visible by default on map load
2026-05-22 23:53:09 -04:00
shawn a0e6f627f4 Revert "Add clear GPS endpoint + frontend button"
This reverts commit ca77a29d7d.
2026-05-22 23:32:51 -04:00
shawn ca77a29d7d Add clear GPS endpoint + frontend button
- DELETE /api/assets/{id}/gps sets lat/lng to NULL
- Detail view shows 'Clear GPS' button when asset has GPS coords
- Confirmation modal before clearing
- Tests: 204 success + 404 not found
2026-05-22 23:30:07 -04:00
shawn d91c1f0861 fix: normalize GF Bev/GF Food category casing throughout
- import_machines.py: add GF Bev/GF Food/Bev to category_map
- static/index.html: CATEGORY_MAP keys Gf bev→GF Bev, Gf food→GF Food
- DB: migrated 236 Gf bev→GF Bev, 173 Gf food→GF Food
2026-05-22 19:15:57 -04:00
shawn db6859a0f5 fix: Add Cache-Control: no-cache to static files to prevent stale HTML/image caching 2026-05-22 18:44:36 -04:00
shawn 8c828000bb feat: Swap snack icon from popcorn 🍿 to bag of chips image
Replaced the Snack category emoji with an actual chips illustration
served from static/icons/chips.png. The icon renders in the asset
list category badges as a 28x28 object-fit:contain image within
the 42x42 circular badge.
2026-05-22 18:40:26 -04:00
shawn e8a918fc7b feat: add is_disney column for reliable Disney/Non-Disney filtering
- Added is_disney INTEGER DEFAULT 0 column to assets table
- Backfilled 996 Disney assets based on D- customer prefix
- Server: is_disney in AssetCreate/AssetUpdate models, INSERT/UPDATE
- Server: disney_filter query param now uses is_disney column (no JOIN needed)
- import_cantaloupe.py: sets is_disney=1 when customer starts with D-
- import_machines.py: sets is_disney=1 when customer starts with D-
- disney_classify.py: sets is_disney=1 when classifying
- Frontend: new Location Type dropdown with Disney/Non-Disney toggle
  (replaces the __disney__/__non_disney__ park dropdown pseudo-options)
2026-05-22 18:34:13 -04:00
shawn 60dfd3434a feat: add Disney/Non-Disney quick filter to dropdown panel
Backend:
- disney_filter query param on /api/assets (values: 'disney' | 'non_disney')
- Detects Disney by checking disney_park IS NOT NULL OR customer name LIKE 'D-%'
- Catches ESPN, Wide World of Sports, all Disney resorts, etc.
- Adds LEFT JOIN to customers table when disney_filter is active

Frontend:
- '🏰 Disney only' and '🏢 Non-Disney' options in Disney Park dropdown
- Active filter tag shows which is selected
- Works together with other dropdown filters
2026-05-22 18:25:54 -04:00
shawn 11628898ff chore: remove orphaned renderFilterPills function 2026-05-22 18:21:32 -04:00
shawn c19295f1a2 fix: remove inline filter pills, keep only Filters button + dropdown panel 2026-05-22 18:21:07 -04:00
shawn e0412acbe3 feat: add Category, Make, Disney Park to filter panel dropdowns with backend endpoints 2026-05-22 18:19:32 -04:00
shawn d98a791c96 feat: improved asset filter UX - collapsible filter panel, customer/location cascade, status dropdown, assigned_to filter, active filter tags, result count
Backend:
- GET /api/customers — lists all customers for filter dropdown
- GET /api/locations?customer_id=X — cascading locations per customer
- GET /api/users — lists users for assigned_to filter

Frontend:
- Collapsible 'Filters' panel with Customer→Location cascade, Status, Assigned To
- Active filter tags showing what's currently filtering, removable individually
- 'Clear all filters' button to reset all at once
- Result count display ('X assets found')
- Better search placeholder showing searchable fields
- Filter count badge on the toggle button
2026-05-22 18:15:35 -04:00
shawn dec6a90409 Add proper category icons for all 10 types, remove stale CSS 2026-05-22 18:02:41 -04:00
shawn 7394d44caf Load all assets at once (no pagination), add server-side disney_park filter 2026-05-22 17:59:33 -04:00
shawn 95b5bd26eb Add pagination (Prev/Next with counter) and X-Total-Count header 2026-05-22 17:57:24 -04:00
shawn cce5cf2b5f Add Disney park badges, filter pills, editable classification, and service entrances to asset detail view 2026-05-22 17:53:32 -04:00
shawn 72003e2742 fix: GPS chip visibility and Leaflet Draw geofencing
- Removed hardcoded display:none from GPS center chip on map tab
- Chip now shows when GPS position is obtained in startVisitTracking
- Added leaflet-draw CSS and JS to HTML head
- Added draw control (polygon, rectangle, circle) to map init
- Emits geofenceDrawn custom event for external hooks
2026-05-22 11:50:44 -04:00
shawn f9f6d9d5ce Fix manual photo UI: both gallery picker + camera button visible
- Manual photo area now shows two clear choices:
  📁 Choose from Gallery — plain accept=image/* (EXIF preserved)
  📸 Take Photo — calls openManualCamera() for quick capture
- Each has a label explaining EXIF behavior
- openManualCamera() now appends input to DOM before click()
  (required by mobile Chrome for programmatic file input clicks)
- Dynamically created camera input is cleaned up after use
- camera-area overflow:visible to not clip the taller placeholder
2026-05-21 23:12:43 -04:00
shawn 90bd3f0e4b Remove capture=environment from manual photo input — plain file picker preserves EXIF
- manPhotoInput now uses accept=image/* without capture attribute
  -> opens gallery/file picker which gives raw file bytes (EXIF preserved)
- Camera shortcut preserved via openManualCamera() creates a temp
  capture=environment input for quick field capture (no EXIF but available)
- Label updated: 'Tap to take photo' -> 'Tap to choose photo'
- Manual photo capture row references openManualCamera() instead
2026-05-21 23:06:49 -04:00
shawn e2025126a8 v3: GPS tap-to-enable + server-side EXIF bypass for manual creation
GPS:
- initGPS() now shows tappable GPS badge instead of failing silently
- Badge pulses amber with '📍 Tap for GPS' — click triggers permission prompt
- fillGpsParking() auto-triggers badge click if GPS not yet available
- requestLocation() extracted as reusable Promise

EXIF bypass:
- submitManualAsset() now uses server-extracted GPS from raw upload bytes
- Server returns exif_gps in /api/upload/photo response (was already working)
- Client was ignoring it — now fills parking + map link fields automatically
- Bypasses Android content provider EXIF stripping since server reads raw bytes

Admin separation:
- admin.canteen.ourpad.casa now served by systemd service (canteen-admin.service)
- Main server also systemd (canteen-main.service) — both auto-restart on crash/boot
2026-05-21 22:53:14 -04:00
shawn 02a38a8459 fix: EXIF data now visible in photo preview card
- extractExifData() now async — reads real EXIF tags (Make, Model,
  DateTimeOriginal, ISO, FNumber, ExposureTime, FocalLength, GPS)
  via exifr.parse() and displays them in the UI
- handlePickedPhoto() + reader.onload made async to support this
- GPS extraction already auto-triggers on photo pick (tryExtractGpsFromPicked)
- Server-side exif_data round-trip already functional (verified)
- DNG/RAW-02: .dng allowed, 20MB limit
2026-05-21 22:45:28 -04:00
shawn 43bdedde61 T1: Clean up duplicate function declarations
- Removed duplicate clearPickedPhoto() at old line ~1772
  (kept newer version at line ~3857 with pickedPhotoGps clearing)
- Removed duplicate extractGpsFromPicked() at old line ~1831
  (kept newer async version at line ~3766 that uses exifr)
- Both ocrPickedPhoto() and createAssetFromPicked() were already
  singular — they were refactored in a prior edit

Verified: no duplicate function names, JS syntax OK, no regressions
2026-05-21 22:23:30 -04:00
shawn eaa2dab365 fix: createAssetFromPicked() now passes original File with EXIF
- Set manualPhotoFile = pickedPhotoFile directly (preserves EXIF)
- Set manualPhotoBlob = pickedPhotoFile (preserves EXIF)
- Keep FileReader for preview display only
- Trigger EXIF GPS extraction via extractGpsFromPhoto()
- Fill GPS coordinates into manual form (parking + map link)

Previously the function used readAsDataURL -> atob -> Blob
which stripped all EXIF metadata. Now the original File
flows through to submitManualAsset where exifr.parse()
re-embeds EXIF on the server.
2026-05-21 22:19:32 -04:00
shawn df90ebf5eb t_63e94899: Remove duplicate ocrPickedPhoto/createAssetFromPicked stubs — originals at 1783/1797 now resolve correctly 2026-05-21 22:15:20 -04:00
shawn e9f6af0590 fix: Map tab initialization (H1, H2)
- Added global let map/heatLayer/heatVisible declarations
- Leaflet map initializes on first visit to Map tab (OSM tiles)
- Implemented loadAssetPins() - fetches assets with coords from API,
  creates Leaflet markers with popups, auto-fits bounds
- Guard toggleHeatmap() with proper declared globals
- Closes #31 H1, #32 H2

Also confirmed M1 (empty asset list) is not a bug: API returns [] because
DB has no assets yet. Empty state renders correctly.
2026-05-21 21:46:59 -04:00
shawn 5822dfc1ab fix: guard stopManualPhoto() call — mode switching threw ReferenceError
setAddAssetMode() called stopManualPhoto() which was removed during
the canvas→file-input refactor. Every mode switch failed silently.
2026-05-21 21:42:01 -04:00
shawn 78dbdfd5c5 EXIF round-trip: read before upload, re-embed server-side
- Client: exifr.parse() reads full EXIF before upload, sent as exif_data
- Server: piexif re-embeds EXIF into saved JPEG after upload
- Auto-extract GPS from picked gallery photos on selection
- Removed Google Photos button (needs OAuth, not feasible)
2026-05-21 21:29:55 -04:00
shawn 906b61143a fix: EXIF GPS extraction now works with native camera capture
- Replaced getUserMedia() + canvas.toBlob() with <input type=file capture=environment>
  → native camera app preserves full EXIF data including GPS
- Defined missing extractGpsFromPhoto() using exifr.gps() library
- Manual mode now uploads the original file (with EXIF) instead of a canvas-encoded blob
- Server-side exif_gps fallback if client-side extraction fails
- Renamed manualPhotoBlob → manualPhotoFile for clarity
2026-05-21 20:51:09 -04:00
shawn 58f8b67b28 auth: full session-based auth with remember-me support
- Seed user: shawn / Brett85!@ (SHA-256)
- Sessions table with expires_at column
- login endpoint supports remember_me flag (30d vs 1d expiry)
- logout endpoint invalidates server-side token
- auth middleware + /api/auth/me check session expiry
- Frontend sends remember_me in login request body
- Frontend calls /api/auth/logout on client logout
- Migration function for existing databases
2026-05-21 20:17:21 -04:00
shawn 921a40a560 Fix: call initAuth() on DOMContentLoaded so login overlay shows on page load. Also includes v3 admin separation changes that weren't committed. 2026-05-21 20:07:52 -04:00
shawn 7cf566ad6d t_2f17b1b4: Add EXIF GPS extraction to upload/photo and OCR endpoints
- _extract_gps_from_bytes helper reads GPSInfo IFD via PIL get_ifd()
- Converts DMS to decimal degrees, returns {lat, lng} or None
- /api/upload/photo: reads bytes first, extracts GPS before saving
- /api/ocr: extracts GPS from raw bytes before OCR processing
- exif_gps returned alongside path/machine_id in both endpoints
- Photos without EXIF GPS omit the exif_gps key entirely
- Raw file bytes preserved (no re-compression, no EXIF stripping)
2026-05-21 17:17:38 -04:00
shawn 5d2d169885 fix: rename photoPicker to cameraInput/galleryInput broke JS IIFE
Two places referenced old photoPicker element ID that was removed:
1. connectFromGallery() — switched to window._connectMode flag + galleryInput.click()
2. Connect mode hook IIFE — now hooks both cameraInput and galleryInput via
   shared hookPicker() helper using window._connectMode instead of DOM attr

This was crashing the entire inline script (TypeError on null) ~line 4040,
preventing initAuth()/checkAuthGate() from ever running — no login screen.
2026-05-21 16:16:53 -04:00
shawn e16f2ec90f gallery: split Take Photo vs Pick from Gallery inputs
- Two dedicated <input> elements: cameraInput (capture=environment) for
  direct camera, galleryInput (no capture) for gallery picker
- Both wired to handlePickedPhoto() via a shared wireInput helper
- refreshGPS() fires on button click (before picker opens) for a fresh
  high-accuracy GPS fix — no maximumAge, so it pulls current location
  instead of relying on cached values from page-load initGPS()
- Workaround for browser EXIF GPS stripping: camera captures may preserve
  EXIF GPS, gallery picks fall through to device geolocation API
2026-05-21 15:29:03 -04:00
shawn 28f1a33a2d gallery: increase server OCR timeout to 20s
- 6s was too short for 13MB DNG upload on mobile
- Bumped to 20s to handle large RAW photos
- Added 'Uploading to server OCR...' status message during upload
- Server OCR handles DNG correctly (tested via curl), so the
  timeout was the root cause of gallery upload failures
2026-05-21 14:55:13 -04:00
shawn 0cf5dddf83 DNG/RAW photo support for gallery upload
- Server: accept .dng in OCR endpoint (PIL opens as TIFF)
- Server: bump OCR size limit to 20MB (DNG files can be large)
- Client: gallery upload tries server OCR first (with 6s timeout),
  then falls back to client-side Tesseract.js — matches 📸 capture flow
- DNG photos now work with 'Pick from Gallery'
2026-05-21 14:51:57 -04:00
shawn 97699a95ea gallery upload: better OCR error messages
- Catches 'unknown error' from Tesseract.js (unsupported
  image formats like RAW/DNG) and shows helpful guidance:
  'Use the 📸 live camera instead'
- Falls through to actual error message for other failures
- Better null-safety on error object
2026-05-21 14:47:34 -04:00
shawn 78c3c19ce4 gallery upload: add FileReader + img error handlers
- reader.onerror: shows error message on file read failure
- img.onerror: detects unsupported image formats (HEIC, RAW)
- Shows clear feedback: 'unsupported format' or 'format not supported'
- Helps diagnose gallery upload issues
2026-05-21 14:44:02 -04:00
shawn 6403699486 fix: login lag + 📸 overlay visibility + status text
- Login lag: camera/GPS/assets no longer start before login.
  Only initAuth() + initOfflineSupport() run on pageload.
  Camera/GPS/assets start after successful login (or auto-login
  from stored session).
- 📸 capture overlay: was defined in HTML but never shown.
  Now shown when camera starts, hidden when scanning stops.
- Status text: updated from 'Point camera at a barcode' to
  'Point camera at a barcode, or tap 📸 for text stickers'
2026-05-21 14:16:24 -04:00
shawn d55cc4aa44 gallery upload: GPS fallback + auto-lookup
- GPS: when EXIF stripped by mobile browser, falls back to
  navigator.geolocation.getCurrentPosition() for device location
- OCR result now populates scanMachineId hidden field so Create works
- createFromScanPhoto also checks AppState.gpsLat as fallback
- Gallery OCR now auto-lookups via handleBarcode() — routes to confirm
  card (existing asset) or new asset form (not found)
- Manual entry button also calls handleBarcode() after prompt
2026-05-21 14:09:14 -04:00