216 lines
8.4 KiB
JavaScript
216 lines
8.4 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", "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", "Укажите задачу для проверки относительно активного фокуса.");
|
|
}
|
|
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 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("Сохранённая сессия больше не действует."));
|
|
}
|