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
- Added disney_park TEXT DEFAULT NULL to assets table in _create_v2_tables
- Created service_entrances table with id, location_id, name, latitude,
longitude, notes, created_at, updated_at; location_id FK to locations
with ON DELETE CASCADE; name/lat/lon are NOT NULL
- Added v4 migration in init_db: ALTER TABLE for disney_park on existing
DBs, schema-aware recreation of service_entrances if name column lacks
NOT NULL constraint
- 44/45 tests pass (1 pre-existing 404 failure on stats endpoint)
The test suite login as 'admin'/'changeme' but only 'shawn' was seeded.
Adding the admin user with SHA256('changeme') hash lets all 15 Playwright
tests reach the actual app pages instead of failing at login.
- 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
- 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)
- _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)
- 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
- 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'
- 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)
- Fix Active Technicians: query users WHERE role='technician' instead of counting from visit data
- Interactive bar charts: hover tooltips show label+count, click drills down to filtered asset list
- Quick action icons: coin for Assets, Sites, Check-in replaces CSV export buttons
- No visit data: suggestion card with 'Add a Check-in' button
- No manufacturer data: hide section entirely when empty
- 30-second TTL cache on both backend (/api/stats) and frontend (loadDashboard)
- Add dashboard cache module with time-based invalidation
F1: Scan existing asset → auto check-in (handleBarcode now autoCheckins)
F2: Auto-generate address from GPS reverse geocode (Nominatim, all 3 modes)
F3: Larger camera viewport (3:4 aspect ratio, full-width)
F4: Building # already syncs to Address/Trailer field (existing)
F5: Parking location GPS button already exists (fillGpsParking)
F6: Navigation tool with route line on map (existing navigateToAsset)
Backend: Added GET /api/geocode endpoint for reverse geocoding
Previously assets never had coordinates populated - only checkins did.
Now when a checkin is created with GPS data, the asset's latitude/longitude
fields are auto-updated. Also backfilled existing assets from their latest
checkin history. This fixes empty maps — loadAssetPins() filters by
a.latitude != null.
- OCR: Extract only last 5 digits from Connect ID (XXXXX-XXXXXX → last 5)
- Barcode: Switch to decodeFromVideoDevice with 1D-only format hints
(Code 128/39/EAN/UPC/ITF/Codabar), TRY_HARDER, 50ms scan interval
- Auto check-in: New assets auto-checkin with GPS on creation via
all three entry modes (barcode, OCR, manual)
- Tests: Updated e2e tests for login wait and API flow