Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff270697d5 | |||
| 25aec1490a | |||
| 4ab08ca7bd | |||
| 92e5170b47 | |||
| fb98de229d | |||
| 7a2d283379 | |||
| ed5e3ff9ec | |||
| 7024b5f820 | |||
| a3328c765d | |||
| 2cafe6879a | |||
| 6a37ea9f77 | |||
| 6bf8a3e970 | |||
| 510bafc919 | |||
| 605353c533 |
+3
-5
@@ -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
|
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
+11
-5
@@ -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,
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
Reference in New Issue
Block a user