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("Сохранённая сессия больше не действует.")); }