Files
Assistant/cmd/creator-web/static/assets/console.js
T
m 543b93105b
CI / test (push) Successful in 1m15s
feat: add Continuity register to Domovoy
2026-08-16 15:59:19 +03:00

313 lines
13 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 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;
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();
const context = saveContext();
if (context["session-id"] && context["workspace-id"]) loadDashboard();
}
function endSession(message = "Сессия завершена.") {
sessionToken = "";
sessionStorage.removeItem("domovoy.creator.session");
consoleView.hidden = true;
loginView.hidden = false;
loginForm.reset();
loginError.textContent = "";
notify(message);
}
loginForm.addEventListener("submit", async (event) => {
event.preventDefault();
const button = byId("login-button");
const bootstrap = byId("bootstrap-token").value.trim();
loginError.textContent = "";
button.disabled = true;
button.firstElementChild.textContent = "Проверяю ключ…";
try {
const result = await request("/api/session", {method: "POST", headers: {Authorization: `Bearer ${bootstrap}`}});
sessionToken = result.Token;
sessionStorage.setItem("domovoy.creator.session", sessionToken);
byId("bootstrap-token").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("bootstrap-token");
input.type = input.type === "password" ? "text" : "password";
byId("toggle-secret").setAttribute("aria-label", input.type === "password" ? "Показать ключ" : "Скрыть ключ");
});
contextForm.addEventListener("submit", (event) => {
event.preventDefault();
saveContext();
loadDashboard();
});
byId("refresh-all").addEventListener("click", loadDashboard);
byId("logout").addEventListener("click", () => endSession());
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 {
setSignal("task", "нет задачи", "idle");
setBadge("task", "НЕ ЗАГРУЖЕНА", "neutral");
setText("task-message", "Укажите задачу для проверки относительно активного фокуса.");
}
work.push(loadProspectiveRegister(context["prospective-id"]));
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 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}`);
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");
} catch (error) {
setSignal("task", "отклонена", "error");
setBadge("task", "ВНЕ ФОКУСА", "bad");
setText("task-message", error.message);
}
}
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)}`);
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);
} catch (error) {
setSignal("prospective", "ошибка", "error");
setBadge("prospective", "НЕДОСТУПНА", "bad");
setText("prospective-state", "UNKNOWN");
setText("prospective-message", error.message);
}
}
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);
}
}
if (sessionToken) {
showConsole().catch(() => endSession("Сохранённая сессия больше не действует."));
}