#!/usr/bin/env python3 """Serve SPA dist + proxy /pb to PocketBase + /deepseek to api.deepseek.com. Reproducible from the spq-v2 deployment session. Drops into any PocketBase-backed React SPA project that uses /pb as its PocketBase base URL. Serves the Vite build output with SPA fallback, CORS headers, and PB/deepseek API proxying. Usage: python3 spa-pb-proxy.py Env vars (optional — defaults are for the ShopProQuote spq-v2 deployment): DIST_DIR - path to the Vite dist/ directory PB_URL - PocketBase URL (default: http://127.0.0.1:8091) PORT - listen port (default: 4173) DS_KEY_FILE - path to file containing DeepSeek API key (default: /tmp/deepseek_key.txt) """ import os import urllib.request from http.server import HTTPServer, SimpleHTTPRequestHandler DIST = os.environ.get('DIST_DIR', '/path/to/dist') PB = os.environ.get('PB_URL', 'http://127.0.0.1:8091') PORT = int(os.environ.get('PORT', '4173')) DS_KEY_FILE = os.environ.get('DS_KEY_FILE', '/tmp/deepseek_key.txt') 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', '/api', '/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', '/api', '/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): # DeepSeek API if self.path.startswith('/deepseek'): url = 'https://api.deepseek.com' + self.path[len('/deepseek'):] # PB SDK: /pb/api/... → http://pb/api/... elif self.path.startswith('/pb'): url = PB + self.path[3:] else: url = PB + self.path 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) if self.path.startswith('/deepseek') and os.path.exists(DS_KEY_FILE): with open(DS_KEY_FILE) 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', PORT), Handler) print(f'Serving {DIST} on http://0.0.0.0:{PORT} (PB → {PB}, DeepSeek → api.deepseek.com)') server.serve_forever()