Phase 1: scaffold local backend — models, schemas, routers, printer service, Docker
This commit is contained in:
85
PLANS AND STRATEGIES/00_PROJECT_OVERVIEW.md
Normal file
85
PLANS AND STRATEGIES/00_PROJECT_OVERVIEW.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# POS System — Master Project Overview
|
||||
|
||||
## What We're Building
|
||||
A locally-operated Point of Sale system for restaurants. It digitizes order-taking and tracking — think pen-and-paper workflows, modernized. No legal/IRS integration. No payment processing. Pure order management and accountability.
|
||||
|
||||
## Core Philosophy
|
||||
- **Local-first**: Everything runs on the restaurant's LAN. Internet outage = zero impact on operations.
|
||||
- **Simple auth**: Username + PIN. Waiters use their own phones.
|
||||
- **Fraud prevention**: Waiters cannot delete items or orders. Only managers can.
|
||||
- **Zone printing**: Each printer receives only items relevant to its area.
|
||||
|
||||
---
|
||||
|
||||
## System Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ CLOUD (Your VPS) │
|
||||
│ Cloud Backend (FastAPI) │
|
||||
│ Sysadmin Panel (React) │
|
||||
│ - License management │
|
||||
│ - Remote lock/unlock per site │
|
||||
│ - Site registration │
|
||||
└──────────────────┬──────────────────────────┘
|
||||
│ Periodic check-in (24h grace)
|
||||
│ HTTPS
|
||||
┌──────────────────▼──────────────────────────┐
|
||||
│ LOCAL (Restaurant LAN) │
|
||||
│ Local Backend (FastAPI) │
|
||||
│ SQLite Database │
|
||||
│ Printer routing (python-escpos) │
|
||||
│ Static local IP (e.g. 192.168.1.10) │
|
||||
│ Runs on Linux (RPi or old PC) │
|
||||
└────────┬────────────────┬───────────────────┘
|
||||
│ LAN │ LAN
|
||||
┌────────▼──────┐ ┌──────▼────────────────────┐
|
||||
│ Waiter PWA │ │ Manager Dashboard │
|
||||
│ (phones) │ │ (tablet / laptop) │
|
||||
│ React + SW │ │ React │
|
||||
└───────────────┘ └───────────────────────────┘
|
||||
│ LAN
|
||||
┌────────▼──────────────┐
|
||||
│ Thermal Printers │
|
||||
│ Zone A: Kitchen │
|
||||
│ Zone B: Bar │
|
||||
│ Zone C: etc. │
|
||||
└───────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Three User Roles
|
||||
|
||||
| Role | Access | Frontend |
|
||||
|------|--------|----------|
|
||||
| Waiter | Their tables only. Open, add items, view total, mark paid, close. | PWA on phone |
|
||||
| Manager | All tables + orders. Full menu/waiter/table management. Shift reports. | Web Dashboard |
|
||||
| Sysadmin (you) | Everything above + system control, remote lock, multi-site management. | Cloud Panel |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Local Backend | FastAPI (Python) |
|
||||
| Database | SQLite (via SQLAlchemy) |
|
||||
| Printer | python-escpos |
|
||||
| Waiter App | React + Vite (PWA) |
|
||||
| Manager Dashboard | React + Vite |
|
||||
| Styling | TailwindCSS |
|
||||
| Cloud Backend | FastAPI (Python) |
|
||||
| Sysadmin Panel | React + Vite |
|
||||
|
||||
---
|
||||
|
||||
## Build Order
|
||||
|
||||
1. `01_LOCAL_BACKEND.md` — Database schema + all API endpoints
|
||||
2. `02_WAITER_PWA.md` — Waiter-facing PWA
|
||||
3. `03_MANAGER_DASHBOARD.md` — Manager-facing web app
|
||||
4. `04_CLOUD_BACKEND.md` — Cloud licensing + remote control
|
||||
5. `05_SYSADMIN_PANEL.md` — Sysadmin cloud frontend
|
||||
|
||||
Each guide is self-contained with full details for Claude Code to implement.
|
||||
307
PLANS AND STRATEGIES/01_LOCAL_BACKEND.md
Normal file
307
PLANS AND STRATEGIES/01_LOCAL_BACKEND.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# Guide 01 — Local Backend (FastAPI)
|
||||
|
||||
## Overview
|
||||
The local backend is the heart of the entire system. It runs on a Linux machine on the restaurant's LAN, operates fully without internet, and handles all business logic: orders, tables, waiters, products, and printer routing.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
local_backend/
|
||||
├── main.py # FastAPI app entry point
|
||||
├── config.py # Settings (printer IPs, secret keys, cloud URL, etc.)
|
||||
├── database.py # SQLAlchemy engine + session
|
||||
├── models/
|
||||
│ ├── user.py
|
||||
│ ├── table.py
|
||||
│ ├── product.py
|
||||
│ ├── order.py
|
||||
│ └── printer.py
|
||||
├── schemas/ # Pydantic request/response models (mirror models/)
|
||||
├── routers/
|
||||
│ ├── auth.py
|
||||
│ ├── tables.py
|
||||
│ ├── products.py
|
||||
│ ├── orders.py
|
||||
│ ├── waiters.py
|
||||
│ ├── reports.py
|
||||
│ └── system.py
|
||||
├── services/
|
||||
│ ├── printer_service.py # Routing logic + escpos calls
|
||||
│ └── cloud_sync.py # Periodic check-in with cloud backend
|
||||
├── middleware/
|
||||
│ └── license_check.py # Validates local license token, blocks if expired
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### `users` table
|
||||
```
|
||||
id INTEGER PK
|
||||
username TEXT UNIQUE NOT NULL
|
||||
pin_hash TEXT NOT NULL # bcrypt hash of PIN
|
||||
role TEXT NOT NULL # 'waiter' | 'manager' | 'sysadmin'
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
created_at DATETIME
|
||||
```
|
||||
|
||||
### `assistant_assignments` table
|
||||
```
|
||||
id INTEGER PK
|
||||
primary_waiter_id INTEGER FK(users.id)
|
||||
assistant_waiter_id INTEGER FK(users.id)
|
||||
assigned_at DATETIME
|
||||
# If waiter B is assistant to waiter A, waiter B can manage all of A's tables
|
||||
```
|
||||
|
||||
### `tables` table
|
||||
```
|
||||
id INTEGER PK
|
||||
number INTEGER UNIQUE NOT NULL # Table number shown to users
|
||||
label TEXT # Optional custom label
|
||||
is_active BOOLEAN DEFAULT TRUE # Manager can deactivate
|
||||
floor_x FLOAT # For future floorplan feature
|
||||
floor_y FLOAT # For future floorplan feature
|
||||
```
|
||||
|
||||
### `categories` table
|
||||
```
|
||||
id INTEGER PK
|
||||
name TEXT NOT NULL
|
||||
color TEXT # Hex color for UI display
|
||||
sort_order INTEGER DEFAULT 0
|
||||
```
|
||||
|
||||
### `products` table
|
||||
```
|
||||
id INTEGER PK
|
||||
name TEXT NOT NULL
|
||||
category_id INTEGER FK(categories.id)
|
||||
base_price FLOAT NOT NULL
|
||||
is_available BOOLEAN DEFAULT TRUE
|
||||
printer_zone_id INTEGER FK(printers.id) # Which printer zone gets this product
|
||||
```
|
||||
|
||||
### `product_options` table
|
||||
```
|
||||
id INTEGER PK
|
||||
product_id INTEGER FK(products.id)
|
||||
name TEXT NOT NULL # e.g. "Size", "Temperature"
|
||||
extra_cost FLOAT DEFAULT 0.0
|
||||
```
|
||||
|
||||
### `product_ingredients` table
|
||||
```
|
||||
id INTEGER PK
|
||||
product_id INTEGER FK(products.id)
|
||||
name TEXT NOT NULL # e.g. "Onions", "Ice"
|
||||
# Can be removed by waiter when ordering
|
||||
```
|
||||
|
||||
### `printers` table
|
||||
```
|
||||
id INTEGER PK
|
||||
name TEXT NOT NULL # e.g. "Kitchen", "Bar"
|
||||
ip_address TEXT NOT NULL
|
||||
port INTEGER DEFAULT 9100
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
```
|
||||
|
||||
### `orders` table
|
||||
```
|
||||
id INTEGER PK
|
||||
table_id INTEGER FK(tables.id)
|
||||
opened_by INTEGER FK(users.id) # Waiter who opened it
|
||||
opened_at DATETIME
|
||||
status TEXT # 'open' | 'partially_paid' | 'paid' | 'closed' | 'cancelled'
|
||||
closed_at DATETIME NULL
|
||||
closed_by INTEGER FK(users.id) NULL
|
||||
notes TEXT NULL
|
||||
```
|
||||
|
||||
### `order_waiters` table
|
||||
```
|
||||
id INTEGER PK
|
||||
order_id INTEGER FK(orders.id)
|
||||
waiter_id INTEGER FK(users.id)
|
||||
assigned_at DATETIME
|
||||
# Supports multiple waiters per table (manager can add/remove)
|
||||
```
|
||||
|
||||
### `order_items` table
|
||||
```
|
||||
id INTEGER PK
|
||||
order_id INTEGER FK(orders.id)
|
||||
product_id INTEGER FK(products.id)
|
||||
added_by INTEGER FK(users.id)
|
||||
quantity INTEGER NOT NULL
|
||||
unit_price FLOAT NOT NULL # Price AT TIME OF ORDER (snapshot)
|
||||
selected_options TEXT NULL # JSON array of option ids chosen
|
||||
removed_ingredients TEXT NULL # JSON array of ingredient ids removed
|
||||
notes TEXT NULL # Freetext note per item
|
||||
status TEXT DEFAULT 'active' # 'active' | 'paid' | 'cancelled'
|
||||
added_at DATETIME
|
||||
printed BOOLEAN DEFAULT FALSE
|
||||
```
|
||||
|
||||
### `print_log` table
|
||||
```
|
||||
id INTEGER PK
|
||||
order_id INTEGER FK(orders.id)
|
||||
printer_id INTEGER FK(printers.id)
|
||||
printed_at DATETIME
|
||||
item_ids TEXT # JSON array of order_item ids printed
|
||||
success BOOLEAN
|
||||
error_message TEXT NULL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Auth — `/api/auth`
|
||||
```
|
||||
POST /login
|
||||
Body: { username, pin }
|
||||
Returns: { access_token, user: { id, username, role } }
|
||||
Note: JWT token, short expiry (8h), auto-refresh on activity
|
||||
|
||||
POST /refresh
|
||||
Header: Bearer token
|
||||
Returns: new access_token
|
||||
|
||||
POST /logout
|
||||
Invalidates token server-side (token blacklist in memory or DB)
|
||||
```
|
||||
|
||||
### Tables — `/api/tables`
|
||||
```
|
||||
GET / # All tables with current status (manager/waiter)
|
||||
POST / # Create table (manager only)
|
||||
PUT /:id # Edit table number/label (manager only)
|
||||
DELETE /:id # Deactivate table (manager only)
|
||||
GET /:id/status # Single table status + active order summary
|
||||
PUT /:id/floorplan # Update x/y position (manager only, future feature)
|
||||
```
|
||||
|
||||
### Products — `/api/products`
|
||||
```
|
||||
GET / # All products with options + ingredients (all roles)
|
||||
POST / # Create product (manager only)
|
||||
PUT /:id # Edit product (manager only)
|
||||
DELETE /:id # Deactivate product (manager only)
|
||||
|
||||
GET /categories # All categories
|
||||
POST /categories # Create category (manager only)
|
||||
PUT /categories/:id # Edit category (manager only)
|
||||
DELETE /categories/:id # Delete category (manager only)
|
||||
```
|
||||
|
||||
### Orders — `/api/orders`
|
||||
```
|
||||
GET / # All orders, filterable by status/date/waiter (manager only)
|
||||
GET /my # Current waiter's active orders
|
||||
GET /:id # Full order detail with items
|
||||
POST / # Open new order { table_id }
|
||||
POST /:id/items # Add batch of items to order
|
||||
Body: { items: [{ product_id, quantity, selected_options, removed_ingredients, notes }] }
|
||||
Triggers: printer routing for new items
|
||||
PUT /:id/items/:item_id # (manager only) edit item
|
||||
DELETE /:id/items/:item_id # (manager only) cancel/remove item
|
||||
POST /:id/pay # Mark items as paid
|
||||
Body: { item_ids: [...] } # Supports partial payment
|
||||
POST /:id/close # Close order (waiter who owns it, or manager)
|
||||
DELETE /:id # Cancel entire order (manager only)
|
||||
PUT /:id/assign-waiter # Add waiter to order (manager only)
|
||||
DELETE /:id/waiters/:waiter_id # Remove waiter from order (manager only)
|
||||
```
|
||||
|
||||
### Waiters — `/api/waiters`
|
||||
```
|
||||
GET / # All waiter accounts (manager only)
|
||||
POST / # Create waiter account (manager only)
|
||||
PUT /:id # Edit waiter (manager only)
|
||||
PUT /:id/reset-pin # Reset PIN (manager only)
|
||||
PUT /:id/block # Block/unblock waiter (manager only)
|
||||
DELETE /:id # Delete waiter (manager only)
|
||||
POST /:id/assign-assistant # Assign assistant waiter (manager only)
|
||||
DELETE /:id/assistant # Remove assistant assignment (manager only)
|
||||
```
|
||||
|
||||
### Reports — `/api/reports`
|
||||
```
|
||||
GET /shift # End-of-shift summary
|
||||
Query: ?date=YYYY-MM-DD&waiter_id=X
|
||||
Returns: per-waiter breakdown of orders + totals
|
||||
|
||||
GET /orders/history # Paginated order history with filters
|
||||
Query: ?from=&to=&waiter_id=&status=
|
||||
|
||||
GET /tables/summary # Current snapshot of all tables
|
||||
```
|
||||
|
||||
### System — `/api/system`
|
||||
```
|
||||
GET /health # Liveness check (no auth required, used by cloud)
|
||||
GET /status # System info: version, uptime, printer statuses
|
||||
POST /printers/test # Send test print to a printer (manager/sysadmin)
|
||||
PUT /printers/:id # Edit printer config (sysadmin only)
|
||||
POST /lock # Lock the system (cloud backend calls this)
|
||||
POST /unlock # Unlock (requires valid cloud token)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Business Logic Notes
|
||||
|
||||
### Printer Routing (printer_service.py)
|
||||
When items are submitted via `POST /orders/:id/items`:
|
||||
1. Group items by their `product.printer_zone_id`
|
||||
2. For each group, format an ESC/POS receipt: table number, waiter name, timestamp, list of items with options/notes
|
||||
3. Send to the corresponding printer IP
|
||||
4. Log result in `print_log`
|
||||
5. Mark items as `printed = TRUE`
|
||||
6. If a printer fails, log the error but do NOT fail the order submission — the order is saved regardless
|
||||
|
||||
### Authorization Rules
|
||||
- Waiter can only access orders where their `user.id` is in `order_waiters` OR they are assigned as assistant to the order's primary waiter
|
||||
- Manager can access everything on the local system
|
||||
- Token middleware should attach user role to every request
|
||||
|
||||
### License Check Middleware
|
||||
- On every request, check in-memory license state
|
||||
- License state is refreshed by `cloud_sync.py` background task every 6 hours
|
||||
- If cloud is unreachable, use last known state + grace period (24h)
|
||||
- If license is expired/locked, return `HTTP 402` or `HTTP 423` on all endpoints except `/api/system/health`
|
||||
|
||||
### cloud_sync.py
|
||||
- Runs as a FastAPI background task (lifespan event)
|
||||
- Every 6 hours: POST to cloud backend with site ID + heartbeat
|
||||
- Cloud responds with: `{ licensed: bool, locked: bool, expires_at: datetime }`
|
||||
- Store response locally (SQLite or a flat JSON file)
|
||||
|
||||
---
|
||||
|
||||
## Environment / Config (config.py)
|
||||
```python
|
||||
SITE_ID = "..." # Unique ID registered on cloud
|
||||
CLOUD_URL = "https://..."
|
||||
SECRET_KEY = "..." # JWT signing key
|
||||
LICENSE_GRACE_HOURS = 24
|
||||
DATABASE_URL = "sqlite:///./pos.db"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running Locally
|
||||
```bash
|
||||
pip install fastapi uvicorn sqlalchemy python-escpos bcrypt pyjwt
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
The `--host 0.0.0.0` is critical — this makes it accessible from other devices on the LAN.
|
||||
Set a static IP on the host machine at the router level (DHCP reservation).
|
||||
227
PLANS AND STRATEGIES/02_WAITER_PWA.md
Normal file
227
PLANS AND STRATEGIES/02_WAITER_PWA.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# Guide 02 — Waiter PWA (React + Vite)
|
||||
|
||||
## Overview
|
||||
A Progressive Web App installed on waiters' personal phones. Minimal, fast, touch-optimized. Works only when connected to the restaurant LAN. No offline functionality needed — show a clear error if backend is unreachable.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
waiter_pwa/
|
||||
├── public/
|
||||
│ ├── manifest.json # PWA manifest (name, icons, display: standalone)
|
||||
│ └── icons/ # App icons (192x192, 512x512)
|
||||
├── src/
|
||||
│ ├── main.jsx
|
||||
│ ├── App.jsx
|
||||
│ ├── api/
|
||||
│ │ └── client.js # Axios instance pointed at LOCAL backend IP
|
||||
│ ├── store/
|
||||
│ │ └── authStore.js # Zustand store for auth state
|
||||
│ ├── pages/
|
||||
│ │ ├── LoginPage.jsx
|
||||
│ │ ├── TableListPage.jsx
|
||||
│ │ ├── TableDetailPage.jsx
|
||||
│ │ ├── AddItemsPage.jsx
|
||||
│ │ └── OfflinePage.jsx
|
||||
│ ├── components/
|
||||
│ │ ├── PinPad.jsx
|
||||
│ │ ├── TableCard.jsx
|
||||
│ │ ├── OrderSummary.jsx
|
||||
│ │ ├── ProductPicker.jsx
|
||||
│ │ ├── ItemOptionsModal.jsx
|
||||
│ │ └── ConnectionBanner.jsx
|
||||
│ └── service-worker.js # Minimal SW — caches app shell only
|
||||
├── vite.config.js
|
||||
└── package.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PWA Setup
|
||||
|
||||
### manifest.json
|
||||
```json
|
||||
{
|
||||
"name": "TableServe",
|
||||
"short_name": "TableServe",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0f172a",
|
||||
"theme_color": "#0f172a",
|
||||
"icons": [
|
||||
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### vite.config.js
|
||||
Use `vite-plugin-pwa` for service worker generation:
|
||||
```js
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
export default {
|
||||
plugins: [
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
|
||||
// Cache app shell only — no API responses cached
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Client (client.js)
|
||||
|
||||
```js
|
||||
import axios from 'axios'
|
||||
|
||||
// This base URL must point to the LOCAL backend static IP
|
||||
// It should be configurable — read from an env variable at build time
|
||||
const BASE_URL = import.meta.env.VITE_API_URL || 'http://192.168.1.10:8000'
|
||||
|
||||
const client = axios.create({ baseURL: BASE_URL })
|
||||
|
||||
// Attach token to every request
|
||||
client.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
return config
|
||||
})
|
||||
|
||||
// On 402/423 — show license error screen
|
||||
// On network error — show offline screen
|
||||
client.interceptors.response.use(
|
||||
res => res,
|
||||
err => {
|
||||
if (!err.response) {
|
||||
// Network error = backend unreachable
|
||||
window.dispatchEvent(new Event('backend-offline'))
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
export default client
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Auth Flow
|
||||
|
||||
### First-time login (no saved user)
|
||||
1. Show username input + PIN pad
|
||||
2. POST `/api/auth/login` → store token + username in localStorage
|
||||
3. Redirect to Table List
|
||||
|
||||
### Returning user (username saved)
|
||||
1. Show "Welcome back, [Name]" + PIN pad only
|
||||
2. Same login flow
|
||||
3. "Not you?" link → clears saved username → shows full login
|
||||
|
||||
### PIN Pad Component
|
||||
- 10 digit buttons (0–9) + backspace + confirm
|
||||
- Large touch targets (minimum 64px)
|
||||
- Dots display (●●●●) as PIN is entered
|
||||
- No keyboard shown — native keyboard is clunky for PINs
|
||||
|
||||
---
|
||||
|
||||
## Pages
|
||||
|
||||
### LoginPage
|
||||
- Logo / app name at top
|
||||
- If username saved: greeting + PIN pad
|
||||
- If no username: username text input + PIN pad
|
||||
- "Logout / Switch User" link at bottom
|
||||
|
||||
### TableListPage
|
||||
- Header: waiter's name + logout icon
|
||||
- `ConnectionBanner` shown if backend unreachable
|
||||
- Grid of `TableCard` components
|
||||
- Each card shows: table number, status badge (Free / Active / Your Order)
|
||||
- Filter tabs: All | My Tables | Free Tables
|
||||
- Tap a table → `TableDetailPage`
|
||||
|
||||
### TableDetailPage
|
||||
- Header: "Table #X" + back button
|
||||
- If no active order: large "Open Order" button
|
||||
- If active order exists and waiter is assigned:
|
||||
- Order summary (list of items, quantities, prices)
|
||||
- Total at bottom
|
||||
- "Add Items" button
|
||||
- "Mark as Paid" button (can select specific items for partial payment)
|
||||
- "Close Order" button (only enabled when all items are paid)
|
||||
- If active order exists but waiter is NOT assigned: read-only view, no actions
|
||||
|
||||
### AddItemsPage
|
||||
- Accessed from TableDetailPage → "Add Items"
|
||||
- Category tabs at top (scrollable horizontal)
|
||||
- Product grid/list below
|
||||
- Tap product → `ItemOptionsModal` (select options, remove ingredients, add note, set quantity)
|
||||
- Staging area at bottom: items added so far (like a cart)
|
||||
- "Send Order" button — submits entire batch to backend, triggers printing
|
||||
|
||||
### ItemOptionsModal (bottom sheet)
|
||||
- Product name + base price
|
||||
- Options list (radio or checkbox depending on type) with price adjustments shown
|
||||
- Ingredients list with toggle to remove each
|
||||
- Freetext note input
|
||||
- Quantity stepper (+/-)
|
||||
- "Add to Order" button
|
||||
|
||||
### OfflinePage
|
||||
- Shown when backend is unreachable
|
||||
- Simple message: "Cannot reach the system. Please check your WiFi connection."
|
||||
- Retry button that pings `/api/system/health`
|
||||
|
||||
---
|
||||
|
||||
## UI Design Direction
|
||||
- **Theme**: Dark. Deep navy/slate background (`#0f172a`). This is a working tool used in dim restaurant lighting.
|
||||
- **Accent**: Warm amber or teal — something that reads clearly as "action" on dark backgrounds.
|
||||
- **Typography**: Clean, highly legible. Large touch targets. No tiny text.
|
||||
- **Table cards**: Color-coded by status. Free = subtle/muted. Active = accented. Your table = highlighted.
|
||||
- **Touch targets**: All interactive elements minimum 48px height. Prefer 64px for primary actions.
|
||||
- **Transitions**: Subtle slide transitions between pages. No heavy animations — this is a tool, not a showcase.
|
||||
|
||||
---
|
||||
|
||||
## State Management (Zustand)
|
||||
|
||||
### authStore
|
||||
```js
|
||||
{
|
||||
user: null, // { id, username, role }
|
||||
token: null,
|
||||
savedUsername: null, // persisted in localStorage
|
||||
login(user, token),
|
||||
logout(),
|
||||
}
|
||||
```
|
||||
|
||||
### No complex global state needed beyond auth.
|
||||
- Table list: fetched on mount, local component state
|
||||
- Active order: fetched when opening TableDetailPage
|
||||
- AddItems cart: local component state, discarded on submit or back navigation
|
||||
|
||||
---
|
||||
|
||||
## Key UX Rules
|
||||
1. Every destructive or confirm action requires a **second tap** (e.g. "Close Order" → confirmation sheet)
|
||||
2. After "Send Order" succeeds, immediately navigate back to TableDetailPage and show updated order
|
||||
3. If "Send Order" fails (network), show error toast — do NOT clear the staged items
|
||||
4. Loading states on all async actions — buttons disabled while in-flight
|
||||
5. "Add Items" should show a badge count of staged items so waiter doesn't lose track
|
||||
|
||||
---
|
||||
|
||||
## Installation Instructions (for deployment)
|
||||
Include a simple printed QR code at the restaurant pointing to `http://[LOCAL_IP]:5173`
|
||||
- iOS: Open in Safari → Share → Add to Home Screen
|
||||
- Android: Open in Chrome → Menu → Add to Home Screen
|
||||
172
PLANS AND STRATEGIES/03_MANAGER_DASHBOARD.md
Normal file
172
PLANS AND STRATEGIES/03_MANAGER_DASHBOARD.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Guide 03 — Manager Dashboard (React + Vite)
|
||||
|
||||
## Overview
|
||||
A full web application used by the restaurant manager on a tablet or laptop. Touch-friendly but not phone-optimized. Clean, minimal UI. Full control over orders, tables, products, waiters, and reports.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
manager_dashboard/
|
||||
├── src/
|
||||
│ ├── main.jsx
|
||||
│ ├── App.jsx
|
||||
│ ├── api/
|
||||
│ │ └── client.js # Same axios setup as waiter PWA
|
||||
│ ├── store/
|
||||
│ │ └── authStore.js
|
||||
│ ├── pages/
|
||||
│ │ ├── LoginPage.jsx
|
||||
│ │ ├── DashboardPage.jsx # Live table overview (home)
|
||||
│ │ ├── OrderDetailPage.jsx
|
||||
│ │ ├── ProductsPage.jsx
|
||||
│ │ ├── WaitersPage.jsx
|
||||
│ │ ├── TablesPage.jsx
|
||||
│ │ ├── ReportsPage.jsx
|
||||
│ │ └── SettingsPage.jsx
|
||||
│ ├── components/
|
||||
│ │ ├── Sidebar.jsx
|
||||
│ │ ├── TableGrid.jsx
|
||||
│ │ ├── OrderItemList.jsx
|
||||
│ │ ├── ProductForm.jsx
|
||||
│ │ ├── WaiterForm.jsx
|
||||
│ │ ├── ConfirmModal.jsx
|
||||
│ │ └── ShiftSummaryTable.jsx
|
||||
│ └── layouts/
|
||||
│ └── AppLayout.jsx # Sidebar + main content area
|
||||
├── vite.config.js
|
||||
└── package.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout
|
||||
|
||||
### AppLayout
|
||||
- Left sidebar (collapsible on tablet): navigation links
|
||||
- Main content area: renders current page
|
||||
- Top bar: current user name, clock, logout button
|
||||
|
||||
### Sidebar Navigation
|
||||
```
|
||||
📊 Dashboard (live table overview)
|
||||
🪑 Tables (manage table list + floorplan future)
|
||||
📦 Products (menu management)
|
||||
👥 Waiters (account management)
|
||||
📋 Reports (shift summaries + history)
|
||||
⚙️ Settings (printer config, system info)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pages
|
||||
|
||||
### LoginPage
|
||||
- Username + PIN pad (same component as waiter but slightly larger for tablet)
|
||||
- Manager/Sysadmin roles land here
|
||||
|
||||
### DashboardPage (Home)
|
||||
- **Primary view**: Grid of all tables
|
||||
- Each table card shows:
|
||||
- Table number
|
||||
- Status: Free | Active | Partially Paid
|
||||
- Assigned waiter name(s)
|
||||
- Order total (if active)
|
||||
- Time elapsed since order opened
|
||||
- Click a table → slide-over panel or modal with `OrderDetailPage` content
|
||||
- Auto-refresh every 30 seconds (or use polling)
|
||||
- Filter bar: All | Active | Free | Partially Paid
|
||||
|
||||
### OrderDetailPage
|
||||
- Full order details: table, waiter(s), opened at, items list
|
||||
- Each item shows: product name, options, notes, quantity, price, status (active/paid/cancelled)
|
||||
- Actions:
|
||||
- Remove individual item (with confirmation)
|
||||
- Cancel entire order (with confirmation + reason optional)
|
||||
- Add waiter to order
|
||||
- Remove waiter from order
|
||||
- Mark specific items as paid (same as waiter partial payment)
|
||||
- Close order
|
||||
- Print receipt (sends formatted summary to a selected printer)
|
||||
|
||||
### ProductsPage
|
||||
- Left panel: category list with "Add Category" button
|
||||
- Right panel: products in selected category
|
||||
- Each product card: name, price, availability toggle
|
||||
- "Add Product" / "Edit Product" opens a form panel (not a separate page):
|
||||
- Name
|
||||
- Category (dropdown)
|
||||
- Base price
|
||||
- Printer zone assignment (which printer this product routes to)
|
||||
- Availability toggle
|
||||
- Options section: list of options, each with name + extra cost (add/remove)
|
||||
- Ingredients section: list of ingredients (add/remove)
|
||||
|
||||
### WaitersPage
|
||||
- Table of all waiter accounts:
|
||||
- Username, status (active/blocked), orders today, total today
|
||||
- Actions per row:
|
||||
- Reset PIN → opens modal to set new PIN
|
||||
- Block / Unblock toggle
|
||||
- Edit username
|
||||
- Delete (with confirmation)
|
||||
- Manage assistants → modal showing current assistant assignments
|
||||
- "Add Waiter" button → form: username + initial PIN + role
|
||||
|
||||
### TablesPage
|
||||
- List of all tables (number, label, active/inactive)
|
||||
- Add table: just a number + optional label
|
||||
- Edit: change number or label
|
||||
- Deactivate/reactivate table
|
||||
- (Future) Floorplan editor placeholder section
|
||||
|
||||
### ReportsPage
|
||||
- **Shift Summary tab**:
|
||||
- Date picker (default: today)
|
||||
- Per-waiter summary table:
|
||||
- Waiter name | Orders completed | Items sold | Total revenue
|
||||
- Export as CSV button
|
||||
- **Order History tab**:
|
||||
- Filterable by date range, waiter, status
|
||||
- Paginated table of orders
|
||||
- Click order → opens OrderDetailPage in read-only mode
|
||||
|
||||
### SettingsPage (Manager/Sysadmin)
|
||||
- Printer list: name, IP, zone, test print button
|
||||
- System info: version, uptime
|
||||
- (Sysadmin only): license status, cloud connection status, lock/unlock system button
|
||||
|
||||
---
|
||||
|
||||
## UI Design Direction
|
||||
- **Theme**: Light. Clean white/light gray. This is used on a well-lit counter or office.
|
||||
- **Accent**: Deep teal or navy for primary actions. Red for destructive actions.
|
||||
- **Typography**: Slightly larger than standard web — readable at arm's length on a tablet.
|
||||
- **Table cards on Dashboard**: Color-coded status indicators. Bold table numbers. Clear waiter attribution.
|
||||
- **Touch targets**: 44px minimum on all interactive elements.
|
||||
- **Tables/lists**: Comfortable row height (52–64px), clear hover states.
|
||||
- **Forms**: Single-column, generous spacing. No cramped inputs.
|
||||
- **Modals/panels**: Slide-over from right for detail views. Centered modals only for confirmations.
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### authStore (Zustand) — same as waiter PWA
|
||||
|
||||
### Per-page state: local React state + React Query
|
||||
Use **React Query** (TanStack Query) for all data fetching:
|
||||
- Auto-refetch on window focus
|
||||
- Background polling for DashboardPage (every 30s)
|
||||
- Optimistic updates for quick actions (toggle availability, etc.)
|
||||
- Automatic cache invalidation after mutations
|
||||
|
||||
---
|
||||
|
||||
## Key UX Rules
|
||||
1. **Destructive actions always require confirmation** — modal with clear description of what will happen
|
||||
2. **Dashboard is the home** — manager should be able to see everything at a glance without drilling in
|
||||
3. **Inline editing where possible** — don't navigate away for simple changes
|
||||
4. **Clear feedback on every action** — toast notifications for success/error
|
||||
5. **Manager cannot be locked out** — if cloud check-in fails, manager still has full local access
|
||||
113
PLANS AND STRATEGIES/04_CLOUD_BACKEND.md
Normal file
113
PLANS AND STRATEGIES/04_CLOUD_BACKEND.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Guide 04 — Cloud Backend (FastAPI on VPS)
|
||||
|
||||
## Overview
|
||||
A lightweight FastAPI backend hosted on your VPS. It does NOT handle restaurant operations — that's the local backend's job. Its sole responsibilities are: site registration, license management, remote lock/unlock, and receiving periodic heartbeats from local backends.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
cloud_backend/
|
||||
├── main.py
|
||||
├── config.py # VPS settings, master secret key
|
||||
├── database.py # PostgreSQL (or SQLite for simplicity)
|
||||
├── models/
|
||||
│ ├── site.py
|
||||
│ └── admin.py
|
||||
├── schemas/
|
||||
├── routers/
|
||||
│ ├── auth.py # Sysadmin login
|
||||
│ ├── sites.py # Site management
|
||||
│ └── heartbeat.py # Receives check-ins from local backends
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### `admins` table
|
||||
```
|
||||
id INTEGER PK
|
||||
username TEXT UNIQUE
|
||||
password_hash TEXT
|
||||
role TEXT # 'sysadmin' (you, always)
|
||||
```
|
||||
|
||||
### `sites` table
|
||||
```
|
||||
id INTEGER PK
|
||||
site_id TEXT UNIQUE # UUID, generated on registration
|
||||
name TEXT # Restaurant name
|
||||
owner_name TEXT
|
||||
contact_email TEXT
|
||||
secret_key TEXT # Shared secret for heartbeat auth (hashed)
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
is_locked BOOLEAN DEFAULT FALSE
|
||||
lock_reason TEXT NULL
|
||||
license_expires_at DATETIME
|
||||
created_at DATETIME
|
||||
last_seen_at DATETIME NULL # Updated on each heartbeat
|
||||
last_seen_ip TEXT NULL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Auth — `/api/auth`
|
||||
```
|
||||
POST /login
|
||||
Body: { username, password }
|
||||
Returns: JWT token (for sysadmin panel use)
|
||||
```
|
||||
|
||||
### Sites — `/api/sites`
|
||||
All routes require sysadmin JWT.
|
||||
```
|
||||
GET / # List all sites with status
|
||||
POST / # Register new site → returns site_id + secret_key
|
||||
GET /:site_id # Site detail + last heartbeat info
|
||||
PUT /:site_id # Update site info / extend license
|
||||
POST /:site_id/lock # Remotely lock a site
|
||||
Body: { reason: "..." }
|
||||
POST /:site_id/unlock # Remotely unlock a site
|
||||
DELETE /:site_id # Deregister site
|
||||
```
|
||||
|
||||
### Heartbeat — `/api/heartbeat`
|
||||
Called by local backends every 6 hours. No sysadmin auth — uses site's own secret key.
|
||||
```
|
||||
POST /
|
||||
Header: X-Site-ID: <site_id>
|
||||
Header: X-Site-Key: <secret_key>
|
||||
Body: { version: "1.0.0", uptime_seconds: 12345 }
|
||||
Returns: {
|
||||
licensed: true,
|
||||
locked: false,
|
||||
lock_reason: null,
|
||||
expires_at: "2026-12-31T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Health — `/health`
|
||||
```
|
||||
GET /health # Public, no auth. Cloud liveness check.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment Notes
|
||||
- Host on your VPS with nginx reverse proxy + SSL (Let's Encrypt)
|
||||
- Use PostgreSQL for the cloud DB (more robust than SQLite for a server)
|
||||
- Run with: `uvicorn main:app --host 0.0.0.0 --port 8001`
|
||||
- Set up systemd service for auto-restart
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
- The `secret_key` per site is generated once at registration and never shown again (like an API key)
|
||||
- Local backends store it in their `config.py` / environment variable
|
||||
- Heartbeat endpoint rate-limited to prevent abuse
|
||||
- All sysadmin routes require JWT with short expiry
|
||||
73
PLANS AND STRATEGIES/05_SYSADMIN_PANEL.md
Normal file
73
PLANS AND STRATEGIES/05_SYSADMIN_PANEL.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Guide 05 — Sysadmin Panel (React + Vite, Cloud-hosted)
|
||||
|
||||
## Overview
|
||||
A simple web app hosted on your VPS alongside the cloud backend. Used exclusively by you (sysadmin) to manage all registered restaurant sites. Not used in day-to-day restaurant operations.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
sysadmin_panel/
|
||||
├── src/
|
||||
│ ├── main.jsx
|
||||
│ ├── App.jsx
|
||||
│ ├── api/client.js # Points to CLOUD backend URL
|
||||
│ ├── store/authStore.js
|
||||
│ ├── pages/
|
||||
│ │ ├── LoginPage.jsx
|
||||
│ │ ├── SitesPage.jsx # Home — list of all sites
|
||||
│ │ ├── SiteDetailPage.jsx
|
||||
│ │ └── RegisterSitePage.jsx
|
||||
│ └── components/
|
||||
│ ├── SiteCard.jsx
|
||||
│ ├── LicenseStatus.jsx
|
||||
│ └── ConfirmModal.jsx
|
||||
└── package.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pages
|
||||
|
||||
### LoginPage
|
||||
- Username + password (full password, not PIN — this is your personal admin tool)
|
||||
- Redirects to SitesPage on success
|
||||
|
||||
### SitesPage
|
||||
- Cards or table of all registered sites
|
||||
- Each shows: name, owner, license expiry, last heartbeat, status (active/locked)
|
||||
- Color indicator: green (active, recent heartbeat) | yellow (no heartbeat >12h) | red (locked/expired)
|
||||
- "Register New Site" button
|
||||
- Click site → SiteDetailPage
|
||||
|
||||
### SiteDetailPage
|
||||
- Site info: name, owner, contact, site ID
|
||||
- License section: expiry date, "Extend License" button (date picker)
|
||||
- Status section: last heartbeat timestamp + IP, current lock status
|
||||
- Actions:
|
||||
- Lock Site (with reason input) → calls `POST /api/sites/:id/lock`
|
||||
- Unlock Site → calls `POST /api/sites/:id/unlock`
|
||||
- Delete/Deregister site (with confirmation)
|
||||
- Heartbeat history (last 10 check-ins): timestamp + IP
|
||||
|
||||
### RegisterSitePage
|
||||
- Form: restaurant name, owner name, contact email
|
||||
- On submit: calls `POST /api/sites` → displays the generated `site_id` and `secret_key`
|
||||
- **One-time display warning**: "Copy this secret key now. It will not be shown again."
|
||||
- These credentials are then configured in the local backend's environment
|
||||
|
||||
---
|
||||
|
||||
## UI Design Direction
|
||||
- **Theme**: Dark, utilitarian. This is your ops tool, not a customer-facing product.
|
||||
- **Accent**: Cyan or electric blue. Clear status colors (green/yellow/red) for site health.
|
||||
- **Density**: Compact. You want to see all sites at a glance.
|
||||
- No over-design needed — functional and clear is the goal.
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
- Build with `vite build`, serve static files via nginx on your VPS
|
||||
- Or run as a separate Vite dev server proxied through nginx
|
||||
- Protect with nginx basic auth as an extra layer (optional but recommended)
|
||||
68
PLANS AND STRATEGIES/CLAUDE_CODE_INSTRUCTIONS.md
Normal file
68
PLANS AND STRATEGIES/CLAUDE_CODE_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Claude Code — Session Instructions
|
||||
|
||||
This file is your starting point for every Claude Code session on this project.
|
||||
Paste it (or reference it) at the start of each session to give Claude Code full context.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
## 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)
|
||||
|
||||
## 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).
|
||||
- Always commit before starting a new phase or major refactor.
|
||||
- Keep commit messages short and descriptive. No co-author lines needed.
|
||||
- Never commit `.env`, `*.db`, or `license_state.json` — they are in `.gitignore`.
|
||||
|
||||
## Ground Rules for Claude Code
|
||||
1. **Read the guide before writing code.** Each guide has schema, endpoints, and UX specs. Follow them.
|
||||
2. **Local backend first.** Nothing else can be built or tested without it.
|
||||
3. **Ask before deviating.** If something in the spec seems wrong or ambiguous, ask — don't invent.
|
||||
4. **Keep business logic in the backend.** Frontends are display + interaction only.
|
||||
5. **Never store sensitive data in frontend localStorage beyond token + username.**
|
||||
6. **All prices are stored and calculated on the backend.** Frontend only displays them.
|
||||
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] Scaffolded (models, schemas, all routers, printer service, license middleware, Docker). Needs first run + smoke test.
|
||||
> Phase 2: Waiter PWA — [ ] Not Started
|
||||
> Phase 3: Manager Dashboard — [ ] Not Started
|
||||
> Phase 4: Cloud Backend — [ ] Not Started
|
||||
> Phase 5: Sysadmin Panel — [ ] Not Started
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Local Backend (.env)
|
||||
```
|
||||
SITE_ID=
|
||||
CLOUD_URL=https://your-vps.com
|
||||
SECRET_KEY=generate-a-long-random-string
|
||||
LICENSE_GRACE_HOURS=24
|
||||
DATABASE_URL=sqlite:///./pos.db
|
||||
```
|
||||
|
||||
### Waiter PWA (.env)
|
||||
```
|
||||
VITE_API_URL=http://192.168.1.10:8000
|
||||
```
|
||||
|
||||
### Manager Dashboard (.env)
|
||||
```
|
||||
VITE_API_URL=http://192.168.1.10:8000
|
||||
```
|
||||
|
||||
### Cloud Backend (.env)
|
||||
```
|
||||
SECRET_KEY=different-long-random-string
|
||||
DATABASE_URL=postgresql://... (or sqlite for dev)
|
||||
```
|
||||
Reference in New Issue
Block a user