Backend (deployed live on AD2, service restarted, + repo copy resynced — it was far behind the deployed server): - /api/search: add whitelisted sort/dir (NULLS LAST) so sortable headers and the "Latest uploads" preset work. web_status filter and POST /api/upload already existed on the server; the stale repo copy now matches live. Frontend (redesign prototype): - "Latest uploads" preset (web_status=on + sort=api_uploaded_at desc) and "Not yet published" (web_status=off) are now active presets. - Push to Web (inspector) + Re-push (multi-select) wired to POST /api/upload behind a confirm() gate; refresh WEB status after. Validated idempotently on a published record (unchanged:1, errors:0). - "Retested units" stays disabled — needs a retest flag in the pipeline (next). tools/preview-proxy.py: forward POST so the publish buttons work in same-origin preview. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
3.0 KiB
Python
72 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Same-origin preview proxy for the testdatadb front-end.
|
|
|
|
Serves the static prototype from ROOT and reverse-proxies /api/* to the live
|
|
AD2 testdatadb server, so the app AND the cert iframe share one origin
|
|
(http://127.0.0.1:PORT). That lets the same-origin-only cert styling/fit logic
|
|
(styleCert/fitCert read iframe.contentDocument) actually run during preview —
|
|
which it can't when the iframe is loaded cross-origin straight from AD2.
|
|
|
|
Usage: python preview-proxy.py <port> <root-dir> [target]
|
|
"""
|
|
import http.server, socketserver, urllib.request, urllib.error, os, sys
|
|
|
|
PORT = int(sys.argv[1])
|
|
ROOT = os.path.abspath(sys.argv[2]) if len(sys.argv) > 2 else "."
|
|
TARGET = sys.argv[3] if len(sys.argv) > 3 else "http://192.168.0.6:3000"
|
|
|
|
class H(http.server.BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path.startswith("/api/"):
|
|
try:
|
|
with urllib.request.urlopen(TARGET + self.path, timeout=30) as r:
|
|
body = r.read(); ct = r.headers.get("Content-Type", "application/octet-stream")
|
|
self._send(200, ct, body)
|
|
except urllib.error.HTTPError as e:
|
|
self._send(e.code, "application/json", e.read())
|
|
except Exception as e:
|
|
self._send(502, "text/plain", str(e).encode())
|
|
else:
|
|
p = self.path.split("?")[0]
|
|
p = "/index.html" if p == "/" else p
|
|
fp = os.path.join(ROOT, p.lstrip("/"))
|
|
if os.path.isfile(fp):
|
|
ct = "text/html; charset=utf-8" if fp.endswith(".html") else "application/octet-stream"
|
|
self._send(200, ct, open(fp, "rb").read())
|
|
else:
|
|
self._send(404, "text/plain", b"not found")
|
|
|
|
def do_POST(self):
|
|
if self.path.startswith("/api/"):
|
|
n = int(self.headers.get("Content-Length", 0))
|
|
body = self.rfile.read(n) if n else b""
|
|
req = urllib.request.Request(
|
|
TARGET + self.path, data=body, method="POST",
|
|
headers={"Content-Type": self.headers.get("Content-Type", "application/json")})
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=120) as r:
|
|
self._send(200, r.headers.get("Content-Type", "application/json"), r.read())
|
|
except urllib.error.HTTPError as e:
|
|
self._send(e.code, "application/json", e.read())
|
|
except Exception as e:
|
|
self._send(502, "text/plain", str(e).encode())
|
|
else:
|
|
self._send(404, "text/plain", b"not found")
|
|
|
|
def _send(self, code, ct, body):
|
|
self.send_response(code)
|
|
self.send_header("Content-Type", ct)
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.send_header("Cache-Control", "no-store")
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, *a): pass
|
|
|
|
class S(socketserver.ThreadingTCPServer):
|
|
allow_reuse_address = True
|
|
|
|
print(f"preview proxy: http://127.0.0.1:{PORT} root={ROOT} -> {TARGET}")
|
|
S(("127.0.0.1", PORT), H).serve_forever()
|