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()