104 lines
5.5 KiB
HTML
104 lines
5.5 KiB
HTML
|
|
{{define "head"}}<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
|
|
<title>{{.}} - LEM.Lab</title>
|
||
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==" crossorigin="anonymous" referrerpolicy="no-referrer"/>
|
||
|
|
<style>
|
||
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
||
|
|
:root{--bg:#0a0a0f;--surface:#12121a;--border:#1e1e2e;--text:#e0e0e8;--muted:#8888a0;--accent:#7c6ff0;--accent-dim:#5a4fd0;--green:#4ade80;--red:#f87171;--yellow:#fbbf24}
|
||
|
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;line-height:1.6;font-size:.9375rem}
|
||
|
|
a{color:var(--accent);text-decoration:none;transition:color .2s}
|
||
|
|
a:hover{color:var(--green)}
|
||
|
|
nav{display:flex;align-items:center;gap:1.5rem;padding:.75rem 1.5rem;border-bottom:1px solid var(--border);background:var(--surface)}
|
||
|
|
nav .logo{font-size:1.25rem;font-weight:700;letter-spacing:-.02em}
|
||
|
|
nav .logo span{color:var(--accent)}
|
||
|
|
nav .links{display:flex;gap:.25rem}
|
||
|
|
nav .links a{padding:.375rem .75rem;border-radius:6px;font-size:.8125rem;color:var(--muted);transition:all .2s}
|
||
|
|
nav .links a:hover,nav .links a.active{color:var(--text);background:var(--bg)}
|
||
|
|
.container{max-width:1600px;margin:0 auto;padding:1.5rem}
|
||
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-bottom:1.5rem}
|
||
|
|
.card{padding:1.25rem;border:1px solid var(--border);border-radius:8px;background:var(--surface)}
|
||
|
|
.card h3{font-size:.8125rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.5rem}
|
||
|
|
.card .value{font-size:1.75rem;font-weight:700;line-height:1.2}
|
||
|
|
.card .sub{font-size:.8125rem;color:var(--muted);margin-top:.25rem}
|
||
|
|
.status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:.375rem}
|
||
|
|
.status-ok .status-dot{background:var(--green)}
|
||
|
|
.status-warn .status-dot{background:var(--yellow)}
|
||
|
|
.status-err .status-dot{background:var(--red)}
|
||
|
|
.status-ok .label{color:var(--green)}
|
||
|
|
.status-warn .label{color:var(--yellow)}
|
||
|
|
.status-err .label{color:var(--red)}
|
||
|
|
.progress-bar{width:100%;height:8px;background:var(--border);border-radius:4px;overflow:hidden;margin:.5rem 0}
|
||
|
|
.progress-bar .fill{height:100%;background:var(--accent);border-radius:4px;transition:width .5s}
|
||
|
|
table{width:100%;border-collapse:collapse;font-size:.8125rem}
|
||
|
|
th{text-align:left;color:var(--muted);font-weight:600;padding:.5rem .75rem;border-bottom:1px solid var(--border);text-transform:uppercase;letter-spacing:.05em;font-size:.75rem}
|
||
|
|
td{padding:.5rem .75rem;border-bottom:1px solid var(--border)}
|
||
|
|
tr:last-child td{border-bottom:none}
|
||
|
|
code{font-family:"SF Mono",Consolas,monospace;font-size:.75rem;background:var(--bg);padding:.125rem .375rem;border-radius:4px;border:1px solid var(--border)}
|
||
|
|
.badge{display:inline-block;padding:.125rem .5rem;border-radius:4px;font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em}
|
||
|
|
.badge-ok{background:rgba(74,222,128,.15);color:var(--green)}
|
||
|
|
.badge-err{background:rgba(248,113,113,.15);color:var(--red)}
|
||
|
|
.badge-info{background:rgba(124,111,240,.15);color:var(--accent)}
|
||
|
|
.empty{text-align:center;padding:2rem;color:var(--muted)}
|
||
|
|
.section-title{font-size:1rem;font-weight:600;margin-bottom:1rem;color:var(--text)}
|
||
|
|
footer{text-align:center;padding:1rem;color:var(--muted);font-size:.75rem;border-top:1px solid var(--border);margin-top:2rem}
|
||
|
|
@media(max-width:640px){.grid{grid-template-columns:1fr}nav{flex-wrap:wrap;gap:.75rem}}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>{{end}}
|
||
|
|
|
||
|
|
{{define "nav"}}
|
||
|
|
<nav>
|
||
|
|
<div class="logo">LEM<span>.Lab</span></div>
|
||
|
|
<div class="links">
|
||
|
|
<a href="/"{{if eq . "dashboard"}} class="active"{{end}}>Dashboard</a>
|
||
|
|
<a href="/models"{{if eq . "models"}} class="active"{{end}}>Models</a>
|
||
|
|
<a href="/training"{{if eq . "training"}} class="active"{{end}}>Training</a>
|
||
|
|
<a href="/dataset"{{if eq . "dataset"}} class="active"{{end}}>Dataset</a>
|
||
|
|
<a href="/agents"{{if eq . "agents"}} class="active"{{end}}>Agents</a>
|
||
|
|
<a href="/services"{{if eq . "services"}} class="active"{{end}}>Services</a>
|
||
|
|
</div>
|
||
|
|
</nav>
|
||
|
|
<div class="container">{{end}}
|
||
|
|
|
||
|
|
{{define "footer"}}
|
||
|
|
</div>
|
||
|
|
<footer>LEM.Lab · live · <a href="https://forge.lthn.io/agentic">forge.lthn.io</a></footer>
|
||
|
|
<script>
|
||
|
|
// SSE live update: fetches same-origin page on data change, swaps container content.
|
||
|
|
// Safe: only fetches from same origin (our own server), no user input involved.
|
||
|
|
(function(){
|
||
|
|
var es, timer;
|
||
|
|
function connect(){
|
||
|
|
es = new EventSource('/events');
|
||
|
|
es.onmessage = function(){
|
||
|
|
clearTimeout(timer);
|
||
|
|
timer = setTimeout(refresh, 500);
|
||
|
|
};
|
||
|
|
es.onerror = function(){
|
||
|
|
es.close();
|
||
|
|
setTimeout(connect, 5000);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function refresh(){
|
||
|
|
fetch(location.href).then(function(r){ return r.text(); }).then(function(html){
|
||
|
|
var doc = new DOMParser().parseFromString(html, 'text/html');
|
||
|
|
var fresh = doc.querySelector('.container');
|
||
|
|
var current = document.querySelector('.container');
|
||
|
|
if(fresh && current){
|
||
|
|
// Save active tab before replacing DOM.
|
||
|
|
var activeTab = document.querySelector('.chart-panel.active');
|
||
|
|
var tabName = activeTab ? activeTab.getAttribute('data-tab') : null;
|
||
|
|
current.replaceWith(fresh);
|
||
|
|
// Restore active tab after DOM swap.
|
||
|
|
if(tabName && typeof showTab === 'function') showTab(tabName);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
connect();
|
||
|
|
})();
|
||
|
|
</script>
|
||
|
|
</body></html>{{end}}
|