Phase 1: scaffold local backend — models, schemas, routers, printer service, Docker

This commit is contained in:
2026-04-20 11:22:55 +03:00
commit 4ffe27df95
44 changed files with 2729 additions and 0 deletions

View 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.

View 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).

View 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 (09) + 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

View 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 (5264px), 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

View 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

View 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)

View 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)
```