Docs: add PROJECT_REFERENCE, TESTING_CHECKLIST, update session instructions for Phase 4+5 completion
This commit is contained in:
@@ -6,15 +6,17 @@ Paste it (or reference it) at the start of each session to give Claude Code full
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Project Summary
|
## Project Summary
|
||||||
We are building a local-first restaurant POS system. Full architecture and specs live in the `/pos-build-guide/` folder. Always read the relevant guide file before starting work on any component.
|
We are building a local-first restaurant POS system. Full architecture and specs live in the `PLANS AND STRATEGIES/` folder. Always read the relevant guide file before starting work on any component. For a complete picture of the entire system read `PROJECT_REFERENCE.md`.
|
||||||
|
|
||||||
## Guide Files
|
## Guide Files
|
||||||
- `00_PROJECT_OVERVIEW.md` — Architecture, stack, build order
|
- `00_PROJECT_OVERVIEW.md` — Architecture, stack, build order
|
||||||
- `01_LOCAL_BACKEND.md` — FastAPI backend (build this first)
|
- `01_LOCAL_BACKEND.md` — FastAPI backend spec
|
||||||
- `02_WAITER_PWA.md` — Waiter-facing PWA (build second)
|
- `02_WAITER_PWA.md` — Waiter-facing PWA spec
|
||||||
- `03_MANAGER_DASHBOARD.md` — Manager web app (build third)
|
- `03_MANAGER_DASHBOARD.md` — Manager web app spec
|
||||||
- `04_CLOUD_BACKEND.md` — Cloud licensing backend (build fourth)
|
- `04_CLOUD_BACKEND.md` — Cloud licensing backend spec
|
||||||
- `05_SYSADMIN_PANEL.md` — Sysadmin cloud panel (build last)
|
- `05_SYSADMIN_PANEL.md` — Sysadmin cloud panel spec
|
||||||
|
- `TESTING_CHECKLIST.md` — End-to-end testing guide (step-by-step)
|
||||||
|
- `PROJECT_REFERENCE.md` — Master reference: full system description, wiring, all env vars
|
||||||
|
|
||||||
## Git Workflow
|
## Git Workflow
|
||||||
- The project uses git. **Commit after every meaningful milestone** (e.g. after scaffolding a phase, after a feature is working, after a bug fix).
|
- The project uses git. **Commit after every meaningful milestone** (e.g. after scaffolding a phase, after a feature is working, after a bug fix).
|
||||||
@@ -32,13 +34,17 @@ We are building a local-first restaurant POS system. Full architecture and specs
|
|||||||
7. **The `unit_price` on `order_items` is a snapshot** — it must be copied from the product price at the time of ordering, not referenced dynamically.
|
7. **The `unit_price` on `order_items` is a snapshot** — it must be copied from the product price at the time of ordering, not referenced dynamically.
|
||||||
8. **Printer failures must never block order saves.** Log and continue.
|
8. **Printer failures must never block order saves.** Log and continue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Current Build Phase
|
## Current Build Phase
|
||||||
> Update this line as you progress:
|
> All 5 phases are built. Now in testing / polish.
|
||||||
> Phase 1: Local Backend — [x] Complete. Smoke tested: health, auth, products, tables, orders, printer routing all working.
|
> Phase 1: Local Backend — [x] Complete. Smoke tested.
|
||||||
> Phase 2: Waiter PWA — [x] Complete. Smoke tested end-to-end: login, table list, open order, add items, select/pay with confirmation, close order. See "Phase 2 Known Issues & Fixes" below.
|
> Phase 2: Waiter PWA — [x] Complete. Smoke tested end-to-end.
|
||||||
> Phase 3: Manager Dashboard — [x] Complete. Scaffolded and smoke tested. Known issues remain (see below) — not blockers for Phase 4.
|
> Phase 3: Manager Dashboard — [x] Complete. Scaffolded and smoke tested. Known rough edges remain.
|
||||||
> Phase 4: Cloud Backend — [ ] Not Started
|
> Phase 4: Cloud Backend — [x] Complete. Built, not yet smoke tested end-to-end with a real site.
|
||||||
> Phase 5: Sysadmin Panel — [ ] Not Started
|
> Phase 5: Sysadmin Panel — [x] Complete. Built, not yet smoke tested.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Phase 2 Known Issues & Fixes Applied
|
## Phase 2 Known Issues & Fixes Applied
|
||||||
- `OrderItemOut` schema now includes `product { id, name }` via `ProductNameOut` — required for item names to show correctly in the PWA.
|
- `OrderItemOut` schema now includes `product { id, name }` via `ProductNameOut` — required for item names to show correctly in the PWA.
|
||||||
@@ -68,38 +74,76 @@ We are building a local-first restaurant POS system. Full architecture and specs
|
|||||||
- Auto-migration on startup adds new columns to existing SQLite DB without dropping data.
|
- Auto-migration on startup adds new columns to existing SQLite DB without dropping data.
|
||||||
|
|
||||||
## Phase 3 Known Remaining Issues (to revisit)
|
## Phase 3 Known Remaining Issues (to revisit)
|
||||||
- Some PWA/manager interactions still have rough edges — to be addressed in a dedicated polish pass before or after Phase 5.
|
- Some PWA/manager interactions still have rough edges — to be addressed in a dedicated polish pass.
|
||||||
- Product image upload requires the product to already exist (no upload on creation, only on edit).
|
- Product image upload requires the product to already exist (no upload on creation, only on edit).
|
||||||
|
|
||||||
## Phase 2 Dev Data (seeded manually, not in seed.py)
|
## Phase 4 — What Was Built
|
||||||
- Tables 1–6 exist (table 1 was from Phase 1 smoke test)
|
- Cloud Backend: FastAPI, SQLite (dev) / PostgreSQL (prod), JWT auth. Port 8001.
|
||||||
- Category "food" (id=1) exists from Phase 1 smoke test — contains product "arakas"
|
- Endpoints: `POST /api/auth/login`, `GET|POST|PUT|DELETE /api/sites/:id`, lock/unlock actions, `POST /api/heartbeat/`.
|
||||||
|
- Admin account seeded from env vars on first startup (`ADMIN_USERNAME`, `ADMIN_PASSWORD`).
|
||||||
|
- Heartbeat: local backend posts every 6 hours with `X-Site-ID` + `X-Site-Key` headers; cloud returns license status.
|
||||||
|
- `cloud_sync.py` in local backend runs as background asyncio task, persists state to `license_state.json`.
|
||||||
|
- Grace period: if cloud unreachable, local backend continues operating for `LICENSE_GRACE_HOURS` (default 24h).
|
||||||
|
- Docker Compose service: `cloud_backend`, depends on nothing, data volume at `./data/cloud`.
|
||||||
|
|
||||||
|
## Phase 5 — What Was Built
|
||||||
|
- Sysadmin Panel: React+Vite, TailwindCSS, dark theme, cyan accent. Port 5175.
|
||||||
|
- Points to cloud backend (`VITE_CLOUD_URL`), NOT the local backend.
|
||||||
|
- Pages: LoginPage (username+password), SitesPage (card grid, 30s polling, summary counts), SiteDetailPage (lock/unlock/extend license/delete with confirmation modals), RegisterSitePage (form + one-time secret key display with copy button).
|
||||||
|
- Token stored as `sysadmin_token` in localStorage (separate key from manager dashboard).
|
||||||
|
- Docker Compose service: `sysadmin_panel`, depends on `cloud_backend`.
|
||||||
|
|
||||||
|
## Dev Data (seeded / created during development)
|
||||||
|
- Tables 1–6 exist in local DB
|
||||||
|
- Category "food" (id=1) + arakas product from Phase 1 smoke test
|
||||||
- Categories: Ποτά, Σαλάτες, Κυρίως — 3 products each
|
- Categories: Ποτά, Σαλάτες, Κυρίως — 3 products each
|
||||||
- Printer zones can now be assigned via ProductsPage in the Manager Dashboard.
|
- Printer zones assignable via ProductsPage in Manager Dashboard
|
||||||
|
- Cloud DB: admin `sysadmin` / `changeme` seeded on first startup
|
||||||
|
|
||||||
## Environment Variables
|
---
|
||||||
|
|
||||||
### Local Backend (.env)
|
## Environment Variables (current actual values)
|
||||||
|
|
||||||
|
### Local Backend (`local_backend/.env`)
|
||||||
```
|
```
|
||||||
SITE_ID=
|
SITE_ID=<from sysadmin panel site registration>
|
||||||
CLOUD_URL=https://your-vps.com
|
SITE_KEY=<secret key shown once at registration>
|
||||||
SECRET_KEY=generate-a-long-random-string
|
CLOUD_URL=http://cloud_backend:8001
|
||||||
|
SECRET_KEY=<long random string>
|
||||||
LICENSE_GRACE_HOURS=24
|
LICENSE_GRACE_HOURS=24
|
||||||
DATABASE_URL=sqlite:///./pos.db
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Waiter PWA (.env)
|
### Waiter PWA (`waiter_pwa/.env`)
|
||||||
```
|
```
|
||||||
VITE_API_URL=http://192.168.1.10:8000
|
VITE_API_URL=http://<local-machine-ip>:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manager Dashboard (.env)
|
### Manager Dashboard (`manager_dashboard/.env`)
|
||||||
```
|
```
|
||||||
VITE_API_URL=http://192.168.1.10:8000
|
VITE_API_URL=http://<local-machine-ip>:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cloud Backend (.env)
|
### Cloud Backend (`cloud_backend/.env`)
|
||||||
```
|
```
|
||||||
SECRET_KEY=different-long-random-string
|
SECRET_KEY=<long random string — different from local>
|
||||||
DATABASE_URL=postgresql://... (or sqlite for dev)
|
DATABASE_URL=sqlite:////app/data/cloud.db
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
|
ADMIN_USERNAME=sysadmin
|
||||||
|
ADMIN_PASSWORD=<your password>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sysadmin Panel (`sysadmin_panel/.env`)
|
||||||
|
```
|
||||||
|
VITE_CLOUD_URL=http://localhost:8001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ports
|
||||||
|
| Service | Port |
|
||||||
|
|---|---|
|
||||||
|
| Local Backend | 8000 |
|
||||||
|
| Cloud Backend | 8001 |
|
||||||
|
| Waiter PWA | 5173 |
|
||||||
|
| Manager Dashboard | 5174 |
|
||||||
|
| Sysadmin Panel | 5175 |
|
||||||
|
|||||||
441
PLANS AND STRATEGIES/PROJECT_REFERENCE.md
Normal file
441
PLANS AND STRATEGIES/PROJECT_REFERENCE.md
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
# POS System — Master Project Reference
|
||||||
|
|
||||||
|
> Read this file at the start of any new Claude Code session to get a complete picture of the system.
|
||||||
|
> For session-specific instructions and known issues, also read `CLAUDE_CODE_INSTRUCTIONS.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What We're Building
|
||||||
|
|
||||||
|
A **local-first restaurant Point of Sale system**. Waiters use their phones to take orders at tables. Orders are tracked in real time, routed to thermal printers in the kitchen and bar, and managed by a supervisor via a web dashboard. The system runs entirely on the restaurant's local network — internet outages have zero impact on operations.
|
||||||
|
|
||||||
|
A lightweight **cloud backend** handles licensing only: it verifies that each restaurant installation has a valid license and can remotely lock a site if needed. It does not touch orders, menus, or users.
|
||||||
|
|
||||||
|
**What it is NOT:**
|
||||||
|
- Not a payment processor (no card terminals, no fiscal integration)
|
||||||
|
- Not an IRS/tax reporting system
|
||||||
|
- Not a kitchen display system (KDS) — printing only
|
||||||
|
- Not SaaS — each restaurant runs its own local instance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ YOUR VPS (Cloud) │
|
||||||
|
│ │
|
||||||
|
│ cloud_backend (FastAPI, port 8001) │
|
||||||
|
│ sysadmin_panel (React/Vite, port 5175) │
|
||||||
|
│ │
|
||||||
|
│ Responsibilities: │
|
||||||
|
│ - Site registration (generates site_id + key) │
|
||||||
|
│ - License management (expiry dates) │
|
||||||
|
│ - Remote lock/unlock per site │
|
||||||
|
│ - Receives periodic heartbeats from local sites │
|
||||||
|
└─────────────────┬────────────────────────────────┘
|
||||||
|
│ HTTPS — heartbeat every 6h
|
||||||
|
│ 24h grace period if unreachable
|
||||||
|
┌─────────────────▼────────────────────────────────┐
|
||||||
|
│ RESTAURANT LOCAL NETWORK │
|
||||||
|
│ │
|
||||||
|
│ backend (FastAPI, port 8000) │
|
||||||
|
│ SQLite DB (pos.db) │
|
||||||
|
│ Runs on: Raspberry Pi 4 or any Linux box │
|
||||||
|
│ Static LAN IP (e.g. 192.168.1.10) │
|
||||||
|
│ │
|
||||||
|
│ Responsibilities: │
|
||||||
|
│ - All business logic (orders, tables, users) │
|
||||||
|
│ - Printer routing (ESC/POS over TCP) │
|
||||||
|
│ - Auth (JWT, username+PIN) │
|
||||||
|
│ - License enforcement (from cloud heartbeat) │
|
||||||
|
└───────────┬──────────────────┬───────────────────┘
|
||||||
|
│ LAN │ LAN
|
||||||
|
┌───────────▼──────┐ ┌────────▼──────────────────┐
|
||||||
|
│ waiter_pwa │ │ manager_dashboard │
|
||||||
|
│ React PWA │ │ React web app │
|
||||||
|
│ Port 5173 │ │ Port 5174 │
|
||||||
|
│ Waiters' phones │ │ Supervisor tablet/laptop │
|
||||||
|
└───────────┬──────┘ └───────────────────────────┘
|
||||||
|
│ LAN (TCP/9100)
|
||||||
|
┌───────────▼───────────────────────────────────────┐
|
||||||
|
│ Thermal Printers (Jolimark TP850UE) │
|
||||||
|
│ Zone A: Kitchen printer (e.g. 10.98.20.25:9100) │
|
||||||
|
│ Zone B: Bar printer (e.g. 10.98.20.26:9100) │
|
||||||
|
└───────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Roles
|
||||||
|
|
||||||
|
| Role | Auth | Frontend | Can Do |
|
||||||
|
|------|------|----------|--------|
|
||||||
|
| Waiter | Username + PIN | Waiter PWA (phone) | Open orders, add items, mark paid, close. Own tables only (or assistant tables). |
|
||||||
|
| Manager | Username + PIN | Manager Dashboard | Everything waiters can + manage menu, waiters, tables, view reports. |
|
||||||
|
| Sysadmin (you) | Username + Password | Sysadmin Panel (cloud) | Register sites, manage licenses, lock/unlock sites. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
| Component | Technology | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Local Backend | FastAPI (Python 3.13) | SQLAlchemy ORM, Pydantic v2, JWT auth |
|
||||||
|
| Local Database | SQLite | File: `pos.db`. Auto-migrated on startup. |
|
||||||
|
| Printer Protocol | python-escpos | ESC/POS over TCP. CP737 (n=29) for Greek. |
|
||||||
|
| Waiter PWA | React 18 + Vite | TailwindCSS, Zustand, Axios. Service worker for PWA. |
|
||||||
|
| Manager Dashboard | React 18 + Vite | TailwindCSS, React Query, Zustand, react-hot-toast. |
|
||||||
|
| Cloud Backend | FastAPI (Python) | SQLite (dev) / PostgreSQL (prod). JWT auth. |
|
||||||
|
| Sysadmin Panel | React 18 + Vite | TailwindCSS, Zustand, Axios. Dark theme, cyan accent. |
|
||||||
|
| Containerization | Docker + Docker Compose | All services in docker-compose.yml |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
simple-pos-system/
|
||||||
|
├── docker-compose.yml # All 5 services defined here
|
||||||
|
├── local_backend/
|
||||||
|
│ ├── main.py # FastAPI app, lifespan, middleware
|
||||||
|
│ ├── config.py # Settings from .env (SITE_ID, SITE_KEY, CLOUD_URL, etc.)
|
||||||
|
│ ├── database.py # SQLAlchemy engine + SessionLocal
|
||||||
|
│ ├── models/ # SQLAlchemy ORM models
|
||||||
|
│ │ ├── user.py # User, AssistantAssignment
|
||||||
|
│ │ ├── table.py # Table, TableGroup
|
||||||
|
│ │ ├── product.py # Product, Category, Option, Ingredient, PreferenceSet
|
||||||
|
│ │ ├── order.py # Order, OrderItem, OrderWaiter, PrintLog
|
||||||
|
│ │ └── printer.py # Printer
|
||||||
|
│ ├── schemas/ # Pydantic request/response schemas
|
||||||
|
│ ├── routers/
|
||||||
|
│ │ ├── auth.py # POST /api/auth/login, /refresh, /me
|
||||||
|
│ │ ├── tables.py # CRUD tables + table groups
|
||||||
|
│ │ ├── products.py # CRUD products + categories + image upload
|
||||||
|
│ │ ├── orders.py # Order lifecycle + item management
|
||||||
|
│ │ ├── waiters.py # Waiter management
|
||||||
|
│ │ ├── reports.py # Shift reports, order history
|
||||||
|
│ │ └── system.py # Health, printer test, lock/unlock
|
||||||
|
│ ├── services/
|
||||||
|
│ │ ├── printer_service.py # ESC/POS print routing logic
|
||||||
|
│ │ └── cloud_sync.py # Background heartbeat task
|
||||||
|
│ ├── middleware/
|
||||||
|
│ │ └── license_check.py # Blocks all requests if locked/expired
|
||||||
|
│ ├── seed.py # Dev data seeder
|
||||||
|
│ ├── pos.db # SQLite DB (not in git)
|
||||||
|
│ └── license_state.json # Persisted license state (not in git)
|
||||||
|
│
|
||||||
|
├── cloud_backend/
|
||||||
|
│ ├── main.py # FastAPI app, seeds default admin on startup
|
||||||
|
│ ├── config.py # Settings from .env (SECRET_KEY, ADMIN_USERNAME, etc.)
|
||||||
|
│ ├── database.py
|
||||||
|
│ ├── auth_utils.py # JWT creation + verification, password hashing
|
||||||
|
│ ├── models/
|
||||||
|
│ │ ├── admin.py # Admin table (sysadmin accounts)
|
||||||
|
│ │ └── site.py # Site table (registered restaurants)
|
||||||
|
│ ├── schemas/
|
||||||
|
│ │ ├── admin.py # LoginRequest, TokenOut
|
||||||
|
│ │ └── site.py # SiteCreate, SiteOut, SiteCreatedOut, LockRequest, HeartbeatRequest/Response
|
||||||
|
│ └── routers/
|
||||||
|
│ ├── auth.py # POST /api/auth/login
|
||||||
|
│ ├── sites.py # Full CRUD + lock/unlock for sites
|
||||||
|
│ └── heartbeat.py # POST /api/heartbeat/ (called by local backends)
|
||||||
|
│
|
||||||
|
├── waiter_pwa/
|
||||||
|
│ └── src/
|
||||||
|
│ ├── api/client.js # Axios, points to VITE_API_URL
|
||||||
|
│ ├── store/authStore.js # Zustand: token + user
|
||||||
|
│ └── pages/
|
||||||
|
│ ├── LoginPage.jsx
|
||||||
|
│ ├── TablesPage.jsx
|
||||||
|
│ └── OrderPage.jsx
|
||||||
|
│
|
||||||
|
├── manager_dashboard/
|
||||||
|
│ └── src/
|
||||||
|
│ ├── api/client.js # Axios, points to VITE_API_URL
|
||||||
|
│ ├── store/authStore.js
|
||||||
|
│ └── pages/
|
||||||
|
│ ├── LoginPage.jsx # PIN pad (manager/sysadmin only)
|
||||||
|
│ ├── DashboardPage.jsx # Live table grid, 30s polling
|
||||||
|
│ ├── OrderDetailPage.jsx
|
||||||
|
│ ├── ProductsPage.jsx
|
||||||
|
│ ├── WaitersPage.jsx
|
||||||
|
│ ├── TablesPage.jsx
|
||||||
|
│ ├── ReportsPage.jsx
|
||||||
|
│ └── SettingsPage.jsx
|
||||||
|
│
|
||||||
|
├── sysadmin_panel/
|
||||||
|
│ └── src/
|
||||||
|
│ ├── api/client.js # Axios, points to VITE_CLOUD_URL
|
||||||
|
│ ├── store/authStore.js # Token stored as 'sysadmin_token'
|
||||||
|
│ └── pages/
|
||||||
|
│ ├── LoginPage.jsx # Username + password (NOT PIN)
|
||||||
|
│ ├── SitesPage.jsx # Card grid, status colors, 30s polling
|
||||||
|
│ ├── SiteDetailPage.jsx # Lock/unlock/extend/delete
|
||||||
|
│ └── RegisterSitePage.jsx # One-time secret key display
|
||||||
|
│
|
||||||
|
└── PLANS AND STRATEGIES/
|
||||||
|
├── CLAUDE_CODE_INSTRUCTIONS.md # Session start instructions + known issues log
|
||||||
|
├── PROJECT_REFERENCE.md # This file
|
||||||
|
├── TESTING_CHECKLIST.md # Step-by-step end-to-end testing guide
|
||||||
|
├── 00_PROJECT_OVERVIEW.md
|
||||||
|
├── 01_LOCAL_BACKEND.md
|
||||||
|
├── 02_WAITER_PWA.md
|
||||||
|
├── 03_MANAGER_DASHBOARD.md
|
||||||
|
├── 04_CLOUD_BACKEND.md
|
||||||
|
└── 05_SYSADMIN_PANEL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker Compose Services
|
||||||
|
|
||||||
|
All services defined in `docker-compose.yml` at project root.
|
||||||
|
|
||||||
|
| Service | Image | Port | Depends On | Env File |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `cloud_backend` | Custom (Dockerfile) | 8001 | — | `cloud_backend/.env` |
|
||||||
|
| `backend` | Custom (Dockerfile) | 8000 | — | `local_backend/.env` |
|
||||||
|
| `waiter_pwa` | node:20-alpine | 5173 | `backend` | `waiter_pwa/.env` |
|
||||||
|
| `manager_dashboard` | node:20-alpine | 5174 | `backend` | `manager_dashboard/.env` |
|
||||||
|
| `sysadmin_panel` | node:20-alpine | 5175 | `cloud_backend` | `sysadmin_panel/.env` |
|
||||||
|
|
||||||
|
**Volumes:**
|
||||||
|
- `./data/cloud:/app/data` — cloud DB
|
||||||
|
- `./local_backend/pos.db:/app/pos.db` — local DB
|
||||||
|
- `./local_backend/license_state.json:/app/license_state.json` — persisted license state
|
||||||
|
- `./data/product_images:/app/data/product_images` — uploaded product photos
|
||||||
|
- `./logo.png:/app/logo.png:ro` — restaurant logo for receipts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Wiring: How Everything Connects
|
||||||
|
|
||||||
|
### 1. Waiter/Manager → Local Backend
|
||||||
|
- All REST API calls go to `VITE_API_URL` (e.g. `http://192.168.1.10:8000`)
|
||||||
|
- JWT Bearer token in `Authorization` header (stored in localStorage)
|
||||||
|
- On 401 response: token cleared, redirect to `/login`
|
||||||
|
|
||||||
|
### 2. Local Backend → Cloud Backend (Heartbeat)
|
||||||
|
- Background task in `cloud_sync.py`, fires on startup then every 6 hours
|
||||||
|
- `POST {CLOUD_URL}/api/heartbeat/`
|
||||||
|
- Headers: `X-Site-ID: <site_id>`, `X-Site-Key: <secret_key>`
|
||||||
|
- Body: `{ version, uptime_seconds }`
|
||||||
|
- Response: `{ licensed, locked, lock_reason, expires_at }`
|
||||||
|
- Result stored in memory (`license_state` dict) and persisted to `license_state.json`
|
||||||
|
- If cloud unreachable: uses last known state + 24h grace period
|
||||||
|
|
||||||
|
### 3. License Enforcement (Local Backend)
|
||||||
|
- `LicenseCheckMiddleware` runs on every request
|
||||||
|
- Reads `license_state` dict (in memory, updated by cloud sync)
|
||||||
|
- If `locked = True` → HTTP 423 on all endpoints
|
||||||
|
- If `licensed = False` → HTTP 402 on all endpoints
|
||||||
|
- Exempt: `GET /api/system/health` always passes through
|
||||||
|
|
||||||
|
### 4. Sysadmin Panel → Cloud Backend
|
||||||
|
- All API calls go to `VITE_CLOUD_URL` (e.g. `http://localhost:8001`)
|
||||||
|
- JWT Bearer token in `Authorization` header (stored as `sysadmin_token`)
|
||||||
|
- On 401: redirect to `/login`
|
||||||
|
|
||||||
|
### 5. Printer Routing (Local Backend)
|
||||||
|
- Triggered automatically on `POST /api/orders/{id}/items`
|
||||||
|
- `printer_service.py` groups items by `product.printer_zone_id`
|
||||||
|
- For each printer zone: formats ESC/POS receipt, connects via TCP to `printer.ip_address:printer.port`
|
||||||
|
- Greek text encoded as CP737 (n=29) bytes — raw `p._raw()` only, never `p.text()`
|
||||||
|
- Print failure is logged but does NOT fail the order save (orders always save)
|
||||||
|
- Items with no `printer_zone_id` are silently skipped
|
||||||
|
|
||||||
|
### 6. Site Registration Flow
|
||||||
|
1. Sysadmin opens Sysadmin Panel → Register New Site
|
||||||
|
2. Form submitted → `POST /api/sites/` on cloud backend
|
||||||
|
3. Cloud generates `site_id` (UUID) and `secret_key` (urlsafe random), stores hashed key
|
||||||
|
4. Response includes plaintext `secret_key` — **shown once only**
|
||||||
|
5. Sysadmin copies `SITE_ID` and `SITE_KEY` into `local_backend/.env`
|
||||||
|
6. Local backend restarts → next heartbeat authenticates successfully
|
||||||
|
7. Sysadmin Panel shows site as "Active" once heartbeat is received
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema Summary
|
||||||
|
|
||||||
|
### Local Backend (SQLite: pos.db)
|
||||||
|
|
||||||
|
| Table | Key Fields |
|
||||||
|
|---|---|
|
||||||
|
| `users` | id, username, pin_hash, role (waiter/manager/sysadmin), is_active |
|
||||||
|
| `tables` | id, number, label, is_active, group_id |
|
||||||
|
| `table_groups` | id, name |
|
||||||
|
| `categories` | id, name, color, sort_order |
|
||||||
|
| `products` | id, name, category_id, base_price, is_available, printer_zone_id, image_url |
|
||||||
|
| `product_options` | id, product_id, name, extra_cost |
|
||||||
|
| `product_ingredients` | id, product_id, name, extra_cost |
|
||||||
|
| `product_preference_sets` | id, product_id, name (exclusive-choice groups) |
|
||||||
|
| `product_preference_choices` | id, preference_set_id, name, price_delta |
|
||||||
|
| `printers` | id, name, ip_address, port, is_active |
|
||||||
|
| `orders` | id, table_id, opened_by, status, opened_at, closed_at, closed_by |
|
||||||
|
| `order_waiters` | order_id, waiter_id (many-to-many) |
|
||||||
|
| `order_items` | id, order_id, product_id, quantity, unit_price (snapshot!), selected_options (JSON), removed_ingredients (JSON), status, printed |
|
||||||
|
| `print_log` | id, order_id, printer_id, printed_at, item_ids (JSON), success, error_message |
|
||||||
|
|
||||||
|
### Cloud Backend (SQLite: cloud.db in dev, PostgreSQL in prod)
|
||||||
|
|
||||||
|
| Table | Key Fields |
|
||||||
|
|---|---|
|
||||||
|
| `admins` | id, username, password_hash, role |
|
||||||
|
| `sites` | id, site_id (UUID), name, owner_name, contact_email, secret_key_hash, is_active, is_locked, lock_reason, license_expires_at, last_seen_at, last_seen_ip |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Surface Summary
|
||||||
|
|
||||||
|
### Local Backend (`http://<ip>:8000`)
|
||||||
|
|
||||||
|
| Method | Path | Auth | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| POST | `/api/auth/login` | None | Body: `{username, pin}` → JWT |
|
||||||
|
| GET | `/api/auth/me` | JWT | Returns current user |
|
||||||
|
| GET | `/api/tables/` | JWT | All tables + active order status |
|
||||||
|
| POST | `/api/tables/` | Manager | Create table |
|
||||||
|
| POST | `/api/tables/batch` | Manager | Bulk create with prefix+count |
|
||||||
|
| GET/POST/PUT/DELETE | `/api/tables/groups` | Manager | Table group management |
|
||||||
|
| GET | `/api/products/` | JWT | `?all=true` includes unavailable (manager) |
|
||||||
|
| POST | `/api/products/{id}/image` | Manager | Upload product image |
|
||||||
|
| GET/POST/PUT/DELETE | `/api/products/categories` | Manager | Category management |
|
||||||
|
| GET | `/api/orders/` | Manager | All orders, filterable |
|
||||||
|
| GET | `/api/orders/my` | Waiter | Own active orders |
|
||||||
|
| POST | `/api/orders/` | Waiter | Open new order `{table_id}` |
|
||||||
|
| POST | `/api/orders/{id}/items` | Waiter | Add items, triggers printing |
|
||||||
|
| POST | `/api/orders/{id}/pay` | Waiter | Mark items paid `{item_ids}` |
|
||||||
|
| POST | `/api/orders/{id}/close` | Waiter/Manager | Close order |
|
||||||
|
| DELETE | `/api/orders/{id}/items/{item_id}` | Manager | Cancel item |
|
||||||
|
| GET | `/api/waiters/` | Manager | All waiters |
|
||||||
|
| POST/PUT/DELETE | `/api/waiters/` | Manager | Waiter management |
|
||||||
|
| GET | `/api/reports/shift` | Manager | Shift summary |
|
||||||
|
| GET | `/api/system/health` | None | Liveness (exempt from license check) |
|
||||||
|
|
||||||
|
### Cloud Backend (`http://<vps>:8001`)
|
||||||
|
|
||||||
|
| Method | Path | Auth | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| POST | `/api/auth/login` | None | Body: `{username, password}` → JWT |
|
||||||
|
| GET | `/api/sites/` | Sysadmin JWT | List all sites |
|
||||||
|
| POST | `/api/sites/` | Sysadmin JWT | Register site → returns secret_key once |
|
||||||
|
| GET | `/api/sites/{site_id}` | Sysadmin JWT | Site detail |
|
||||||
|
| PUT | `/api/sites/{site_id}` | Sysadmin JWT | Update info / extend license |
|
||||||
|
| POST | `/api/sites/{site_id}/lock` | Sysadmin JWT | Lock with reason |
|
||||||
|
| POST | `/api/sites/{site_id}/unlock` | Sysadmin JWT | Unlock |
|
||||||
|
| DELETE | `/api/sites/{site_id}` | Sysadmin JWT | Deregister |
|
||||||
|
| POST | `/api/heartbeat/` | Site headers | Called by local backends |
|
||||||
|
| GET | `/health` | None | Liveness |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### `local_backend/.env`
|
||||||
|
```
|
||||||
|
SITE_ID=<UUID from site registration>
|
||||||
|
SITE_KEY=<secret key shown once at registration>
|
||||||
|
CLOUD_URL=http://cloud_backend:8001 # Use service name in Docker, or VPS URL in prod
|
||||||
|
SECRET_KEY=<long random string for JWT>
|
||||||
|
LICENSE_GRACE_HOURS=24
|
||||||
|
```
|
||||||
|
|
||||||
|
### `cloud_backend/.env`
|
||||||
|
```
|
||||||
|
SECRET_KEY=<different long random string>
|
||||||
|
DATABASE_URL=sqlite:////app/data/cloud.db # Or postgresql://... in prod
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
|
ADMIN_USERNAME=sysadmin
|
||||||
|
ADMIN_PASSWORD=<your password>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `waiter_pwa/.env`
|
||||||
|
```
|
||||||
|
VITE_API_URL=http://<local-backend-ip>:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### `manager_dashboard/.env`
|
||||||
|
```
|
||||||
|
VITE_API_URL=http://<local-backend-ip>:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### `sysadmin_panel/.env`
|
||||||
|
```
|
||||||
|
VITE_CLOUD_URL=http://localhost:8001 # Or https://your-vps.com in prod
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Printer Configuration
|
||||||
|
|
||||||
|
**Hardware:** Jolimark TP850UE, USB + Ethernet, static IP via NetFinder software.
|
||||||
|
**Protocol:** ESC/POS over raw TCP, port 9100.
|
||||||
|
**Paper:** 80mm = 48 characters wide at standard font.
|
||||||
|
|
||||||
|
**Critical Greek text rules:**
|
||||||
|
- Code page: `n=29` (CP737). Set immediately after connecting: `p._raw(b'\x1b\x74\x1d')`
|
||||||
|
- ALL text sent as raw CP737 bytes: `text.encode('cp737', errors='replace')`
|
||||||
|
- Never use `p.text()` for Greek — it does not handle CP737 correctly
|
||||||
|
- `p._raw(b'\x1b\x40')` to reset printer before each job
|
||||||
|
|
||||||
|
Printers are configured in the local DB (`printers` table) via Manager Dashboard → Settings. Each product gets assigned a `printer_zone_id` via Manager Dashboard → Products. Items with no zone assigned are silently skipped at print time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth Flow Details
|
||||||
|
|
||||||
|
### Waiter / Manager
|
||||||
|
1. `POST /api/auth/login` with `{username, pin}`
|
||||||
|
2. Backend verifies PIN with bcrypt, issues JWT (8h expiry)
|
||||||
|
3. Token stored in `localStorage`
|
||||||
|
4. All requests send `Authorization: Bearer <token>`
|
||||||
|
5. On 401: token cleared, redirect to `/login`
|
||||||
|
6. Manager Dashboard rehydrates user from `GET /api/auth/me` on app load
|
||||||
|
|
||||||
|
### Sysadmin (Cloud)
|
||||||
|
1. `POST /api/auth/login` on cloud backend with `{username, password}`
|
||||||
|
2. JWT stored as `sysadmin_token` in localStorage
|
||||||
|
3. All cloud API requests send the token
|
||||||
|
4. On 401: redirect to `/login`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Order Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
Table available
|
||||||
|
↓
|
||||||
|
POST /api/orders/ → status: "open"
|
||||||
|
↓
|
||||||
|
POST /api/orders/{id}/items → items added, printer triggered
|
||||||
|
↓
|
||||||
|
POST /api/orders/{id}/pay (partial) → status: "partially_paid"
|
||||||
|
↓
|
||||||
|
POST /api/orders/{id}/pay (all items) → status: "paid"
|
||||||
|
↓
|
||||||
|
POST /api/orders/{id}/close → status: "closed", table becomes available
|
||||||
|
```
|
||||||
|
|
||||||
|
Manager can also `DELETE` the order (status: "cancelled") or cancel individual items.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Issues & Limitations (as of Phase 5 completion)
|
||||||
|
|
||||||
|
1. **Product image upload** only works on existing products — no upload at creation time.
|
||||||
|
2. **First heartbeat** happens on local backend startup, not on a fixed schedule. If the cloud is down at startup, the first successful heartbeat may be up to 6h later.
|
||||||
|
3. **Some Manager Dashboard rough edges** remain (noted during Phase 3 smoke test) — not blockers.
|
||||||
|
4. **SITE_KEY** must be copied immediately at registration — the cloud never stores or shows the plaintext key again. If lost, deregister and re-register the site.
|
||||||
|
5. **Cloud sync on Docker networking**: `CLOUD_URL=http://cloud_backend:8001` works within Docker Compose (same network). For production VPS deployment, use the public HTTPS URL.
|
||||||
|
6. **SQLite for cloud backend** is fine for a small number of sites (< 50). For scale, switch `DATABASE_URL` to PostgreSQL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Deployment Notes
|
||||||
|
|
||||||
|
- **Local backend**: Docker Compose on a Raspberry Pi 4 or any Linux box. Static LAN IP via DHCP reservation at the router.
|
||||||
|
- **Cloud backend**: VPS with nginx reverse proxy + SSL (Let's Encrypt). Run `cloud_backend` container or as systemd service.
|
||||||
|
- **Sysadmin panel**: Build with `vite build`, serve static files via nginx. Optionally protect with nginx basic auth.
|
||||||
|
- **Waiter PWA**: Accessible from any phone on the LAN. For PWA install, serve over HTTPS (or use localhost on dev).
|
||||||
|
- **Printers**: Static IPs assigned via NetFinder (Jolimark utility). Docker host must be on the same VLAN as printers, or routing must be configured.
|
||||||
328
PLANS AND STRATEGIES/TESTING_CHECKLIST.md
Normal file
328
PLANS AND STRATEGIES/TESTING_CHECKLIST.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# POS System — End-to-End Testing Checklist
|
||||||
|
|
||||||
|
Work through this top-to-bottom. Each section depends on the one above it.
|
||||||
|
Mark `[x]` as you go. Note failures with a short comment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Prerequisites
|
||||||
|
|
||||||
|
- [ ] Docker Desktop is running
|
||||||
|
- [ ] You are in the project root: `c:\development\simple-pos-system`
|
||||||
|
- [ ] All `.env` files are filled in correctly (see below)
|
||||||
|
|
||||||
|
### .env quick reference
|
||||||
|
|
||||||
|
**`cloud_backend/.env`**
|
||||||
|
```
|
||||||
|
SECRET_KEY=<long random string>
|
||||||
|
DATABASE_URL=sqlite:////app/data/cloud.db
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
|
ADMIN_USERNAME=sysadmin
|
||||||
|
ADMIN_PASSWORD=<your password>
|
||||||
|
```
|
||||||
|
|
||||||
|
**`local_backend/.env`**
|
||||||
|
```
|
||||||
|
SITE_ID=<filled in after step 2.4>
|
||||||
|
SITE_KEY=<filled in after step 2.4>
|
||||||
|
CLOUD_URL=http://cloud_backend:8001
|
||||||
|
SECRET_KEY=<long random string>
|
||||||
|
LICENSE_GRACE_HOURS=24
|
||||||
|
```
|
||||||
|
|
||||||
|
**`sysadmin_panel/.env`**
|
||||||
|
```
|
||||||
|
VITE_CLOUD_URL=http://localhost:8001
|
||||||
|
```
|
||||||
|
|
||||||
|
**`waiter_pwa/.env`**
|
||||||
|
```
|
||||||
|
VITE_API_URL=http://<local-backend-ip>:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
**`manager_dashboard/.env`**
|
||||||
|
```
|
||||||
|
VITE_API_URL=http://<local-backend-ip>:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Cloud Backend
|
||||||
|
|
||||||
|
### 1.1 Start
|
||||||
|
```bash
|
||||||
|
docker compose up cloud_backend -d
|
||||||
|
```
|
||||||
|
- [ ] Container starts without errors (`docker compose logs cloud_backend`)
|
||||||
|
|
||||||
|
### 1.2 Health check
|
||||||
|
```
|
||||||
|
GET http://localhost:8001/health
|
||||||
|
```
|
||||||
|
- [ ] Returns `{"status": "ok"}`
|
||||||
|
|
||||||
|
### 1.3 Sysadmin login
|
||||||
|
```
|
||||||
|
POST http://localhost:8001/api/auth/login
|
||||||
|
Body: { "username": "sysadmin", "password": "changeme" }
|
||||||
|
```
|
||||||
|
- [ ] Returns a JWT `access_token`
|
||||||
|
- [ ] Save token for next steps
|
||||||
|
|
||||||
|
### 1.4 List sites (empty)
|
||||||
|
```
|
||||||
|
GET http://localhost:8001/api/sites/
|
||||||
|
Header: Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
- [ ] Returns `[]` (empty list)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Sysadmin Panel
|
||||||
|
|
||||||
|
### 2.1 Start
|
||||||
|
```bash
|
||||||
|
docker compose up sysadmin_panel -d
|
||||||
|
```
|
||||||
|
- [ ] Container starts, `npm install` completes, dev server on port 5175
|
||||||
|
|
||||||
|
### 2.2 Open in browser
|
||||||
|
```
|
||||||
|
http://localhost:5175
|
||||||
|
```
|
||||||
|
- [ ] Redirects to `/login`
|
||||||
|
- [ ] Login form appears (username + password, NOT pin pad)
|
||||||
|
|
||||||
|
### 2.3 Login
|
||||||
|
- [ ] Enter `sysadmin` / `changeme` → redirects to `/sites`
|
||||||
|
- [ ] Sites page shows "0 registered", "Register New Site" button visible
|
||||||
|
- [ ] Summary counts (Active / Locked / Expired) visible
|
||||||
|
|
||||||
|
### 2.4 Register a site
|
||||||
|
- Click "Register New Site"
|
||||||
|
- [ ] Form fields: Restaurant Name, Owner Name, Contact Email, License Expiry
|
||||||
|
- [ ] Fill in test data, set expiry date to at least 1 year from now
|
||||||
|
- [ ] Submit → success screen shows
|
||||||
|
- [ ] **Site ID** and **Secret Key** are displayed
|
||||||
|
- [ ] Warning banner "Copy this secret key now" is visible
|
||||||
|
- [ ] "Copy .env vars" button works (copies to clipboard)
|
||||||
|
- Copy the `SITE_ID` and `SITE_SECRET` values
|
||||||
|
- Paste them into `local_backend/.env` as `SITE_ID=` and `SITE_KEY=`
|
||||||
|
|
||||||
|
### 2.5 Verify site in list
|
||||||
|
- [ ] Navigate to `/sites` → new site card appears
|
||||||
|
- [ ] Status shows yellow "No Heartbeat" (expected — local backend hasn't connected yet)
|
||||||
|
- [ ] Click site card → SiteDetailPage shows correct name, owner, expiry
|
||||||
|
- [ ] Lock/Unlock/Extend License buttons are visible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Local Backend
|
||||||
|
|
||||||
|
### 3.1 Start
|
||||||
|
```bash
|
||||||
|
docker compose up backend -d
|
||||||
|
```
|
||||||
|
- [ ] Container starts without errors
|
||||||
|
- [ ] Logs show: `Application startup complete`
|
||||||
|
- [ ] Logs show cloud sync attempt: either `Cloud sync OK` or `Cloud sync failed` (if cloud URL not yet reachable from inside Docker — check `CLOUD_URL=http://cloud_backend:8001`)
|
||||||
|
|
||||||
|
### 3.2 Health check
|
||||||
|
```
|
||||||
|
GET http://localhost:8000/api/system/health
|
||||||
|
```
|
||||||
|
- [ ] Returns `{"status": "ok"}` (this endpoint is exempt from license check)
|
||||||
|
|
||||||
|
### 3.3 Heartbeat appears in sysadmin panel
|
||||||
|
- [ ] In Sysadmin Panel → SiteDetailPage: "Last seen" updates to recent timestamp
|
||||||
|
- [ ] Status indicator turns green "Active"
|
||||||
|
- [ ] Last IP field shows the Docker container IP
|
||||||
|
|
||||||
|
> Note: Cloud sync runs immediately on startup, so this should update within seconds.
|
||||||
|
|
||||||
|
### 3.4 Auth — create initial manager
|
||||||
|
If no manager user exists in the DB yet, seed one via the seed script or direct API call:
|
||||||
|
```bash
|
||||||
|
docker compose exec backend python seed.py
|
||||||
|
```
|
||||||
|
- [ ] Seed runs without errors (or manager already exists)
|
||||||
|
|
||||||
|
### 3.5 Auth — login as manager
|
||||||
|
```
|
||||||
|
POST http://localhost:8000/api/auth/login
|
||||||
|
Body: { "username": "manager", "pin": "1234" }
|
||||||
|
```
|
||||||
|
- [ ] Returns `{ access_token, user: { id, username, role: "manager" } }`
|
||||||
|
|
||||||
|
### 3.6 Auth — login as waiter
|
||||||
|
```
|
||||||
|
POST http://localhost:8000/api/auth/login
|
||||||
|
Body: { "username": "<waiter-username>", "pin": "<pin>" }
|
||||||
|
```
|
||||||
|
- [ ] Returns token with `role: "waiter"`
|
||||||
|
|
||||||
|
### 3.7 Tables endpoint
|
||||||
|
```
|
||||||
|
GET http://localhost:8000/api/tables/
|
||||||
|
Header: Authorization: Bearer <manager-token>
|
||||||
|
```
|
||||||
|
- [ ] Returns list of tables (may be empty if not seeded)
|
||||||
|
|
||||||
|
### 3.8 Products endpoint
|
||||||
|
```
|
||||||
|
GET http://localhost:8000/api/products/
|
||||||
|
Header: Authorization: Bearer <manager-token>
|
||||||
|
```
|
||||||
|
- [ ] Returns list of products and categories
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Manager Dashboard
|
||||||
|
|
||||||
|
### 4.1 Start
|
||||||
|
```bash
|
||||||
|
docker compose up manager_dashboard -d
|
||||||
|
```
|
||||||
|
- [ ] Dev server starts on port 5174
|
||||||
|
|
||||||
|
### 4.2 Open in browser
|
||||||
|
```
|
||||||
|
http://localhost:5174
|
||||||
|
```
|
||||||
|
- [ ] Redirects to `/login`
|
||||||
|
- [ ] PIN pad + username field visible
|
||||||
|
|
||||||
|
### 4.3 Login
|
||||||
|
- [ ] Enter manager username + PIN → redirects to `/dashboard`
|
||||||
|
- [ ] Dashboard shows table grid (empty or with existing tables)
|
||||||
|
- [ ] No console errors
|
||||||
|
|
||||||
|
### 4.4 Tables page
|
||||||
|
- [ ] Navigate to Tables → existing tables shown
|
||||||
|
- [ ] Create a new table (e.g. Table 10) → appears in list
|
||||||
|
- [ ] Table groups section visible
|
||||||
|
|
||||||
|
### 4.5 Products page
|
||||||
|
- [ ] Navigate to Products → existing products shown
|
||||||
|
- [ ] Create a new category
|
||||||
|
- [ ] Create a new product in that category, assign a price
|
||||||
|
- [ ] Toggle product availability → greyed out when unavailable
|
||||||
|
- [ ] Assign a printer zone to a product (if printer is configured)
|
||||||
|
- [ ] Product image upload works on an existing product
|
||||||
|
|
||||||
|
### 4.6 Waiters page
|
||||||
|
- [ ] Navigate to Waiters → existing waiters shown
|
||||||
|
- [ ] Create a new waiter (username + PIN)
|
||||||
|
- [ ] Block/unblock a waiter works
|
||||||
|
- [ ] Reset PIN works
|
||||||
|
|
||||||
|
### 4.7 Dashboard live grid
|
||||||
|
- [ ] Open an order from the Waiter PWA (step 5 below), then return here
|
||||||
|
- [ ] Table card updates within 30 seconds (polling interval)
|
||||||
|
- [ ] Click table → OrderDetailPage loads with correct items
|
||||||
|
|
||||||
|
### 4.8 Reports page
|
||||||
|
- [ ] Navigate to Reports → no crash
|
||||||
|
- [ ] Date picker works, summary loads (may be empty)
|
||||||
|
|
||||||
|
### 4.9 Settings page
|
||||||
|
- [ ] Navigate to Settings → no crash
|
||||||
|
- [ ] Printer config visible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Waiter PWA
|
||||||
|
|
||||||
|
### 5.1 Open on phone (or browser)
|
||||||
|
```
|
||||||
|
http://<local-machine-ip>:5173
|
||||||
|
```
|
||||||
|
- [ ] App loads, redirects to `/login`
|
||||||
|
- [ ] Username field + PIN pad visible
|
||||||
|
|
||||||
|
### 5.2 Login as waiter
|
||||||
|
- [ ] Enter waiter credentials → redirects to table list
|
||||||
|
- [ ] Only active tables visible (not deactivated ones)
|
||||||
|
|
||||||
|
### 5.3 Open a table order
|
||||||
|
- [ ] Tap a table → if no open order, "Open Order" button appears
|
||||||
|
- [ ] Tap "Open Order" → order is created, table turns active
|
||||||
|
|
||||||
|
### 5.4 Add items
|
||||||
|
- [ ] Category tabs visible at top
|
||||||
|
- [ ] Tap a product → added to order
|
||||||
|
- [ ] Products with options show option selector
|
||||||
|
- [ ] Products with removable ingredients show toggle list
|
||||||
|
- [ ] Item with note field → can add freetext note
|
||||||
|
|
||||||
|
### 5.5 Verify print routing
|
||||||
|
- [ ] After adding items: check printer (if connected) received the ticket
|
||||||
|
- [ ] If no physical printer: check `docker compose logs backend` — should show print attempt log (success or failure, NOT a crash)
|
||||||
|
|
||||||
|
### 5.6 View order total
|
||||||
|
- [ ] Order total shows correct sum (base price + selected option deltas)
|
||||||
|
- [ ] Individual item prices shown
|
||||||
|
|
||||||
|
### 5.7 Partial payment
|
||||||
|
- [ ] Select some items → "Mark as Paid" → those items turn grey/paid
|
||||||
|
- [ ] Order status changes to "partially_paid"
|
||||||
|
- [ ] Remaining total updates
|
||||||
|
|
||||||
|
### 5.8 Full payment and close
|
||||||
|
- [ ] Mark all items as paid → order status = "paid"
|
||||||
|
- [ ] "Close Order" button becomes active
|
||||||
|
- [ ] Tap "Close Order" → confirmation prompt
|
||||||
|
- [ ] Confirm → order closes, table returns to "available"
|
||||||
|
|
||||||
|
### 5.9 Multi-waiter scenario
|
||||||
|
- [ ] From Manager Dashboard, add a second waiter to the order
|
||||||
|
- [ ] Second waiter can see and manage the table in their PWA
|
||||||
|
|
||||||
|
### 5.10 Blocked waiter
|
||||||
|
- [ ] From Manager Dashboard, block a waiter
|
||||||
|
- [ ] That waiter is immediately redirected to login on their next PWA request (401 interceptor)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. License / Lock Testing
|
||||||
|
|
||||||
|
### 6.1 Lock site from sysadmin panel
|
||||||
|
- [ ] In Sysadmin Panel → SiteDetailPage → Lock Site → enter reason → confirm
|
||||||
|
- [ ] Site shows "Locked" status with red indicator
|
||||||
|
- [ ] Within the next sync cycle (or restart local backend to force immediate sync):
|
||||||
|
- Local backend logs show lock status received
|
||||||
|
- All local backend endpoints return HTTP 423 (except `/api/system/health`)
|
||||||
|
- Waiter PWA shows error / cannot load data
|
||||||
|
- Manager Dashboard shows error
|
||||||
|
|
||||||
|
### 6.2 Unlock
|
||||||
|
- [ ] In Sysadmin Panel → Unlock Site
|
||||||
|
- [ ] After next sync: local backend resumes normal operation
|
||||||
|
|
||||||
|
### 6.3 Expired license
|
||||||
|
- [ ] In Sysadmin Panel → Extend License → set expiry date in the PAST → save
|
||||||
|
- [ ] After local backend sync: returns HTTP 402 on all endpoints
|
||||||
|
- [ ] Restore to future date → normal operation resumes
|
||||||
|
|
||||||
|
### 6.4 Cloud unreachable (grace period)
|
||||||
|
- [ ] Stop cloud backend: `docker compose stop cloud_backend`
|
||||||
|
- [ ] Local backend continues to serve requests (last known state + 24h grace)
|
||||||
|
- [ ] Logs show repeated `Cloud sync failed` warnings
|
||||||
|
- [ ] Restart cloud backend → next sync succeeds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Known Issues to Watch For
|
||||||
|
|
||||||
|
- Product image upload only works on existing products (not at creation time)
|
||||||
|
- Printer zone must be assigned via Manager Dashboard before items will route to a printer
|
||||||
|
- Some manager/PWA interactions have rough edges (noted in CLAUDE_CODE_INSTRUCTIONS.md)
|
||||||
|
- `SITE_KEY` in `local_backend/.env` must match the secret generated at site registration (copy it immediately, it's shown only once)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Done?
|
||||||
|
|
||||||
|
All steps checked → the system is fully operational end-to-end.
|
||||||
|
Log any failures as GitHub issues or in `CLAUDE_CODE_INSTRUCTIONS.md` under a new "Phase 5 Known Issues" section.
|
||||||
Reference in New Issue
Block a user