146 lines
7.0 KiB
HTML
146 lines
7.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Kazeia-central — console de flotte</title>
|
|
<style>
|
|
:root {
|
|
--bg:#0f1115; --panel:#171a21; --panel2:#1e222b; --line:#2a2f3a;
|
|
--txt:#e6e8ec; --muted:#8b93a3; --accent:#7aa2f7; --ok:#7ee787;
|
|
--warn:#e3b341; --err:#f7768e;
|
|
}
|
|
* { box-sizing:border-box; }
|
|
body { margin:0; font:14px/1.5 system-ui,Segoe UI,Roboto,sans-serif;
|
|
background:var(--bg); color:var(--txt); }
|
|
header { padding:14px 20px; border-bottom:1px solid var(--line);
|
|
display:flex; align-items:center; gap:12px; }
|
|
header h1 { font-size:16px; margin:0; font-weight:600; }
|
|
header .sub { color:var(--muted); font-size:12px; }
|
|
header button { margin-left:auto; }
|
|
.layout { display:flex; height:calc(100vh - 53px); }
|
|
aside { width:300px; border-right:1px solid var(--line); overflow:auto; padding:10px; }
|
|
main { flex:1; overflow:auto; padding:18px 22px; }
|
|
.dev { padding:10px 12px; border:1px solid var(--line); border-radius:8px;
|
|
margin-bottom:8px; cursor:pointer; background:var(--panel); }
|
|
.dev:hover { border-color:var(--accent); }
|
|
.dev.sel { border-color:var(--accent); background:var(--panel2); }
|
|
.dev .serial { font-family:ui-monospace,monospace; font-weight:600; }
|
|
.dev .meta { color:var(--muted); font-size:12px; }
|
|
.badge { display:inline-block; padding:1px 7px; border-radius:10px; font-size:11px;
|
|
border:1px solid var(--line); }
|
|
.badge.device { color:var(--ok); border-color:var(--ok); }
|
|
.badge.offline,.badge.unauthorized { color:var(--err); border-color:var(--err); }
|
|
button { background:var(--panel2); color:var(--txt); border:1px solid var(--line);
|
|
padding:6px 12px; border-radius:7px; cursor:pointer; font-size:13px; }
|
|
button:hover { border-color:var(--accent); }
|
|
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:14px; }
|
|
.card { background:var(--panel); border:1px solid var(--line); border-radius:10px;
|
|
padding:14px 16px; }
|
|
.card h3 { margin:0 0 10px; font-size:13px; color:var(--muted); text-transform:uppercase;
|
|
letter-spacing:.04em; font-weight:600; }
|
|
.kv { display:flex; justify-content:space-between; gap:10px; padding:3px 0;
|
|
border-bottom:1px dashed var(--line); }
|
|
.kv:last-child { border-bottom:0; }
|
|
.kv .k { color:var(--muted); } .kv .v { font-family:ui-monospace,monospace; }
|
|
table { width:100%; border-collapse:collapse; font-size:13px; }
|
|
th,td { text-align:left; padding:6px 8px; border-bottom:1px solid var(--line); }
|
|
th { color:var(--muted); font-weight:600; font-size:12px; }
|
|
.full { grid-column:1/-1; }
|
|
.err { color:var(--err); font-size:12px; }
|
|
.muted { color:var(--muted); }
|
|
.dot { width:7px; height:7px; border-radius:50%; display:inline-block; margin-right:6px; }
|
|
.dot.on { background:var(--ok); } .dot.off { background:var(--err); }
|
|
#placeholder { color:var(--muted); margin-top:40px; text-align:center; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Kazeia-central</h1>
|
|
<span class="sub" id="sub">console de flotte</span>
|
|
<button onclick="loadDevices()">↻ Rafraîchir le parc</button>
|
|
</header>
|
|
<div class="layout">
|
|
<aside><div id="devices" class="muted">chargement…</div></aside>
|
|
<main><div id="detail"><div id="placeholder">← sélectionne une tablette</div></div></main>
|
|
</div>
|
|
|
|
<script>
|
|
const $ = (id) => document.getElementById(id);
|
|
let selected = null;
|
|
|
|
async function api(path) {
|
|
const r = await fetch("/api" + path);
|
|
if (!r.ok) {
|
|
let detail = r.status;
|
|
try { detail = (await r.json()).detail || detail; } catch {}
|
|
throw new Error(detail);
|
|
}
|
|
return r.json();
|
|
}
|
|
|
|
async function loadDevices() {
|
|
const box = $("devices");
|
|
try {
|
|
const devs = await api("/devices");
|
|
$("sub").textContent = devs.length + " tablette(s) découverte(s)";
|
|
if (!devs.length) { box.innerHTML = '<div class="muted">aucune tablette branchée</div>'; return; }
|
|
box.innerHTML = "";
|
|
for (const d of devs) {
|
|
const el = document.createElement("div");
|
|
el.className = "dev" + (d.serial === selected ? " sel" : "");
|
|
el.innerHTML = `<div class="serial">${d.serial}</div>
|
|
<div class="meta">${d.props?.model || "?"} · <span class="badge ${d.state}">${d.state}</span></div>`;
|
|
el.onclick = () => selectDevice(d.serial);
|
|
box.appendChild(el);
|
|
}
|
|
} catch (e) {
|
|
box.innerHTML = `<div class="err">erreur adb : ${e.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function card(title, body, cls="") { return `<div class="card ${cls}"><h3>${title}</h3>${body}</div>`; }
|
|
function kv(k, v) { return `<div class="kv"><span class="k">${k}</span><span class="v">${v ?? "—"}</span></div>`; }
|
|
function dot(on) { return `<span class="dot ${on ? "on":"off"}"></span>`; }
|
|
|
|
async function panel(serial, path, render) {
|
|
try { return render(await api(`/devices/${serial}/${path}`)); }
|
|
catch (e) { return `<div class="err">${path} : ${e.message}</div>`; }
|
|
}
|
|
|
|
async function selectDevice(serial) {
|
|
selected = serial;
|
|
loadDevices();
|
|
const d = $("detail");
|
|
d.innerHTML = `<div class="muted">interrogation de <b>${serial}</b>…</div>`;
|
|
|
|
const [state, updates, rag, profiles, convs, crashes] = await Promise.all([
|
|
panel(serial, "state", s => card("État runtime",
|
|
kv("pid", s.pid) + kv("uptime (s)", s.uptime_seconds) + kv("PSS (Mo)", s.pss_mb) +
|
|
kv("ION (Mo)", s.ion_mb) + kv("RAM dispo (Mo)", s.mem_available_mb))),
|
|
panel(serial, "updates", u => card("Mises à jour",
|
|
kv("phase", u.phase) + kv("app MAJ dispo", u.app_update_available) +
|
|
kv("version", u.app_version_name) + kv("dernier check", u.last_check))),
|
|
panel(serial, "rag/status", r => card("RAG",
|
|
`<div class="kv"><span class="k">état</span><span class="v">${dot(r.ready)}${r.ready?"prêt":"non prêt"}</span></div>` +
|
|
kv("modèle", r.model) + kv("dim", r.dim) + kv("docs", r.doc_count) + kv("chunks", r.chunk_count))),
|
|
panel(serial, "profiles", ps => card("Profils ("+ps.length+")",
|
|
`<table><tr><th>id</th><th>nom</th><th>voix</th><th>PIN</th><th>tours</th></tr>` +
|
|
ps.map(p=>`<tr><td>${p.id}</td><td>${p.display_name??"—"}</td><td>${p.voice_id??"—"}</td><td>${p.has_pin?"🔒":"—"}</td><td>${p.turn_count??0}</td></tr>`).join("") +
|
|
`</table>`, "full")),
|
|
panel(serial, "conversations", cs => card("Conversations ("+cs.length+" sessions)",
|
|
cs.length ? `<table><tr><th>profil</th><th>session</th><th>tours</th><th>début</th></tr>` +
|
|
cs.map(c=>`<tr><td>${c.profile_id}</td><td>${c.session_id}</td><td>${c.turn_count??0}</td><td>${c.started_at?new Date(c.started_at).toLocaleString():"—"}</td></tr>`).join("") +
|
|
`</table>` : `<span class="muted">aucune session</span>`, "full")),
|
|
panel(serial, "crashes", cr => card("Crashes ("+cr.length+")",
|
|
cr.length ? cr.map(c=>kv(c.component||"?", c.message||"")).join("") : `<span class="muted">aucun</span>`)),
|
|
]);
|
|
|
|
d.innerHTML = `<div class="grid">${state}${updates}${rag}${profiles}${convs}${crashes}</div>`;
|
|
}
|
|
|
|
loadDevices();
|
|
</script>
|
|
</body>
|
|
</html>
|