- 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
- Added .dng to PHOTO_ALLOWED_EXTS
- Bumped PHOTO_MAX_SIZE from 10MB to 20MB
- DNG files (~13MB from Pixel 9a) now accepted by all upload paths:
/api/upload/photo, /api/ocr, connect_labels
- Server validates extension+size only; DNG EXIF preserved naturally
by write_bytes(); _re_embed_exif() correctly skips non-JPEG;
_extract_gps_from_bytes() opens DNG as TIFF via PIL (already works)
Verified: .dng upload via /api/upload/photo and /api/ocr both succeed,
file saved byte-identical.
- 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
- backup-db.sh: copies assets.db to /home/oplabs/backups/canteen-db/
- start.sh: runs backup automatically before every launch
- Daily cron at 3am for unattended backups
- Keeps last 30 backups, prunes oldest
- Replaced loadSettingsCache() with hardcoded seed data (categories, makes, keys, badges)
- Removed tabActivity lifecycle hook (activity log moved to admin app)
- Updated version to v3.0 + Admin Panel link in drawer footer
- Updated PROJECT.md with v3 project structure (3 repos, admin server docs)
- Removed backup files (.bak, .orig)
- Main index.html reduced from 7645 to 4228 lines
- _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
- Removed extension whitelist — no more 'unsupported format' errors
- Falls back to .jpg temp file for unknown/no extension
- PIL detects actual image format from file bytes (handles DNG as TIFF)
- Android browsers often strip DNG extensions when picking from gallery
- Tested: works with .dng, .jpg, no extension, and .bogus
- 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