diff --git a/cmd/creator-web/server_test.go b/cmd/creator-web/server_test.go
index 1d90693..e1f46e7 100644
--- a/cmd/creator-web/server_test.go
+++ b/cmd/creator-web/server_test.go
@@ -233,6 +233,12 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Домовой") ||
+ !strings.Contains(response.Body.String(), "context-preset") ||
+ !strings.Contains(response.Body.String(), "context-state") ||
+ !strings.Contains(response.Body.String(), "copy-context") ||
+ !strings.Contains(response.Body.String(), "К содержимому") ||
+ !strings.Contains(response.Body.String(), "aria-current=\"page\"") ||
+ !strings.Contains(response.Body.String(), "section-nav-link") ||
!strings.Contains(response.Body.String(), "satisfaction-dialog") || !strings.Contains(response.Body.String(), "Подтверждаю переход") ||
!strings.Contains(response.Body.String(), "action-history") || !strings.Contains(response.Body.String(), "Журнал действий") ||
!strings.Contains(response.Body.String(), "advisory-form") || !strings.Contains(response.Body.String(), "Спросить Домового") ||
diff --git a/cmd/creator-web/static/assets/console.css b/cmd/creator-web/static/assets/console.css
index 8dbd237..008a05d 100644
--- a/cmd/creator-web/static/assets/console.css
+++ b/cmd/creator-web/static/assets/console.css
@@ -1,12 +1,12 @@
:root {
- --ink: #e9eee7;
- --muted: #8c9a91;
- --faint: #58645d;
- --base: #0b100e;
- --panel: #101714;
- --panel-high: #151e1a;
- --line: #2a352f;
- --line-soft: #1d2822;
+ --ink: #eef2ec;
+ --muted: #a7b2aa;
+ --faint: #718078;
+ --base: #0f1512;
+ --panel: #17201b;
+ --panel-high: #202b25;
+ --line: #34423a;
+ --line-soft: #26332c;
--signal: #c9a85f;
--signal-bright: #e7c97b;
--good: #78c6a3;
@@ -21,13 +21,15 @@
* { box-sizing: border-box; }
[hidden] { display: none !important; }
-html { background: var(--base); color: var(--ink); font-family: var(--sans); }
-body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .15), transparent 38%), var(--base); }
+html { background: var(--base); color: var(--ink); font-family: var(--sans); scroll-behavior: smooth; }
+body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .13), transparent 38%), var(--base); }
button, input, textarea { font: inherit; }
button { color: inherit; }
-button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
+button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
+.skip-link { position: fixed; top: 10px; left: 10px; z-index: 30; padding: 10px 13px; color: #11150f; background: var(--signal-bright); font: 600 10px/1 var(--mono); text-decoration: none; transform: translateY(-160%); transition: transform .18s; }
+.skip-link:focus { transform: translateY(0); }
-.grain { position: fixed; inset: 0; pointer-events: none; opacity: .18; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); }
+.grain { position: fixed; inset: 0; pointer-events: none; opacity: .045; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); }
.eyebrow, .rail-index { margin: 0 0 12px; color: var(--signal); font: 600 11px/1 var(--mono); letter-spacing: .18em; text-transform: uppercase; }
.login-shell { min-height: 100vh; display: grid; place-items: center; padding: 32px 20px; position: relative; overflow: hidden; }
@@ -63,6 +65,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.wordmark strong { font: 400 20px/1 var(--serif); }
.wordmark small { margin-top: 4px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.topbar-actions { display: flex; align-items: center; gap: 10px; }
+.topbar-nav { display: flex; align-items: center; gap: 4px; margin-right: 8px; }
+.topbar-nav a { padding: 9px 10px; color: var(--muted); text-decoration: none; font: 600 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
+.topbar-nav a:hover, .topbar-nav a:focus-visible { color: var(--ink); background: var(--panel-high); outline: 0; }
.connection { margin-right: 12px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.connection i { width: 7px; height: 7px; display: inline-block; margin-right: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 14px rgba(120, 198, 163, .6); }
.connection.offline i { background: var(--bad); box-shadow: none; }
@@ -73,17 +78,54 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.context-rail h2 { margin: 0; font: 400 29px/1.1 var(--serif); }
.rail-copy { margin: 14px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.context-form { display: grid; }
+.context-status-row { margin-bottom: 16px; padding: 10px 11px; display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--line-soft); background: rgba(120,198,163,.04); }
+.context-status-label { color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; }
+.context-status-row strong { color: var(--warn); font: 600 9px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
+.context-status-row strong[data-state="ready"] { color: var(--good); }
+.context-presets { display: grid; gap: 7px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--line-soft); }
+.context-presets label { margin: 0; }
+.context-presets select { width: 100%; min-height: 38px; padding: 0 9px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 10px/1 var(--mono); }
+.context-presets small { color: var(--faint); font-size: 10px; line-height: 1.35; }
+.context-field { display: grid; gap: 7px; }
+.context-field + .context-field { margin-top: 13px; }
+.context-field label { margin: 0; }
+.context-field label span { color: var(--signal); font-size: 8px; letter-spacing: .06em; }
+.context-input-row { display: grid; grid-template-columns: minmax(0, 1fr) 34px; gap: 5px; }
+.context-input-row input { min-width: 0; }
+.copy-context { width: 34px; height: 40px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted); background: transparent; cursor: pointer; }
+.copy-context:hover { color: var(--ink); border-color: var(--faint); background: var(--panel-high); }
+.id-actions { min-height: 31px; display: flex; align-items: center; gap: 7px; margin-top: 12px; }
+.id-action { min-height: 30px; padding: 0 9px; border: 1px solid var(--line-soft); border-radius: var(--radius); color: var(--muted); background: transparent; cursor: pointer; font: 9px/1 var(--mono); letter-spacing: .04em; }
+.id-action:hover { color: var(--ink); border-color: var(--signal); background: rgba(201,168,95,.07); }
+.icon-id-action { width: 30px; padding: 0; color: var(--faint); }
.context-form label { margin-top: 15px; }
.context-form label:first-child { margin-top: 0; }
.context-form input { height: 40px; padding: 0 10px; font: 11px/1 var(--mono); }
+.context-hint { margin: 12px 0 0; color: var(--faint); font-size: 10px; line-height: 1.45; }
+.advanced-context { margin-top: 17px; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
+.advanced-context summary { padding: 12px 0; color: var(--muted); cursor: pointer; list-style: none; font: 600 9px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
+.advanced-context summary::before { content: "+"; display: inline-block; width: 18px; color: var(--signal); font-size: 13px; }
+.advanced-context[open] summary::before { content: "−"; }
+.advanced-context > div { padding: 0 0 10px; }
+.advanced-context label { margin-top: 13px; }
+.context-actions { display: grid; gap: 8px; }
+.context-actions .secondary-button { margin-top: 18px; }
+.context-actions .text-button { margin: 0; justify-self: start; padding-inline: 0; }
.secondary-button { height: 42px; margin-top: 20px; color: #11150f; background: var(--signal); font-size: 12px; }
+.section-nav { display: grid; gap: 3px; margin-top: 4px; }
+.section-nav-link { display: grid; grid-template-columns: 28px 1fr; gap: 2px 10px; padding: 11px 10px; border-left: 2px solid transparent; color: var(--muted); text-decoration: none; transition: background .18s, border-color .18s, color .18s; }
+.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="page"] { border-left-color: var(--signal); color: var(--ink); background: rgba(201,168,95,.07); outline: 0; }
+.section-nav-link span { grid-row: 1 / span 2; color: var(--signal); font: 9px/1.4 var(--mono); }
+.section-nav-link b { font: 600 11px/1.2 var(--sans); }
+.section-nav-link small { color: var(--faint); font: 9px/1.2 var(--mono); }
.session-stamp { margin-top: auto; padding-top: 20px; display: grid; gap: 6px; border-top: 1px solid var(--line); }
.session-stamp span { margin-top: 8px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .14em; }
.session-stamp strong { overflow: hidden; color: var(--muted); font: 10px/1.4 var(--mono); text-overflow: ellipsis; }
-.workspace { grid-column: 2; grid-row: 2; padding: clamp(28px, 4vw, 58px); min-width: 0; }
+.workspace { grid-column: 2; grid-row: 2; width: min(100%, 1740px); justify-self: start; padding: 42px clamp(24px, 3vw, 48px) 64px; min-width: 0; }
+.workspace > section { scroll-margin-top: 94px; }
.workspace-head { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 34px; }
-.workspace-head h1 { margin: 0; font: 400 clamp(36px, 5vw, 58px)/1 var(--serif); letter-spacing: -.035em; }
+.workspace-head h1 { margin: 0; font: 400 clamp(34px, 4vw, 50px)/1 var(--serif); letter-spacing: -.035em; }
.last-update { color: var(--faint); font: 10px/1.4 var(--mono); text-align: right; }
.signal-strip { display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 18px; border: 1px solid var(--line); background: var(--panel); }
.signal-strip div { padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-right: 1px solid var(--line); }
@@ -94,6 +136,46 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.signal-strip strong[data-state="error"] { color: var(--bad); }
.signal-strip strong[data-state="loading"] { color: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
+.diagnostics-panel, .settings-panel { margin-bottom: 18px; border: 1px solid var(--line); background: rgba(16,23,20,.82); }
+.diagnostics-panel { border-color: rgba(120,198,163,.32); box-shadow: inset 3px 0 0 rgba(120,198,163,.45); }
+.panel-heading { min-height: 68px; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
+.panel-heading p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
+.panel-heading h2 { margin: 0; font: 400 23px/1 var(--serif); }
+.panel-heading-tools { display: flex; align-items: center; gap: 14px; }
+.panel-heading-tools > span, .settings-saved { color: var(--muted); font: 9px/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
+.compact-button { width: auto; min-width: 142px; height: 38px; margin: 0; padding: 0 13px; }
+.diagnostic-intro { padding: 13px 18px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--line-soft); color: var(--muted); }
+.diagnostic-intro p { margin: 0; font-size: 11px; line-height: 1.45; }
+.diagnostic-pulse { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; background: var(--good); box-shadow: 0 0 12px rgba(120,198,163,.7); }
+.diagnostic-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
+.diagnostic-list[aria-busy="true"] { opacity: .7; }
+.diagnostic-item { min-width: 0; min-height: 92px; padding: 14px 16px; display: grid; grid-template-columns: 9px 1fr auto; gap: 9px; align-items: start; border-right: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
+.diagnostic-item:nth-child(4n) { border-right: 0; }
+.diagnostic-item > i { width: 7px; height: 7px; margin-top: 4px; border-radius: 50%; background: var(--faint); }
+.diagnostic-item[data-state="ok"] > i { background: var(--good); box-shadow: 0 0 9px rgba(120,198,163,.6); }
+.diagnostic-item[data-state="error"] > i { background: var(--bad); }
+.diagnostic-item[data-state="loading"] > i { background: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
+.diagnostic-item strong { display: block; color: var(--ink); font: 600 10px/1.3 var(--mono); }
+.diagnostic-item span { display: block; margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.35; }
+.diagnostic-item em { color: var(--faint); font: normal 9px/1.2 var(--mono); text-transform: uppercase; }
+.diagnostic-item[data-state="ok"] em { color: var(--good); }
+.diagnostic-item[data-state="error"] em { color: var(--bad); }
+.settings-grid { padding: 18px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 20px; }
+.setting-field, .setting-check { min-width: 0; display: grid; gap: 7px; color: var(--ink); }
+.setting-field > span, .setting-check b { font: 600 10px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
+.setting-field small, .setting-check small { color: var(--muted); font-size: 11px; line-height: 1.4; }
+.setting-field select { width: 100%; min-height: 40px; padding: 0 10px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 11px/1 var(--mono); }
+.setting-check { grid-template-columns: 18px 1fr; align-items: start; gap: 10px; padding: 10px 12px; border: 1px solid var(--line-soft); }
+.setting-check input { width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--signal); }
+.setting-check span { display: grid; gap: 7px; }
+.settings-actions { display: flex; align-items: center; justify-content: flex-end; gap: 14px; grid-column: 1 / -1; padding-top: 2px; }
+.settings-actions .text-button { margin: 0; }
+html[data-density="compact"] .data-card { padding: 18px; }
+html[data-density="compact"] .data-card dl div { padding-block: 10px; }
+html[data-density="compact"] .activity-row { min-height: 58px; padding-block: 8px; }
+html[data-density="compact"] .learning-row { min-height: 54px; padding-block: 9px; }
+html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before, html[data-reduced-motion="true"] *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: .01ms !important; }
+
.advisor-panel { margin-bottom: 18px; padding: 24px; display: grid; grid-template-columns: minmax(220px, .72fr) minmax(320px, 1.28fr); gap: 18px 34px; border: 1px solid rgba(201,168,95,.42); background: linear-gradient(125deg, rgba(201,168,95,.1), rgba(16,23,20,.88) 44%, rgba(12,20,17,.96)); box-shadow: inset 3px 0 0 rgba(201,168,95,.58); }
.advisor-intro { align-self: center; }
.advisor-intro > p { margin: 0 0 10px; color: var(--signal); font: 600 9px/1 var(--mono); letter-spacing: .15em; }
@@ -278,6 +360,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.advisor-panel { grid-template-columns: 1fr; }
.capability-card { grid-column: auto; }
.prospective-card { grid-column: auto; }
+ .diagnostic-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .diagnostic-item:nth-child(4n) { border-right: 1px solid var(--line-soft); }
+ .diagnostic-item:nth-child(2n) { border-right: 0; }
}
@media (max-width: 680px) {
@@ -285,14 +370,25 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.console { display: block; }
.topbar { height: 66px; padding: 0 14px; }
.wordmark small, .connection, .text-button { display: none; }
+ .topbar-nav { display: none; }
.context-rail { padding: 24px 18px; border-right: 0; border-bottom: 1px solid var(--line); }
.rail-copy { max-width: 48ch; }
.session-stamp { margin-top: 8px; grid-template-columns: auto 1fr; align-items: center; }
.session-stamp span { margin: 0; }
.workspace { padding: 30px 16px 50px; }
+ .section-nav { display: flex; gap: 6px; margin: 10px -18px -24px; padding: 10px 18px; overflow-x: auto; border-top: 1px solid var(--line-soft); background: #0e1512; scrollbar-width: thin; }
+ .section-nav-link { min-width: 142px; padding: 10px; border-left: 0; border-bottom: 2px solid transparent; }
+ .section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="page"] { border-left-color: transparent; border-bottom-color: var(--signal); }
.workspace-head { align-items: start; flex-direction: column; }
.last-update { text-align: left; }
.signal-strip { grid-template-columns: 1fr; }
+ .panel-heading { align-items: flex-start; flex-direction: column; }
+ .panel-heading-tools { width: 100%; justify-content: space-between; }
+ .diagnostic-list { grid-template-columns: 1fr; }
+ .diagnostic-item, .diagnostic-item:nth-child(2n), .diagnostic-item:nth-child(4n) { border-right: 0; }
+ .settings-grid { grid-template-columns: 1fr; padding: 16px; }
+ .settings-actions { grid-column: auto; align-items: stretch; flex-direction: column; }
+ .settings-actions .primary-button { width: 100%; }
.signal-strip div { border-right: 0; border-bottom: 1px solid var(--line); }
.advisor-panel { padding: 20px 16px; gap: 20px; }
.advisor-controls { align-items: stretch; flex-direction: column; }
diff --git a/cmd/creator-web/static/assets/console.js b/cmd/creator-web/static/assets/console.js
index 9ccec82..b201a79 100644
--- a/cmd/creator-web/static/assets/console.js
+++ b/cmd/creator-web/static/assets/console.js
@@ -7,14 +7,50 @@ 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"];
+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 sectionNavLinks = Array.from(document.querySelectorAll(".section-nav-link"));
+const cabinetSections = ["overview", "diagnostics", "operations", "settings"]
+ .map((id) => byId(id))
+ .filter(Boolean);
+
+function setActiveSection(id) {
+ sectionNavLinks.forEach((link) => {
+ const active = link.getAttribute("href") === `#${id}`;
+ link.classList.toggle("active", active);
+ if (active) link.setAttribute("aria-current", "page");
+ else link.removeAttribute("aria-current");
+ });
+}
+
+if ("IntersectionObserver" in window) {
+ const sectionObserver = new IntersectionObserver((entries) => {
+ const visible = entries.filter((entry) => entry.isIntersecting)
+ .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
+ if (visible) setActiveSection(visible.target.id);
+ }, {rootMargin: "-18% 0px -64% 0px", threshold: [0, .25, .6, 1]});
+ cabinetSections.forEach((section) => sectionObserver.observe(section));
+}
+
+sectionNavLinks.forEach((link) => link.addEventListener("click", () => {
+ const id = link.getAttribute("href").slice(1);
+ setActiveSection(id);
+}));
+window.addEventListener("hashchange", () => {
+ const id = window.location.hash.slice(1);
+ if (cabinetSections.some((section) => section.id === id)) setActiveSection(id);
+});
function setText(id, value, fallback = "—") {
byId(id).textContent = value === undefined || value === null || value === "" ? fallback : String(value);
@@ -39,6 +75,18 @@ function notify(message) {
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}`);
@@ -50,7 +98,9 @@ async function request(path, options = {}) {
}
if (!response.ok) {
const detail = (await response.text()).trim();
- throw new Error(detail || `Ошибка ${response.status}`);
+ const error = new Error(detail || `Ошибка ${response.status}`);
+ error.status = response.status;
+ throw error;
}
return response.json();
}
@@ -64,15 +114,57 @@ function restoreContext() {
} 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);
@@ -80,6 +172,7 @@ async function showConsole() {
setText("session-expiry", expiry);
loginView.hidden = true;
consoleView.hidden = false;
+ restoreSettings();
restoreContext();
await loadAdvisoryPolicy();
const context = saveContext();
@@ -88,10 +181,13 @@ async function showConsole() {
loadActionHistory();
loadLearning();
}
+ runDiagnostics();
}
function endSession(message = "Сессия завершена.") {
sessionToken = "";
+ clearInterval(refreshTimer);
+ refreshTimer = null;
sessionStorage.removeItem("domovoy.creator.session");
consoleView.hidden = true;
loginView.hidden = false;
@@ -128,7 +224,7 @@ loginForm.addEventListener("submit", async (event) => {
byId("creator-password").value = "";
await showConsole();
} catch (error) {
- loginError.textContent = error.message === "creator authentication denied" ? "Логин или пароль не принят." : error.message;
+ loginError.textContent = safeClientError(error);
} finally {
button.disabled = false;
button.firstElementChild.textContent = "Открыть консоль";
@@ -144,14 +240,129 @@ byId("toggle-secret").addEventListener("click", () => {
contextForm.addEventListener("submit", (event) => {
event.preventDefault();
- saveContext();
+ 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("logout").addEventListener("click", () => endSession());
+byId("run-diagnostics").addEventListener("click", runDiagnostics);
advisoryForm.addEventListener("submit", async (event) => {
event.preventDefault();
@@ -185,7 +396,7 @@ advisoryForm.addEventListener("submit", async (event) => {
byId("advisory-result").hidden = false;
} catch (error) {
setText("advisory-state", "ОШИБКА");
- setText("advisory-text", error.message);
+ setText("advisory-text", safeClientError(error));
byId("advisory-result").hidden = false;
} finally {
byId("advisory-egress-ack").checked = false;
@@ -246,6 +457,57 @@ async function loadDashboard() {
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 loadLearning() {
const experienceList = byId("experience-list");
const reflectionList = byId("reflection-list");
@@ -272,7 +534,7 @@ async function loadLearning() {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
- failed.textContent = error.message;
+ failed.textContent = safeClientError(error);
list.append(failed);
});
setText("learning-count", "ОШИБКА");
@@ -371,7 +633,7 @@ async function loadActionHistory() {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
- failed.textContent = error.message;
+ failed.textContent = safeClientError(error);
list.append(failed);
setText("action-count", "ОШИБКА");
} finally {
@@ -430,7 +692,7 @@ async function reconcileHistoryAction(actionID, button) {
await loadActionHistory();
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
} catch (error) {
- notify(error.message);
+ notify(safeClientError(error));
button.disabled = false;
button.textContent = "Сверить";
}
@@ -450,7 +712,7 @@ async function loadFocus(query) {
} catch (error) {
setSignal("focus", "ошибка", "error");
setBadge("focus", "НЕДОСТУПЕН", "bad");
- setText("focus-message", error.message);
+ setText("focus-message", safeClientError(error));
byId("connection-state").classList.add("offline");
}
}
@@ -472,7 +734,7 @@ async function loadTask(taskID, query) {
currentTask = null;
setSignal("task", "отклонена", "error");
setBadge("task", "ВНЕ ФОКУСА", "bad");
- setText("task-message", error.message);
+ setText("task-message", safeClientError(error));
updateActionReadiness();
}
}
@@ -511,12 +773,12 @@ async function loadProspectiveRegister(selectedID) {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty";
- failed.textContent = error.message;
+ failed.textContent = safeClientError(error);
list.append(failed);
setText("prospective-count", "ОШИБКА");
setSignal("prospective", "ошибка", "error");
setBadge("prospective", "НЕДОСТУПНА", "bad");
- setText("prospective-message", error.message);
+ setText("prospective-message", safeClientError(error));
}
}
@@ -572,7 +834,7 @@ async function loadProspective(prospectiveID) {
setSignal("prospective", "ошибка", "error");
setBadge("prospective", "НЕДОСТУПНА", "bad");
setText("prospective-state", "UNKNOWN");
- setText("prospective-message", error.message);
+ setText("prospective-message", safeClientError(error));
updateActionReadiness();
}
}
@@ -594,7 +856,7 @@ async function loadCapability(capabilityID) {
setSignal("capability", "ошибка", "error");
setBadge("capability", "НЕДОСТУПНА", "bad");
setText("capability-state", "UNKNOWN");
- setText("capability-reason", error.message);
+ setText("capability-reason", safeClientError(error));
}
}
@@ -700,7 +962,7 @@ byId("confirm-notification").addEventListener("click", async () => {
await loadActionHistory();
notify(result.State === "SUCCEEDED" ? "Receiver принял уведомление." : "Исход отправки требует сверки.");
} catch (error) {
- setText("notification-confirmation-error", error.message);
+ setText("notification-confirmation-error", safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Подтверждаю отправку";
@@ -730,7 +992,7 @@ byId("confirm-satisfaction").addEventListener("click", async () => {
notify(result.State === "SUCCEEDED" ? "Исполнение зафиксировано через Commit." : "Исход требует проверки.");
await loadProspectiveRegister(command.ProspectiveItemID);
} catch (error) {
- setText("confirmation-error", error.message);
+ setText("confirmation-error", safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Подтверждаю переход";
@@ -764,7 +1026,7 @@ byId("reconcile-action").addEventListener("click", async () => {
notify(result.State === "SUCCEEDED" ? "Исход подтверждён." : "Исход пока неизвестен.");
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
} catch (error) {
- notify(error.message);
+ notify(safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Проверить исход";
diff --git a/cmd/creator-web/static/index.html b/cmd/creator-web/static/index.html
index 86e9619..942183e 100644
--- a/cmd/creator-web/static/index.html
+++ b/cmd/creator-web/static/index.html
@@ -6,9 +6,10 @@
Домовой — консоль создателя
-
+
+ К содержимому
@@ -44,6 +45,12 @@
@@ -56,18 +63,45 @@
Укажите рабочий контекст. Он сохраняется только в этом браузере и не даёт дополнительных полномочий.
+
ACTOR
—
@@ -77,7 +111,7 @@
-
+
операционная картина
Состояние сейчас
@@ -92,6 +126,20 @@
ВОЗМОЖНОСТЬожидает
+
+
+
+
+
Здесь видны только ответы текущей авторизованной сессии. Проверка не меняет данные и помогает отличить отсутствие контекста от недоступности сервиса.
+
+
+
Нажмите «Проверить всё», чтобы получить актуальный снимок.
+
+
+
COGNITIVE / ADVISORY V1
@@ -120,7 +168,7 @@
-
+
Continuity / ближайшие
Реестр ожиданий
0 ЗАПИСЕЙ
@@ -167,6 +215,19 @@
+
+
+ Локальные предпочтения
Настройки рабочего кабинета
+ Сохраняются в этом браузере
+
+
+
+
@@ -188,6 +249,7 @@
Ревизия—
Укажите задачу для проверки относительно активного фокуса.
+
@@ -205,6 +267,7 @@
Ревизия—
Укажите идентификатор ProspectiveItem для проверки владельца и состояния.
+
Commit / контролируемый переход
@@ -240,6 +303,7 @@
Ревизия—
Наблюдалось—
+
@@ -286,6 +350,6 @@
-
+