Update "unnamed"

2026-05-29 11:56:30 +00:00
parent b90ce3f148
commit 04b8f7d56a
2 changed files with 0 additions and 358 deletions
-358
@@ -1,358 +0,0 @@
# Canteen Asset Tracker
Mobile-first web app for tracking physical assets via barcode scanning, OCR sticker reading, and GPS check-ins.
## Quick Links
- **URL:** https://canteen.ourpad.casa
- **Source:** ~/projects/canteen-asset-tracker/
- **Start:** `./start.sh`
- **Repo:** https://gitea.ourpad.casa/shawn/canteen-asset-tracker
## Latest Changes
### 2026-05-29: Detail view missing location fields
- **Problem:** List cards showed building/floor/place info but the detail view's main Asset Details card omitted these fields. Users had to scroll to the collapsed Directions card to see them.
- **Fix:** Added `place`, `building_name`, `building_number`, `floor`, `room`, `trailer_number` to the main detail fields in `viewAsset()`. Only populated fields render (empty values are skipped).
- **Commit:** 5180d81 | **Issue:** #49
### May 28: Major Overhaul
- **DB rebuilt** with **9,636 MSFS-merged assets** via `import_msfs.py`
- **Find Asset tab** (renamed from Add/Find): lookup-only mode — barcode scanning and OCR only, no new asset creation. OCR EXIF GPS auto-saved to asset when lat/lng blank.
- **Assets tab**: removed import functionality
- **Map tab**: removed geofence drawing tools
- **OSRM navigation**: added to Map tab with driving/walking mode toggle, turn-by-turn directions, distance/ETA
- **OCR 500 errors**: fixed with proper try/except error handling and user-facing status messages
- **Extraction DB symlink**: route planner symlinked to extraction DB for unified path
## Port Consistency (2026-05-23)
- **Main** :8901 — `canteen-asset-tracker.service` (user systemd, boot-enabled, runs `start.sh` → uvicorn server:app)
- **Admin** :8090 — `canteen-admin.service` (system systemd, boot-enabled, runs uvicorn admin_server:app)
- Duplicate `canteen-main.service` (fought for port 8901) removed. Stale admin process on 8090 (from Hermes gateway) killed.
## Default Login
- Username: `admin`
- Password: `Brett85!@`
### v4 DB Schema — disney_park + service_entrances (May 2026)
- **Commit:** 27f372e
- **Assets table:** Added `disney_park TEXT DEFAULT NULL` column for Disney park classification (Magic Kingdom, Epcot, Hollywood Studios, Animal Kingdom, Disney Springs, Resort, Office, Other)
- **New table:** `service_entrances` — id, location_id (FK→locations, ON DELETE CASCADE), name (NOT NULL), latitude (NOT NULL), longitude (NOT NULL), notes (DEFAULT ''), created_at, updated_at
- **Migration:** `init_db()` auto-adds `disney_park` column to existing DBs. Detects and recreates `service_entrances` if schema was cached without NOT NULL constraints
### T8 Location Data Extraction from Asset Names — May 2026
- **Script:** `scripts/extract_asset_location_data.py`
- **What it does:** Connects to assets.db and parses `name` fields to extract floor, building_number, room, trailer_number, and building_name using regex patterns. Also detects and deduplicates repeated name segments (e.g. "X-X" → "X").
- **Usage:** `python3 scripts/extract_asset_location_data.py` (dry-run) or `--apply` to write changes.
- **Coverage:** 1848 assets + 364 locations processed. Floor: 484, Building#: 258, Room: 16, Trailer: 15, Building name: 1005.
- **Patterns handled:** Floors (1ST FL, 2nd Floor, FL02NEWBLD), Buildings (BLDG 215, Building 1180, #6328), Rooms (Room 1290, SUITE 100, Ste 400), Trailers (Trailer 1051, Trailer 1135). Edge cases: ignores 'FL' in Florida/FL4950/FL8108, doesn't overwrite existing data.
- Commit: 13bf61d
### T4 Connect Label Upload — May 2026
- **"🏷️ Connect" mode toggle** on Add Asset tab (alongside Barcode/OCR/Manual)
- **Camera + Gallery buttons**: `connectFromCamera()` opens device camera, `connectFromGallery()` triggers file picker in connect mode
- **`processConnectLabelPhoto(file)`**: Runs OCR (server-first, Tesseract.js client-side fallback) and GPS extraction (exifr.js) in parallel
- **Results card**: Photo thumbnail, editable Machine ID, editable GPS coords (lat/lng), Leaflet mini-map preview, category select
- **`POST /api/connect-label`**: Multipart endpoint accepting photo upload + form fields (machine_id, name, latitude, longitude, category). Auto reverse-geocodes address from GPS. Saves photo to uploads/connect_labels/
- **Connect Label section** in Assets list: Shows last 20 scanned labels from localStorage with machine_id, GPS coords, and timestamp. Clear button to reset history
- **Edge cases**: OCR failure → manual machine_id entry allowed; missing EXIF GPS → prompt to enter coordinates or skip
- JS functions: `connectFromCamera()`, `connectFromGallery()`, `captureConnectPhoto()`, `stopConnectCamera()`, `processConnectLabelPhoto()`, `runConnectOcr()`, `renderConnectResults()`, `createConnectAsset()`, `resetConnectMode()`, `saveConnectLabel()`, `renderConnectLabels()`, `clearConnectLabels()`
- Server: `ConnectLabelRequest` Pydantic model, `connect_label()` async endpoint
- Commit: 6229966
### T2 EXIF Preservation Fix (createAssetFromPicked) — May 2026
- **Bug**: `createAssetFromPicked()` used `readAsDataURL -> atob -> Blob` conversion which stripped all EXIF metadata. The manual form received an empty photo — user had to re-select.
- **Fix** (commit eaa2dab): Pass the original `pickedPhotoFile` directly — sets `manualPhotoFile` and `manualPhotoBlob` from the original File object (preserves EXIF). Uses FileReader only for preview display.
- **GPS extraction**: Calls `extractGpsFromPhoto(pickedPhotoFile)` to extract EXIF GPS coordinates and auto-fills `parking_location` + map link in manual form.
- **End-to-end**: `submitManualAsset` already reads `manualPhotoFile` with `exifr.parse()` before upload — the original File now flows through naturally, so EXIF round-trips correctly.
- **Gitea issue**: #33 — created and closed
### T5 DNG/RAW-02 Photo Upload Support — May 2026
- **Added `.dng`** to `PHOTO_ALLOWED_EXTS` — Pixel 9a RAW-02 DNG photos now accepted
- **Bumped `PHOTO_MAX_SIZE`** from 10MB to 20MB — RAW-02 DNG files are ~13MB
- **All upload paths covered:** `/api/upload/photo`, `/api/ocr`, `/api/connect-label`
- **No EXIF handling changes needed:** `write_bytes()` preserves DNG EXIF naturally; `_re_embed_exif()` correctly skips non-JPEG; `_extract_gps_from_bytes()` opens DNG as TIFF via PIL (already works)
- **Verified:** Synthetic DNG upload via `/api/upload/photo` succeeded, file saved byte-identical. OCR endpoint also accepts `.dng`.
- Commit: 497d110
### T5 Offline Queue + Background Sync — May 2026
- **Service Worker** (`static/sw.js`): Caches app shell (index.html) + CDN deps (Leaflet, ZXing) on install. Network-first with cache fallback for API GET calls — offline reads still work from cache. POST/PUT/DELETE pass through to the main thread. Cache version: `canteen-v2`.
- **IndexedDB offline queue** (`CanteenOfflineDB`): `queueOfflineCreate(assetPayload, photoBlob)` stores asset creation payloads with GPS data and photo blobs. Queue items tracked with status: pending/syncing/done/failed. Retry limit of 5.
- **Sync processing**: `processOfflineQueue()` replays pending/failed items when back online — uploads photo, creates asset via API, creates check-in. Auto-refreshes asset list on success.
- **Online/offline detection**: `window.addEventListener("online"/"offline")` with amber banner showing "📡 Offline mode — changes will sync when connected". Banner auto-hides when connectivity restored and triggers sync.
- **Queue status indicator**: Header badge showing pending item count. Clickable to manually trigger sync. Syncing animation (accent color).
- **Conflict handling**: Duplicate/uniqueness errors from the server mark queue items as 'done' (not 'failed') to prevent retry loops.
- **API wrapper**: `window.api` function intercepts POST to `/api/assets` when offline, queues the payload instead of failing.
- **Fix**: Missing `#offlineBanner` HTML element was added (CSS + JS referenced it but the DOM element didn't exist). `queueCountBadge` span inside the banner shows pending count.
- Commit: 37ba54b
### T3 EXIF GPS Extraction from Photo — May 2026
- **exifr.js CDN**: Loaded `exifr@7/dist/lite.umd.js` in `<head>` for JPEG EXIF GPS reading
- **`extractGpsFromPhoto(file)`**: Async function using `exifr.gps()`, returns `{lat, lng, accuracy}` or null, catches errors gracefully
- **`tryExtractGpsFromPicked()`**: Orchestrates extraction for the picked photo, renders GPS section in EXIF metadata display
- **`renderGpsPreview(lat, lng, containerEl)`**: Leaflet mini-map (160px) with marker and OSM tiles, non-interactive static preview
- **`fillManualFromGps()`**: Auto-fills `parking_location` + OSM map link in manual form, switches to manual mode
- **`openInMaps()`**: Opens Google Maps at extracted GPS coordinates
- **GPS badge**: Green "📍 GPS from photo" on success; red "📍 No GPS in photo" with explanation on failure (phones often strip EXIF when sharing)
- **Manual edit**: "📝 Fill Form" loads coords into editable form fields; user can modify before saving
- **Button handler**: `extractGpsFromPicked()` for manual re-extraction from photo preview toolbar
- Commit: f895bf9 (included with T2)
### T2 Photo Gallery Picker — May 2026
- **Hidden file input**: `<input type="file" id="photoPicker" accept="image/*" capture="environment">` in Add Asset tab
- **"📱 Pick from Gallery" button** in mode toggles bar alongside Barcode/OCR/Manual
- **Photo preview card**: Dark-themed card (matching app theme) with rounded corners and shadow, displays picked image
- **Toolbar buttons**: [🔍 OCR this photo] [📍 Extract GPS] [ Create Asset]
- **EXIF data**: File name, size, type, last modified date in styled rows
- **Clear button**: Resets picker and hides preview
- **Manual mode integration**: " Create Asset" loads photo into Manual form with pre-filled image
- JS functions: `handlePickedPhoto(file)`, `clearPickedPhoto()`, `extractExifData()`, `formatFileSize()`, `ocrPickedPhoto()`, `extractGpsFromPicked()`, `createAssetFromPicked()`
- Commit: f895bf9
### T1 Tesseract.js Client-Side OCR Fallback — May 2026
- **CDN**: `tesseract.js@5` added to `<head>`
- **Server-first, client-fallback**: `captureOcr()` tries `/api/ocr` with 8s AbortController timeout; on failure/timeout, falls back to `ocrImageClient()` using Tesseract.js (WASM-based, in-browser)
- **`ocrImageClient(imageBlob)`**: Calls `Tesseract.recognize()` with progress logger, 30s timeout via `Promise.race`, extracts machine_id using same `XXXXX-XXXXXX` regex pattern as server-side
- **`updateOcrProgress(m)`**: Progress bar showing % complete; auto-hides on done/error
- **`parseMachineId(text)`**: JS port of server regex — 5digit-6digit pattern → last 5 digits; fallback to any 5+ digit number
- **Offline badge**: Amber "📡 Offline OCR" shown in status bar during fallback and on result card
- **Progress bar UI**: Small animated bar near OCR status area
- Commit: 5cb8e8a
### T11b Page Transitions + Micro-animations — May 2026
- **Direction-aware tab transitions** — Forward navigation slides in from right, back navigation (to `_prevTab`) slides in from left using reverse animation classes
- **FAB duplicate cleanup** — Removed duplicate FAB HTML element and conflicting CSS block; unified under single `.fab` definition with `.fab.pulse` class
- **FAB visibility** — Converted all `style.display` manipulation to `hidden-fab` CSS class with opacity/transform transitions for smooth show/hide
- **FAB pulse persistence** — FAB maintains pulse animation across tab switches; click restarts the animation via class removal/re-add with forced reflow
- **FAB init timing** — Moved initialization script after FAB HTML element so `hidden-fab` applies correctly on initial page load
- **Button micro-interactions** — `.btn:hover` scale(1.03), `.btn:active` scale(0.96), `.btn-pressed` haptic class, ripple effect, all with cubic-bezier easing
- **Success toast slide-up** — Already present via translateY transition
- **prefers-reduced-motion** — Already respected (animation-duration: 0.01ms override)
- Commit: b398bc1
### T6 Map Page Improvements — May 2026
- All 8 requirements verified: asset markers from location data (3-level fallback: own coords → location coords → geocoded address), marker clustering (Leaflet.markercluster), real GPS heatmap from `/api/checkins/heatmap`, geofence via custom modal (not prompt()), popup detail links, dismissible GPS warning with re-enable, map legend with category colors
- **Geolocation**: Friendly error messages (Location blocked / GPS unavailable / GPS timed out) with ✕ dismiss + tap-to-re-enable
- **Server API**: `list_assets` returns `location_latitude`, `location_longitude`, `location_address`, `location_building_name` via LEFT JOIN; `/api/checkins/heatmap` returns recency-weighted GPS points
- **Cleanup**: Removed scope-creep FAB, pull-to-refresh, skeleton loading (commit 111a412)
- Commits: 88694e6, 111a412
### Disney Park Map Outlines — May 2026
- Colored polygon outlines on the map for all 4 theme parks + Disney Springs + Resort areas
- Color-coded to match asset badge colors (Magic Kingdom=purple, Epcot=cyan, Hollywood Studios=amber, Animal Kingdom=green, Disney Springs=pink, Resort=blue)
- Labels with park icons at polygon centers
- Toggle chip (🏰 Parks) in map controls to show/hide outlines
- Resort areas use dashed outline style
- Outlines visible by default on map load
- Commit: 5380bf7
### T10 Navigation Consistency Overhaul — May 2026
- **Drawer nav reordered** — Add Asset, Assets, Map, Navigate, Dashboard, then expandable items (Customers, Reports, Activity, Settings)
- **Drawer active state** — Left accent border + stronger background highlight on current page
- **Bottom nav labels** — Only active tab shows text label (Material Design 3 icon-only style)
- **More popover** — 'More' tab opens popover with Customers, Reports, Activity, Settings (replaces drawer open)
- **Slide transitions** — Smooth slide animations when switching between tab pages
### T1 Empty States & Onboarding — May 2026
- **Consistent design pattern** across all pages: `renderEmptyState(icon, heading, desc, btnLabel, btnAction)`
- **Customers:** "No Customers Yet" with 🏢 icon, description, "Add Customer" CTA
- **Locations (customer detail):** "No Locations Yet" with 📍 icon, "Add Location" CTA
- **Dashboard:** Per-section empty states with descriptive hints of what would appear (categories, status bars, visit data, activity)
- **Reports:** All 4 sections (Service Summary, Visit Frequency, Time-on-Site per Tech, Time-on-Site per Asset) use empty state cards with guidance
- **Activity Feed:** "No Activity Yet" with CTA when no events exist
- **Onboarding:** Welcome overlay on first login with localStorage `canteen_onboarded` flag
- **Accessibility:** Modal overlay toggles `aria-hidden` on open/close for screen readers
- Commits: 6c1d5c3 (main), 1a87427 (geofence fix), 845523c (aria-hidden)
### T9 Modal/Dialog System — May 2026
- **Custom modal system** replaced all native browser `prompt()`, `alert()`, `confirm()` calls
- Supports 4 modal types: **confirm** (yes/no), **alert** (OK only), **prompt** (text input), **form** (multi-field)
- **CSS animations:** fade overlay + scale-in modal card
- **Keyboard accessible:** ESC closes, Tab/Shift+Tab focus trap, Enter submits prompts
- **Click-outside** overlay dismisses modal
- **Accessibility:** `role="dialog"`, `aria-modal="true"`, `aria-labelledby`
- **Backward-compatible** with all existing `showModal()` calls
- Replaced 3 native `prompt()` calls: geofence naming (2x), database reset confirmation (1x)
- 7 Playwright E2E tests passing
### T7 Reports Page UX — May 2026
- **Date range presets** — Today, This Week, This Month, Last Month, All Time quick-select buttons with active highlight
- **Empty state guidance cards** — Each report section shows helpful guidance with icons when no data exists (instead of bare "No visits in range")
- **CSV download** — Button hidden when no data, appears when reports have results
- **Loading spinner** — Shows while report generates, button disabled during fetch
- **Per Technician / Per Asset** — Time-on-site sections show guidance cards when empty
- **View on Map button** — Location-based entries in Service Summary table get a 📍 Map button that flies to the asset on the map
- **Report caching** — Data cached client-side; switching between reports tabs does not re-fetch from API
- **Backend fix** — Added `a.latitude, a.longitude` to `/api/visits` SQL query (was missing, so map button never rendered)
- Commits: 6c1d5c3 (HTML/JS/CSS), 51f9bcf (backend lat/lng fix), b31f8e8 (Navigate GPS improvements committed alongside)
- Verified all 7 requirements complete on 2026-05-21 pass
- **Date range presets** — Today, This Week, This Month, Last Month, All Time quick-select buttons with active highlight
- **Empty state guidance cards** — Each report section shows helpful guidance with icons when no data exists (instead of bare "No visits in range")
- **CSV download** — Button hidden when no data, appears when reports have results
- **Loading spinner** — Shows while report generates, button disabled during fetch
- **Per Technician / Per Asset** — Time-on-site sections show guidance cards when empty
- **View on Map button** — Location-based entries in Service Summary table get a 📍 Map button that flies to the asset on the map
- **Report caching** — Data cached client-side; switching between reports tabs does not re-fetch from API
- **Backend fix** — Added `a.latitude, a.longitude` to `/api/visits` SQL query (was missing, so map button never rendered)
- Commits: 6c1d5c3 (HTML/JS/CSS), 51f9bcf (backend lat/lng fix)
### T5 Dashboard Fixes — May 2026
- **Active Technicians** now queries `users WHERE role='technician'` (was counting from visit data — showed 0 when no visits yet)
- **Interactive bar charts** — hover shows label + count tooltip; click drills down to filtered asset list (Category, Status, Make)
- **Quick Actions** reorganized: 🪙 Assets, 🏢 Sites, 📍 Check-in (replaced CSV export buttons)
- **No visit data** — suggestion card with " Add a Check-in" button instead of empty state
- **No manufacturer data** — section hidden entirely when empty
- **30-second cache TTL** on both backend (`/api/stats`) and frontend (dashboard) to avoid reloading on every tab switch
### UX Sweep — May 2026
- [[UX-Sweep]] — Full UX improvement sweep across all pages: forms, search, map, dashboard, modals, navigation, and polish. Managed via `canteen-ux-sweep` kanban board.
### Reverse Geocode — Backend Auto-fill Address from GPS
`reverse_geocode()` helper calls OpenStreetMap Nominatim. Wired into all 3 server-side asset creation/update paths:
- **POST /api/assets** — if lat/lng present and address empty, auto-populates `address`, `building_name`, `building_number`
- **PUT /api/assets/{id}** — when lat/lng updated, also reverse geocodes address fields
- **POST /api/checkins** — when auto-updating asset location from check-in, also reverse geocodes address
- **GET /api/geocode?lat=&lng=** — public API endpoint using same helper
- Graceful fallback: returns None on Nominatim failure, address fields left as-is
### GPS Button for Parking Location
Added 📍 GPS button next to `parking_location` field in both Add Asset and Edit forms. Clicking fills current GPS coordinates into the text field. Shows toast if GPS unavailable.
### Navigation Tool
Tap "🧭 Navigate" on any asset detail view to draw a route line on the map from your current GPS position to the asset. Shows distance, cardinal direction, bearing, ETA, OSRM road routing, turn-by-turn directions, driving/walking mode toggle, and a Google Maps link. Also accessible from the dedicated Navigate tab (🧭 bottom nav).
**T4b Route-Finding UI** (commit 0aa9e1f):
- GPS-aware empty state: shows "Find your way" (GPS on) or "Location needed" (GPS off) with enable link
- Amber GPS warning banner when no GPS and no route active
- User's current location pin + accuracy circle on map when GPS available but no route
- Enhanced search results: GPS on/off badges (🧭/📍/⚠), non-navigable assets greyed out
- Back button navigates to previous tab (or Map fallback)
- `navigateToAsset()` still goes to Navigate tab even without GPS (shows warning UI)
### Navigate Tab Switching Fix (May 2026)
Fixed a regression where the Navigate tab panel remained permanently visible after the first visit, overlapping any subsequent tab content. Root cause: `switchTab()` had an early `return` after `closeNavigate()` that skipped the actual tab panel DOM swap (slide animation, active class management). `closeNavigate()` only resets navigation state — it does not switch panels. Removed the early return so the normal tab switching logic runs after cleanup. Bottom tabs, More popover, and drawer nav all verified working in both directions. Commit 389f1f1.
**T4a verification pass (May 2026):** End-to-end verified — all 9 tabs cycle correctly, both bottom nav (🧭 Nav) and drawer (🧭 Navigate) buttons route properly, no regression in panel DOM swap or slide animations. Fix already applied — no new code changes needed.
### Larger Camera Viewport
### Map Pins Fix
Assets now show on the map. Check-ins auto-update the asset's `latitude`/`longitude` fields so `loadAssetPins()` can find them. Existing assets backfilled from their check-in history.
### OCR Fix
Extracts only the last 5 digits from Connect ID stickers.
- Before: `POST /api/ocr``05912-095330`
- After: `POST /api/ocr``95330`
- Added `raw_match` field to response for debugging
### Barcode Scanner Fix
- Switched from `decodeFromVideoElement` to `decodeFromVideoDevice`
- 1D-only format hints (Code 128, Code 39, EAN/UPC, ITF, Codabar)
- Added `TRY_HARDER` hint for better low-light/angle detection
- 50ms scan interval (was ~500ms default)
- **Fixed:** `listVideoInputDevices()` — called on reader instance (not static), with try/catch fallback to default camera
### Auto Check-in on Barcode Scan
When scanning an existing (already-known) asset barcode, the system now auto-checks in with GPS coordinates instead of showing the manual check-in card. Shows "✓ Auto checked in" indicator after scan. Best-effort — silently skips if GPS not available.
### Auto Check-in on Creation
New assets automatically check in with GPS coordinates on creation via all three entry modes (barcode, OCR, manual). Best-effort — silently skips if GPS is not available yet.
### Building # → Trailer Number Sync
When building_number is entered in the manual asset form or location form, it auto-copies to the trailer_number field via `oninput` handler (commit 02b3ba6).
## API
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/auth/login` | POST | Login, returns Bearer token |
| `/api/assets` | CRUD | Asset management |
| `/api/assets/search?machine_id=` | GET | Lookup by machine ID |
| `/api/checkins` | POST/GET | GPS check-ins |
| `/api/ocr` | POST | Extract machine_id from sticker image; auto-saves EXIF GPS to asset lat/lng if blank |
| `/api/connect-label` | POST | Unified photo + OCR + GPS asset creation |
- **MSFS enrichment:** Asset detail view (`GET /api/assets/{id}`) now includes `msfs` block with Dynamics 365 Field Service data — Connect ID, manufacturer, install date, work order count, etc.
| `/api/geocode?lat=&lng=` | GET | Reverse geocode GPS → address |
| `/api/geocode-address?address=` | GET | Forward geocode address → GPS |
| `/api/checkins/heatmap?days=&limit=` | GET | Real check-in GPS coords for heatmap |
| `/api/export/assets` | GET | CSV export |
## Environment
| Variable | Default |
|----------|---------|
| `CANTEEN_PORT` | `8901` |
| `CANTEEN_DB_PATH` | `./assets.db` |
| `CANTEEN_SKIP_AUTH` | (empty) |
## DB Schema
### Assets table (v5)
Two new columns added May 2026:
- `dex_report_date TEXT DEFAULT NULL` — Cantaloupe 'Last Dex Report Time' as ISO datetime
- `install_date TEXT DEFAULT NULL` — deployment date (from Excel 'Deployed' + 'Added Date', or set on replacement approval)
Migration pattern: PRAGMA table_info + ALTER TABLE ADD COLUMN (idempotent, runs in init_db()).
Applied in both server.py and admin_server.py.
## Links
- [README](https://gitea.ourpad.casa/shawn/canteen-asset-tracker/src/branch/main/README.md)
- [PROJECT.md](https://gitea.ourpad.casa/shawn/canteen-asset-tracker/src/branch/main/PROJECT.md)
## Incident: 502 — NPMPlus HTTPS → HTTP backend mismatch (2026-05-21)
**Symptom:** `canteen.ourpad.casa` returned Cloudflare 502 Bad Gateway.
**Root cause:** NPMPlus proxy host #13 was configured with `forward_scheme: https` to backend `192.168.0.127:8901`, but the canteen backend (FastAPI/uvicorn) only serves plain HTTP — no TLS. The TLS handshake from NPMPlus to the backend failed silently, producing a 502.
**Fix:**
1. Changed `forward_scheme` from `https``http` in `/opt/npmplus/nginx/proxy_host/13.conf`
2. Updated `proxy_pass https://...``proxy_pass http://...` in same file
3. Reloaded nginx: `docker exec npmplus nginx -s reload`
4. Updated NPMPlus SQLite DB: `UPDATE proxy_host SET forward_scheme='http' WHERE id=13` (prevents revert on next UI edit)
**Verification:** `curl -H "Host: canteen.ourpad.casa" https://192.168.0.115/ -k` → 200 ✓
## T11c - Pull-to-Refresh + Swipe Gestures
**Date:** 2026-05-21
Mobile touch gesture support for the asset list:
- **Pull-to-refresh:** touchstart/touchmove/touchend on `#assetsListView` with visual pull indicator (spinner, drag progress, "Release to refresh" label). Only activates when scrolled to top.
- **Swipe-to-delete:** Left-swipe on `.asset-item-swipe-wrap` items reveals delete background. Swipe past 60px threshold to activate, snap-back otherwise.
- **Undo toast:** After delete, `#undoToast` appears at bottom with 5-second auto-dismiss. Undo button cancels the deletion.
- **Desktop fallback:** `body.has-mouse` class shows a 🔄 Refresh button in the toolbar. Mouse detection via mousemove event.
- **Gesture isolation:** PTR only triggers at scroll top; swipe requires horizontal movement (rejects vertical scroll).
- **Idempotent init:** `_ptrInited` and `_swipeInited` guards prevent duplicate event listeners on tab switches.
**Implementation:** Single consolidated implementation replacing 2 prior conflicting versions. Removed duplicate `let ptrState` declaration that caused SyntaxError.
## Cantaloupe Sync — Automated Export + Import Pipeline
**Date:** 2026-05-21
The [[canteen-admin-server]] (port 8090, proxy: `admin.canteen.ourpad.casa`) includes a Cantaloupe sync API for staged import of machine data from Cantaloupe Excel exports.
### Cron Job (`5ababff9c6e9`)
- **Script:** `~/.hermes/scripts/cantaloupe-sync.sh`
- **Schedule:** Every 6 hours
- **Pipeline:**
1. Runs `python -m cantaloupe export --scheduled --output ~/cantaloupe-exports/` from `~/projects/cantaloupe-downloader/`
2. Gets admin Bearer token from `/api/auth/login`
3. Triggers `/api/admin/cantaloupe/sync` to parse, diff, and stage the data
- **Credentials:** `~/.cantaloupe.env` — currently has dummy values; needs real [mycantaloupe.com](https://mycantaloupe.com) account credentials
- **Status:** Active; results saved locally (check cron status for output)
### Sync API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/admin/cantaloupe/sync` | Upload Excel or trigger export; parse, diff, stage |
| GET | `/api/admin/cantaloupe/batches` | List all batches (filterable by status) |
| GET | `/api/admin/cantaloupe/batches/{id}` | Full batch detail with row-level diff |
| POST | `/api/admin/cantaloupe/batches/{id}/approve` | Apply staged changes to assets DB |
| POST | `/api/admin/cantaloupe/batches/{id}/reject` | Discard batch |
### Fix: Export Subprocess CWD (e9d18cb, issue #6)
The `subprocess.run` for cantaloupe export had `cwd` pointing to the admin server directory. The cantaloupe module is a local module at `~/projects/cantaloupe-downloader/` and cannot be imported from the admin server directory. Fixed by changing `cwd` to `os.path.expanduser("~/projects/cantaloupe-downloader")`.
### Known Issue: Dummy Credentials (issue #7)
`~/.cantaloupe.env` contains dummy values (`user@example.com`). Real [mycantaloupe.com](https://mycantaloupe.com) account credentials needed before the export will work.