From 3ed64631f6a0defbf90c7981179f99d26de34ca9 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Aug 2026 02:48:45 +0300 Subject: [PATCH] feat: make Creator cabinet operable for humans --- cmd/creator-web/static/assets/console.css | 60 ++++++++++ cmd/creator-web/static/assets/console.js | 106 ++++++++++++++++++ cmd/creator-web/static/index.html | 43 ++++++- dev/ACTIVE_TASK.json | 4 +- ...DEV-000074-creator-cabinet-operations.json | 92 +++++++++++++++ 5 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 dev/tasks/DEV-000074-creator-cabinet-operations.json diff --git a/cmd/creator-web/static/assets/console.css b/cmd/creator-web/static/assets/console.css index 8dbd237..6fe659a 100644 --- a/cmd/creator-web/static/assets/console.css +++ b/cmd/creator-web/static/assets/console.css @@ -63,6 +63,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; } @@ -77,6 +80,12 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .context-form label:first-child { margin-top: 0; } .context-form input { height: 40px; padding: 0 10px; font: 11px/1 var(--mono); } .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 { 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; } @@ -94,6 +103,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 +327,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,6 +337,7 @@ 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; } @@ -293,6 +346,13 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .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 c4e32f5..3eb6830 100644 --- a/cmd/creator-web/static/assets/console.js +++ b/cmd/creator-web/static/assets/console.js @@ -7,14 +7,17 @@ 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"; 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}; function setText(id, value, fallback = "—") { byId(id).textContent = value === undefined || value === null || value === "" ? fallback : String(value); @@ -92,6 +95,7 @@ async function showConsole() { setText("session-expiry", expiry); loginView.hidden = true; consoleView.hidden = false; + restoreSettings(); restoreContext(); await loadAdvisoryPolicy(); const context = saveContext(); @@ -100,10 +104,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; @@ -160,10 +167,59 @@ contextForm.addEventListener("submit", (event) => { loadDashboard(); }); +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(); @@ -258,6 +314,56 @@ 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 (_) { + 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"); diff --git a/cmd/creator-web/static/index.html b/cmd/creator-web/static/index.html index 86e9619..aa1ab78 100644 --- a/cmd/creator-web/static/index.html +++ b/cmd/creator-web/static/index.html @@ -44,6 +44,12 @@
система доступна +
@@ -68,6 +74,12 @@ +
ACTOR — @@ -77,7 +89,7 @@
-
+

операционная картина

Состояние сейчас

@@ -92,6 +104,20 @@
ВОЗМОЖНОСТЬожидает
+
+
+

Проверяемая система

Диагностика кабинета

+
Проверка ещё не запускалась
+
+
+ +

Здесь видны только ответы текущей авторизованной сессии. Проверка не меняет данные и помогает отличить отсутствие контекста от недоступности сервиса.

+
+
+

Нажмите «Проверить всё», чтобы получить актуальный снимок.

+
+
+

COGNITIVE / ADVISORY V1

@@ -120,7 +146,7 @@
-
+

Continuity / ближайшие

Реестр ожиданий

0 ЗАПИСЕЙ @@ -167,6 +193,19 @@
+
+
+

Локальные предпочтения

Настройки рабочего кабинета

+ Сохраняются в этом браузере +
+
+ + + +
+
+
+
01

Situation

Активный фокус

НЕ ЗАГРУЖЕН
diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 268f982..2a3d773 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000073-production-compose-fail-closed.json", - "task_id": "DEV-000073", + "task_file": "dev/tasks/DEV-000074-creator-cabinet-operations.json", + "task_id": "DEV-000074", "task_revision": 1 } diff --git a/dev/tasks/DEV-000074-creator-cabinet-operations.json b/dev/tasks/DEV-000074-creator-cabinet-operations.json new file mode 100644 index 0000000..d4db07e --- /dev/null +++ b/dev/tasks/DEV-000074-creator-cabinet-operations.json @@ -0,0 +1,92 @@ +{ + "task_id": "DEV-000074", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "407275c", + "risk_tier": "E", + "intent": "Make the authenticated Creator Web cabinet understandable and operable for a human by adding section navigation, read-only diagnostics, and browser-local interface preferences without changing domain state or authority boundaries.", + "requirement_ids": [ + "Human can move between overview, diagnostics, operations and settings", + "Human can run all currently exposed read-only Creator checks from one diagnostic panel", + "Interface preferences remain local to the browser and never change canonical state", + "Existing explicit confirmation remains required for canonical mutations", + "Desktop and mobile layouts remain usable" + ], + "allowed_change_scope": { + "modules": ["cmd/creator-web"], + "paths": [ + "cmd/creator-web/static/index.html", + "cmd/creator-web/static/assets/console.css", + "cmd/creator-web/static/assets/console.js", + "cmd/creator-web/server_test.go", + "cmd/creator-web/chat_test.go", + "docs/PROJECT_STATUS.md", + "dev/ACTIVE_TASK.json", + "dev/tasks/DEV-000074-creator-cabinet-operations.json" + ] + }, + "forbidden_change_scope": { + "paths": [ + "architecture/**", + "security/**", + "database/**", + "modules/**", + "contracts/**", + "adapters/**", + "cmd/core-runtime/**", + "cmd/commit-runtime/**", + "cmd/continuity-runtime/**", + "deploy/**" + ] + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 0, + "new_third_party_dependencies": 0, + "new_provider_adapters": 0, + "new_database_owner_roles": 0 + }, + "contracts": { + "consume": ["existing authenticated Creator gateway endpoints"], + "may_change": [] + }, + "semantic_owner": "none-interface-adapter", + "canonical_state": [], + "state_transitions": [ + "diagnostic request -> read-only gateway observation", + "settings form submit -> browser-local preference update" + ], + "side_effects_and_privileges": [ + "No new privilege or mutation endpoint", + "No database migration", + "Existing Commit actions retain explicit confirmation dialogs", + "No deployment or neighboring project mutation" + ], + "persistence_and_migrations": ["No canonical persistence; localStorage only for interface preferences"], + "failure_retry_restart": ["Diagnostic failures are shown per check without exposing upstream error details"], + "assumptions": [ + "Existing Creator API routes remain the source of truth", + "Browser-local settings are disposable and do not affect domain behavior" + ], + "acceptance_scenarios": [ + "Authenticated cabinet exposes navigation to all four human-facing sections", + "read-only diagnostics checks session, advisory policy, capability, today, attention, actions and learning", + "density, auto-refresh and reduced-motion preferences save and reset locally", + "mobile layout remains readable", + "existing Creator Web tests pass", + "architecture and diff scope checks pass" + ], + "required_tests": [ + "node --check cmd/creator-web/static/assets/console.js", + "go test ./cmd/creator-web", + "go test ./...", + "go vet ./...", + "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", + "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", + "go run ./cmd/devctl check-architecture", + "git diff --check", + "responsive browser inspection" + ], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +}