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__/
|
||||
*.pyc
|
||||
assets.db
|
||||
uploads/
|
||||
.venv/
|
||||
*.db
|
||||
*.db.real
|
||||
*.xlsx
|
||||
admin.db
|
||||
canteen_admin.db
|
||||
.env
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
live_data = _fetch_live_data(conn)
|
||||
diff = compute_diff(mapped_rows, live_data)
|
||||
@@ -863,8 +869,8 @@ def approve_batch(
|
||||
if val is not None and str(val).strip() != "":
|
||||
updates[db_col] = str(val).strip()
|
||||
|
||||
# Handle numeric fields
|
||||
for num_field in ("latitude", "longitude", "geofence_radius_meters"):
|
||||
# Handle numeric fields (exclude GPS — comes only from real check-ins)
|
||||
for num_field in ("geofence_radius_meters",):
|
||||
val = item.get(num_field)
|
||||
if val is not None and str(val).strip() != "":
|
||||
try:
|
||||
@@ -942,8 +948,8 @@ def approve_batch(
|
||||
cust_id,
|
||||
loc_id,
|
||||
1 if cust_name and cust_name.startswith("D-") else 0,
|
||||
_safe_float(item.get("latitude")),
|
||||
_safe_float(item.get("longitude")),
|
||||
None, # GPS only from real check-ins
|
||||
None, # GPS only from real check-ins
|
||||
_safe_int(item.get("geofence_radius_meters"), 50),
|
||||
str(item.get("dex_report_date", "")).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", []):
|
||||
old_val = ch.get("old_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({
|
||||
"machine_id": mid,
|
||||
"name": name,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the admin server — uses same DB as main canteen-asset-tracker
|
||||
# CANTEEN_DB_PATH defaults to ../canteen-asset-tracker/assets.db
|
||||
# Run the admin server — MUST use same DB as main canteen-asset-tracker
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${PORT:-8090}"
|
||||
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 \
|
||||
--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