Files
weval-consulting/l99-saas.html.gold-8avr2
2026-04-08 01:50:03 +02:00

305 lines
21 KiB
Plaintext

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Cache-Control" content="no-cache,no-store,must-revalidate">
<title>L99 Mission Control — WEVAL Sovereign QA</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--void:#05080f;--deep:#0b1120;--pn:#111a2e;--ph:#162040;--rim:#1a2744;--rl:#243456;--cy:#22d3ee;--em:#34d399;--am:#fbbf24;--ro:#fb7185;--wh:#f0f4f8;--mu:#8892a4;--dm:#4a5568;--lv:#a78bfa}
body{background:var(--void);color:var(--wh);font-family:'DM Sans',sans-serif;min-height:100vh;overflow-x:hidden}
.mono{font-family:'JetBrains Mono',monospace}
nav{padding:14px 28px;border-bottom:1px solid var(--rim);display:flex;align-items:center;justify-content:space-between;background:rgba(11,17,32,.9);backdrop-filter:blur(12px);position:sticky;top:0;z-index:50}
.logo{width:32px;height:32px;border-radius:8px;background:linear-gradient(135deg,var(--cy),var(--lv));display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px}
.tag{display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:6px;font-size:10px;font-weight:700;letter-spacing:.5px}
.tag::before{content:'';width:5px;height:5px;border-radius:50%;animation:pulse 2s ease infinite}
@keyframes pulse{0%,100%{opacity:.4;transform:scale(1)}50%{opacity:0;transform:scale(2.2)}}
.tag-em{background:#34d39915;color:var(--em)}.tag-em::before{background:var(--em)}
.tag-cy{background:#22d3ee15;color:var(--cy)}.tag-cy::before{background:var(--cy)}
.tag-am{background:#fbbf2415;color:var(--am)}.tag-am::before{background:var(--am)}
.tag-lv{background:#a78bfa15;color:var(--lv)}.tag-lv::before{background:var(--lv)}
.tag-mu{background:#8892a415;color:var(--mu)}.tag-mu::before{background:var(--mu)}
.tabs{padding:16px 28px 0;display:flex;gap:4px}
.tab{padding:10px 20px;border:none;cursor:pointer;font-size:12px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;border-radius:8px 8px 0 0;transition:all .2s;background:transparent;color:var(--dm);border-bottom:2px solid transparent}
.tab.on{background:var(--pn);color:var(--cy);border-bottom-color:var(--cy)}
.main{padding:20px 28px 40px;max-width:1440px;margin:0 auto}
.card{background:var(--pn);border-radius:14px;border:1px solid var(--rim);padding:20px 24px;position:relative;overflow:hidden}
.card .glow{position:absolute;top:0;right:0;width:80px;height:80px;border-radius:50%;filter:blur(40px);opacity:.1}
.kpi .lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:2px;color:var(--dm);margin-bottom:10px}
.kpi .val{font-size:34px;font-weight:800;line-height:1;font-family:'JetBrains Mono',monospace}
.kpi .sub{font-size:11px;color:var(--mu);margin-top:8px}
.bar{width:100%;height:3px;border-radius:3px;background:var(--rl);overflow:hidden}
.bar-fill{height:100%;border-radius:3px;transition:width 1.2s cubic-bezier(.4,0,.2,1)}
.g3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
.g4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
.g5{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px}
.g7{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:12px}
.ring-wrap{position:relative;display:flex;align-items:center;justify-content:center}
.ring-text{position:absolute;text-align:center}
.fail-item{padding:10px 12px;border-radius:8px;background:#fb718508;border:1px solid #fb718520;margin-bottom:6px}
.phase{padding:14px;border-radius:10px;background:var(--ph)}
.srv{text-align:center;padding:20px}
.footer{text-align:center;padding:20px 0 24px;font-size:10px;color:var(--dm);letter-spacing:1.5px;text-transform:uppercase}
canvas#bg{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}
.z10{position:relative;z-index:10}
@media(max-width:1000px){.g7{grid-template-columns:repeat(4,minmax(0,1fr))}.g5{grid-template-columns:repeat(3,minmax(0,1fr))}}
</style>
</head>
<body>
<canvas id="bg"></canvas>
<nav class="z10">
<div style="display:flex;align-items:center;gap:14px">
<div class="logo">L</div>
<div>
<div style="font-size:16px;font-weight:800;letter-spacing:-.3px">L99 <span style="color:var(--cy)">Mission Control</span></div>
<div style="font-size:10px;color:var(--dm);letter-spacing:1px">WEVAL SOVEREIGN QA PLATFORM</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:14px">
<span class="tag tag-em">ALIVE</span>
<span class="tag tag-em" id="rate-tag">—</span>
<span class="mono" style="font-size:12px;color:var(--dm)" id="clock">—</span>
</div>
</nav>
<div class="z10" style="padding:6px 28px;border-bottom:1px solid var(--rim);display:flex;gap:6px;flex-wrap:wrap;background:rgba(11,17,32,.6);backdrop-filter:blur(8px)">
<a href="/l99-saas.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:700;text-decoration:none;background:var(--cy);color:var(--void)">L99 Mission Control</a>
<a href="/l99.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:var(--rl);color:var(--mu)">L99 Classic</a>
<a href="/api/l99-report.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:var(--rl);color:var(--mu)">L99 Report</a>
<span style="width:1px;background:var(--rim);margin:0 4px"></span>
<a href="/realtime-monitor.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:var(--rl);color:var(--mu)">Realtime Monitor</a>
<a href="/cyber-monitor.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:var(--rl);color:var(--mu)">Cyber Monitor</a>
<a href="/crons-monitor.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:var(--rl);color:var(--mu)">Crons</a>
<span style="width:1px;background:var(--rim);margin:0 4px"></span>
<a href="/agents-goodjob.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:var(--rl);color:var(--mu)">Enterprise Viz</a>
<a href="/admin-saas.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:var(--rl);color:var(--mu)">Admin</a>
<a href="/sovereign-claude.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:var(--rl);color:var(--mu)">Sovereign Claude</a> <a href="/l99-brain.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;margin-left:6px">L99 Brain</a> <a href="/l99-fullscreen.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:#22c55e;color:#fff;margin-left:6px">Fullscreen</a>
<a href="/wevia.html" style="padding:4px 12px;border-radius:6px;font-size:10px;font-weight:600;text-decoration:none;background:linear-gradient(135deg,#22c55e,#10b981);color:#fff;margin-left:6px">WEVIA Chat</a></div>
<div class="tabs z10" id="tabs-bar"></div>
<div class="main z10" id="content"></div>
<div class="footer z10">L99 Mission Control · WEVAL Consulting · Sovereign QA Platform</div>
<script>
// L99 Dynamic Failures — fetch from state
fetch('/api/l99-api.php?action=failures').then(r=>r.json()).then(d=>{
window._l99failures=(d.failures||[]).map(f=>[f.name||f[0],f.layer||f[1],f.since||f[2]]);
if(typeof refreshDash==='function')refreshDash();
}).catch(()=>{window._l99failures=[];});
const TABS={command:'Command center',functional:'Functional (42)',visual:'Visual (30)',coverage:'Test coverage',servers:'Server health',history:'Run history'};
let tab='command';
let DATA={tests:197,pass:194,fail:3,warn:0,layers:9,ss:14,vid:32};
// Clock
setInterval(()=>{document.getElementById('clock').textContent=new Date().toLocaleTimeString('fr-FR')},1000);
// BG canvas
(function(){
const c=document.getElementById('bg'),x=c.getContext('2d');let f=0;
function d(){c.width=innerWidth;c.height=innerHeight;x.clearRect(0,0,c.width,c.height);
x.strokeStyle='#ffffff04';x.lineWidth=.5;
for(let i=0;i<c.width;i+=60){x.beginPath();x.moveTo(i,0);x.lineTo(i,c.height);x.stroke()}
for(let i=0;i<c.height;i+=60){x.beginPath();x.moveTo(0,i);x.lineTo(c.width,i);x.stroke()}
const y=(f*.5)%c.height,g=x.createLinearGradient(0,y-30,0,y+2);
g.addColorStop(0,'transparent');g.addColorStop(1,'#22d3ee08');
x.fillStyle=g;x.fillRect(0,y-30,c.width,32);f++;requestAnimationFrame(d)}d()
})();
// Fetch live data
async function fetchLive(){
try{
const r=await fetch('/api/l99-api.php?action=results');
const d=await r.json();
if(d.tests){
DATA.tests=d.tests.length;
DATA.pass=d.tests.filter(t=>t.status==='P').length;
DATA.fail=d.tests.filter(t=>t.status==='F').length;
DATA.warn=d.tests.filter(t=>t.status==='W').length;
DATA.layers=new Set(d.tests.map(t=>t.layer)).size;
}
}catch(e){}
document.getElementById('rate-tag').textContent=((DATA.pass/DATA.tests)*100).toFixed(1)+'% pass';
render();
}
const LAYERS=[{n:'DOCKER',t:11,p:11,c:'cy'},{n:'CAPABILITIES',t:1,p:0,c:'ro'},{n:'CRONS',t:3,p:3,c:'cy'},{n:'SYSTEMD',t:4,p:3,c:'cy'},{n:'PORTS-S204',t:8,p:8,c:'lv'},{n:'S95-HEALTH',t:3,p:3,c:'em'},{n:'SOVEREIGN',t:10,p:10,c:'lv'},{n:'QDRANT',t:4,p:4,c:'em'},{n:'NONREG',t:153,p:152,c:'em'}];
const SERVERS=[
{n:'S204',ip:'204.168.152.13',role:'Primary AI Hub',ports:48,docker:19,disk:'82%',s:'up'},
{n:'S95',ip:'95.216.167.89',role:'Email & Database',ports:36,docker:0,disk:'—',s:'up'},
{n:'S151',ip:'151.80.235.110',role:'Tracking Relay',ports:7,docker:0,disk:'45%',s:'up'},
{n:'Blade',ip:'dynamic',role:'Desktop Agent',ports:0,docker:0,disk:'—',s:'up'},
{n:'S88',ip:'88.198.4.195',role:'GPU (dead)',ports:0,docker:0,disk:'—',s:'dead'}
];
const HIST=[
{t:'20:28',type:'alive',tests:43,pass:39,fail:4,dur:'92s'},
{t:'18:00',type:'nonreg',tests:148,pass:148,fail:0,dur:'9.2s'},
{t:'14:40',type:'manual',tests:32,pass:32,fail:0,dur:'—'},
{t:'06:00',type:'nonreg',tests:148,pass:148,fail:0,dur:'8.9s'},
{t:'02:51',type:'alive',tests:627,pass:600,fail:1,dur:'45s'}
];
function kpi(lbl,val,sub,color){
return '<div class="card kpi"><div class="glow" style="background:var(--'+color+')"></div><div class="lbl">'+lbl+'</div><div class="val" style="color:var(--'+color+')">'+val+'</div>'+(sub?'<div class="sub">'+sub+'</div>':'')+'</div>';
}
function bar(pct,color,h){h=h||3;return '<div class="bar" style="height:'+h+'px"><div class="bar-fill" style="width:'+pct+'%;background:var(--'+color+')"></div></div>';}
function tag(text,color){return '<span class="tag tag-'+color+'">'+text+'</span>';}
function render(){
// Tabs
let tb='';for(let k in TABS)tb+='<button class="tab'+(k===tab?' on':'')+'" onclick="tab=\''+k+'\';render()">'+TABS[k]+'</button>';
document.getElementById('tabs-bar').innerHTML=tb;
let h='';
const rate=((DATA.pass/DATA.tests)*100).toFixed(1);
if(tab==='command'){
h+='<div class="g7" style="margin-bottom:24px">';
h+=kpi('Total tests',DATA.tests,'','cy')+kpi('Pass',DATA.pass,'','em')+kpi('Fail',DATA.fail,'','ro');
h+=kpi('Warn',DATA.warn,'','am')+kpi('Layers',DATA.layers,'','lv')+kpi('Screenshots',DATA.ss,'','mu')+kpi('Videos',DATA.vid,'','mu');
h+='</div>';
// Ring + layers + failures
h+='<div style="display:grid;grid-template-columns:280px 1fr 1fr;gap:20px;margin-bottom:24px">';
// Ring
h+='<div class="card" style="display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px">';
const r2=(140-8)/2,circ=2*Math.PI*r2,off=circ-(rate/100)*circ;
h+='<svg width="140" height="140" style="transform:rotate(-90deg)"><circle cx="70" cy="70" r="'+r2+'" fill="none" stroke="var(--rim)" stroke-width="8"/>';
h+='<circle cx="70" cy="70" r="'+r2+'" fill="none" stroke="var(--em)" stroke-width="8" stroke-dasharray="'+circ+'" stroke-dashoffset="'+off+'" stroke-linecap="round" style="transition:stroke-dashoffset 1.5s"/></svg>';
h+='<div style="position:absolute;text-align:center"><div class="mono" style="font-size:28px;font-weight:800;color:var(--em)">'+rate+'%</div><div style="font-size:10px;color:var(--dm);letter-spacing:1px">PASS RATE</div></div>';
h+='<div style="margin-top:14px;font-size:11px;color:var(--mu)">Last: '+new Date().toLocaleDateString('fr-FR')+' 20:28</div></div>';
// Layers
h+='<div class="card"><div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:var(--dm);margin-bottom:14px">Active layers</div>';
h+='<div style="max-height:220px;overflow-y:auto">';
LAYERS.filter(l=>l.n!=='OTHER').forEach(l=>{
const ok=l.p===l.t;
h+='<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px"><span style="width:6px;height:6px;border-radius:50%;background:var(--'+l.c+')"></span>';
h+='<span class="mono" style="font-size:11px;color:var(--mu);flex:1">'+l.n+'</span>';
h+='<span class="mono" style="font-size:11px;font-weight:700;color:var(--'+(ok?'em':'am')+')">'+l.p+'/'+l.t+'</span>';
h+='<div style="width:60px">'+bar((l.p/l.t)*100,ok?'em':'am')+'</div></div>';
});
h+='</div></div>';
// Failures
h+='<div class="card"><div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:var(--dm);margin-bottom:14px">Active failures</div>';
(window._l99failures||[]).forEach(f=>{
h+='<div class="fail-item"><div style="font-size:12px;font-weight:600;color:var(--ro)">'+f[0]+'</div><div style="font-size:10px;color:var(--dm);margin-top:2px">'+f[1]+' · since '+f[2]+'</div></div>';
});
h+='</div></div>';
// Auto-evolve engine
h+='<div class="card"><div style="display:flex;align-items:center;gap:10px;margin-bottom:14px">'+tag('ALIVE','em')+'<span style="font-size:13px;font-weight:700">L99-ALIVE auto-evolution engine</span><span style="font-size:10px;color:var(--dm);margin-left:auto">runs every 30 min</span></div>';
h+='<div class="g4">';
[['1. Detect','Scan pages, APIs, Docker, S95, S151, Blade','853 checks'],['2. Test','Playwright 14 key pages + screenshots','14/14 PASS'],['3. Infra','Docker 22/22, Systemd, Ports, NonReg 153/153','ALL OK'],['4. Report','State save, merge results, Telegram alert','Saved']].forEach(p=>{
h+='<div class="phase"><div style="font-size:12px;font-weight:700;color:var(--cy);margin-bottom:4px">'+p[0]+'</div><div style="font-size:11px;color:var(--mu);margin-bottom:8px">'+p[1]+'</div><div class="mono" style="font-size:10px;color:var(--em)">'+p[2]+'</div></div>';
});
h+='</div></div>';
}
else if(tab==='functional'){
var ft=FUNC_DATA.tests||[];
var fp=ft.filter(function(t){return t.status==='PASS'}).length;
var ff=ft.filter(function(t){return t.status==='FAIL'}).length;
var fw=ft.filter(function(t){return t.status==='WARN'}).length;
h+='<div class="g5" style="margin-bottom:24px">';
h+=kpi('Total',ft.length,'functional tests','cy');
h+=kpi('Pass',fp,'','em');h+=kpi('Fail',ff,'','ro');
h+=kpi('Warn',fw,'','am');
h+=kpi('Rate',ft.length?((fp/ft.length)*100).toFixed(1)+'%':'--','','lv');
h+='</div><div class="card"><div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:var(--dm);margin-bottom:14px">Functional test results</div>';
var phases={};ft.forEach(function(t){var ph=t.phase||'other';if(!phases[ph])phases[ph]=[];phases[ph].push(t);});
Object.keys(phases).forEach(function(ph){
h+='<div style="margin-bottom:16px"><div style="font-size:12px;font-weight:700;color:var(--cy);margin-bottom:8px">'+ph+'</div>';
phases[ph].forEach(function(t){
var col=t.status==='PASS'?'em':t.status==='FAIL'?'ro':'am';
h+='<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--rim)">';
h+='<span style="width:8px;height:8px;border-radius:50%;background:var(--'+col+')"></span>';
h+='<span class="mono" style="font-size:12px;flex:1">'+t.name+'</span>';
h+=tag(t.status,col);
if(t.error)h+='<span style="font-size:10px;color:var(--dm);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+t.error+'</span>';
h+='<span class="mono" style="font-size:10px;color:var(--dm)">'+(t.ms||'--')+'ms</span></div>';
});h+='</div>';
});h+='</div>';
}
else if(tab==='visual'){
var vp=VIS_DATA.pages||[];
var vpass=vp.filter(function(t){return t.status==='PASS'}).length;
var vfail=vp.filter(function(t){return t.status==='FAIL'}).length;
var vsso=vp.filter(function(t){return t.status==='SSO'}).length;
h+='<div class="g5" style="margin-bottom:24px">';
h+=kpi('Targets',vp.length,'visual checks','cy');
h+=kpi('Pass',vpass,'','em');h+=kpi('Fail',vfail,'','ro');
h+=kpi('SSO',vsso,'auth blocked','am');
h+=kpi('Rate',vp.length?((vpass/vp.length)*100).toFixed(1)+'%':'--','','lv');
h+='</div><div class="card"><div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:var(--dm);margin-bottom:14px">Visual regression results</div>';
vp.forEach(function(p){
var col=p.status==='PASS'?'em':p.status==='FAIL'?'ro':p.status==='SSO'?'am':'dm';
h+='<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--rim)">';
h+='<span style="width:8px;height:8px;border-radius:50%;background:var(--'+col+')"></span>';
h+='<span class="mono" style="font-size:12px;flex:1">'+p.name+'</span>';
h+='<span class="mono" style="font-size:11px;color:var(--dm)">['+p.machine+']</span>';
h+=tag(p.status,col);
h+='<span class="mono" style="font-size:10px;color:var(--'+(p.diff_pct>2?'ro':'dm')+')">'+String(p.diff_pct)+'%</span></div>';
});h+='</div>';
}
else if(tab==='coverage'){
h+='<div class="g3" style="margin-bottom:24px">'+kpi('Test layers','107','unique layers','cy')+kpi('Page coverage','14/82','HTML auto-tested','lv')+kpi('Infra coverage','4/4','servers + Blade','em')+'</div>';
h+='<div class="card">';
LAYERS.forEach(l=>{
const ok=l.p===l.t;
h+='<div style="display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--rim)"><span style="width:8px;height:8px;border-radius:50%;background:var(--'+(ok?'em':'am')+')"></span>';
h+='<span class="mono" style="font-size:13px;font-weight:600;flex:1">'+l.n+'</span>';
h+='<span class="mono" style="font-size:12px;color:var(--'+(ok?'em':'am')+');font-weight:700">'+l.p+'/'+l.t+'</span>';
h+='<div style="width:120px">'+bar((l.p/l.t)*100,ok?'em':'am',4)+'</div>';
h+='<span class="mono" style="font-size:11px;color:var(--dm);width:50px;text-align:right">'+((l.p/l.t)*100).toFixed(0)+'%</span></div>';
});
h+='</div>';
}
else if(tab==='servers'){
h+='<div class="g5" style="margin-bottom:24px">';
SERVERS.forEach(s=>{
h+='<div class="card srv" style="'+(s.s==='dead'?'border-color:var(--ro)40':'')+'"><div style="font-size:20px;font-weight:800">'+s.n+'</div>';
h+='<div class="mono" style="font-size:10px;color:var(--dm)">'+s.ip+'</div>';
h+='<div style="font-size:11px;color:var(--mu);margin:8px 0">'+s.role+'</div>';
h+=tag(s.s==='up'?'Online':'Dead',s.s==='up'?'em':'ro');
if(s.ports)h+='<div class="mono" style="font-size:28px;font-weight:800;color:var(--cy);margin-top:10px">'+s.ports+'<span style="font-size:11px;color:var(--dm)"> ports</span></div>';
if(s.docker)h+='<div style="font-size:11px;color:var(--mu)">'+s.docker+' Docker</div>';
if(s.disk!=='—')h+='<div style="margin-top:8px">'+tag('Disk '+s.disk,parseInt(s.disk)>85?'am':'em')+'</div>';
h+='</div>';
});
h+='</div>';
}
else if(tab==='history'){
h+='<div class="card">';
h+='<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:var(--dm);margin-bottom:14px">Run history — 2 avril 2026</div>';
HIST.forEach(r=>{
h+='<div style="display:flex;align-items:center;gap:16px;padding:14px 0;border-bottom:1px solid var(--rim)">';
h+='<span class="mono" style="font-size:14px;font-weight:700;color:var(--cy);width:50px">'+r.t+'</span>';
h+=tag(r.type,r.type==='alive'?'lv':r.type==='nonreg'?'em':'mu');
h+='<div style="flex:1">'+bar((r.pass/r.tests)*100,r.fail===0?'em':'am',6)+'</div>';
h+='<span class="mono" style="font-size:13px;font-weight:700;color:var(--'+(r.fail===0?'em':'am')+')">'+r.pass+'/'+r.tests+'</span>';
h+='<span style="font-size:11px;color:var(--dm);width:50px;text-align:right">'+r.dur+'</span></div>';
});
h+='</div>';
}
document.getElementById('content').innerHTML=h;
}
fetchLive();
setInterval(fetchLive,30000);
setInterval(function(){fetch('/api/l99-functional-result.json').then(function(r){return r.json()}).then(function(d){FUNC_DATA=d;}).catch(function(){});fetch('/api/l99-visual-result.json').then(function(r){return r.json()}).then(function(d){VIS_DATA=d;}).catch(function(){});},60000);
render();
</script>
</body>
</html>