diff --git a/PLANS AND STRATEGIES/CLAUDE_CODE_INSTRUCTIONS.md b/PLANS AND STRATEGIES/CLAUDE_CODE_INSTRUCTIONS.md index c08b21a..4cbf89e 100644 --- a/PLANS AND STRATEGIES/CLAUDE_CODE_INSTRUCTIONS.md +++ b/PLANS AND STRATEGIES/CLAUDE_CODE_INSTRUCTIONS.md @@ -6,15 +6,17 @@ Paste it (or reference it) at the start of each session to give Claude Code full --- ## 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 - `00_PROJECT_OVERVIEW.md` — Architecture, stack, build order -- `01_LOCAL_BACKEND.md` — FastAPI backend (build this first) -- `02_WAITER_PWA.md` — Waiter-facing PWA (build second) -- `03_MANAGER_DASHBOARD.md` — Manager web app (build third) -- `04_CLOUD_BACKEND.md` — Cloud licensing backend (build fourth) -- `05_SYSADMIN_PANEL.md` — Sysadmin cloud panel (build last) +- `01_LOCAL_BACKEND.md` — FastAPI backend spec +- `02_WAITER_PWA.md` — Waiter-facing PWA spec +- `03_MANAGER_DASHBOARD.md` — Manager web app spec +- `04_CLOUD_BACKEND.md` — Cloud licensing backend spec +- `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 - 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. 8. **Printer failures must never block order saves.** Log and continue. +--- + ## Current Build Phase -> Update this line as you progress: -> Phase 1: Local Backend — [x] Complete. Smoke tested: health, auth, products, tables, orders, printer routing all working. -> 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 3: Manager Dashboard — [x] Complete. Scaffolded and smoke tested. Known issues remain (see below) — not blockers for Phase 4. -> Phase 4: Cloud Backend — [ ] Not Started -> Phase 5: Sysadmin Panel — [ ] Not Started +> All 5 phases are built. Now in testing / polish. +> Phase 1: Local Backend — [x] Complete. Smoke tested. +> Phase 2: Waiter PWA — [x] Complete. Smoke tested end-to-end. +> Phase 3: Manager Dashboard — [x] Complete. Scaffolded and smoke tested. Known rough edges remain. +> Phase 4: Cloud Backend — [x] Complete. Built, not yet smoke tested end-to-end with a real site. +> Phase 5: Sysadmin Panel — [x] Complete. Built, not yet smoke tested. + +--- ## 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. @@ -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. ## 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). -## Phase 2 Dev Data (seeded manually, not in seed.py) -- Tables 1–6 exist (table 1 was from Phase 1 smoke test) -- Category "food" (id=1) exists from Phase 1 smoke test — contains product "arakas" +## Phase 4 — What Was Built +- Cloud Backend: FastAPI, SQLite (dev) / PostgreSQL (prod), JWT auth. Port 8001. +- 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 -- 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= -CLOUD_URL=https://your-vps.com -SECRET_KEY=generate-a-long-random-string +SITE_ID= +SITE_KEY= +CLOUD_URL=http://cloud_backend:8001 +SECRET_KEY= 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://:8000 ``` -### Manager Dashboard (.env) +### Manager Dashboard (`manager_dashboard/.env`) ``` -VITE_API_URL=http://192.168.1.10:8000 +VITE_API_URL=http://:8000 ``` -### Cloud Backend (.env) +### Cloud Backend (`cloud_backend/.env`) ``` -SECRET_KEY=different-long-random-string -DATABASE_URL=postgresql://... (or sqlite for dev) +SECRET_KEY= +DATABASE_URL=sqlite:////app/data/cloud.db +ACCESS_TOKEN_EXPIRE_MINUTES=60 +ADMIN_USERNAME=sysadmin +ADMIN_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 | diff --git a/PLANS AND STRATEGIES/PROJECT_REFERENCE.md b/PLANS AND STRATEGIES/PROJECT_REFERENCE.md new file mode 100644 index 0000000..2024204 --- /dev/null +++ b/PLANS AND STRATEGIES/PROJECT_REFERENCE.md @@ -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: `, `X-Site-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://: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://: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= +SITE_KEY= +CLOUD_URL=http://cloud_backend:8001 # Use service name in Docker, or VPS URL in prod +SECRET_KEY= +LICENSE_GRACE_HOURS=24 +``` + +### `cloud_backend/.env` +``` +SECRET_KEY= +DATABASE_URL=sqlite:////app/data/cloud.db # Or postgresql://... in prod +ACCESS_TOKEN_EXPIRE_MINUTES=60 +ADMIN_USERNAME=sysadmin +ADMIN_PASSWORD= +``` + +### `waiter_pwa/.env` +``` +VITE_API_URL=http://:8000 +``` + +### `manager_dashboard/.env` +``` +VITE_API_URL=http://: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 ` +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. diff --git a/PLANS AND STRATEGIES/TESTING_CHECKLIST.md b/PLANS AND STRATEGIES/TESTING_CHECKLIST.md new file mode 100644 index 0000000..bfdfbdb --- /dev/null +++ b/PLANS AND STRATEGIES/TESTING_CHECKLIST.md @@ -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= +DATABASE_URL=sqlite:////app/data/cloud.db +ACCESS_TOKEN_EXPIRE_MINUTES=60 +ADMIN_USERNAME=sysadmin +ADMIN_PASSWORD= +``` + +**`local_backend/.env`** +``` +SITE_ID= +SITE_KEY= +CLOUD_URL=http://cloud_backend:8001 +SECRET_KEY= +LICENSE_GRACE_HOURS=24 +``` + +**`sysadmin_panel/.env`** +``` +VITE_CLOUD_URL=http://localhost:8001 +``` + +**`waiter_pwa/.env`** +``` +VITE_API_URL=http://:8000 +``` + +**`manager_dashboard/.env`** +``` +VITE_API_URL=http://: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 +``` +- [ ] 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": "", "pin": "" } +``` +- [ ] Returns token with `role: "waiter"` + +### 3.7 Tables endpoint +``` +GET http://localhost:8000/api/tables/ +Header: Authorization: Bearer +``` +- [ ] Returns list of tables (may be empty if not seeded) + +### 3.8 Products endpoint +``` +GET http://localhost:8000/api/products/ +Header: Authorization: Bearer +``` +- [ ] 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://: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.