#!/usr/bin/env python3 """Serve admin-h5 dist for /psy/admin (nginx strips prefix → this listens on :8003).""" from __future__ import annotations import mimetypes import os import sys from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path ROOT = Path(__file__).resolve().parent DIST = ROOT / "apps" / "admin-h5" / "dist" PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8003 class Handler(BaseHTTPRequestHandler): def log_message(self, fmt: str, *args) -> None: sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args)) def do_GET(self) -> None: # noqa: N802 path = self.path.split("?", 1)[0] if path in ("", "/"): path = "/index.html" # security: no path escape rel = path.lstrip("/") candidate = (DIST / rel).resolve() if not str(candidate).startswith(str(DIST.resolve())): self.send_error(400) return if candidate.is_file(): self._file(candidate) return # hashed asset miss → 404 (not SPA), so MIME errors are obvious if "/assets/" in path or path.endswith((".js", ".css", ".map", ".png", ".svg", ".ico", ".woff2")): self.send_error(404) return index = DIST / "index.html" if index.is_file(): self._file(index) return self.send_error(404) def _file(self, file_path: Path) -> None: data = file_path.read_bytes() ctype, _ = mimetypes.guess_type(str(file_path)) self.send_response(200) self.send_header("Content-Type", ctype or "application/octet-stream") self.send_header("Content-Length", str(len(data))) self.send_header("Cache-Control", "no-cache" if file_path.name == "index.html" else "public, max-age=86400") self.end_headers() self.wfile.write(data) def main() -> None: if not DIST.is_dir(): raise SystemExit(f"admin dist missing: {DIST} (run npm run build:admin)") httpd = ThreadingHTTPServer(("127.0.0.1", PORT), Handler) print(f"admin-h5 on http://127.0.0.1:{PORT}/ (nginx → /psy/admin/)", flush=True) httpd.serve_forever() if __name__ == "__main__": main()