initial commit
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
# Local Testing — Serving SPQ Builds
|
||||
|
||||
Two approaches depending on which version you're testing.
|
||||
|
||||
## v1: Vanilla JS (standalone HTML files)
|
||||
|
||||
Served from `/mnt/seagate8tb/Websites/ShopProQuote/` (no build step, just HTML+JS+CSS).
|
||||
|
||||
```bash
|
||||
cd /mnt/seagate8tb/Websites/ShopProQuote
|
||||
python3 -m http.server 4173 --bind 0.0.0.0
|
||||
```
|
||||
|
||||
## v2: React SPA (built with Vite)
|
||||
|
||||
Served from `/mnt/seagate8tb/Websites/ShopProQuote/spq-v2/dist/`. **Plain http.server breaks login** because the built frontend connects to PocketBase at `/pb` (relative path). The simple server returns 404 for `/pb/*` requests. You need a Python reverse proxy that:
|
||||
|
||||
1. Serves static files from `dist/`
|
||||
2. Has SPA fallback (all non-file routes → `index.html`)
|
||||
3. Proxies `/pb/*` → PocketBase at `127.0.0.1:8091/api/*`
|
||||
4. Sends CORS headers on every response (including OPTIONS preflight)
|
||||
|
||||
### Template: `/tmp/spq-backup-server.py`
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Serve SPQ dist + proxy /pb to PocketBase with CORS."""
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
import urllib.request
|
||||
import os
|
||||
|
||||
DIST = '/mnt/seagate8tb/Websites/ShopProQuote/spq-v2/dist'
|
||||
PB = 'http://127.0.0.1:8091'
|
||||
|
||||
class Handler(SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=DIST, **kwargs)
|
||||
|
||||
def end_headers(self):
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Authorization, Content-Type')
|
||||
super().end_headers()
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
if self.path.startswith('/pb') or self.path.startswith('/api') or self.path.startswith('/deepseek'):
|
||||
self.proxy()
|
||||
else:
|
||||
full = os.path.join(DIST, self.path.lstrip('/'))
|
||||
if not os.path.exists(full) or (os.path.isdir(full) and self.path != '/'):
|
||||
self.path = '/index.html'
|
||||
super().do_GET()
|
||||
|
||||
def do_POST(self):
|
||||
if self.path.startswith('/pb') or self.path.startswith('/api') or self.path.startswith('/deepseek'):
|
||||
self.proxy()
|
||||
else:
|
||||
self.send_error(405)
|
||||
|
||||
def do_PUT(self): self.proxy()
|
||||
def do_PATCH(self): self.proxy()
|
||||
def do_DELETE(self): self.proxy()
|
||||
|
||||
def proxy(self):
|
||||
# PB SDK calls /pb/api/collections/... — path already includes /api/
|
||||
# DeepSeek calls go to api.deepseek.com
|
||||
if self.path.startswith('/deepseek'):
|
||||
url = 'https://api.deepseek.com' + self.path[len('/deepseek'):]
|
||||
else:
|
||||
url = PB + self.path
|
||||
if self.path.startswith('/pb'):
|
||||
url = PB + self.path[3:] # /pb/api/... → /api/... on PB
|
||||
if not url.startswith(PB + '/api'):
|
||||
url = PB + '/api' + self.path[3:]
|
||||
body = None
|
||||
if self.headers.get('Content-Length'):
|
||||
body = self.rfile.read(int(self.headers['Content-Length']))
|
||||
req = urllib.request.Request(url, data=body, method=self.command)
|
||||
for k, v in self.headers.items():
|
||||
if k.lower() not in ('host', 'connection', 'origin', 'referer'):
|
||||
req.add_header(k, v)
|
||||
# Add DeepSeek API key
|
||||
if self.path.startswith('/deepseek'):
|
||||
with open('/tmp/deepseek_key.txt') as f:
|
||||
req.add_header('Authorization', f'Bearer {f.read().strip()}')
|
||||
try:
|
||||
resp = urllib.request.urlopen(req)
|
||||
self.send_response(resp.status)
|
||||
for k, v in resp.headers.items():
|
||||
if k.lower() not in ('transfer-encoding', 'connection'):
|
||||
self.send_header(k, v)
|
||||
self.end_headers()
|
||||
self.wfile.write(resp.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
self.send_response(e.code)
|
||||
self.end_headers()
|
||||
self.wfile.write(e.read())
|
||||
|
||||
if __name__ == '__main__':
|
||||
server = HTTPServer(('0.0.0.0', 4173), Handler)
|
||||
print('Serving SPQ on http://0.0.0.0:4173 (PB proxy → 8091)')
|
||||
server.serve_forever()
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
**IMPORTANT**: The proxy also routes `/deepseek/*` → `https://api.deepseek.com/` using the API key from `/tmp/deepseek_key.txt`. The key is extracted from `/etc/nginx/sites-enabled/shopproquote` on startup. Without this, AI Write, Generate Priorities, and AI Suggest all fail silently.
|
||||
|
||||
```bash
|
||||
# Extract API key and start proxy
|
||||
python3 -c "
|
||||
import re
|
||||
with open('/etc/nginx/sites-enabled/shopproquote') as f:
|
||||
m = re.search(r'Bearer\s+(sk-\S+)', f.read())
|
||||
if m:
|
||||
with open('/tmp/deepseek_key.txt','w') as out:
|
||||
out.write(m.group(1).rstrip('\"'))
|
||||
print('Key saved')
|
||||
"
|
||||
|
||||
python3 /tmp/spq-backup-server.py &
|
||||
# Verify
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:4173/
|
||||
# → 200
|
||||
curl -s -X POST http://localhost:4173/pb/collections/users/auth-with-password \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"identity":"demo@shop.com","password":"test1234"}'
|
||||
# → {"token":"..."} (login works through proxy)
|
||||
|
||||
# Verify DeepSeek
|
||||
curl -s -X POST http://localhost:4173/deepseek/v1/chat/completions \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"Say hello"}],"temperature":0,"thinking":{"type":"disabled"},"max_tokens":50}'
|
||||
# → {"choices":[{"message":{"content":"Hello!"}}]} (AI works)
|
||||
|
||||
### Pitfalls
|
||||
|
||||
- **No `--bind 0.0.0.0` on plain http.server** → LAN devices get connection refused. The proxy template above binds `0.0.0.0` by default.
|
||||
- **Missing CORS headers** → browser blocks all PB requests. The proxy adds CORS on every response + handles OPTIONS preflight.
|
||||
- **Missing SPA fallback** → `/dashboard`, `/quote` etc. return 404 on page refresh. The proxy falls back to `index.html` for non-file paths.
|
||||
- **PB URL mismatch** — v2's `pocketbase.ts` uses `VITE_PB_URL` env var (defaults to `/pb`). If the build was made with a different URL, adjust the proxy or rebuild.
|
||||
- **Backup paths vary** — the timestamped backup folder (`ShopProQuote.backup-YYYYMMDD-HHMM`) changes per backup. Update DIST path in the script.
|
||||
- **Dashboard 400 / Something went wrong** — if the PocketBase quotes collection is missing the created system field, the sort=-created query returns HTTP 400. Fix: use sort=-id instead. Same issue applies to repairOrders collection.
|
||||
|
||||
- **AI features return no results** — the /deepseek/ path must be proxied to https://api.deepseek.com/ with the API key from /tmp/deepseek_key.txt. The AI module at src/lib/ai.ts calls /deepseek/v1/chat/completions with model: deepseek-v4-flash and thinking: disabled. Without proxy setup, all AI calls return null.
|
||||
|
||||
- **React input focus loss** — if a ServiceRow-like component is defined as a nested function inside its parent, every zustand state update causes a re-render that creates a new function identity. React unmounts/remounts the DOM, killing input focus. Fix: extract to a file-level component wrapped in memo, pass all state via props.
|
||||
|
||||
## V2 Feature Coverage
|
||||
|
||||
As of 2026-06-27, spq-v2 covers ~90% of v1's feature set. See `references/v1-v2-feature-gap.md` for a detailed comparison table of what's built vs what's still only in v1. Missing PocketBase collections (`repair_orders`, `invoices`, `service_advisors`, `vehicles`) cause error states on some pages — create them via admin UI to unlock full functionality.
|
||||
|
||||
For mass porting patterns (parallel delegation), see `references/mass-port-delegation.md`.
|
||||
|
||||
## Common Port
|
||||
|
||||
Use **4173** for local SPQ testing. Check availability:
|
||||
|
||||
```bash
|
||||
ss -tlnp | grep 4173 || echo "port free"
|
||||
```
|
||||
|
||||
Kill any existing server: `pkill -f "spq-backup-server.py"` or `pkill -f "python3 -m http.server 4173"`
|
||||
Reference in New Issue
Block a user