From c2335e859df9c4451ca356c69e964db58daef037 Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Thu, 18 Jun 2026 09:45:34 -0700 Subject: [PATCH] dataforth/testdatadb UI: fix cert fit (transform-scale) + publish-state chips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fitCert: replace the flaky CSS `zoom` (Firefox support is recent/inconsistent) with transform:scale() measured against the widest line (+ right margin and font-load retries) so the cert always scales to fit the inspector with no horizontal clip. Validated live on a narrow 5B cert (0.74x) and a wide DSCA45 cert (0.55x) against the real AD2 dataset. - inspector Web field -> Published (green) / Not published (amber) chips. - widen default inspector 480 -> 500px. - tools/preview-proxy.py: serve the prototype AND reverse-proxy /api to the live AD2 server so the cert iframe is same-origin during preview — styleCert/fitCert read iframe.contentDocument, which silently no-ops when the iframe is loaded cross-origin straight from AD2 (why the fit looked broken in earlier previews). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../testdatadb-fix/public/index.redesign.html | 27 ++++++---- projects/dataforth-dos/tools/preview-proxy.py | 54 +++++++++++++++++++ 2 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 projects/dataforth-dos/tools/preview-proxy.py diff --git a/projects/dataforth-dos/testdatadb-fix/public/index.redesign.html b/projects/dataforth-dos/testdatadb-fix/public/index.redesign.html index 3884988d..8bb3c490 100644 --- a/projects/dataforth-dos/testdatadb-fix/public/index.redesign.html +++ b/projects/dataforth-dos/testdatadb-fix/public/index.redesign.html @@ -12,7 +12,7 @@ --hover:#f1f5f9; --sel:#e0e7ff; --desk:#e7ecf1; --mono:ui-monospace,"SFMono-Regular",Consolas,"Liberation Mono",monospace; --sans:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; - --r:6px; --insp:480px; + --r:6px; --insp:500px; } *{box-sizing:border-box} html,body{height:100%;margin:0} @@ -112,6 +112,10 @@ .pill{display:inline-block;font-size:10.5px;font-weight:700;padding:1px 8px;border-radius:4px;font-family:var(--mono)} .pill.PASS{background:var(--pass-bg);color:var(--pass-ink)} .pill.FAIL{background:var(--fail-bg);color:var(--fail-ink)} + .tag{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:2px 8px;border-radius:20px;font-family:var(--sans)} + .tag::before{content:"";width:7px;height:7px;border-radius:50%} + .tag.pub{background:var(--pass-bg);color:var(--pass-ink)} .tag.pub::before{background:var(--pass-ink)} + .tag.unpub{background:#fef3c7;color:#92400e} .tag.unpub::before{background:#d97706;background:none;box-shadow:inset 0 0 0 1.5px #d97706} .web{font-size:13px} .pager{display:flex;align-items:center;gap:10px;padding:7px 12px;border-top:1px solid var(--border);font-size:12px;color:var(--ink-2)} .pager button{font-size:12px;height:28px;padding:0 11px;border:1px solid var(--border-strong);border-radius:var(--r);background:#fff;cursor:pointer;color:var(--ink)} @@ -321,7 +325,7 @@ function select(id,auto){
Result
${esc(r.overall_result)}
Log
${esc(r.log_type)}
${r.work_order?`
WO
${esc(r.work_order)}
`:''} -
Web
${r.api_uploaded_at?'published '+fmtDate(r.api_uploaded_at):'not published'}
`; +
Web
${r.api_uploaded_at?`Published ${fmtDate(r.api_uploaded_at)}`:'Not published'}
`; const ds=API+'/api/datasheet/'+id; $('acts').style.display='flex'; $('acts').innerHTML=`Open ↗ @@ -338,18 +342,23 @@ function select(id,auto){ } function loadCert(ds){ $('viewer').innerHTML=''; - const f=$('dsframe'); f.onload=()=>{styleCert();fitCert();}; f.src=ds+'?format=html'; + const f=$('dsframe'); f.onload=()=>{styleCert();fitCert();setTimeout(fitCert,120);setTimeout(fitCert,350);}; f.src=ds+'?format=html'; } function styleCert(){ const f=$('dsframe'); try{ const doc=f.contentDocument; if(!doc)return; if(!doc.getElementById('_inj')){ const s=doc.createElement('style'); s.id='_inj'; - s.textContent='html,body{background:#fff!important;margin:0!important}body{padding:22px 26px!important;color:#0f172a}pre{margin:0;font-family:'+getComputedStyle(document.body).getPropertyValue('--mono')+';font-size:12.5px;line-height:1.32}'; + s.textContent='html,body{background:#fff!important;margin:0!important}body{padding:16px 20px!important;color:#0f172a}pre{margin:0;font-family:'+getComputedStyle(document.body).getPropertyValue('--mono')+';font-size:12.5px;line-height:1.32}'; (doc.head||doc.documentElement).appendChild(s); } }catch(e){} } -function fitCert(){ const f=$('dsframe'); if(!f)return; try{ const doc=f.contentDocument; if(!doc)return; - const root=doc.documentElement; root.style.zoom=''; - const nat=Math.max(doc.body?doc.body.scrollWidth:0,root.scrollWidth), av=f.clientWidth-2; - root.style.zoom=(nat>av)?Math.max(.45,av/nat):1; - f.style.height=Math.ceil((doc.body?doc.body.scrollHeight:600)*(nat>av?av/nat:1)+4)+'px'; +function fitCert(){ const f=$('dsframe'); if(!f)return; try{ const doc=f.contentDocument; if(!doc||!doc.body)return; + const b=doc.body, root=doc.documentElement; + // measure natural content width (transform doesn't reflow, so text never rewraps) + b.style.transform='none'; b.style.width='max-content'; b.style.transformOrigin='0 0'; + root.style.overflow='hidden'; + const nat=Math.max(b.scrollWidth,root.scrollWidth), av=f.clientWidth-12; // widest line + right margin + if(!nat){ return; } // not laid out yet — the retry will catch it + const k=nat>av ? Math.max(.4, av/nat) : 1; + b.style.transform='scale('+k+')'; + f.style.height=Math.ceil(b.scrollHeight*k+2)+'px'; // size the frame to the scaled cert, no v-scroll-in-frame }catch(e){} } function printCert(){ const f=$('dsframe'); if(f&&f.contentWindow){f.contentWindow.focus();f.contentWindow.print();} } window.addEventListener('resize',()=>{fitCert();}); diff --git a/projects/dataforth-dos/tools/preview-proxy.py b/projects/dataforth-dos/tools/preview-proxy.py new file mode 100644 index 00000000..4fe18223 --- /dev/null +++ b/projects/dataforth-dos/tools/preview-proxy.py @@ -0,0 +1,54 @@ +#!/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 [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 _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()