Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eb2f5f313 | ||
|
|
39a27102c9 | ||
|
|
7ac9a2697c | ||
|
|
f2e8ec08c5 | ||
|
|
1cfbb4ae66 | ||
|
|
7618a0bcbe | ||
|
|
dcdc9a2e64 | ||
|
|
5783faf9dc | ||
|
|
9a968fe2f6 | ||
|
|
41d4d7202f | ||
|
|
ed3932c57a | ||
|
|
3367361483 | ||
|
|
51638d6e8c | ||
|
|
0a1ad9f7d5 |
@@ -32,6 +32,29 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicSurfacesSeparateChatAndConsole(t *testing.T) {
|
||||
handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
path string
|
||||
must string
|
||||
mustNot string
|
||||
}{
|
||||
{path: "/", must: "Напишите Домовому обычное сообщение", mustNot: "context-preset"},
|
||||
{path: "/admin", must: "context-preset", mustNot: "Напишите Домовому обычное сообщение"},
|
||||
{path: "/chat", must: "Напишите Домовому обычное сообщение", mustNot: "context-preset"},
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, test.path, nil))
|
||||
body := recorder.Body.String()
|
||||
if recorder.Code != http.StatusOK || !strings.Contains(body, test.must) || strings.Contains(body, test.mustNot) {
|
||||
t.Fatalf("%s response=%d contains expected=%t forbidden=%t", test.path, recorder.Code, strings.Contains(body, test.must), strings.Contains(body, test.mustNot))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
|
||||
var cognitiveRequest cognitive.GatewayAdvisoryRequest
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -112,7 +112,12 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/" {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
serveEmbeddedHTML(w, "chat.html")
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/admin" {
|
||||
serveEmbeddedHTML(w, "index.html")
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/chat" {
|
||||
r = r.Clone(r.Context())
|
||||
@@ -122,6 +127,17 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
g.static.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func serveEmbeddedHTML(w http.ResponseWriter, name string) {
|
||||
body, err := staticFiles.ReadFile("static/" + name)
|
||||
if err != nil {
|
||||
http.Error(w, "page unavailable", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
type browserAdvisoryRequest struct {
|
||||
Message string `json:"message"`
|
||||
SessionID string `json:"session_id"`
|
||||
|
||||
@@ -229,7 +229,7 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
request := httptest.NewRequest(http.MethodGet, "/admin", nil)
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Домовой") ||
|
||||
@@ -237,8 +237,13 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
|
||||
!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(), "aria-current=\"location\"") ||
|
||||
!strings.Contains(response.Body.String(), "section-nav-link") ||
|
||||
!strings.Contains(response.Body.String(), "data-page=\"overview\"") ||
|
||||
!strings.Contains(response.Body.String(), "data-page=\"diagnostics\"") ||
|
||||
!strings.Contains(response.Body.String(), "data-page=\"operations\"") ||
|
||||
!strings.Contains(response.Body.String(), "data-page=\"learning\"") ||
|
||||
!strings.Contains(response.Body.String(), "data-page=\"settings\"") ||
|
||||
!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(), "Спросить Домового") ||
|
||||
|
||||
@@ -114,7 +114,7 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-
|
||||
.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:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="location"] { 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); }
|
||||
@@ -123,6 +123,7 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-
|
||||
.session-stamp strong { overflow: hidden; color: var(--muted); font: 10px/1.4 var(--mono); text-overflow: ellipsis; }
|
||||
|
||||
.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 > [data-page][hidden] { display: none !important; }
|
||||
.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(34px, 4vw, 50px)/1 var(--serif); letter-spacing: -.035em; }
|
||||
@@ -378,7 +379,7 @@ html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before,
|
||||
.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); }
|
||||
.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="location"] { 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; }
|
||||
|
||||
@@ -20,37 +20,29 @@ 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);
|
||||
const pageNavLinks = Array.from(document.querySelectorAll(".section-nav-link, .topbar-nav a"));
|
||||
const pageViews = Array.from(document.querySelectorAll("[data-page]"));
|
||||
const cabinetPages = ["overview", "diagnostics", "operations", "learning", "settings"];
|
||||
|
||||
function setActiveSection(id) {
|
||||
sectionNavLinks.forEach((link) => {
|
||||
const active = link.getAttribute("href") === `#${id}`;
|
||||
function setActivePage(id, updateHistory = false) {
|
||||
const page = cabinetPages.includes(id) ? id : "overview";
|
||||
pageViews.forEach((view) => { view.hidden = view.dataset.page !== page; });
|
||||
document.body.dataset.page = page;
|
||||
pageNavLinks.forEach((link) => {
|
||||
const active = link.getAttribute("href") === `#${page}`;
|
||||
link.classList.toggle("active", active);
|
||||
if (active) link.setAttribute("aria-current", "page");
|
||||
if (active) link.setAttribute("aria-current", "location");
|
||||
else link.removeAttribute("aria-current");
|
||||
});
|
||||
if (updateHistory && window.location.hash !== `#${page}`) window.history.pushState({}, "", `#${page}`);
|
||||
}
|
||||
|
||||
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);
|
||||
pageNavLinks.forEach((link) => link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
setActivePage(link.getAttribute("href").slice(1), true);
|
||||
}));
|
||||
window.addEventListener("hashchange", () => {
|
||||
const id = window.location.hash.slice(1);
|
||||
if (cabinetSections.some((section) => section.id === id)) setActiveSection(id);
|
||||
});
|
||||
window.addEventListener("popstate", () => setActivePage(window.location.hash.slice(1)));
|
||||
setActivePage(window.location.hash.slice(1));
|
||||
|
||||
function setText(id, value, fallback = "—") {
|
||||
byId(id).textContent = value === undefined || value === null || value === "" ? fallback : String(value);
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
|
||||
<section id="chat-view" class="chat-view" hidden>
|
||||
<header class="chat-header">
|
||||
<a href="/chat" aria-label="Домовой">Домовой</a>
|
||||
<a href="/" aria-label="Домовой">Домовой</a>
|
||||
<nav class="human-nav" aria-label="Разделы Домового">
|
||||
<button class="nav-tab active" data-tab="chat-panel" type="button">Чат</button>
|
||||
<button class="nav-tab" data-tab="today-panel" type="button">Сегодня</button>
|
||||
<button class="nav-tab" data-tab="history-panel" type="button">История</button>
|
||||
<a href="/" class="operator-link">Оператор</a>
|
||||
<a href="/admin" class="operator-link">Консоль</a>
|
||||
</nav>
|
||||
<span id="active-focus">фокус: не выбран</span>
|
||||
<span id="session-state">сессия активна</span>
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
<a href="#overview">Обзор</a>
|
||||
<a href="#diagnostics">Проверки</a>
|
||||
<a href="#operations">Операции</a>
|
||||
<a href="#learning">Опыт</a>
|
||||
<a href="#settings">Настройки</a>
|
||||
</nav>
|
||||
<button id="refresh-all" class="text-button" type="button">Обновить</button>
|
||||
@@ -97,10 +98,11 @@
|
||||
<div class="context-actions"><button class="secondary-button" type="submit">Загрузить контекст</button><button id="reset-context" class="text-button" type="button">Сбросить</button></div>
|
||||
</form>
|
||||
<nav class="section-nav" aria-label="Навигация по кабинету">
|
||||
<a class="section-nav-link active" aria-current="page" href="#overview"><span>01</span><b>Состояние</b><small>фокус и сигналы</small></a>
|
||||
<a class="section-nav-link active" aria-current="location" href="#overview"><span>01</span><b>Состояние</b><small>фокус и сигналы</small></a>
|
||||
<a class="section-nav-link" href="#diagnostics"><span>02</span><b>Проверки</b><small>доступность и границы</small></a>
|
||||
<a class="section-nav-link" href="#operations"><span>03</span><b>Операции</b><small>ожидания и действия</small></a>
|
||||
<a class="section-nav-link" href="#settings"><span>04</span><b>Настройки</b><small>интерфейс и обновление</small></a>
|
||||
<a class="section-nav-link" href="#learning"><span>04</span><b>Опыт</b><small>reflection и навыки</small></a>
|
||||
<a class="section-nav-link" href="#settings"><span>05</span><b>Настройки</b><small>интерфейс и обновление</small></a>
|
||||
</nav>
|
||||
<div class="session-stamp">
|
||||
<span>ACTOR</span>
|
||||
@@ -111,7 +113,7 @@
|
||||
</aside>
|
||||
|
||||
<main class="workspace">
|
||||
<section id="overview" class="workspace-head">
|
||||
<section id="overview" class="workspace-head" data-page="overview">
|
||||
<div>
|
||||
<p class="eyebrow">операционная картина</p>
|
||||
<h1>Состояние сейчас</h1>
|
||||
@@ -119,14 +121,14 @@
|
||||
<p id="last-update" class="last-update">Данные ещё не запрошены</p>
|
||||
</section>
|
||||
|
||||
<section class="signal-strip" aria-label="Сводка состояния">
|
||||
<section class="signal-strip" aria-label="Сводка состояния" data-page="overview">
|
||||
<div><span>ФОКУС</span><strong id="focus-signal" data-state="idle">ожидает</strong></div>
|
||||
<div><span>ЗАДАЧА</span><strong id="task-signal" data-state="idle">ожидает</strong></div>
|
||||
<div><span>CONTINUITY</span><strong id="prospective-signal" data-state="idle">ожидает</strong></div>
|
||||
<div><span>ВОЗМОЖНОСТЬ</span><strong id="capability-signal" data-state="idle">ожидает</strong></div>
|
||||
</section>
|
||||
|
||||
<section id="diagnostics" class="diagnostics-panel" aria-labelledby="diagnostics-title">
|
||||
<section id="diagnostics" class="diagnostics-panel" aria-labelledby="diagnostics-title" data-page="diagnostics">
|
||||
<header class="panel-heading">
|
||||
<div><p>Проверяемая система</p><h2 id="diagnostics-title">Диагностика кабинета</h2></div>
|
||||
<div class="panel-heading-tools"><span id="diagnostic-summary">Проверка ещё не запускалась</span><button id="run-diagnostics" class="secondary-button compact-button" type="button">Проверить всё</button></div>
|
||||
@@ -140,7 +142,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="advisor-panel" aria-labelledby="advisor-title">
|
||||
<section class="advisor-panel" aria-labelledby="advisor-title" data-page="diagnostics">
|
||||
<div class="advisor-intro">
|
||||
<p>COGNITIVE / ADVISORY V1</p>
|
||||
<h2 id="advisor-title">Спросить Домового</h2>
|
||||
@@ -168,7 +170,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="operations" class="register-panel" aria-labelledby="register-title">
|
||||
<section id="operations" class="register-panel" aria-labelledby="register-title" data-page="operations">
|
||||
<header>
|
||||
<div><p>Continuity / ближайшие</p><h2 id="register-title">Реестр ожиданий</h2></div>
|
||||
<span id="prospective-count">0 ЗАПИСЕЙ</span>
|
||||
@@ -178,7 +180,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="activity-panel" aria-labelledby="activity-title">
|
||||
<section class="activity-panel" aria-labelledby="activity-title" data-page="operations">
|
||||
<header>
|
||||
<div><p>Commit / владелец</p><h2 id="activity-title">Журнал действий</h2></div>
|
||||
<div class="activity-tools">
|
||||
@@ -191,7 +193,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="learning-panel" aria-labelledby="learning-title">
|
||||
<section id="learning" class="learning-panel" aria-labelledby="learning-title" data-page="learning">
|
||||
<header>
|
||||
<div><p>Learning / evidence</p><h2 id="learning-title">Опыт и reflection</h2></div>
|
||||
<div class="activity-tools">
|
||||
@@ -215,7 +217,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="settings" class="settings-panel" aria-labelledby="settings-title">
|
||||
<section id="settings" class="settings-panel" aria-labelledby="settings-title" data-page="settings">
|
||||
<header class="panel-heading">
|
||||
<div><p>Локальные предпочтения</p><h2 id="settings-title">Настройки рабочего кабинета</h2></div>
|
||||
<span id="settings-saved" class="settings-saved">Сохраняются в этом браузере</span>
|
||||
@@ -228,7 +230,7 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="data-grid">
|
||||
<section class="data-grid" data-page="overview">
|
||||
<article class="data-card focus-card">
|
||||
<header><span class="card-number">01</span><div><p>Situation</p><h2>Активный фокус</h2></div><span id="focus-badge" class="status-badge neutral">НЕ ЗАГРУЖЕН</span></header>
|
||||
<dl>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000074-creator-cabinet-operations.json",
|
||||
"task_id": "DEV-000074",
|
||||
"task_revision": 2
|
||||
"task_file": "dev/tasks/DEV-000075-creator-chat-primary-surface.json",
|
||||
"task_id": "DEV-000075",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"task_id": "DEV-000074",
|
||||
"task_revision": 2,
|
||||
"task_revision": 6,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "11a6a23f6e4f44dd88265a04846419a27f54df98",
|
||||
"base_repo_revision": "1cfbb4ae66e24d514e59dabafb936c35546bd418",
|
||||
"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": [
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"task_id": "DEV-000075",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "39a27102c9665e41f29e85bc8f82a316bbbe3962",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make live conversation with Domovoy the public starting surface and keep the Creator operator console on a separate admin route.",
|
||||
"requirement_ids": [
|
||||
"Public root opens the human chat",
|
||||
"Operator console remains available at a separate admin route",
|
||||
"Existing /chat route remains compatible",
|
||||
"Chat does not expose provider, model, binding, session or workspace internals"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cmd/creator-web"],
|
||||
"paths": [
|
||||
"cmd/creator-web/server.go",
|
||||
"cmd/creator-web/server_test.go",
|
||||
"cmd/creator-web/chat_test.go",
|
||||
"cmd/creator-web/static/chat.html",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000075-creator-chat-primary-surface.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 /chat and authenticated Creator endpoints"], "may_change": []},
|
||||
"semantic_owner": "none-interface-adapter",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["GET / -> chat surface", "GET /admin -> operator console surface"],
|
||||
"side_effects_and_privileges": ["No new privilege", "No canonical mutation", "No model/provider routing change"],
|
||||
"persistence_and_migrations": ["No persistence or migration"],
|
||||
"failure_retry_restart": ["Existing chat failure behavior remains unchanged"],
|
||||
"assumptions": ["/chat is the existing human chat surface", "The operator console may be reached at /admin"],
|
||||
"acceptance_scenarios": ["Root serves chat", "Admin serves console", "Chat compatibility remains", "Internal IDs remain absent from chat HTML"],
|
||||
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
Reference in New Issue
Block a user