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
- 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
- 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
- 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
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
- 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
- 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
- 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.
- 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.
- 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)
- 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
- _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)
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.
- 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
- 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
- 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'
- 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
- 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'
- 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
- Existing assets now show a confirm card with machine ID + ✓ Confirm & Check In / ✏️ Edit buttons
- Confirm triggers auto check-in then shows the result card
- Edit opens inline machine ID editor with Re-lookup
- Cancel button returns to original confirmation
- New asset flow unchanged (form already editable)
- Removed separate OCR and Connect mode toggles — merged into Scan
- Renamed Barcode mode to Scan with OCR fallback + gallery upload
- Added captureScanOcr() — tap to capture frame for OCR when barcode fails
- handleBarcode() now compares barcode vs OCR result, picks valid ID
- Gallery photo picker integrated into Scan mode (OCR + EXIF GPS)
- Removed old photoPreviewCard and connect-mode HTML sections
- Removed orphaned functions referencing removed elements
- Added missing #offlineBanner HTML element with queueCountBadge
(showOfflineBanner/hideOfflineBanner were referencing it but it didn't exist)
- Enhanced SW: API GET calls now use network-first with cache fallback
(POST/PUT/DELETE still pass through — main thread handles offline queuing)
- Bumped SW cache from canteen-v1 to canteen-v2
The reverted code had two bugs in the tab switching logic:
1. closeNavigate() called switchTab() -> infinite recursion since
switchTab() calls closeNavigate() when leaving tabNavigate
2. currentTab was updated AFTER closeNavigate(), so the recursive
call hit the same condition over and over
Fix: move currentTab assignment before cleanup, use a wasNavigating
flag to track the old tab state, and make closeNavigate() only reset
nav UI (caller handles tab transition).
- Remove duplicate FAB HTML element (was two #fabAdd buttons)
- Remove duplicate FAB CSS block (keep single definition with pulse class)
- Add .fab:hover to remaining FAB CSS block
- Convert all fab.style.display usage to hidden-fab CSS class for smooth transitions
- Add direction-aware tab transitions: forward slides from right, reverse (back) slides from left
- Fix FAB pulse persistence: restart animation on click instead of removing permanently
- Move FAB init code after FAB HTML element so hidden-fab applies on initial load
- Remove redundant style.display FAB visibility in switchTab (now handled by updateFabVisibility)
Tab transition animations already existed (slide+fade with cubic-bezier easing).
Button press/hover scale effects and .btn-pressed haptic class already existed.
Success toast slide-up already existed via translateY transition.
Consistent easing curve (cubic-bezier 0.4,0,0.2,1) already applied across transitions.
prefers-reduced-motion already respected (animation-duration: 0.01ms at line 1696).
- Improved geolocation error messages (maps error codes to friendly text: Location blocked, GPS unavailable, GPS timed out)
- Added location data to list_assets API (location_latitude, longitude, address, building_name, building_number, name) via LEFT JOIN
- Map pins now use location coordinates as direct fallback when asset lacks own lat/lng
- GeocodeAndPin now falls back to location address fields when asset address is empty
- All 96 map-related tests pass (66 API + 30 frontend)
- Added GPS-aware empty state with location needed/gps off messaging
- Added GPS warning banner with 'enable location access' link in Navigate tab
- Show user's current location pin + accuracy circle on map when GPS available
- Enhanced destination search: GPS-aware filtering, badges for nav status
- Back button navigates to previous tab (or Map tab as fallback)
- Added navBackToPrevious() function with proper switchTab recursion avoidance
- Added CSS: .gps-warning, .gps-enable-link, .nav-route-info, .nav-route-item
- navigateToAsset() now goes to Nav tab even without GPS to show warning UI
- Improved search results: shows GPS on/off badge, disables non-navigable assets
- All navigation and smoke tests pass (5/5)
- Add GPS warning banner with enable-location link when GPS unavailable
- Refactor route info cards from inline styles to CSS classes
- Update empty state messaging based on GPS availability
- Show user location marker with accuracy circle on nav map when GPS available
- Hide map container when no GPS (show only empty state + search)
- Handle both [lat, lng] array and {lat, lng} object formats in geofence rendering
- All empty states verified: consistent icon + heading + description + CTA pattern
- Onboarding welcome overlay with localStorage flag
- Add missing assetCountBar div with count bar styling
- Add missing clearFiltersBtn with show/hide logic
- Add .search-highlight CSS for matching term highlighting
- Add .ai-location CSS for location context in asset items
- Add loading spinner overlay during search debounce
- Fix filter pills to use JS string escaping (not HTML escaping)
- Add 'make' filter parameter to API query
- Include 'make' in filter detection for count bar
The switchTab function had an early return after closeNavigate() that
skipped the actual tab panel switching (slide animation, active class
management). closeNavigate() only cleans up navigation state (map, data,
UI elements) — it does NOT switch panels. This caused the Navigate
panel to remain visible permanently after the first visit, overlapping
with any subsequent tab content.
Removed the early return so the normal tab switching logic runs after
closeNavigate() cleanup.