| ${esc(l.level)} |
${l.paused ? 'paused'
: l.busy ? 'busy'
@@ -316,15 +379,26 @@ function setView(v){
});
document.querySelectorAll(".view").forEach(s => s.classList.toggle("active", s.id === "view-" + v));
try { localStorage.simbiotView = v; } catch(e){}
- if (v === "chat") { chatTick(); } // первый рендер сразу, без ожидания таймера
+ if (v === "chat") { chatTick(); }
+ if (v === "agent") { if (!agentLevel) agentLevel = "min"; startAgentMonitor(agentLevel); }
}
try {
- // deep-link: ?view=chat|dash (проверяемый вход «открыть чат по ссылке», приоритет над localStorage)
- const q = new URLSearchParams(location.search).get("view");
- const ql = q === "chat" ? "chat" : (q === "dash" ? "dash" : null);
+ // deep-link: ?view=chat|dash|agent&level=min (проверяемый вход «открыть чат по ссылке», приоритет над localStorage)
+ const _qp = new URLSearchParams(location.search);
+ const q = _qp.get("view");
+ const ql = q === "chat" ? "chat" : (q === "dash" ? "dash" : (q === "agent" ? "agent" : null));
const v0 = ql || localStorage.simbiotView;
- if (v0 === "chat") setView("chat");
+ if (v0 === "agent") {
+ const hl = _qp.get("level") || ((location.hash.match(/^#agent=([a-z_]+)$/) || [])[1]);
+ if (hl) agentLevel = hl;
+ setView("agent");
+ } else if (v0 === "chat") setView("chat");
} catch(e){}
+//.hash = #agent= — клик по уровню в лестнице или прямая ссылка
+window.addEventListener("hashchange", () => {
+ const m = (decodeURIComponent(location.hash).match(/^#agent=([a-z_]+)$/) || [])[1];
+ if (m && getViewName() === "agent") { startAgentMonitor(m); }
+});
const CHAT_REFRESH = 10; // сек: канал тикает по сек-рефлексу, чаще нет смысла
let chatTimer = null;
@@ -401,6 +475,106 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
chatTimer = setInterval(chatTick, CHAT_REFRESH * 1000);
+
+// ── детали агента (t-261002175316-eb24cc) ───────────────────────────────
+var agentLevel = null;
+var agentTimer = null;
+
+function barChart(el, data, fmt){
+ const max = Math.max(1, ...data.map(d => d.v));
+ el.innerHTML = data.map(d =>
+ ``).join("");
+}
+
+function fmtDur(sec){
+ if (sec == null) return "—";
+ sec = Math.round(sec);
+ if (sec < 60) return sec + "с";
+ if (sec < 3600) return Math.round(sec / 60) + "м";
+ return (sec / 3600).toFixed(1) + "ч";
+}
+
+function agentTick(){
+ if (!agentLevel) return;
+ fetch("api/agent?level=" + encodeURIComponent(agentLevel) + "&t=" + Date.now(), {cache: "no-store"})
+ .then(r => { if(!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
+ .then(d => {
+ if (d.error) throw new Error(d.error);
+ $("errbox").style.display = "none";
+ const a = d.agent;
+ $("ag-title").textContent = "агент · " + a.level;
+ $("ag-status").innerHTML = a.paused ? 'paused'
+ : a.busy ? 'busy'
+ : 'ok' +
+ (a.current_tasks.length ? ` 🔧 ${esc(a.current_tasks[0].title.slice(0,34))} · ${esc(fmtDur(a.current_tasks[0].duration_sec))}` : "");
+ $("ag-updated").textContent = (d.generated_at || "").slice(11, 19) + " UTC";
+ $("ag-stats").innerHTML = [
+ [a.ticks_24h, "тиков 24ч"], [a.absorbed_24h, "поглощено 24ч"],
+ [a.errors_24h, "ошибок 24ч"], [fmtDur(a.time_in_work_24h_sec), "в работе 24ч"],
+ [a.ticks_7d, "тиков 7д"], [a.errors_7d, "ошибок 7д"],
+ [a.llm_24h.calls, "LLM-вызовы 24ч"], [(a.llm_24h.prompt/1e6).toFixed(1)+"M", "prompt 24ч"],
+ [(a.llm_24h.completion/1e3).toFixed(0)+"k", "completion 24ч"],
+ ].map(([n, l]) => `${esc(String(n))} ${esc(l)} `).join("");
+ // часы: 24 ведра, ноль — прозрачным
+ const hm = {}; (d.hourly || []).forEach(x => hm[x.h] = x.c);
+ const hours = []; for (let h = 0; h < 24; h++) {
+ const k = String(h).padStart(2, "0"), v = hm[k] || 0;
+ hours.push({v, t: k + ":00 UTC — " + v + " тиков"});
+ }
+ barChart($("ag-hours"), hours);
+ const spends = (d.spend_rows || []).map(r =>
+ ({v: r.prompt + r.completion, t: (r.trace_id || "?") + " · " + r.prompt + "pt + " + r.completion + "ct"}));
+ barChart($("ag-spend"), spends);
+ const tasks = d.tasks || [];
+ $("ag-taskcount").textContent = `(${tasks.length})`;
+ $("ag-tasks").innerHTML = tasks.length ? tasks.map(t => `
+ | ${esc(t.id)} |
+ ${esc(t.title)}${t.error ? ` ${esc(String(t.error).slice(0,110))}` : ""} |
+ ${pill(t.status)} |
+ ${esc(t.claimed_at ? fmtTime(t.claimed_at) : "—")} |
+ ${t.finished ? esc(fmtTime(t.finished)) : "—"} |
+ ${fmtDur(t.duration_sec)} |
+ `).join("") : `| этот уровень задач пока не брал | `;
+ $("ag-digests").innerHTML = (d.digests || []).length
+ ? d.digests.map(x => `${esc(x.period || "")} · ${esc(fmtTime(x.ts))} · ref ${esc(x.summary_ref || "—")} ${esc((x.text || "").slice(0, 400))} `).join("")
+ : `дайджестов уровня нет `;
+ const esc5 = d.escalations_open || [];
+ $("ag-esccount").textContent = `(${esc5.length})`;
+ $("ag-esc").innerHTML = esc5.length
+ ? `| id | sev | маршрут | причина | поднята | ` +
+ esc5.map(e => `| ${esc(e.id)} | ${esc(e.severity || "?")} |
+ ${esc(e.from_level || "?")} → ${esc(e.to_level || "?")} | ${esc(e.reason || "")} |
+ ${esc(fmtTime(e.ts))} | `).join("") + ` `
+ : `открытых эскалаций уровня нет `;
+ const errs = d.recent_errors || [];
+ $("ag-errcount").textContent = `(${errs.length})`;
+ $("ag-errors").innerHTML = errs.length
+ ? errs.map(e => `${esc(fmtTime(e.ts))} ${esc(e.error)} `).join("")
+ : `ошибок за 7д нет `;
+ $("ag-tickcount").textContent = `(${(d.recent_ticks || []).length})`;
+ $("ag-ticks").innerHTML = (d.recent_ticks || []).length
+ ? d.recent_ticks.map(t => `${esc((t.seq || "").split(":").slice(1).join(":"))}`).join("")
+ : `тиков не найдено `;
+ })
+ .catch(e => {
+ const box = $("errbox");
+ box.style.display = "block";
+ box.textContent = "агент " + agentLevel + " недоступен: " + e.message + " — попробую снова на следующем тике";
+ });
+}
+
+function startAgentMonitor(level){
+ agentLevel = level;
+ if (agentTimer) { clearInterval(agentTimer); agentTimer = null; }
+ agentTick();
+ agentTimer = setInterval(agentTick, REFRESH * 1000);
+}
+
+function openAgent(level){
+ try { location.hash = "agent=" + encodeURIComponent(level); } catch(e){}
+ startAgentMonitor(level);
+ setView("agent");
+}
|