diff --git a/cmd/creator-web/chat_test.go b/cmd/creator-web/chat_test.go index da67997..3bd0a9e 100644 --- a/cmd/creator-web/chat_test.go +++ b/cmd/creator-web/chat_test.go @@ -25,6 +25,11 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) { t.Fatalf("expected /chat status 200, got %d", recorder.Code) } body := recorder.Body.String() + for _, required := range []string{"cognitive-status", "aria-live=\"polite\""} { + if !strings.Contains(body, required) { + t.Fatalf("/chat is missing human status marker %q", required) + } + } for _, forbidden := range []string{"provider_id", "model_id", "binding_id", "workspace-id", "session-id"} { if strings.Contains(body, forbidden) { t.Fatalf("/chat leaks internal field %q", forbidden) diff --git a/cmd/creator-web/static/assets/chat.css b/cmd/creator-web/static/assets/chat.css index a1c36df..4a83344 100644 --- a/cmd/creator-web/static/assets/chat.css +++ b/cmd/creator-web/static/assets/chat.css @@ -34,6 +34,12 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px; .chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); } .chat-header span { color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; } #active-focus { margin-left: auto; color: var(--good); } +.cognitive-status { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; } +.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--signal); } +.cognitive-status[data-state="available"] { color: var(--good); } +.cognitive-status[data-state="available"]::before { background: var(--good); } +.cognitive-status[data-state="unavailable"] { color: var(--bad); } +.cognitive-status[data-state="unavailable"]::before { background: var(--bad); } .chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; } .human-nav { display: flex; align-items: center; gap: 8px; } .nav-tab, .operator-link { height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--muted); background: #0b110e; text-decoration: none; font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; } @@ -71,6 +77,7 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px; .composer button { width: 100%; } .chat-header { gap: 10px; padding: 0 12px; } .chat-header span { display: none; } + .chat-header .cognitive-status { display: inline-flex; } .human-nav { overflow-x: auto; } .operator-link, .nav-tab { padding: 0 9px; } } diff --git a/cmd/creator-web/static/assets/chat.js b/cmd/creator-web/static/assets/chat.js index 70c6dd1..f5613d1 100644 --- a/cmd/creator-web/static/assets/chat.js +++ b/cmd/creator-web/static/assets/chat.js @@ -8,6 +8,7 @@ const messageInput = document.querySelector("#message"); const sendButton = document.querySelector("#send"); const logoutButton = document.querySelector("#logout"); const activeFocus = document.querySelector("#active-focus"); +const cognitiveStatus = document.querySelector("#cognitive-status"); const todayList = document.querySelector("#today-list"); const refreshToday = document.querySelector("#refresh-today"); const historyList = document.querySelector("#history-list"); @@ -16,6 +17,44 @@ const navTabs = Array.from(document.querySelectorAll(".nav-tab")); let token = sessionStorage.getItem("domovoy.chat.token") || ""; let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || ""; let localHistory = []; +let cognitiveStatusTimer = null; + +function renderCognitiveStatus(state) { + if (!cognitiveStatus) return; + const normalized = String(state || "UNKNOWN").toUpperCase(); + let displayState = "degraded"; + if (normalized === "AVAILABLE") displayState = "available"; + if (normalized === "UNAVAILABLE") displayState = "unavailable"; + const labels = { + available: "канал мышления доступен", + degraded: "канал мышления работает с ограничениями", + unavailable: "основной канал мышления недоступен", + }; + cognitiveStatus.dataset.state = displayState; + cognitiveStatus.textContent = labels[displayState]; +} + +async function loadCognitiveStatus() { + if (!cognitiveStatus || !token) return; + try { + const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"}); + renderCognitiveStatus(result.State || result.state || "UNKNOWN"); + } catch { + renderCognitiveStatus("UNAVAILABLE"); + } +} + +function startCognitiveStatusPolling() { + if (cognitiveStatusTimer) return; + loadCognitiveStatus(); + cognitiveStatusTimer = window.setInterval(loadCognitiveStatus, 30000); +} + +function stopCognitiveStatusPolling() { + if (!cognitiveStatusTimer) return; + window.clearInterval(cognitiveStatusTimer); + cognitiveStatusTimer = null; +} function showChat() { loginView.hidden = true; @@ -23,10 +62,12 @@ function showChat() { if (!messages.children.length) { appendMessage("assistant", "Я рядом. Напишите обычное сообщение, без внутренних идентификаторов."); } + startCognitiveStatusPolling(); messageInput.focus(); } function showLogin() { + stopCognitiveStatusPolling(); chatView.hidden = true; loginView.hidden = false; } diff --git a/cmd/creator-web/static/chat.html b/cmd/creator-web/static/chat.html index 08211d3..4457ba7 100644 --- a/cmd/creator-web/static/chat.html +++ b/cmd/creator-web/static/chat.html @@ -33,6 +33,7 @@ Консоль фокус: не выбран + проверяю канал мышления сессия активна diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 02bc6ad..c5c79de 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000075-creator-chat-primary-surface.json", - "task_id": "DEV-000075", + "task_file": "dev/tasks/DEV-000076-creator-chat-cognitive-status.json", + "task_id": "DEV-000076", "task_revision": 1 } diff --git a/dev/tasks/DEV-000076-creator-chat-cognitive-status.json b/dev/tasks/DEV-000076-creator-chat-cognitive-status.json new file mode 100644 index 0000000..0c73f29 --- /dev/null +++ b/dev/tasks/DEV-000076-creator-chat-cognitive-status.json @@ -0,0 +1,47 @@ +{ + "task_id": "DEV-000076", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "26cf85108a63dfbd67866d9df0445dbcb54983be", + "risk_tier": "E", + "intent": "Show the human user whether Domovoy's primary cognitive channel is available without exposing provider internals or mixing technical events into the conversation.", + "requirement_ids": [ + "Chat shows a human-readable cognitive channel status", + "Status is read-only and refreshed periodically", + "Chat does not expose provider, model or binding identifiers", + "Chat remains usable while status observation fails" + ], + "allowed_change_scope": { + "modules": ["cmd/creator-web"], + "paths": [ + "cmd/creator-web/chat_test.go", + "cmd/creator-web/static/chat.html", + "cmd/creator-web/static/assets/chat.css", + "cmd/creator-web/static/assets/chat.js", + "dev/ACTIVE_TASK.json", + "dev/tasks/DEV-000076-creator-chat-cognitive-status.json" + ] + }, + "forbidden_change_scope": { + "paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "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 /api/capabilities/cognitive-proposal/health endpoint"], "may_change": []}, + "semantic_owner": "none-interface-adapter", + "canonical_state": [], + "state_transitions": ["Browser polls existing cognitive capability health observation", "Browser renders coarse human-readable status"], + "side_effects_and_privileges": ["Read-only observation", "No new privilege", "No provider/model routing change"], + "persistence_and_migrations": ["No persistence or migration"], + "failure_retry_restart": ["Failed observation renders an unavailable/degraded notice; chat remains available", "Polling retries on the next interval"], + "assumptions": ["The existing health endpoint returns State or state with AVAILABLE, DEGRADED or UNAVAILABLE semantics", "The chat token authorizes the existing health endpoint"], + "acceptance_scenarios": ["Authenticated chat displays a status indicator", "Health polling does not reveal internal identifiers", "Mobile header keeps the status visible", "A failed health request does not disable the composer"], + "required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +}