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