22"""Read-only LAN dashboard for an existing V100 installation; no model reload."""
33
44import argparse
5+ import base64
6+ import hashlib
57import ipaddress
68import json
79import math
1012import sys
1113import threading
1214import time
15+ from html .parser import HTMLParser
1316from http .server import BaseHTTPRequestHandler , ThreadingHTTPServer
1417from pathlib import Path
1518from urllib .parse import unquote , urlsplit
2831:root{color-scheme:dark}body{font:15px system-ui;background:#101721;color:#e9eef6;margin:0;padding:24px;max-width:1200px;margin:auto}h1{font-size:26px}h2{font-size:19px}small,.muted{color:#adbdd1}a{color:#9ac5ff}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.card,section{background:#192433;border:1px solid #304259;border-radius:12px;padding:18px;margin:14px 0}.card{margin:0}.value{display:block;font-size:22px;margin-top:8px;overflow-wrap:anywhere}progress{width:100%;height:24px}pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;max-height:420px;overflow:auto}table{border-collapse:collapse;width:100%}td,th{text-align:left;padding:10px;border-bottom:1px solid #304259}svg{width:100%;max-height:250px}#warning{color:#ffbf86}button{padding:8px 14px;background:#294464;border:0;color:white;border-radius:6px;cursor:pointer}
2932</style>
3033<h1>V100 - postęp misji</h1><p class="muted">Podgląd odczytu. Paper P&L i backtesty nie są realnym dochodem.</p>
31- <p id="fresh">Ładowanie danych...</p><p id="warning"></p><button id="reload">Odśwież widok</button>
34+ <p id="fresh">Ładowanie danych...</p><p id="warning"></p><p id="layout" class="muted"></p>< button id="reload">Odśwież widok</button>
3235<div id="cards" class="grid"></div>
3336<section><h2>Testy bieżącego przebiegu</h2><p id="evaluation"></p><progress id="progress" max="1" value="0"></progress><p id="case" class="muted"></p></section>
3437<section><h2>Ostatnie backtesty - eksploracyjne</h2><p class="muted">Wynik netto prób historycznych. Założenia kosztów nie są potwierdzeniem dostępnych opłat ani przewagi strategii.</p><svg id="chart" viewBox="0 0 800 220" role="img" aria-label="Wyniki ostatnich backtestów"></svg><table><thead><tr><th>Próba</th><th>Wynik netto</th><th>Obsunięcie</th><th>Wykonania</th></tr></thead><tbody id="backtests"></tbody></table></section>
3942<details><summary>Dowody treningu i ostatnie błędy</summary><pre id="evidence"></pre></details>
4043<script>
4144const $=id=>document.getElementById(id), fmt=x=>x===null||x===undefined?'brak potwierdzonych danych':String(x), pct=x=>typeof x==='number'&&Number.isFinite(x)?(x*100).toFixed(2)+'%':'brak danych';
45+ const layoutVersion='__LAYOUT_SHA__';
4246function card(label,value){const node=document.createElement('div');node.className='card';const title=document.createElement('small');title.textContent=label;const strong=document.createElement('strong');strong.className='value';strong.textContent=fmt(value);node.append(title,strong);$('cards').append(node)}
4347function svg(tag,attrs,text){let node=document.createElementNS('http://www.w3.org/2000/svg',tag);for(const [key,val] of Object.entries(attrs))node.setAttribute(key,val);if(text!==undefined)node.textContent=text;return node}
4448function render(data){const m=data.mission||{},r=data.report||{},ev=m.evaluation||{},learn=m.learning||{},gpu=data.gpu||{};
49+ if(data.layout?.active_sha256&&layoutVersion!==data.layout.active_sha256){location.reload();return}$('layout').textContent='HTML mastera: /workspace/dashboard/index.html | '+(data.layout?.state||'oczekiwanie')+(data.layout?.error?' | '+data.layout.error:'');
4550$('fresh').textContent='Zebrano: '+new Date(data.collected_at*1000).toLocaleString()+' | Raport zbierany co 20 s | Przebieg: '+fmt(m.run);
4651$('warning').textContent=(data.errors||[]).join('\n');$('cards').replaceChildren();card('Misja',m.running?'działa':'zatrzymana');card('Faza',m.state?.phase);card('Cykle uczenia',learn.completed_cycles);card('Zaakceptowane aktualizacje wag',r.accepted_weight_updates_this_run);card('Potwierdzone kroki optymalizatora',r.mission_evidence?.optimizer_updates_observed);card('Kontekst',m.state?.context_window);card('GPU - wykorzystanie',gpu.utilization===undefined?null:gpu.utilization+'%');card('VRAM zajęty / razem',gpu.memory_used===undefined?null:gpu.memory_used+' / '+gpu.memory_total+' MiB');card('GPU - temperatura / moc',gpu.temperature===undefined?null:gpu.temperature+' °C / '+gpu.power+' W');
4752$('evaluation').textContent=ev.total?`${ev.completed}/${ev.total} ukończonych, ${ev.passed} zaliczonych (${ev.state})`:'Brak bieżącego licznika ewaluacji';$('progress').max=ev.total||1;$('progress').value=ev.completed||0;$('case').textContent=ev.current_case?('Obecny przypadek: '+ev.current_case):'';
5459</script></html>"""
5560
5661
62+ APP_SCRIPT = PAGE .split ("<script>" , 1 )[1 ].split ("</script>" , 1 )[0 ]
63+ BASE_TEMPLATE = PAGE .split ("<script>" , 1 )[0 ] + "</html>"
64+ GUEST_TEMPLATE = "/workspace/dashboard/index.html"
65+ REQUIRED_IDS = set (re .findall (r"\$\('([a-z]+)'\)" , APP_SCRIPT ))
66+
67+
68+ def validate_css (value ):
69+ value = re .sub (r"/\*.*?\*/" , "" , value , flags = re .S ).lower ()
70+ if "\\ " in value or re .search (r"@import|url\s*\(|expression\s*\(|behavior\s*:" , value ):
71+ raise ValueError ("CSS must not load external resources or execute code" )
72+
73+
74+ class LayoutValidator (HTMLParser ):
75+ allowed_tags = set (
76+ "html head body title meta style h1 h2 h3 h4 h5 h6 p div span section article header footer main nav aside strong small em b i br hr ul ol li a button pre code details summary progress table thead tbody tfoot tr th td svg g rect line polyline polygon circle path text defs lineargradient stop" .split ()
77+ )
78+
79+ def __init__ (self ):
80+ super ().__init__ (convert_charrefs = True )
81+ self .ids = set ()
82+ self .in_style = False
83+ self .styles = []
84+
85+ def handle_starttag (self , tag , attrs ):
86+ if tag not in self .allowed_tags :
87+ raise ValueError ("Unsupported layout element: " + tag )
88+ if tag == "style" :
89+ self .in_style = True
90+ for name , value in attrs :
91+ value = value or ""
92+ if name .startswith ("on" ) or name in (
93+ "src" ,
94+ "srcdoc" ,
95+ "http-equiv" ,
96+ "action" ,
97+ "formaction" ,
98+ "xlink:href" ,
99+ ):
100+ raise ValueError ("Active or external layout attribute is not allowed" )
101+ if name == "href" and not (value .startswith ("#" ) or value .startswith ("/paper/" )):
102+ raise ValueError ("Layout links must target existing paper reports or anchors" )
103+ if name == "href" and any (char in value for char in ("\\ " , "\n " , "\r " )):
104+ raise ValueError ("Invalid layout link" )
105+ if name == "style" :
106+ validate_css (value )
107+ if name == "id" :
108+ if value in self .ids :
109+ raise ValueError ("Duplicate layout ID: " + value )
110+ self .ids .add (value )
111+
112+ def handle_endtag (self , tag ):
113+ if tag not in self .allowed_tags :
114+ raise ValueError ("Unsupported closing element" )
115+ if tag == "style" :
116+ self .in_style = False
117+
118+ def handle_data (self , data ):
119+ if self .in_style :
120+ self .styles .append (data )
121+
122+
123+ def validate_template (content ):
124+ if not isinstance (content , str ) or not 1 <= len (content .encode ()) <= 256 * 1024 :
125+ raise ValueError ("Layout must contain 1..262144 UTF-8 bytes" )
126+ parser = LayoutValidator ()
127+ parser .feed (content )
128+ parser .close ()
129+ if parser .in_style or content .count ("<!--" ) != content .count ("-->" ):
130+ raise ValueError ("Close style elements and HTML comments" )
131+ validate_css ("" .join (parser .styles ))
132+ missing = REQUIRED_IDS - parser .ids
133+ if missing :
134+ raise ValueError ("Keep dashboard component IDs: " + ", " .join (sorted (missing )))
135+ if not re .search (r"</html\s*>\s*\Z" , content , re .I ):
136+ raise ValueError ("Keep the closing html element" )
137+ return hashlib .sha256 (content .encode ()).hexdigest ()
138+
139+
140+ def render_template (content , identity ):
141+ script = "<script>" + APP_SCRIPT .replace ("__LAYOUT_SHA__" , identity ) + "</script>"
142+ return re .sub (
143+ r"</html\s*>\s*\Z" , lambda match : script + match .group (), content , count = 1 , flags = re .I
144+ )
145+
146+
147+ def guest_layout (root ):
148+ from rlm .v100 .desktop import run
149+
150+ seed = base64 .b64encode (BASE_TEMPLATE .encode ()).decode ()
151+ program = (
152+ "import base64,json\n from pathlib import Path\n "
153+ f"p=Path({ GUEST_TEMPLATE !r} )\n p.parent.mkdir(parents=True,exist_ok=True)\n "
154+ f"if not p.exists(): p.write_bytes(base64.b64decode({ seed !r} ))\n "
155+ "if p.stat().st_size>262144: raise ValueError('Layout exceeds 256 KiB')\n "
156+ "print(json.dumps({'html_base64':base64.b64encode(p.read_bytes()).decode()}))\n "
157+ )
158+ script = "python3 - <<'V100_LAYOUT'\n " + program + "V100_LAYOUT\n "
159+ result = run (root , script , seconds = 8 , output_limit = 360000 )
160+ if result ["exit_code" ]:
161+ raise RuntimeError (result ["stderr" ][:300 ] or "Guest layout unavailable" )
162+ value = json .loads (result ["stdout" ])
163+ return base64 .b64decode (value ["html_base64" ], validate = True ).decode ()
164+
165+
57166def validate_bind (address ):
58167 ip = ipaddress .ip_address (address )
59168 if (
@@ -102,6 +211,57 @@ def __init__(self, root, mission_reader):
102211 self .mission_reader = mission_reader
103212 self .data = {"collected_at" : time .time (), "errors" : ["Pierwsze zbieranie danych trwa" ]}
104213 self .stop = threading .Event ()
214+ self .layout = {"guest_file" : GUEST_TEMPLATE , "state" : "waiting for guest template" }
215+ self .page = render_template (BASE_TEMPLATE , validate_template (BASE_TEMPLATE ))
216+ saved = self .root / "research/dashboard/index.html"
217+ content , failure = BASE_TEMPLATE , None
218+ try :
219+ if saved .exists ():
220+ if saved .stat ().st_size > 256 * 1024 :
221+ raise ValueError ("Saved layout exceeds 256 KiB" )
222+ content = saved .read_text ()
223+ validate_template (content )
224+ except (OSError , ValueError ) as error :
225+ content , failure = BASE_TEMPLATE , str (error )[:300 ]
226+ self .publish_layout (content )
227+ if failure :
228+ self .layout ["error" ] = failure
229+
230+ def publish_layout (self , content ):
231+ identity = validate_template (content )
232+ folder = self .root / "research/dashboard"
233+ folder .mkdir (parents = True , exist_ok = True , mode = 0o700 )
234+ history = folder / "layouts"
235+ history .mkdir (exist_ok = True , mode = 0o700 )
236+ version = history / (identity + ".html" )
237+ if not version .exists ():
238+ version .write_text (content )
239+ if self .layout .get ("active_sha256" ) != identity :
240+ temporary = folder / "index.html.tmp"
241+ temporary .write_text (content )
242+ temporary .replace (folder / "index.html" )
243+ self .page = render_template (content , identity )
244+ self .layout = {
245+ "guest_file" : GUEST_TEMPLATE ,
246+ "state" : "validated layout active" ,
247+ "active_sha256" : identity ,
248+ "published_at" : time .time (),
249+ }
250+
251+ def sync_layout (self ):
252+ try :
253+ self .publish_layout (guest_layout (self .root ))
254+ except (OSError , ValueError , RuntimeError , KeyError , TypeError ) as error :
255+ self .layout = {
256+ ** self .layout ,
257+ "state" : "previous validated layout retained" ,
258+ "error" : str (error )[:300 ],
259+ }
260+ folder = self .root / "research/dashboard"
261+ temporary = folder / "layout-status.json.tmp"
262+ temporary .write_text (json .dumps (self .layout , ensure_ascii = False ))
263+ temporary .replace (folder / "layout-status.json" )
264+ self .data = {** self .data , "layout" : self .layout }
105265
106266 def refresh (self ):
107267 errors , report , gpu = [], {}, {}
@@ -186,6 +346,7 @@ def loop(self):
186346 while not self .stop .is_set ():
187347 try :
188348 self .refresh ()
349+ self .sync_layout ()
189350 except (OSError , ValueError , RuntimeError ) as error :
190351 self .data = {"collected_at" : time .time (), "errors" : [str (error )[:600 ]]}
191352 self .stop .wait (20 )
@@ -228,7 +389,7 @@ def do_GET(self): # noqa: N802 - standard HTTP callback
228389 route = urlsplit (self .path ).path
229390 try :
230391 if route == "/" :
231- body , mime = PAGE .encode (), "text/html; charset=utf-8"
392+ body , mime = self . server . state . page .encode (), "text/html; charset=utf-8"
232393 elif route == "/api/status" :
233394 body , mime = (
234395 json .dumps (
@@ -249,7 +410,11 @@ def do_GET(self): # noqa: N802 - standard HTTP callback
249410 self .send_header ("X-Content-Type-Options" , "nosniff" )
250411 self .send_header (
251412 "Content-Security-Policy" ,
252- "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'"
413+ "default-src 'none'; script-src 'sha256-"
414+ + base64 .b64encode (
415+ hashlib .sha256 (body .split (b"<script>" , 1 )[1 ].split (b"</script>" , 1 )[0 ]).digest ()
416+ ).decode ()
417+ + "'; style-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
253418 if route == "/"
254419 else "default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'" ,
255420 )
0 commit comments