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"]; let sessionToken = sessionStorage.getItem("domovoy.creator.session") || ""; let toastTimer; let currentTask = null; let currentProspective = null; let pendingCommand = null; let pendingNotification = null; let lastAuthorizedActionID = ""; let advisoryPolicy = {Mode: "deterministic", ExternalEgress: false, AcknowledgementRequired: false}; 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); } 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(); throw new Error(detail || `Ошибка ${response.status}`); } 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"); } } function saveContext() { const saved = {}; contextFields.forEach((id) => { saved[id] = byId(id).value.trim(); }); localStorage.setItem("domovoy.creator.context", JSON.stringify(saved)); return saved; } 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; restoreContext(); await loadAdvisoryPolicy(); const context = saveContext(); if (context["session-id"] && context["workspace-id"]) loadDashboard(); else { loadActionHistory(); loadLearning(); } } function endSession(message = "Сессия завершена.") { sessionToken = ""; 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 = error.message === "creator authentication denied" ? "Логин или пароль не принят." : error.message; } 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(); saveContext(); loadDashboard(); }); byId("refresh-all").addEventListener("click", loadDashboard); byId("refresh-actions").addEventListener("click", loadActionHistory); byId("refresh-learning").addEventListener("click", loadLearning); byId("logout").addEventListener("click", () => endSession()); 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", error.message); 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()); 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 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 = error.message; 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 = error.message; 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(error.message); 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", error.message); 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", error.message); 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 = error.message; list.append(failed); setText("prospective-count", "ОШИБКА"); setSignal("prospective", "ошибка", "error"); setBadge("prospective", "НЕДОСТУПНА", "bad"); setText("prospective-message", error.message); } } 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", error.message); 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", error.message); } } 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", error.message); } 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", error.message); } 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(error.message); } finally { button.disabled = false; button.textContent = "Проверить исход"; } }); if (sessionToken) { showConsole().catch(() => endSession("Сохранённая сессия больше не действует.")); }