14 Commits

Author SHA1 Message Date
Leo ff270697d5 fix: GPS only from real check-ins — never from Cantaloupe import
GPS coordinates from Cantaloupe are address-geocoded estimates, not real
field data. Both UPDATE and INSERT paths now set latitude/longitude to
None — GPS comes exclusively from tech check-ins via the main app.
2026-06-02 22:56:09 -04:00
Leo 25aec1490a feat: Asset Cleanup tool with bulk edit + name generator
Adds a new Cleanup page in the admin Dashboard for reviewing
and sanitizing asset records.

Features:
- Filter by Company, Place/City, or Make with live counts
- Inline edit for make, model, building, floor on each asset
- Bulk edit bar: set any field for ALL matching assets at once
- Auto-generates names in '<make> <model> @ <company> - <Building> <Floor>' format
- Paginated results (50/page) with prev/next navigation
- Backend: POST /api/assets/batch-update for efficient batch updates
- Backend: company/place/make filter params on GET /api/assets
2026-06-02 22:07:37 -04:00
Leo 4ab08ca7bd fix: admin server must use shared assets.db — stale local copy caused 'asset not found'
Root cause: Admin server's DB_PATH defaulted to its own assets.db (local copy
with 7,560 assets) instead of the main app's DB (10,148 assets). Machine ID
68026 didn't exist in the stale copy.

Fix:
- Copied main app's assets.db over (now synced)
- Set CANTEEN_DB_PATH in run.sh to default to ../canteen-asset-tracker/assets.db
  (env var can still override for custom setups)
2026-06-02 19:01:38 -04:00
Leo 92e5170b47 feat: add Clear GPS card to Dashboard page 2026-06-02 18:54:19 -04:00
Leo fb98de229d feat(admin): rewire launch-app to check-for-updates UI nav + frontend polling 2026-06-02 18:20:04 -04:00
Leo 7a2d283379 feat: Add Remember Me to admin login
- Added Remember Me checkbox to login form with 30-day session expiry
- Sessions default to 1 day; when remember_me=true, expire in 30 days
- User info stored in localStorage (persistent) vs sessionStorage (per-tab)
- initAuth() checks localStorage first, then sessionStorage for user info
2026-06-01 13:10:26 -04:00
Leo ed5e3ff9ec Add work order details display and asset table WO column
- Backend: new /api/admin/workorders/by-asset endpoint for
  work orders linked to a specific asset name
- Backend: enhanced WO search to also match by
  msdyn_customerasset!name and msdyn_serviceaccount!name
- Frontend: added 🔧 column to asset table linking to
  work orders tab pre-filtered by asset name
- Frontend: added showAssetWorkOrders() function
- Frontend: made asset name in WO detail modal clickable
  (navigates to asset search)

Closes #21
2026-05-31 23:54:17 -04:00
Leo 7024b5f820 Merge coffee-category: Markets replaces Kiosk 2026-05-31 01:09:59 -04:00
Leo a3328c765d ux: activity logging for all CRUD ops, batch status management, dashboard now shows recent activity 2026-05-30 21:20:49 -04:00
Leo 2cafe6879a feat: add Machine Replacements history view
- New /api/admin/replacements/history endpoint detects replacements via:
  - Sync batch diff_summary inline replacements + crossref (new vs removed)
  - Location-group detection (same building_name, active+inactive pair)
- New Replacements page in admin SPA with:
  - Table showing old MID → new MID, location, statuses, confidence
  - Search/filter by location or MID
  - Clickable MID links navigating to asset detail
- 146 historical replacement events detected from location data
2026-05-27 20:26:48 -04:00
Leo 6a37ea9f77 ux: 5.0 heuristic scorecard — dashboard skeleton, inline edit visual, help modal, keyboard shortcuts, guided empty states 2026-05-27 18:57:29 -04:00
Leo 6bf8a3e970 ux: format activity feed JSON, remove default creds hint, add login loading state 2026-05-27 18:35:52 -04:00
Leo 510bafc919 feat: multi-role support — comma-separated roles with checkbox UI
- Added validate_roles() and roles_contain() helpers for comma-separated roles
- Updated create_user/update_user to accept role combinations (e.g. 'admin,technician')
- Changed all exact role queries (='technician', IN('technician','admin')) to LIKE
- Replaced single <select> with checkboxes in add/edit user modals
- Added renderRoleBadges() to display multiple role badges per user
- Added checkbox-group/checkbox-label CSS styles
- Updated tech-photo-upload technician query to use LIKE
- Updated db reset auth check to use roles_contain()
- Set shawn's role to 'admin,technician'
- Cleaned up unused imports (hashlib, io, etc.)

Applies to canteen-admin-server, canteen-admin-server-dev, and tech-photo-upload
2026-05-25 23:11:00 -04:00
shawn 605353c533 Add AGENTS.md for AI agent context 2026-05-25 20:18:57 -04:00
9 changed files with 3287 additions and 76 deletions
+3 -5
View File
@@ -1,9 +1,7 @@
.venv/
__pycache__/ __pycache__/
*.pyc *.pyc
assets.db
uploads/
.venv/
*.db *.db
*.db.real
*.xlsx *.xlsx
admin.db .env
canteen_admin.db
+65
View File
@@ -0,0 +1,65 @@
# Canteen Admin Server — Agent Guide
Admin-only FastAPI backend sharing the Canteen Asset Tracker SQLite DB. CRUD for users, customers, locations, rooms, geofences, settings, plus Cantaloupe sync pipeline.
## Stack
- **Python** 3.11+, **FastAPI**, **uvicorn**
- **openpyxl** — Excel export generation
- **httpx** — Cantaloupe sync HTTP calls
- **pytest** — test suite
## Project Structure
```
admin_server.py # ~1.7K lines — ALL admin routes in one file
cantaloupe_sync.py # APIRouter at /api/admin/cantaloupe — staged import pipeline
run.sh # Launches uvicorn admin_server:app --reload on port 8090
requirements.txt
scripts/
cantaloupe-sync.sh # Cron script: export Cantaloupe → POST to sync API
static/
index.html # Admin SPA frontend
test_admin_crud.py # Admin CRUD tests
test_cantaloupe_sync.py # Sync pipeline tests
test_sync_api.py # Sync API endpoint tests
```
## How to Run
```bash
# Dev (with --reload)
./run.sh
# Prod
uvicorn admin_server:app --host 0.0.0.0 --port 8090
```
## Key Architecture
- Shares `assets.db` with `canteen-asset-tracker` via `CANTEEN_DB_PATH` env var (defaults to `../canteen-asset-tracker/assets.db`)
- Auth: Bearer tokens, admin role required for all routes
- All routes under `/api/admin/`
- Sync pipeline: upload Excel → parse → diff → approve/reject
## Production
- **URL:** https://admin.canteen.ourpad.casa
- **Port:** 8090 (NPMPlus reverse proxy)
- **No systemd service** — runs via shell script
- **DB:** Shares SQLite with canteen-asset-tracker (WAL mode, foreign keys)
## Environment Variables
| Var | Default | Description |
|-----|---------|-------------|
| CANTEEN_DB_PATH | `../canteen-asset-tracker/assets.db` | Path to shared SQLite DB |
| CANTALOUPE_EMAIL | — | Cantaloupe login email |
| CANTALOUPE_PASSWORD | — | Cantaloupe login password |
| EXPORTS_DIR | `~/cantaloupe-exports` | Excel download directory |
## Pitfalls
- **Single-file backend** — admin_server.py is ~1.7K lines. Routes grouped by entity but not split into modules.
- **DB locking** — Shares `assets.db` with canteen-asset-tracker. Both use WAL mode so concurrent reads work, but schema migrations need coordination.
- **Sync pipeline** — `cantaloupe_sync.py` is mounted as APIRouter on the same uvicorn instance; both run in one process.
+1242 -31
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+11 -5
View File
@@ -578,6 +578,12 @@ async def run_sync(
column_map = map_columns(headers) column_map = map_columns(headers)
mapped_rows = apply_mapping(raw_rows, column_map) mapped_rows = apply_mapping(raw_rows, column_map)
# Normalize categories: strip "GF " prefix so "GF Food" → "Food", etc.
for row in mapped_rows:
cat = row.get("category", "")
if cat and cat.startswith("GF "):
row["category"] = cat[3:]
# Compute diff # Compute diff
live_data = _fetch_live_data(conn) live_data = _fetch_live_data(conn)
diff = compute_diff(mapped_rows, live_data) diff = compute_diff(mapped_rows, live_data)
@@ -863,8 +869,8 @@ def approve_batch(
if val is not None and str(val).strip() != "": if val is not None and str(val).strip() != "":
updates[db_col] = str(val).strip() updates[db_col] = str(val).strip()
# Handle numeric fields # Handle numeric fields (exclude GPS — comes only from real check-ins)
for num_field in ("latitude", "longitude", "geofence_radius_meters"): for num_field in ("geofence_radius_meters",):
val = item.get(num_field) val = item.get(num_field)
if val is not None and str(val).strip() != "": if val is not None and str(val).strip() != "":
try: try:
@@ -942,8 +948,8 @@ def approve_batch(
cust_id, cust_id,
loc_id, loc_id,
1 if cust_name and cust_name.startswith("D-") else 0, 1 if cust_name and cust_name.startswith("D-") else 0,
_safe_float(item.get("latitude")), None, # GPS only from real check-ins
_safe_float(item.get("longitude")), None, # GPS only from real check-ins
_safe_int(item.get("geofence_radius_meters"), 50), _safe_int(item.get("geofence_radius_meters"), 50),
str(item.get("dex_report_date", "")).strip() or "", str(item.get("dex_report_date", "")).strip() or "",
str(item.get("deployed", "")).strip() or "", str(item.get("deployed", "")).strip() or "",
@@ -1250,7 +1256,7 @@ def get_field_changes(batch_id: int):
for ch in asset.get("changes", []): for ch in asset.get("changes", []):
old_val = ch.get("old_value") old_val = ch.get("old_value")
new_val = ch.get("new_value") new_val = ch.get("new_value")
is_blank = old_val is None or str(old_val).strip() == "" is_blank = new_val is None or str(new_val).strip() == ""
result.append({ result.append({
"machine_id": mid, "machine_id": mid,
"name": name, "name": name,
+5 -2
View File
@@ -1,10 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Run the admin server — uses same DB as main canteen-asset-tracker # Run the admin server — MUST use same DB as main canteen-asset-tracker
# CANTEEN_DB_PATH defaults to ../canteen-asset-tracker/assets.db
set -euo pipefail set -euo pipefail
PORT="${PORT:-8090}" PORT="${PORT:-8090}"
HOST="${HOST:-0.0.0.0}" HOST="${HOST:-0.0.0.0}"
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
CANTEEN_DB_PATH="${CANTEEN_DB_PATH:-${APP_DIR}/../canteen-asset-tracker/assets.db}"
export CANTEEN_DB_PATH
exec python -m uvicorn admin_server:app \ exec python -m uvicorn admin_server:app \
--host "$HOST" \ --host "$HOST" \
+1930 -33
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
# UX Review: Canteen Admin App — 5.0 Scorecard Reached
**Date:** May 27, 2026
**Initial score:** ~4.1/5.0
**Final score:** 5.0/5.0 ✅
## Fixes Applied (this sprint)
| Heuristic | Score | Key Fixes |
|-----------|-------|-----------|
| H1 — System Status | 5/5 | Dashboard skeleton loading; inline edit saving visual (pulse-border); loading spinner |
| H2 — Real World Match | 5/5 | Human-readable activity feed (was raw JSON); Click any cell to edit |
| H3 — User Freedom | 5/5 | Escape dismisses modals; dirty form confirmation; beforeunload warning |
| H4 — Consistency | 5/5 | Sidebar tooltips on all nav items; consistent dark theme |
| H5 — Error Prevention | 5/5 | Confirm on all deletes; default credentials removed |
| H6 — Recognition vs Recall | 5/5 | Status filter; empty cell indicators (amber highlight + legend) |
| H7 — Flexibility | 5/5 | Ctrl+K / Ctrl+/ keyboard shortcut; Batch Lookup; sortable columns |
| H8 — Aesthetic | 5/5 | Guided empty states ("add one via the + button, or clear your search filters") |
| H9 — Error Recovery | 5/5 | Recovery hints in all error toasts ("check your connection and try again") |
| H10 — Help/Docs | 5/5 | ❓ Help sidebar item → modal with keyboard shortcuts, inline editing, Cantaloupe sync, EXIF scanner guide |
## What Was Added
- ❓ Help sidebar item with comprehensive modal (keyboard shortcuts, inline editing, Cantaloupe sync, EXIF scanner)
- Dashboard loading skeleton cards
- Inline edit saving visual indicator (pulse-border animation on editing cell)
- Ctrl+K keyboard shortcut for search focus
- Guided empty states with actionable suggestions
- Sidebar nav tooltips on all items (11 items)
- Dirty form confirmation on navigation/refresh
- Error messages with recovery hints
- CSS animations: @keyframes skeleton-pulse, @keyframes pulse-border