Files
Assistant/cmd/creator-web/static/assets/console.js
m 6d1d68e406
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 26s
feat: expose agent worker capability status
2026-08-22 17:08:09 +03:00

1126 lines
49 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const byId = (id) => document.getElementById(id);
const loginView = byId("login-view");
const consoleView = byId("console-view");
const loginForm = byId("login-form");
const contextForm = byId("context-form");
const advisoryForm = byId("advisory-form");
const loginError = byId("login-error");
const toast = byId("toast");
const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id", "delegation-id"];
const settingsStorageKey = "domovoy.creator.settings";
const contextPresetsStorageKey = "domovoy.creator.context.presets";
let sessionToken = sessionStorage.getItem("domovoy.creator.session") || "";
let toastTimer;
let refreshTimer;
let currentTask = null;
let currentProspective = null;
let pendingCommand = null;
let pendingNotification = null;
let lastAuthorizedActionID = "";
let advisoryPolicy = {Mode: "deterministic", ExternalEgress: false, AcknowledgementRequired: false};
let uiSettings = {density: "comfortable", refresh: "off", motion: false};
const pageNavLinks = Array.from(document.querySelectorAll(".section-nav-link, .topbar-nav a"));
const pageViews = Array.from(document.querySelectorAll("[data-page]"));
const cabinetPages = ["overview", "diagnostics", "operations", "learning", "settings"];
function setActivePage(id, updateHistory = false) {
const page = cabinetPages.includes(id) ? id : "overview";
pageViews.forEach((view) => { view.hidden = view.dataset.page !== page; });
document.body.dataset.page = page;
pageNavLinks.forEach((link) => {
const active = link.getAttribute("href") === `#${page}`;
link.classList.toggle("active", active);
if (active) link.setAttribute("aria-current", "location");
else link.removeAttribute("aria-current");
});
if (updateHistory && window.location.hash !== `#${page}`) window.history.pushState({}, "", `#${page}`);
}
pageNavLinks.forEach((link) => link.addEventListener("click", (event) => {
event.preventDefault();
setActivePage(link.getAttribute("href").slice(1), true);
}));
window.addEventListener("popstate", () => setActivePage(window.location.hash.slice(1)));
setActivePage(window.location.hash.slice(1));
function setText(id, value, fallback = "—") {
byId(id).textContent = value === undefined || value === null || value === "" ? fallback : String(value);
}
function setSignal(name, text, state) {
const signal = byId(`${name}-signal`);
signal.textContent = text;
signal.dataset.state = state;
}
function setBadge(name, text, tone) {
const badge = byId(`${name}-badge`);
badge.textContent = text;
badge.className = `status-badge ${tone}`;
}
function notify(message) {
toast.textContent = message;
toast.hidden = false;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { toast.hidden = true; }, 4200);
}
function safeClientError(error) {
const message = error && typeof error.message === "string" ? error.message : "";
switch (message) {
case "creator authentication denied":
return "Логин или пароль не принят.";
case "Сессия отклонена":
return "Сессия истекла. Войдите снова.";
default:
return "Операция недоступна. Попробуйте ещё раз.";
}
}
async function request(path, options = {}) {
const headers = new Headers(options.headers || {});
if (sessionToken) headers.set("Authorization", `Bearer ${sessionToken}`);
headers.set("Accept", "application/json");
const response = await fetch(path, {...options, headers});
if (response.status === 401 && path !== "/api/session") {
endSession("Сессия истекла. Войдите снова.");
throw new Error("Сессия отклонена");
}
if (!response.ok) {
const detail = (await response.text()).trim();
const error = new Error(detail || `Ошибка ${response.status}`);
error.status = response.status;
throw error;
}
return response.json();
}
function restoreContext() {
try {
const saved = JSON.parse(localStorage.getItem("domovoy.creator.context") || "{}");
contextFields.forEach((id) => {
if (saved[id]) byId(id).value = saved[id];
});
} catch (_) {
localStorage.removeItem("domovoy.creator.context");
}
renderContextState();
renderContextPresets();
}
function saveContext() {
const saved = {};
contextFields.forEach((id) => { saved[id] = byId(id).value.trim(); });
localStorage.setItem("domovoy.creator.context", JSON.stringify(saved));
renderContextState();
return saved;
}
function readContextPresets() {
try {
const value = JSON.parse(localStorage.getItem(contextPresetsStorageKey) || "[]");
return Array.isArray(value) ? value.filter((item) => item && item.session && item.workspace) : [];
} catch (_) {
localStorage.removeItem(contextPresetsStorageKey);
return [];
}
}
function renderContextState() {
const state = byId("context-state");
if (!state) return;
const ready = Boolean(byId("session-id").value.trim() && byId("workspace-id").value.trim());
state.textContent = ready ? "готов к проверке" : "не выбран";
state.dataset.state = ready ? "ready" : "empty";
}
function renderContextPresets() {
const select = byId("context-preset");
if (!select) return;
const presets = readContextPresets();
select.replaceChildren(new Option("Выберите сохранённый набор…", ""));
presets.forEach((preset, index) => {
const session = preset.session.length > 18 ? `${preset.session.slice(0, 8)}…${preset.session.slice(-6)}` : preset.session;
const workspace = preset.workspace.length > 18 ? `${preset.workspace.slice(0, 8)}…${preset.workspace.slice(-6)}` : preset.workspace;
select.appendChild(new Option(`${session} · ${workspace}`, String(index)));
});
}
function rememberContextPreset(context) {
if (!context["session-id"] || !context["workspace-id"]) return;
const next = {session: context["session-id"], workspace: context["workspace-id"], savedAt: Date.now()};
const presets = readContextPresets().filter((item) => item.session !== next.session || item.workspace !== next.workspace);
presets.unshift(next);
localStorage.setItem(contextPresetsStorageKey, JSON.stringify(presets.slice(0, 5)));
renderContextPresets();
}
async function showConsole() {
const session = await request("/api/session/current");
setText("session-actor", session.ActorID);
const expiry = session.ExpiresAtUnix ? new Date(session.ExpiresAtUnix * 1000).toLocaleString("ru-RU") : "—";
setText("session-expiry", expiry);
loginView.hidden = true;
consoleView.hidden = false;
restoreSettings();
restoreContext();
await loadAdvisoryPolicy();
const context = saveContext();
if (context["session-id"] && context["workspace-id"]) loadDashboard();
else {
loadActionHistory();
loadLearning();
loadFleetReadiness();
loadAgentDelegation();
loadAgentWorkerStatus();
}
runDiagnostics();
}
function endSession(message = "Сессия завершена.") {
sessionToken = "";
clearInterval(refreshTimer);
refreshTimer = null;
sessionStorage.removeItem("domovoy.creator.session");
consoleView.hidden = true;
loginView.hidden = false;
loginForm.reset();
loginError.textContent = "";
currentTask = null;
currentProspective = null;
pendingCommand = null;
pendingNotification = null;
lastAuthorizedActionID = "";
advisoryForm.reset();
advisoryPolicy = {Mode: "deterministic", ExternalEgress: false, AcknowledgementRequired: false};
renderAdvisoryPolicy();
byId("advisory-result").hidden = true;
notify(message);
}
loginForm.addEventListener("submit", async (event) => {
event.preventDefault();
const button = byId("login-button");
const username = byId("creator-username").value.trim();
const password = byId("creator-password").value;
loginError.textContent = "";
button.disabled = true;
button.firstElementChild.textContent = "Проверяю данные…";
try {
const result = await request("/api/session", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({username, password})
});
sessionToken = result.Token;
sessionStorage.setItem("domovoy.creator.session", sessionToken);
byId("creator-password").value = "";
await showConsole();
} catch (error) {
loginError.textContent = safeClientError(error);
} finally {
button.disabled = false;
button.firstElementChild.textContent = "Открыть консоль";
}
});
byId("toggle-secret").addEventListener("click", () => {
const input = byId("creator-password");
input.type = input.type === "password" ? "text" : "password";
byId("toggle-secret").setAttribute("aria-label", input.type === "password" ? "Показать пароль" : "Скрыть пароль");
byId("toggle-secret").setAttribute("title", input.type === "password" ? "Показать пароль" : "Скрыть пароль");
});
contextForm.addEventListener("submit", (event) => {
event.preventDefault();
const context = saveContext();
rememberContextPreset(context);
loadDashboard();
});
for (const input of [byId("session-id"), byId("workspace-id")]) {
input.addEventListener("input", renderContextState);
}
byId("context-preset").addEventListener("change", (event) => {
const index = Number(event.target.value);
if (!Number.isInteger(index)) return;
const preset = readContextPresets()[index];
if (!preset) return;
byId("session-id").value = preset.session;
byId("workspace-id").value = preset.workspace;
renderContextState();
loadDashboard();
notify("Контекст из недавнего набора загружен.");
});
for (const button of document.querySelectorAll("[data-copy-context]")) {
button.addEventListener("click", async () => {
const value = byId(button.dataset.copyContext).value.trim();
if (!value) {
notify("Сначала укажите идентификатор.");
return;
}
try {
await navigator.clipboard.writeText(value);
notify("Идентификатор скопирован.");
} catch (_) {
notify("Не удалось скопировать идентификатор.");
}
});
}
for (const button of document.querySelectorAll("[data-copy-value], [data-use-value]")) {
button.addEventListener("click", async () => {
const source = byId(button.dataset.copyValue || button.dataset.useValue);
const value = source?.textContent.trim() || "";
if (!value || value === "—") {
notify("Сначала загрузите значение.");
return;
}
if (button.dataset.useValue) {
const target = byId(button.dataset.targetId);
if (!target) return;
target.value = value;
document.querySelector(".advanced-context").open = true;
saveContext();
notify("ID подставлен в расширенную проверку.");
return;
}
try {
await navigator.clipboard.writeText(value);
notify("ID скопирован.");
} catch (_) {
notify("Не удалось скопировать ID.");
}
});
}
byId("reset-context").addEventListener("click", () => {
contextFields.forEach((id) => { byId(id).value = id === "capability-id" ? "cognitive-proposal" : ""; });
localStorage.removeItem("domovoy.creator.context");
loadDashboard();
notify("Рабочий контекст сброшен.");
});
function restoreSettings() {
try {
const saved = JSON.parse(localStorage.getItem(settingsStorageKey) || "{}");
uiSettings = {
density: ["comfortable", "compact"].includes(saved.density) ? saved.density : "comfortable",
refresh: ["off", "30", "60", "300"].includes(saved.refresh) ? saved.refresh : "off",
motion: saved.motion === true,
};
} catch (_) {
uiSettings = {density: "comfortable", refresh: "off", motion: false};
}
byId("setting-density").value = uiSettings.density;
byId("setting-refresh").value = uiSettings.refresh;
byId("setting-motion").checked = uiSettings.motion;
applySettings();
}
function applySettings() {
document.documentElement.dataset.density = uiSettings.density;
document.documentElement.dataset.reducedMotion = uiSettings.motion ? "true" : "false";
clearInterval(refreshTimer);
refreshTimer = null;
if (sessionToken && uiSettings.refresh !== "off") {
refreshTimer = setInterval(() => loadDashboard(), Number(uiSettings.refresh) * 1000);
}
}
byId("settings-form").addEventListener("submit", (event) => {
event.preventDefault();
uiSettings = {
density: byId("setting-density").value,
refresh: byId("setting-refresh").value,
motion: byId("setting-motion").checked,
};
localStorage.setItem(settingsStorageKey, JSON.stringify(uiSettings));
applySettings();
setText("settings-saved", "Сохранено только в этом браузере");
notify("Настройки кабинета сохранены.");
});
byId("reset-settings").addEventListener("click", () => {
uiSettings = {density: "comfortable", refresh: "off", motion: false};
localStorage.removeItem(settingsStorageKey);
restoreSettings();
setText("settings-saved", "Восстановлены значения по умолчанию");
notify("Настройки сброшены.");
});
byId("refresh-all").addEventListener("click", loadDashboard);
byId("refresh-actions").addEventListener("click", loadActionHistory);
byId("refresh-learning").addEventListener("click", loadLearning);
byId("refresh-fleet").addEventListener("click", loadFleetReadiness);
byId("refresh-agent").addEventListener("click", loadAgentDelegation);
byId("logout").addEventListener("click", () => endSession());
byId("run-diagnostics").addEventListener("click", runDiagnostics);
advisoryForm.addEventListener("submit", async (event) => {
event.preventDefault();
const context = saveContext();
const message = byId("advisory-message").value.trim();
const button = byId("advisory-submit");
if (!context["session-id"] || !context["workspace-id"]) {
notify("Сначала укажите сессию и рабочую область контекста.");
return;
}
if (!message) return;
if (advisoryPolicy.AcknowledgementRequired && !byId("advisory-egress-ack").checked) {
notify("Подтвердите передачу этого вопроса внешнему провайдеру.");
byId("advisory-egress-ack").focus();
return;
}
button.disabled = true;
button.firstElementChild.textContent = "Проверяю контекст…";
byId("advisory-result").hidden = true;
try {
const result = await request("/api/advisory", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({message, session_id: context["session-id"], workspace_id: context["workspace-id"], egress_acknowledged: advisoryPolicy.AcknowledgementRequired && byId("advisory-egress-ack").checked})
});
const state = result.FinishReason || "INVALID_PROVIDER_RESPONSE";
setText("advisory-state", state);
byId("advisory-state").dataset.state = state;
const unavailable = advisoryPolicy.ExternalEgress ? "Внешний провайдер недоступен. Ответ не получен." : "Когнитивный провайдер пока не подключён. Наружу ничего не отправлено.";
setText("advisory-text", state === "PROVIDER_UNAVAILABLE" ? unavailable : result.AdvisoryText, "Ответ не получен.");
byId("advisory-result").hidden = false;
} catch (error) {
setText("advisory-state", "ОШИБКА");
setText("advisory-text", safeClientError(error));
byId("advisory-result").hidden = false;
} finally {
byId("advisory-egress-ack").checked = false;
button.disabled = false;
button.firstElementChild.textContent = "Получить совет";
}
});
async function loadAdvisoryPolicy() {
advisoryPolicy = await request("/api/advisory-policy");
renderAdvisoryPolicy();
}
function renderAdvisoryPolicy() {
const external = advisoryPolicy.ExternalEgress === true && advisoryPolicy.AcknowledgementRequired === true;
byId("advisory-egress").hidden = !external;
byId("advisory-egress-ack").checked = false;
byId("advisory-egress-ack").required = external;
setText("advisory-disclosure", advisoryPolicy.Disclosure, "Текст вопроса будет передан согласно активной политике данных.");
setText("advisory-privacy", external ? "внешняя передача по активной политике" : "активной внешней привязки нет");
const privacy = byId("advisory-privacy");
privacy.classList.toggle("external", external);
const marker = document.createElement("i");
marker.setAttribute("aria-hidden", "true");
privacy.prepend(marker);
}
async function loadDashboard() {
const context = saveContext();
const sessionID = context["session-id"];
const workspaceID = context["workspace-id"];
const query = new URLSearchParams({session_id: sessionID, workspace_id: workspaceID});
byId("connection-state").classList.remove("offline");
setText("last-update", "Обновление…");
const work = [];
if (sessionID && workspaceID) work.push(loadFocus(query));
else {
setSignal("focus", "нет контекста", "idle");
setBadge("focus", "НЕ ЗАГРУЖЕН", "neutral");
setText("focus-message", "Введите идентификаторы сессии и рабочей области.");
}
if (sessionID && workspaceID && context["task-id"]) work.push(loadTask(context["task-id"], query));
else {
currentTask = null;
setSignal("task", "нет задачи", "idle");
setBadge("task", "НЕ ЗАГРУЖЕНА", "neutral");
setText("task-message", "Укажите задачу для проверки относительно активного фокуса.");
updateActionReadiness();
}
work.push(loadProspectiveRegister(context["prospective-id"]));
work.push(loadActionHistory());
work.push(loadLearning());
work.push(loadFleetReadiness());
work.push(loadAgentDelegation());
work.push(loadAgentWorkerStatus());
if (context["capability-id"]) work.push(loadCapability(context["capability-id"]));
else setSignal("capability", "не выбрана", "idle");
await Promise.allSettled(work);
setText("last-update", `Обновлено ${new Intl.DateTimeFormat("ru-RU", {hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(new Date())}`);
}
async function runDiagnostics() {
const list = byId("diagnostic-list");
const summary = byId("diagnostic-summary");
const checks = [
["Сессия", "identity", () => request("/api/session/current")],
["Политика совета", "advisory", () => request("/api/advisory-policy")],
["Когнитивная возможность", "capability", () => request("/api/capabilities/cognitive-proposal/health")],
["Сегодня", "today", () => request("/api/today")],
["Внимание", "attention", () => request("/api/attention")],
["Действия", "actions", () => request("/api/actions?limit=1")],
["Learning evidence", "learning", () => request("/api/learning/recent?limit=1")],
];
list.setAttribute("aria-busy", "true");
list.replaceChildren();
checks.forEach(([label, key]) => list.append(createDiagnosticItem(label, key, "loading")));
setText("diagnostic-summary", "Проверяю…");
const results = await Promise.all(checks.map(async ([label, key, run]) => {
const started = performance.now();
try {
await run();
return {label, key, state: "ok", detail: `Ответ получен · ${Math.round(performance.now() - started)} мс`};
} catch (error) {
const detail = error?.status === 400 ? "Нужны параметры рабочего контекста" : error?.status === 401 ? "Сессия истекла" : error?.status >= 500 ? "Сервис временно недоступен" : "Проверка не пройдена";
return {label, key, state: "error", detail};
}
}));
list.replaceChildren(...results.map((result) => createDiagnosticItem(result.label, result.key, result.state, result.detail)));
list.setAttribute("aria-busy", "false");
const passed = results.filter((result) => result.state === "ok").length;
setText("diagnostic-summary", `${passed}/${results.length} проверок пройдено`);
}
function createDiagnosticItem(label, key, state, detail = "Проверяю…") {
const item = document.createElement("article");
item.className = "diagnostic-item";
item.dataset.state = state;
item.dataset.check = key;
const marker = document.createElement("i");
marker.setAttribute("aria-hidden", "true");
const copy = document.createElement("div");
const title = document.createElement("strong");
title.textContent = label;
const description = document.createElement("span");
description.textContent = detail;
copy.append(title, description);
const status = document.createElement("em");
status.textContent = state === "ok" ? "OK" : state === "error" ? "ошибка" : "…";
item.append(marker, copy, status);
return item;
}
async function loadFleetReadiness() {
const list = byId("fleet-list");
list.setAttribute("aria-busy", "true");
list.replaceChildren();
const loading = document.createElement("p");
loading.className = "register-empty";
loading.textContent = "Проверяю доступность model fleet…";
list.append(loading);
try {
const result = await request("/api/fleet/readiness");
const readiness = result.readiness || {};
const members = Array.isArray(readiness.members) ? readiness.members : [];
setText("fleet-selected", result.selected_tier || "не выбран");
setText("fleet-summary", `${members.filter((member) => member.ready).length}/${members.length} ГОТОВЫ`);
setText("fleet-explanation", result.selected_tier ? `Активный маршрут: ${result.selected_tier}. Остальные tier остаются видимыми с их фактической причиной.` : "Ни один tier не выбран: проверьте конфигурацию и health провайдеров.");
list.replaceChildren(...members.map(createFleetRow));
} catch (error) {
setText("fleet-selected", "недоступен");
setText("fleet-summary", "ОШИБКА");
setText("fleet-explanation", safeClientError(error));
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
failed.textContent = safeClientError(error);
list.replaceChildren(failed);
} finally {
list.setAttribute("aria-busy", "false");
}
}
function createFleetRow(member) {
const row = document.createElement("article");
row.className = "fleet-row";
const state = document.createElement("em");
state.className = "learning-state";
state.dataset.state = member.ready ? "READY" : "DEGRADED";
state.textContent = member.ready ? "READY" : "DEGRADED";
const title = document.createElement("strong");
title.textContent = member.tier || "UNKNOWN TIER";
const detail = document.createElement("span");
detail.textContent = `${member.model_descriptor_id || "model"} · ${member.reason || "причина не указана"}`;
row.append(state, title, detail);
return row;
}
async function loadAgentDelegation() {
const delegationID = byId("delegation-id").value.trim();
const empty = byId("agent-empty");
const details = byId("agent-details");
if (!delegationID) {
empty.hidden = false;
details.hidden = true;
setText("agent-summary", "DELEGATION НЕ ВЫБРАНА");
return;
}
empty.hidden = false;
empty.textContent = "Читаю durable delegation…";
details.hidden = true;
try {
const delegation = await request(`/api/delegations/${encodeURIComponent(delegationID)}`);
const checkpoint = await request(`/api/delegations/${encodeURIComponent(delegationID)}/latest-checkpoint`).catch(() => ({}));
setText("agent-summary", delegation.State || "СОСТОЯНИЕ НЕИЗВЕСТНО");
setText("agent-delegation-state", delegation.State);
setText("agent-run-state", `${delegation.LatestRunID || "run не выбран"} · ${delegation.SelectedImplementationID || "implementation не указана"}`);
setText("agent-blocker", checkpoint.BlockerClass || "нет checkpoint");
setText("agent-next-step", checkpoint.NextStep || checkpoint.RemainingSummary || delegation.DesiredOutcome || "Следующий шаг не записан.");
empty.hidden = true;
details.hidden = false;
} catch (error) {
setText("agent-summary", "ОШИБКА");
empty.textContent = safeClientError(error);
empty.hidden = false;
}
}
async function loadAgentWorkerStatus() {
try {
const status = await request("/api/agent-worker/status");
setText("agent-worker-state", status.mode || "UNKNOWN");
setText("agent-worker-message", status.reason || "Причина не указана.");
byId("agent-worker-state").dataset.state = status.ready === true ? "ready" : "degraded";
} catch (error) {
setText("agent-worker-state", "НЕДОСТУПЕН");
setText("agent-worker-message", safeClientError(error));
byId("agent-worker-state").dataset.state = "degraded";
}
}
async function loadLearning() {
const experienceList = byId("experience-list");
const reflectionList = byId("reflection-list");
const skillList = byId("skill-list");
[experienceList, reflectionList, skillList].forEach((list) => {
list.setAttribute("aria-busy", "true");
list.replaceChildren();
const loading = document.createElement("p");
loading.className = "register-empty";
loading.textContent = "Читаю learning evidence…";
list.append(loading);
});
try {
const result = await request("/api/learning/recent?limit=8");
const experiences = Array.isArray(result.Experiences) ? result.Experiences : [];
const reflections = Array.isArray(result.Reflections) ? result.Reflections : [];
const skills = Array.isArray(result.Skills) ? result.Skills : [];
setText("learning-count", `${experiences.length + reflections.length + skills.length} СИГНАЛОВ`);
renderLearningList(experienceList, experiences, createExperienceRow, "Experience пока не записан.");
renderLearningList(reflectionList, reflections, createReflectionRow, "ReflectionCandidate пока нет.");
renderLearningList(skillList, skills, createSkillRow, "SkillProfile пока не загружен.");
} catch (error) {
[experienceList, reflectionList, skillList].forEach((list) => {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
failed.textContent = safeClientError(error);
list.append(failed);
});
setText("learning-count", "ОШИБКА");
} finally {
[experienceList, reflectionList, skillList].forEach((list) => list.setAttribute("aria-busy", "false"));
}
}
function renderLearningList(list, items, rowFactory, emptyText) {
list.replaceChildren();
if (items.length === 0) {
const empty = document.createElement("p");
empty.className = "register-empty";
empty.textContent = emptyText;
list.append(empty);
return;
}
items.forEach((item) => list.append(rowFactory(item)));
}
function createExperienceRow(item) {
const row = document.createElement("article");
row.className = "learning-row";
const state = document.createElement("em");
state.className = "learning-state";
state.dataset.state = item.ActualOutcome || "UNKNOWN";
state.textContent = item.ActualOutcome || "UNKNOWN";
const title = document.createElement("strong");
title.textContent = item.HelpMethod || "help method";
const detail = document.createElement("span");
detail.textContent = item.OutcomeDetail || item.OutcomeRef || "evidence recorded";
const time = document.createElement("time");
time.textContent = item.RecordedAtUnixMillis ? new Date(item.RecordedAtUnixMillis).toLocaleString("ru-RU") : "время неизвестно";
row.append(state, title, detail, time);
return row;
}
function createReflectionRow(item) {
const row = document.createElement("article");
row.className = "learning-row";
const state = document.createElement("em");
state.className = "learning-state";
state.dataset.state = item.State || "OPEN";
state.textContent = item.Trigger || "REFLECTION";
const title = document.createElement("strong");
title.textContent = item.State || "OPEN";
const detail = document.createElement("span");
detail.textContent = item.Summary || "reflection candidate";
const time = document.createElement("time");
time.textContent = item.RecordedAtUnixMillis ? new Date(item.RecordedAtUnixMillis).toLocaleString("ru-RU") : "время неизвестно";
row.append(state, title, detail, time);
return row;
}
function createSkillRow(item) {
const row = document.createElement("article");
row.className = "learning-row";
const state = document.createElement("em");
state.className = "learning-state";
state.dataset.state = item.Status || "UNKNOWN";
state.textContent = item.Status || "UNKNOWN";
const title = document.createElement("strong");
title.textContent = item.Name || "skill profile";
const detail = document.createElement("span");
const support = Array.isArray(item.SupportingExperienceRefs) ? item.SupportingExperienceRefs.length : 0;
const failures = Array.isArray(item.KnownFailureExperienceRefs) ? item.KnownFailureExperienceRefs.length : 0;
detail.textContent = `confidence ${item.ConfidenceMillis ?? "—"} · +${support} / -${failures}`;
const time = document.createElement("time");
time.textContent = item.RecordedAtUnixMillis ? new Date(item.RecordedAtUnixMillis).toLocaleString("ru-RU") : "время неизвестно";
row.append(state, title, detail, time);
return row;
}
async function loadActionHistory() {
const list = byId("action-history");
list.setAttribute("aria-busy", "true");
list.replaceChildren();
const loading = document.createElement("p");
loading.className = "register-empty";
loading.textContent = "Читаю канонический журнал Commit…";
list.append(loading);
try {
const result = await request("/api/actions?limit=25");
const items = Array.isArray(result.Items) ? result.Items : [];
setText("action-count", `${items.length} ${items.length === 1 ? "ЗАПИСЬ" : "ЗАПИСЕЙ"}`);
list.replaceChildren();
if (items.length === 0) {
const empty = document.createElement("p");
empty.className = "register-empty";
empty.textContent = "У текущего Создателя пока нет авторизованных действий.";
list.append(empty);
return;
}
items.forEach((item) => list.append(createActionHistoryRow(item)));
} catch (error) {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
failed.textContent = safeClientError(error);
list.append(failed);
setText("action-count", "ОШИБКА");
} finally {
list.setAttribute("aria-busy", "false");
}
}
function createActionHistoryRow(item) {
const row = document.createElement("article");
row.className = "activity-row";
const state = document.createElement("strong");
state.className = "activity-state";
state.dataset.state = item.State || "UNKNOWN";
state.textContent = item.State || "UNKNOWN";
const operation = document.createElement("div");
operation.className = "activity-operation";
const operationName = document.createElement("b");
operationName.textContent = item.Operation || "неизвестная операция";
const subject = document.createElement("span");
subject.textContent = item.ProspectiveItemID || "без subject";
operation.append(operationName, subject);
const identity = document.createElement("div");
identity.className = "activity-identity";
const actionID = document.createElement("code");
actionID.textContent = item.AuthorizedActionID || "—";
actionID.title = item.AuthorizedActionID || "";
const attemptID = document.createElement("small");
attemptID.textContent = item.ActionAttemptID ? `attempt ${item.ActionAttemptID}` : "попытка ещё не создана";
attemptID.title = item.ActionAttemptID || "";
identity.append(actionID, attemptID);
const time = document.createElement("time");
time.dateTime = item.AuthorizedAtUnixMillis ? new Date(item.AuthorizedAtUnixMillis).toISOString() : "";
time.textContent = item.AuthorizedAtUnixMillis ? new Date(item.AuthorizedAtUnixMillis).toLocaleString("ru-RU") : "время неизвестно";
row.append(state, operation, identity, time);
if (item.State === "UNKNOWN_OUTCOME" && item.AuthorizedActionID) {
const reconcile = document.createElement("button");
reconcile.type = "button";
reconcile.className = "activity-reconcile";
reconcile.textContent = "Сверить";
reconcile.setAttribute("aria-label", `Сверить исход действия ${item.AuthorizedActionID}`);
reconcile.addEventListener("click", () => reconcileHistoryAction(item.AuthorizedActionID, reconcile));
row.append(reconcile);
}
return row;
}
async function reconcileHistoryAction(actionID, button) {
button.disabled = true;
button.textContent = "Сверяю…";
try {
const result = await request(`/api/actions/${encodeURIComponent(actionID)}/reconcile`, {
method: "POST", headers: {"Content-Type": "application/json"}, body: "{}",
});
renderActionResult(result);
notify(result.State === "SUCCEEDED" ? "Исход подтверждён." : "Исход пока неизвестен.");
await loadActionHistory();
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
} catch (error) {
notify(safeClientError(error));
button.disabled = false;
button.textContent = "Сверить";
}
}
async function loadFocus(query) {
setSignal("focus", "загрузка", "loading");
try {
const focus = await request(`/api/focus?${query}`);
setText("focus-project", focus.ProjectID);
setText("focus-goal", focus.GoalID);
setText("focus-scope", focus.ScopeID);
setText("focus-binding", focus.ID);
setText("focus-message", "Фокус принадлежит текущему Actor и прошёл проверку сессии.");
setSignal("focus", "активен", "ok");
setBadge("focus", focus.Active ? "АКТИВЕН" : "НЕАКТИВЕН", focus.Active ? "good" : "warn");
} catch (error) {
setSignal("focus", "ошибка", "error");
setBadge("focus", "НЕДОСТУПЕН", "bad");
setText("focus-message", safeClientError(error));
byId("connection-state").classList.add("offline");
}
}
async function loadTask(taskID, query) {
setSignal("task", "загрузка", "loading");
try {
const task = await request(`/api/tasks/${encodeURIComponent(taskID)}?${query}`);
currentTask = task;
setText("task-value", task.TaskID);
setText("task-goal", task.GoalID);
setText("task-scope", task.ScopeID);
setText("task-revision", task.Revision);
setText("task-message", "Цель и область задачи совпадают с активным фокусом.");
setSignal("task", "в фокусе", "ok");
setBadge("task", "ПРОВЕРЕНА", "good");
updateActionReadiness();
} catch (error) {
currentTask = null;
setSignal("task", "отклонена", "error");
setBadge("task", "ВНЕ ФОКУСА", "bad");
setText("task-message", safeClientError(error));
updateActionReadiness();
}
}
async function loadProspectiveRegister(selectedID) {
const list = byId("prospective-list");
list.replaceChildren();
const loading = document.createElement("p");
loading.className = "register-empty";
loading.textContent = "Загружаю владельческий реестр…";
list.append(loading);
setSignal("prospective", "загрузка", "loading");
try {
const result = await request("/api/prospective-items?limit=25");
const items = Array.isArray(result.Items) ? result.Items : [];
setText("prospective-count", `${items.length} ${items.length === 1 ? "ЗАПИСЬ" : "ЗАПИСЕЙ"}`);
list.replaceChildren();
if (items.length === 0) {
const empty = document.createElement("p");
empty.className = "register-empty";
empty.textContent = "Для текущего Principal ожиданий нет.";
list.append(empty);
setSignal("prospective", "пусто", "idle");
setBadge("prospective", "НЕТ ЗАПИСЕЙ", "neutral");
return;
}
let effectiveID = selectedID;
if (!effectiveID) {
effectiveID = items[0].ID;
byId("prospective-id").value = effectiveID;
saveContext();
}
items.forEach((item) => list.append(createProspectiveRow(item, effectiveID)));
await loadProspective(effectiveID);
} catch (error) {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty";
failed.textContent = safeClientError(error);
list.append(failed);
setText("prospective-count", "ОШИБКА");
setSignal("prospective", "ошибка", "error");
setBadge("prospective", "НЕДОСТУПНА", "bad");
setText("prospective-message", safeClientError(error));
}
}
function createProspectiveRow(item, selectedID) {
const row = document.createElement("button");
row.type = "button";
row.className = "register-row";
row.setAttribute("aria-current", item.ID === selectedID ? "true" : "false");
row.setAttribute("aria-label", `Открыть ожидание ${item.ID}, состояние ${item.State}`);
const id = document.createElement("strong");
id.textContent = item.ID;
const goal = document.createElement("span");
goal.textContent = item.GoalID || "без цели";
const trigger = document.createElement("time");
trigger.textContent = item.TriggerAtUnixMillis ? new Date(item.TriggerAtUnixMillis).toLocaleString("ru-RU") : "без времени";
const state = document.createElement("em");
state.textContent = item.State || "UNKNOWN";
if (["TRIGGERED", "REVALIDATING", "DELIVERED"].includes(item.State)) state.className = "warn";
if (["SUPPRESSED", "EXPIRED"].includes(item.State)) state.className = "bad";
row.append(id, goal, trigger, state);
row.addEventListener("click", async () => {
byId("prospective-id").value = item.ID;
saveContext();
row.parentElement.querySelectorAll(".register-row").forEach((candidate) => candidate.setAttribute("aria-current", candidate === row ? "true" : "false"));
await loadProspective(item.ID);
});
return row;
}
async function loadProspective(prospectiveID) {
setSignal("prospective", "загрузка", "loading");
try {
const item = await request(`/api/prospective-items/${encodeURIComponent(prospectiveID)}`);
currentProspective = item;
const state = item.State || "UNKNOWN";
const goodStates = ["REGISTERED", "ARMED", "SATISFIED"];
const warningStates = ["TRIGGERED", "REVALIDATING", "DELIVERED"];
const tone = goodStates.includes(state) ? "good" : warningStates.includes(state) ? "warn" : "bad";
setText("prospective-state", state);
setText("prospective-trigger", item.TriggerAtUnixMillis ? new Date(item.TriggerAtUnixMillis).toLocaleString("ru-RU") : "не указано");
setText("prospective-value", item.ID);
setText("prospective-commitment", item.CommitmentID);
setText("prospective-goal", item.GoalID);
setText("prospective-scope", item.ScopeID);
setText("prospective-owner", item.OwnerPrincipalID);
setText("prospective-revision", item.Revision);
setText("prospective-message", "Запись принадлежит текущему Principal и прочитана из канонического Continuity state.");
setSignal("prospective", state.toLowerCase(), tone === "bad" ? "error" : "ok");
setBadge("prospective", state, tone);
updateActionReadiness();
} catch (error) {
currentProspective = null;
setSignal("prospective", "ошибка", "error");
setBadge("prospective", "НЕДОСТУПНА", "bad");
setText("prospective-state", "UNKNOWN");
setText("prospective-message", safeClientError(error));
updateActionReadiness();
}
}
async function loadCapability(capabilityID) {
setSignal("capability", "загрузка", "loading");
try {
const health = await request(`/api/capabilities/${encodeURIComponent(capabilityID)}/health`);
const state = health.State || "UNKNOWN";
const tone = state === "AVAILABLE" ? "good" : state === "DEGRADED" ? "warn" : "bad";
setText("capability-state", state);
setText("capability-reason", health.Reason, "NONE");
setText("capability-value", health.CapabilityID);
setText("capability-revision", health.Revision);
setText("capability-observed", health.ObservedAtUnixMillis ? new Date(health.ObservedAtUnixMillis).toLocaleString("ru-RU") : "не указано");
setSignal("capability", state.toLowerCase(), tone === "bad" ? "error" : "ok");
setBadge("capability", state, tone);
} catch (error) {
setSignal("capability", "ошибка", "error");
setBadge("capability", "НЕДОСТУПНА", "bad");
setText("capability-state", "UNKNOWN");
setText("capability-reason", safeClientError(error));
}
}
function updateActionReadiness() {
const button = byId("prepare-satisfaction");
const notificationButton = byId("prepare-notification");
if (!button) return;
const evidence = byId("satisfaction-evidence").value.trim();
let message = "Доступно только для доставленного ожидания с проверенной Work-задачей.";
let ready = false;
if (!currentProspective) message = "Сначала выберите и загрузите ожидание.";
else if (currentProspective.State !== "DELIVERED") message = `Состояние ${currentProspective.State || "UNKNOWN"}: переход доступен только из DELIVERED.`;
else if (!currentTask) message = "Загрузите проверенную Work-задачу как обоснование.";
else if (currentTask.GoalID !== currentProspective.GoalID || currentTask.ScopeID !== currentProspective.ScopeID) message = "Цель или область Work-задачи не совпадает с ожиданием.";
else if (!evidence) message = "Добавьте ссылку на подтверждение результата.";
else {
ready = true;
message = `Готово: ${currentProspective.ID} · rev ${currentProspective.Revision} будет связано с ${currentTask.TaskID} · rev ${currentTask.Revision}.`;
}
button.disabled = !ready;
setText("action-readiness", message);
let notificationReady = false;
let notificationMessage = "Доступно для доставленного ожидания с проверенной Work-задачей.";
if (!currentProspective) notificationMessage = "Сначала выберите и загрузите ожидание.";
else if (currentProspective.State !== "DELIVERED") notificationMessage = `Состояние ${currentProspective.State || "UNKNOWN"}: уведомление доступно только для DELIVERED.`;
else if (!currentTask) notificationMessage = "Загрузите проверенную Work-задачу как обоснование.";
else if (currentTask.GoalID !== currentProspective.GoalID || currentTask.ScopeID !== currentProspective.ScopeID) notificationMessage = "Цель или область Work-задачи не совпадает с ожиданием.";
else {
notificationReady = true;
notificationMessage = `Готово: receiver получит фиксированный шаблон для ${currentProspective.ID} · rev ${currentProspective.Revision}.`;
}
notificationButton.disabled = !notificationReady;
setText("notification-readiness", notificationMessage);
}
byId("satisfaction-evidence").addEventListener("input", updateActionReadiness);
byId("prepare-satisfaction").addEventListener("click", () => {
updateActionReadiness();
if (byId("prepare-satisfaction").disabled || !currentTask || !currentProspective) return;
pendingCommand = {
Target: {
TaskID: currentTask.TaskID,
GoalID: currentTask.GoalID,
ScopeID: currentTask.ScopeID,
Revision: currentTask.Revision,
},
ProspectiveItemID: currentProspective.ID,
ExpectedRevision: currentProspective.Revision,
EvidenceRef: byId("satisfaction-evidence").value.trim(),
};
setText("confirm-prospective", pendingCommand.ProspectiveItemID);
setText("confirm-prospective-revision", pendingCommand.ExpectedRevision);
setText("confirm-task", pendingCommand.Target.TaskID);
setText("confirm-task-revision", pendingCommand.Target.Revision);
setText("confirm-evidence", pendingCommand.EvidenceRef);
setText("confirmation-error", "", "");
byId("satisfaction-dialog").showModal();
});
byId("cancel-satisfaction").addEventListener("click", () => {
pendingCommand = null;
byId("satisfaction-dialog").close();
});
byId("prepare-notification").addEventListener("click", () => {
updateActionReadiness();
if (byId("prepare-notification").disabled || !currentTask || !currentProspective) return;
pendingNotification = {
Target: {TaskID: currentTask.TaskID, GoalID: currentTask.GoalID, ScopeID: currentTask.ScopeID, Revision: currentTask.Revision},
ProspectiveItemID: currentProspective.ID,
ExpectedRevision: currentProspective.Revision,
Confirm: true,
};
setText("notify-confirm-prospective", pendingNotification.ProspectiveItemID);
setText("notify-confirm-prospective-revision", pendingNotification.ExpectedRevision);
setText("notify-confirm-task", pendingNotification.Target.TaskID);
setText("notify-confirm-task-revision", pendingNotification.Target.Revision);
setText("notification-confirmation-error", "", "");
byId("notification-dialog").showModal();
});
byId("cancel-notification").addEventListener("click", () => {
pendingNotification = null;
byId("notification-dialog").close();
});
byId("confirm-notification").addEventListener("click", async () => {
if (!pendingNotification) return;
const command = pendingNotification;
const button = byId("confirm-notification");
const prepare = byId("prepare-notification");
button.disabled = true;
prepare.disabled = true;
button.textContent = "Отправляю…";
setText("notification-confirmation-error", "", "");
try {
const result = await request("/api/actions/notify-delivered", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(command)});
pendingNotification = null;
byId("notification-dialog").close();
renderActionResult(result);
await loadActionHistory();
notify(result.State === "SUCCEEDED" ? "Receiver принял уведомление." : "Исход отправки требует сверки.");
} catch (error) {
setText("notification-confirmation-error", safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Подтверждаю отправку";
updateActionReadiness();
}
});
byId("confirm-satisfaction").addEventListener("click", async () => {
if (!pendingCommand) return;
const command = pendingCommand;
const button = byId("confirm-satisfaction");
const prepare = byId("prepare-satisfaction");
button.disabled = true;
prepare.disabled = true;
button.textContent = "Фиксирую…";
setText("confirmation-error", "", "");
try {
const result = await request("/api/actions/satisfy-delivered", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(command),
});
pendingCommand = null;
byId("satisfaction-dialog").close();
renderActionResult(result);
await loadActionHistory();
notify(result.State === "SUCCEEDED" ? "Исполнение зафиксировано через Commit." : "Исход требует проверки.");
await loadProspectiveRegister(command.ProspectiveItemID);
} catch (error) {
setText("confirmation-error", safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Подтверждаю переход";
updateActionReadiness();
}
});
function renderActionResult(result) {
lastAuthorizedActionID = result.AuthorizedActionID || "";
setText("action-state", result.State);
byId("action-state").dataset.state = result.State || "UNKNOWN";
setText("action-id", result.AuthorizedActionID);
setText("attempt-id", result.ActionAttemptID);
byId("action-result").hidden = false;
byId("reconcile-action").hidden = result.State !== "UNKNOWN_OUTCOME" || !lastAuthorizedActionID;
}
byId("reconcile-action").addEventListener("click", async () => {
if (!lastAuthorizedActionID) return;
const button = byId("reconcile-action");
button.disabled = true;
button.textContent = "Проверяю…";
try {
const result = await request(`/api/actions/${encodeURIComponent(lastAuthorizedActionID)}/reconcile`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: "{}",
});
renderActionResult(result);
await loadActionHistory();
notify(result.State === "SUCCEEDED" ? "Исход подтверждён." : "Исход пока неизвестен.");
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
} catch (error) {
notify(safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Проверить исход";
}
});
if (sessionToken) {
showConsole().catch(() => endSession("Сохранённая сессия больше не действует."));
}