Compare commits

...
Author SHA1 Message Date
m 7eb2f5f313 feat: make chat the public starting surface
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 19s
2026-08-21 08:56:20 +03:00
m 39a27102c9 Merge pull request 'Turn cabinet sections into pages' (#75) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 18s
2026-08-21 02:45:28 +00:00
m 7ac9a2697c chore: align page task with main
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 19s
2026-08-21 05:44:33 +03:00
m f2e8ec08c5 feat: turn cabinet sections into pages 2026-08-21 05:44:19 +03:00
m 1cfbb4ae66 Merge pull request 'Expose learning in top navigation' (#74) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 19s
2026-08-21 02:29:16 +00:00
m 7618a0bcbe feat: expose learning in top navigation
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 19s
2026-08-21 05:27:58 +03:00
m dcdc9a2e64 Merge pull request 'Correct active section accessibility semantics' (#73) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 18s
2026-08-21 02:25:29 +00:00
m 5783faf9dc chore: align task base with merged main
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 18s
2026-08-21 05:24:05 +03:00
m 9a968fe2f6 style: format creator web test
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 18s
2026-08-21 05:23:43 +03:00
m 41d4d7202f test: cover location navigation semantics 2026-08-21 05:23:42 +03:00
m ed3932c57a fix: mark active cabinet location semantically 2026-08-21 05:23:42 +03:00
m 3367361483 Merge pull request 'Add learning section to cabinet navigation' (#72) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 19s
2026-08-21 02:18:16 +00:00
m 51638d6e8c feat: add learning section to cabinet navigation
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 18s
2026-08-21 05:17:08 +03:00
m 0a1ad9f7d5 Merge pull request 'Make Creator cabinet IDs easier to use' (#71) from codex/creator-cabinet-operations-v2 into main
CI / test (push) Successful in 18s
2026-08-21 02:13:09 +00:00
m 4d51e65226 feat: make creator cabinet ids easier to use
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 18s
2026-08-21 05:11:38 +03:00
10 changed files with 662 additions and 56 deletions
+23
View File
@@ -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) {
+17 -1
View File
@@ -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"`
+12 -1
View File
@@ -229,10 +229,21 @@ 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(), "Домовой") ||
!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=\"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(), "Спросить Домового") ||
+111 -14
View File
@@ -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,55 @@ 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="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); }
.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 > [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(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 +137,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 +361,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 +371,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="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; }
.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; }
+270 -16
View File
@@ -7,14 +7,42 @@ 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 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 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", "location");
else link.removeAttribute("aria-current");
});
if (updateHistory && window.location.hash !== `#${page}`) window.history.pushState({}, "", `#${page}`);
}
pageNavLinks.forEach((link) => link.addEventListener("click", (event) => {
event.preventDefault();
setActivePage(link.getAttribute("href").slice(1), true);
}));
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);
@@ -39,6 +67,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 +90,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 +106,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 +164,7 @@ async function showConsole() {
setText("session-expiry", expiry);
loginView.hidden = true;
consoleView.hidden = false;
restoreSettings();
restoreContext();
await loadAdvisoryPolicy();
const context = saveContext();
@@ -88,10 +173,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 +216,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 +232,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 +388,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 +449,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 +526,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 +625,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 +684,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 +704,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 +726,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 +765,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 +826,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 +848,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 +954,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 +984,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 +1018,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 = "Проверить исход";
+2 -2
View File
@@ -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>
+86 -20
View File
@@ -6,9 +6,10 @@
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#101714">
<title>Домовой — консоль создателя</title>
<link rel="stylesheet" href="/assets/console.css">
<link rel="stylesheet" href="/assets/console.css?v=20260821-context">
</head>
<body>
<a class="skip-link" href="#overview">К содержимому</a>
<div class="grain" aria-hidden="true"></div>
<main id="login-view" class="login-shell">
@@ -44,6 +45,13 @@
</a>
<div class="topbar-actions">
<span id="connection-state" class="connection"><i></i>система доступна</span>
<nav class="topbar-nav" aria-label="Разделы кабинета">
<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>
<button id="logout" class="icon-button" type="button" aria-label="Завершить сессию" title="Выйти">↗</button>
</div>
@@ -56,18 +64,46 @@
<p class="rail-copy">Укажите рабочий контекст. Он сохраняется только в этом браузере и не даёт дополнительных полномочий.</p>
</div>
<form id="context-form" class="context-form">
<label for="session-id">Идентификатор сессии</label>
<input id="session-id" autocomplete="off" placeholder="session-id">
<label for="workspace-id">Рабочая область</label>
<input id="workspace-id" autocomplete="off" placeholder="workspace-id">
<label for="task-id">Задача</label>
<input id="task-id" autocomplete="off" placeholder="task-id">
<label for="prospective-id">Выбранное ожидание</label>
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
<label for="capability-id">Возможность</label>
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
<button class="secondary-button" type="submit">Загрузить контекст</button>
<div class="context-status-row">
<span class="context-status-label">ТЕКУЩИЙ КОНТЕКСТ</span>
<strong id="context-state" data-state="empty">не выбран</strong>
</div>
<div class="context-presets">
<label for="context-preset">Недавние наборы</label>
<select id="context-preset">
<option value="">Выберите сохранённый набор…</option>
</select>
<small>Сохраняются только в этом браузере.</small>
</div>
<div class="context-field">
<label for="session-id">Идентификатор сессии <span>обязательно</span></label>
<div class="context-input-row"><input id="session-id" autocomplete="off" spellcheck="false" placeholder="Вставьте session-id"><button class="copy-context" data-copy-context="session-id" type="button" aria-label="Скопировать идентификатор сессии" title="Скопировать">⧉</button></div>
</div>
<div class="context-field">
<label for="workspace-id">Рабочая область <span>обязательно</span></label>
<div class="context-input-row"><input id="workspace-id" autocomplete="off" spellcheck="false" placeholder="Вставьте workspace-id"><button class="copy-context" data-copy-context="workspace-id" type="button" aria-label="Скопировать рабочую область" title="Скопировать">⧉</button></div>
</div>
<p class="context-hint">Для «Состояния» нужны только эти два значения. Задача, ожидание и возможность находятся ниже в расширенных полях.</p>
<details class="advanced-context">
<summary>Расширенные идентификаторы</summary>
<div>
<label for="task-id">Задача</label>
<input id="task-id" autocomplete="off" placeholder="task-id">
<label for="prospective-id">Выбранное ожидание</label>
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
<label for="capability-id">Возможность</label>
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
</div>
</details>
<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="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="#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>
<strong id="session-actor">—</strong>
@@ -77,7 +113,7 @@
</aside>
<main class="workspace">
<section class="workspace-head">
<section id="overview" class="workspace-head" data-page="overview">
<div>
<p class="eyebrow">операционная картина</p>
<h1>Состояние сейчас</h1>
@@ -85,14 +121,28 @@
<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 class="advisor-panel" aria-labelledby="advisor-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>
</header>
<div class="diagnostic-intro">
<span class="diagnostic-pulse" aria-hidden="true"></span>
<p>Здесь видны только ответы текущей авторизованной сессии. Проверка не меняет данные и помогает отличить отсутствие контекста от недоступности сервиса.</p>
</div>
<div id="diagnostic-list" class="diagnostic-list" aria-live="polite" aria-busy="false">
<p class="register-empty">Нажмите «Проверить всё», чтобы получить актуальный снимок.</p>
</div>
</section>
<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>
@@ -120,7 +170,7 @@
</div>
</section>
<section 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>
@@ -130,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">
@@ -143,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">
@@ -167,7 +217,20 @@
</div>
</section>
<section class="data-grid">
<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>
</header>
<form id="settings-form" class="settings-grid">
<label class="setting-field"><span>Плотность данных</span><small>Выберите, сколько строк помещается на экране.</small><select id="setting-density"><option value="comfortable">Комфортная</option><option value="compact">Компактная</option></select></label>
<label class="setting-field"><span>Автообновление</span><small>Повторная проверка состояния без изменения данных.</small><select id="setting-refresh"><option value="off">Выключено</option><option value="30">Каждые 30 секунд</option><option value="60">Каждую минуту</option><option value="300">Каждые 5 минут</option></select></label>
<label class="setting-check"><input id="setting-motion" type="checkbox"><span><b>Сократить анимацию</b><small>Оставить спокойный режим для длительной работы.</small></span></label>
<div class="settings-actions"><button id="save-settings" class="primary-button compact-button" type="submit"><span>Сохранить настройки</span><span aria-hidden="true">→</span></button><button id="reset-settings" class="text-button" type="button">Сбросить</button></div>
</form>
</section>
<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>
@@ -188,6 +251,7 @@
<div><dt>Ревизия</dt><dd id="task-revision">—</dd></div>
</dl>
<p id="task-message" class="card-message">Укажите задачу для проверки относительно активного фокуса.</p>
<div class="id-actions"><button class="id-action" data-use-value="task-value" data-target-id="task-id" type="button">Использовать ID задачи</button><button class="id-action icon-id-action" data-copy-value="task-value" type="button" aria-label="Скопировать ID задачи" title="Скопировать ID">⧉</button></div>
</article>
<article class="data-card prospective-card">
@@ -205,6 +269,7 @@
<div><dt>Ревизия</dt><dd id="prospective-revision">—</dd></div>
</dl>
<p id="prospective-message" class="card-message">Укажите идентификатор ProspectiveItem для проверки владельца и состояния.</p>
<div class="id-actions"><button class="id-action" data-use-value="prospective-value" data-target-id="prospective-id" type="button">Использовать ID ожидания</button><button class="id-action icon-id-action" data-copy-value="prospective-value" type="button" aria-label="Скопировать ID ожидания" title="Скопировать ID">⧉</button></div>
<section class="commit-zone" aria-labelledby="commit-zone-title">
<div class="commit-zone-copy">
<p>Commit / контролируемый переход</p>
@@ -240,6 +305,7 @@
<div><dt>Ревизия</dt><dd id="capability-revision">—</dd></div>
<div><dt>Наблюдалось</dt><dd id="capability-observed">—</dd></div>
</dl>
<div class="id-actions"><button class="id-action" data-use-value="capability-value" data-target-id="capability-id" type="button">Использовать ID возможности</button><button class="id-action icon-id-action" data-copy-value="capability-value" type="button" aria-label="Скопировать ID возможности" title="Скопировать ID">⧉</button></div>
</article>
</section>
</main>
@@ -286,6 +352,6 @@
</dialog>
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
<script src="/assets/console.js" defer></script>
<script src="/assets/console.js?v=20260821-context" defer></script>
</body>
</html>
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000072-semantic-interpretation-deferral-boundary.json",
"task_id": "DEV-000072",
"task_file": "dev/tasks/DEV-000075-creator-chat-primary-surface.json",
"task_id": "DEV-000075",
"task_revision": 1
}
@@ -0,0 +1,92 @@
{
"task_id": "DEV-000074",
"task_revision": 6,
"architecture_revision": "DEV-000073-baseline",
"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": [
"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"]
}
@@ -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"]
}