Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54de3f44d0 |
@@ -32,29 +32,6 @@ 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,12 +112,7 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/" {
|
||||
serveEmbeddedHTML(w, "chat.html")
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/admin" {
|
||||
serveEmbeddedHTML(w, "index.html")
|
||||
return
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
}
|
||||
if r.URL.Path == "/chat" {
|
||||
r = r.Clone(r.Context())
|
||||
@@ -127,17 +122,6 @@ 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,21 +229,10 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodGet, "/admin", nil)
|
||||
request := httptest.NewRequest(http.MethodGet, "/", 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(), "Спросить Домового") ||
|
||||
|
||||
@@ -1,76 +1,488 @@
|
||||
:root {
|
||||
--ink: #e9eee7;
|
||||
--muted: #93a096;
|
||||
--base: #0b100e;
|
||||
--panel: #101714;
|
||||
--line: #2a352f;
|
||||
--signal: #c9a85f;
|
||||
--bad: #de816f;
|
||||
--good: #78c6a3;
|
||||
--radius: 3px;
|
||||
--ink: #e7efe9;
|
||||
--muted: #96a49d;
|
||||
--faint: #5f6e66;
|
||||
--surface: #0f1512;
|
||||
--surface-alt: #151d18;
|
||||
--line: #27322c;
|
||||
--base: #090d0b;
|
||||
--good: #7cc7a4;
|
||||
--signal: #ceb27a;
|
||||
--bad: #d88d78;
|
||||
--radius-lg: 16px;
|
||||
--radius: 10px;
|
||||
--sans: "Bahnschrift", "Aptos Narrow", "Segoe UI", sans-serif;
|
||||
--serif: Georgia, "Times New Roman", serif;
|
||||
--mono: "Cascadia Mono", Consolas, monospace;
|
||||
--shadow: 0 24px 56px rgba(0, 0, 0, .22);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; color: var(--ink); background: var(--base); font-family: var(--sans); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
min-width: 320px;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at 10% 0%, rgba(120, 182, 160, .16), transparent 40%),
|
||||
radial-gradient(circle at 90% 0%, rgba(201, 168, 95, .16), transparent 35%),
|
||||
var(--base);
|
||||
font-family: var(--sans);
|
||||
}
|
||||
|
||||
button, input, textarea { font: inherit; }
|
||||
button { cursor: pointer; color: inherit; border-radius: var(--radius); }
|
||||
.chat-shell { min-height: 100vh; display: grid; }
|
||||
.login-panel { width: min(100% - 32px, 420px); margin: auto; padding: 46px 40px; border: 1px solid var(--line); background: var(--panel); }
|
||||
.brand-mark { width: 46px; height: 46px; display: grid; place-items: center; color: var(--base); background: var(--signal); font: 700 24px/1 var(--serif); clip-path: polygon(50% 0, 92% 22%, 92% 76%, 50% 100%, 8% 76%, 8% 22%); }
|
||||
h1 { margin: 30px 0; font: 400 58px/.95 var(--serif); letter-spacing: -.02em; }
|
||||
label { display: block; margin: 16px 0 8px; color: var(--muted); font: 600 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
|
||||
input, textarea { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; }
|
||||
input { height: 48px; padding: 0 12px; }
|
||||
textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px; line-height: 1.45; }
|
||||
.login-form button, .composer button { border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
|
||||
.login-form button { width: 100%; height: 50px; margin-top: 18px; }
|
||||
#login-error { min-height: 20px; color: var(--bad); font-size: 13px; }
|
||||
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr); }
|
||||
.chat-header { display: flex; align-items: center; gap: 18px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .96); }
|
||||
.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); }
|
||||
.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; }
|
||||
.nav-tab.active { color: #11150f; border-color: var(--signal); background: var(--signal); }
|
||||
.operator-link { color: var(--good); }
|
||||
.surface-panel { min-height: 0; position: relative; }
|
||||
.active-panel { display: grid; min-height: 0; }
|
||||
.messages { width: min(100%, 920px); margin: 0 auto; padding: 28px 18px 120px; display: flex; flex-direction: column; gap: 14px; overflow: auto; }
|
||||
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
|
||||
.message.user { align-self: flex-end; background: #151e1a; }
|
||||
.message.assistant { align-self: flex-start; background: #0f1613; }
|
||||
.message small { display: block; margin-top: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; }
|
||||
.composer { position: sticky; bottom: 0; width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 20px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
|
||||
.composer button { height: 48px; align-self: end; }
|
||||
.composer button:disabled { cursor: wait; filter: saturate(.3); }
|
||||
.attention-surface, .history-surface { width: min(100%, 920px); margin: 0 auto; padding: 34px 18px; align-content: start; gap: 18px; }
|
||||
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 16px; }
|
||||
.surface-heading p { margin: 0; font: 400 36px/.95 var(--serif); }
|
||||
.surface-heading span, .attention-card small, .history-item span { color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .1em; }
|
||||
.surface-heading button { height: 36px; padding: 0 12px; border: 1px solid var(--line); background: transparent; }
|
||||
.attention-list, .history-list { display: grid; gap: 12px; }
|
||||
.attention-card, .history-item { border: 1px solid var(--line); background: #0f1613; padding: 16px; }
|
||||
.attention-card time { color: var(--signal); font: 12px/1 var(--mono); }
|
||||
.attention-card h2 { margin: 10px 0 8px; font: 400 24px/1.1 var(--serif); }
|
||||
.attention-card p, .history-item p { margin: 0 0 10px; color: var(--ink); line-height: 1.45; }
|
||||
.attention-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
|
||||
.attention-actions button { height: 34px; padding: 0 12px; border: 1px solid var(--line); background: #151e1a; color: var(--ink); font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
|
||||
.attention-actions button:first-child { border-color: var(--good); color: var(--good); }
|
||||
.history-item.user { border-color: #34443c; background: #151e1a; }
|
||||
.history-item strong { display: block; margin-bottom: 8px; color: var(--signal); }
|
||||
.empty-state { margin: 24px 0 0; color: var(--muted); }
|
||||
button { color: inherit; border: 0; border-radius: var(--radius); }
|
||||
p { margin: 0; }
|
||||
|
||||
.assistant-shell,
|
||||
.chat-view { min-height: 100vh; }
|
||||
|
||||
.auth-view {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 36px 20px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: min(100%, 440px);
|
||||
padding: 30px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(15, 21, 18, .92);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.brand-block,
|
||||
.header-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
color: var(--base);
|
||||
background: linear-gradient(145deg, var(--good), #7aa7bd);
|
||||
font: 700 24px/1 var(--serif);
|
||||
box-shadow: 0 0 0 4px rgba(124, 199, 164, .12);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font: 500 42px/1.05 var(--serif);
|
||||
}
|
||||
|
||||
.brand-kicker {
|
||||
margin: 0 0 4px;
|
||||
color: var(--faint);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .1em;
|
||||
font-size: 11px;
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.auth-copy {
|
||||
margin: 18px 0 22px;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 6px;
|
||||
color: var(--faint);
|
||||
font-size: 12px;
|
||||
letter-spacing: .03em;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
color: var(--ink);
|
||||
background: #080e0c;
|
||||
}
|
||||
|
||||
input { height: 46px; padding: 0 14px; }
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
.primary-button:focus,
|
||||
.ghost-button:focus,
|
||||
.nav-tab:focus-visible,
|
||||
button:focus-visible,
|
||||
textarea:focus-visible {
|
||||
outline: 2px solid var(--signal);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 96px;
|
||||
max-height: 220px;
|
||||
padding: 14px;
|
||||
line-height: 1.45;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon-button,
|
||||
.ghost-button,
|
||||
.primary-button {
|
||||
height: 44px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.icon-button,
|
||||
.ghost-button {
|
||||
background: rgba(255, 255, 255, .05);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
background: var(--signal);
|
||||
color: #111810;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#login-button,
|
||||
.primary-button { width: 100%; }
|
||||
|
||||
#login-button,
|
||||
#send { margin-top: 14px; }
|
||||
|
||||
.login-form,
|
||||
.composer {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
#login-error {
|
||||
min-height: 20px;
|
||||
color: var(--bad);
|
||||
}
|
||||
|
||||
.chat-view {
|
||||
width: min(100%, 980px);
|
||||
margin: 0 auto;
|
||||
padding: 14px 16px 24px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.assistant-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.assistant-header h1 {
|
||||
font-size: clamp(30px, 4.4vw, 38px);
|
||||
letter-spacing: -.02em;
|
||||
}
|
||||
|
||||
.session-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
padding: 9px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
background: #0f1512;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.human-nav {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 6px;
|
||||
width: fit-content;
|
||||
background: #0c1210;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.nav-tab.active,
|
||||
.nav-tab:hover {
|
||||
background: var(--signal);
|
||||
color: #11150f;
|
||||
}
|
||||
|
||||
.surface-panel {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
min-height: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(180deg, rgba(21, 29, 24, .86), rgba(15, 21, 18, .95));
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.surface-panel.active-panel {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: #0c1210;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-toolbar h2 {
|
||||
margin: 0;
|
||||
font: 500 26px/1.12 var(--serif);
|
||||
}
|
||||
|
||||
.panel-toolbar p {
|
||||
margin-top: 6px;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.focus-chip {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--good);
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(124, 199, 164, .45);
|
||||
border-radius: 999px;
|
||||
background: rgba(124, 199, 164, .12);
|
||||
}
|
||||
|
||||
.messages {
|
||||
padding: 24px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
overflow: auto;
|
||||
min-height: calc(100vh - 280px);
|
||||
max-height: calc(100vh - 280px);
|
||||
}
|
||||
|
||||
.message {
|
||||
width: min(92%, 760px);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
background: #0f1512;
|
||||
}
|
||||
|
||||
.message.assistant { align-self: flex-start; background: #121a16; }
|
||||
.message.user { align-self: flex-end; background: #19231c; border-color: rgba(120, 191, 162, .4); }
|
||||
|
||||
.message-author { color: var(--faint); font-size: 11px; margin-bottom: 6px; font-weight: 700; }
|
||||
.message.user .message-author { color: #b6ddb8; }
|
||||
.message-time { margin-top: 8px; font-size: 11px; color: var(--faint); font-family: var(--mono); }
|
||||
.message small { display: block; margin-top: 6px; color: var(--faint); }
|
||||
|
||||
.composer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 14px 16px 16px;
|
||||
border-top: 1px solid var(--line);
|
||||
background: linear-gradient(transparent, rgba(11, 16, 14, .88));
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.composer-actions { display: grid; align-items: end; }
|
||||
|
||||
.composer button:disabled {
|
||||
cursor: wait;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.surface-panel:not(.active-panel) { display: none; }
|
||||
|
||||
.attention-surface,
|
||||
.history-surface { padding: 0 0 12px; }
|
||||
|
||||
.attention-list,
|
||||
.history-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.attention-card,
|
||||
.history-item {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #0b110f;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.attention-card h3,
|
||||
.history-item strong { margin: 0 0 8px; font: 600 15px/1.2 var(--serif); }
|
||||
|
||||
.attention-card time,
|
||||
.history-item time,
|
||||
.attention-card .muted,
|
||||
.history-item .muted {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.attention-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
|
||||
|
||||
.attention-actions .ghost-button,
|
||||
.action-row button {
|
||||
height: 34px;
|
||||
padding: 0 11px;
|
||||
background: rgba(255, 255, 255, .06);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.attention-actions .ghost-button {
|
||||
border: 1px solid rgba(194, 168, 118, .48);
|
||||
color: #f2e8d0;
|
||||
}
|
||||
|
||||
.attention-actions .ghost-button.primary { border-color: transparent; background: var(--good); color: #11211b; }
|
||||
|
||||
.history-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.history-item p {
|
||||
margin: 0 0 8px;
|
||||
color: var(--ink);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.history-item .history-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
color: var(--faint);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.history-item .message-kind {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: var(--signal);
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
margin: 8px auto 0;
|
||||
max-width: 980px;
|
||||
color: var(--faint);
|
||||
padding: 10px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(15, 21, 18, .8);
|
||||
min-height: 18px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.status-bar.visible { visibility: visible; }
|
||||
|
||||
.composer-error {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
max-width: min(420px, calc(100vw - 32px));
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(216, 141, 120, .36);
|
||||
background: #241715;
|
||||
color: #ffd8ca;
|
||||
padding: 10px 12px;
|
||||
transform: translateY(16px);
|
||||
opacity: 0;
|
||||
transition: .2s ease;
|
||||
}
|
||||
|
||||
.composer-error.show {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
clip-path: inset(50%);
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: var(--muted);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.empty-state.error { color: var(--bad); }
|
||||
@media (max-width: 640px) {
|
||||
.composer { grid-template-columns: 1fr; }
|
||||
.composer button { width: 100%; }
|
||||
.chat-header { gap: 10px; padding: 0 12px; }
|
||||
.chat-header span { display: none; }
|
||||
.human-nav { overflow-x: auto; }
|
||||
.operator-link, .nav-tab { padding: 0 9px; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.chat-view {
|
||||
padding: 10px 10px 18px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.assistant-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.session-state { width: 100%; justify-content: space-between; }
|
||||
|
||||
.messages {
|
||||
min-height: calc(100vh - 320px);
|
||||
max-height: calc(100vh - 320px);
|
||||
}
|
||||
|
||||
.composer {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.attention-list,
|
||||
.history-list {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,227 +2,518 @@ const loginView = document.querySelector("#login-view");
|
||||
const chatView = document.querySelector("#chat-view");
|
||||
const loginForm = document.querySelector("#login-form");
|
||||
const loginError = document.querySelector("#login-error");
|
||||
const togglePassword = document.querySelector("#toggle-password");
|
||||
const usernameInput = document.querySelector("#username");
|
||||
const passwordInput = document.querySelector("#password");
|
||||
const messages = document.querySelector("#messages");
|
||||
const chatForm = document.querySelector("#chat-form");
|
||||
const messageInput = document.querySelector("#message");
|
||||
const sendButton = document.querySelector("#send");
|
||||
const logoutButton = document.querySelector("#logout");
|
||||
const clearHistoryButton = document.querySelector("#clear-history");
|
||||
const activeFocus = document.querySelector("#active-focus");
|
||||
const todayList = document.querySelector("#today-list");
|
||||
const todaySubtitle = document.querySelector("#today-subtitle");
|
||||
const refreshToday = document.querySelector("#refresh-today");
|
||||
const historyList = document.querySelector("#history-list");
|
||||
const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
|
||||
const statusPill = document.querySelector("#connection-state");
|
||||
const statusBar = document.querySelector("#status-bar");
|
||||
const composerError = document.querySelector("#composer-error");
|
||||
|
||||
let token = sessionStorage.getItem("domovoy.chat.token") || "";
|
||||
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
|
||||
const SESSION_KEY = "domovoy.chat.token";
|
||||
const CONVERSATION_KEY = "domovoy.chat.conversation";
|
||||
const SECTION_KEY = "domovoy.chat.section";
|
||||
|
||||
let token = sessionStorage.getItem(SESSION_KEY) || "";
|
||||
let conversationID = sessionStorage.getItem(CONVERSATION_KEY) || "";
|
||||
let localHistory = [];
|
||||
let todayRequestInFlight = false;
|
||||
|
||||
function setConnectionState(text, tone = "neutral") {
|
||||
if (!statusPill) return;
|
||||
statusPill.textContent = text;
|
||||
if (tone === "bad") {
|
||||
statusPill.style.borderColor = "rgba(216, 141, 120, .6)";
|
||||
statusPill.style.color = "var(--bad)";
|
||||
} else {
|
||||
statusPill.style.borderColor = "var(--line)";
|
||||
statusPill.style.color = "var(--muted)";
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
if (!statusBar) return;
|
||||
statusBar.textContent = message;
|
||||
statusBar.classList.add("visible");
|
||||
clearTimeout(showToast._timer);
|
||||
showToast._timer = setTimeout(() => {
|
||||
statusBar.classList.remove("visible");
|
||||
}, 2800);
|
||||
}
|
||||
|
||||
function showComposerError(message) {
|
||||
if (!composerError) return;
|
||||
composerError.textContent = message;
|
||||
composerError.classList.add("show");
|
||||
clearTimeout(showComposerError._timer);
|
||||
showComposerError._timer = setTimeout(() => {
|
||||
composerError.classList.remove("show");
|
||||
}, 2600);
|
||||
}
|
||||
|
||||
function formatTime(value = new Date()) {
|
||||
return new Intl.DateTimeFormat("ru-RU", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function showChat() {
|
||||
setConnectionState("Сессия активна");
|
||||
loginView.hidden = true;
|
||||
chatView.hidden = false;
|
||||
|
||||
if (!messages.children.length) {
|
||||
appendMessage("assistant", "Я рядом. Напишите обычное сообщение, без внутренних идентификаторов.");
|
||||
appendMessage("assistant", "Я рядом. Пишите просто и по-человечески, я всё пойму.");
|
||||
}
|
||||
|
||||
const storedSection = localStorage.getItem(SECTION_KEY);
|
||||
activateTab(storedSection === "today-panel" || storedSection === "history-panel" ? storedSection : "chat-panel");
|
||||
messageInput.focus();
|
||||
}
|
||||
|
||||
function showLogin() {
|
||||
setConnectionState("Вход не выполнен", "bad");
|
||||
chatView.hidden = true;
|
||||
loginView.hidden = false;
|
||||
usernameInput?.focus();
|
||||
}
|
||||
|
||||
function appendMessage(kind, text, meta = "") {
|
||||
const node = document.createElement("div");
|
||||
node.className = `message ${kind}`;
|
||||
node.textContent = text;
|
||||
if (meta) {
|
||||
const small = document.createElement("small");
|
||||
small.textContent = meta;
|
||||
node.appendChild(small);
|
||||
}
|
||||
messages.appendChild(node);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
localHistory.push({kind, text, meta, at: new Date()});
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
function updateActiveFocus(value, provenance = "") {
|
||||
if (!activeFocus) return;
|
||||
activeFocus.textContent = value ? `Сейчас обсуждаем: ${value}` : "";
|
||||
activeFocus.hidden = !value;
|
||||
function endSession(message = "Сессия завершена") {
|
||||
token = "";
|
||||
conversationID = "";
|
||||
sessionStorage.removeItem(SESSION_KEY);
|
||||
sessionStorage.removeItem(CONVERSATION_KEY);
|
||||
messages.replaceChildren();
|
||||
localHistory = [];
|
||||
updateActiveFocus("");
|
||||
setConnectionState("Вход не выполнен", "bad");
|
||||
showToast(message);
|
||||
showLogin();
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const headers = {"Content-Type": "application/json", ...(options.headers || {})};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
const response = await fetch(path, {...options, headers});
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (token) headers.set("Authorization", `Bearer ${token}`);
|
||||
if (!headers.has("Content-Type")) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
const response = await fetch(path, { ...options, headers });
|
||||
|
||||
if (response.status === 401 && path !== "/api/session") {
|
||||
endSession("Сессия просрочена, войдите снова");
|
||||
throw new Error("Требуется авторизация");
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = (await response.text()).trim();
|
||||
throw new Error(detail || `Ошибка ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
showComposerError(message || "Произошла ошибка");
|
||||
}
|
||||
|
||||
function appendMessage(kind, text, meta = "") {
|
||||
const node = document.createElement("article");
|
||||
node.className = `message ${kind}`;
|
||||
|
||||
const who = document.createElement("strong");
|
||||
who.className = "message-author";
|
||||
who.textContent = kind === "user" ? "Ты" : "Домовой";
|
||||
|
||||
const body = document.createElement("p");
|
||||
body.textContent = text;
|
||||
|
||||
node.append(who, body);
|
||||
|
||||
if (meta) {
|
||||
const metaNode = document.createElement("small");
|
||||
metaNode.textContent = meta;
|
||||
node.appendChild(metaNode);
|
||||
}
|
||||
|
||||
const when = document.createElement("time");
|
||||
when.className = "message-time";
|
||||
when.textContent = formatTime();
|
||||
node.appendChild(when);
|
||||
|
||||
messages.appendChild(node);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
|
||||
localHistory.push({
|
||||
kind,
|
||||
text,
|
||||
meta,
|
||||
at: new Date(),
|
||||
});
|
||||
|
||||
if (localHistory.length > 120) {
|
||||
localHistory = localHistory.slice(-120);
|
||||
}
|
||||
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
function updateActiveFocus(value) {
|
||||
if (!activeFocus) return;
|
||||
if (value) {
|
||||
activeFocus.textContent = `Сейчас обсуждаем: ${value}`;
|
||||
activeFocus.hidden = false;
|
||||
} else {
|
||||
activeFocus.hidden = true;
|
||||
activeFocus.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
function humanMeta(result) {
|
||||
if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам.";
|
||||
if (result.delegation_id && result.delegation_state) return `Передал в coding-agent: ${humanDelegationState(result.delegation_state)}.`;
|
||||
if (result.capability_status && result.capability_status !== "AVAILABLE") return "Возможность пока не подключена.";
|
||||
if (result.work_planned) return "Я разложил это как работу.";
|
||||
if (result.reminder_scheduled) {
|
||||
return "Я зафиксировал напоминание и сообщу, когда наступит время.";
|
||||
}
|
||||
if (result.delegation_id && result.delegation_state) {
|
||||
return `Передал дальше: ${humanDelegationState(result.delegation_state)}.`;
|
||||
}
|
||||
if (result.capability_status && result.capability_status !== "AVAILABLE") {
|
||||
return "Эта задача сейчас проверяется отдельно.";
|
||||
}
|
||||
if (result.work_planned) {
|
||||
return "Я взял задачу в работу и отслежу статус.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function humanDelegationState(state) {
|
||||
switch (state) {
|
||||
case "RUNNING":
|
||||
return "работа запущена";
|
||||
return "выполняется";
|
||||
case "WAITING":
|
||||
return "ожидаю следующий шаг";
|
||||
return "ожидает следующего шага";
|
||||
case "BLOCKED":
|
||||
return "нужно решение человека";
|
||||
return "нужен человеческий выбор";
|
||||
case "VERIFYING":
|
||||
return "результат проверяется";
|
||||
return "результат на проверке";
|
||||
case "COMPLETED":
|
||||
return "работа завершена";
|
||||
return "готово";
|
||||
case "FAILED":
|
||||
return "run завершился ошибкой";
|
||||
return "завершилось ошибкой";
|
||||
default:
|
||||
return state.toLowerCase();
|
||||
return String(state || "обрабатывается").toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
if (!historyList) return;
|
||||
|
||||
historyList.replaceChildren();
|
||||
const items = localHistory.slice(-30).reverse();
|
||||
const items = localHistory.slice(-50).slice().reverse();
|
||||
|
||||
if (!items.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "empty-state";
|
||||
empty.textContent = "История появится после сообщений в этом окне.";
|
||||
empty.textContent = "История появится после первых сообщений в этом окне.";
|
||||
historyList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
for (const item of items) {
|
||||
|
||||
items.forEach((item) => {
|
||||
const row = document.createElement("article");
|
||||
row.className = `history-item ${item.kind}`;
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = item.kind === "user" ? "Вы" : "Домовой";
|
||||
row.className = "history-item";
|
||||
|
||||
const who = document.createElement("strong");
|
||||
who.className = "message-kind";
|
||||
who.textContent = item.kind === "user" ? "Ты" : "Домовой";
|
||||
|
||||
const body = document.createElement("p");
|
||||
body.textContent = item.text;
|
||||
const time = document.createElement("span");
|
||||
time.textContent = item.at.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"});
|
||||
row.append(title, body, time);
|
||||
|
||||
const footer = document.createElement("div");
|
||||
footer.className = "history-meta";
|
||||
const time = document.createElement("time");
|
||||
time.textContent = formatTime(item.at);
|
||||
const meta = document.createElement("span");
|
||||
meta.textContent = item.meta || "";
|
||||
footer.append(time, meta);
|
||||
|
||||
row.append(who, body, footer);
|
||||
historyList.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function createTodayCard(item) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "attention-card";
|
||||
|
||||
const title = document.createElement("h3");
|
||||
const when = document.createElement("time");
|
||||
const state = document.createElement("p");
|
||||
const source = document.createElement("p");
|
||||
const hint = document.createElement("p");
|
||||
const actions = document.createElement("div");
|
||||
|
||||
const stamp = item.WhenUnixMillis || item.when_unix_millis || 0;
|
||||
when.textContent = stamp
|
||||
? new Date(stamp).toLocaleString("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: "текущий момент";
|
||||
when.className = "muted";
|
||||
|
||||
title.textContent = item.Title || item.title || "Событие";
|
||||
state.textContent = item.HumanState || item.human_state || "ожидает внимания";
|
||||
state.className = "muted";
|
||||
|
||||
const sourceText = [
|
||||
item.SourceDescription || item.source_description,
|
||||
item.DeliveryState || item.delivery_state,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
source.textContent = sourceText || "Источник не задан";
|
||||
source.className = "muted";
|
||||
|
||||
const hintText = item.ResponseHint || item.response_hint || "";
|
||||
if (hintText) {
|
||||
hint.textContent = hintText;
|
||||
}
|
||||
|
||||
actions.className = "attention-actions";
|
||||
|
||||
const quick = [
|
||||
{ label: "Готово", text: "Готово", tone: "primary" },
|
||||
{ label: "Позже", text: "Отложи на 10 минут" },
|
||||
{ label: "Отменить", text: "Отмени" },
|
||||
];
|
||||
|
||||
quick.forEach((action) => {
|
||||
const actionButton = document.createElement("button");
|
||||
actionButton.type = "button";
|
||||
actionButton.className = `ghost-button ${action.tone || ""}`.trim();
|
||||
actionButton.textContent = action.label;
|
||||
|
||||
actionButton.addEventListener("click", async () => {
|
||||
const previous = actionButton.textContent;
|
||||
actionButton.disabled = true;
|
||||
actionButton.textContent = "Отправляю…";
|
||||
try {
|
||||
await sendChatMessage(action.text, { silentUser: false });
|
||||
await loadToday();
|
||||
activateTab("chat-panel");
|
||||
} finally {
|
||||
actionButton.disabled = false;
|
||||
actionButton.textContent = previous;
|
||||
}
|
||||
});
|
||||
|
||||
actions.appendChild(actionButton);
|
||||
});
|
||||
|
||||
card.append(title, when, state, source);
|
||||
if (hintText) card.append(hint);
|
||||
card.append(actions);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderTodayEmpty(message) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "empty-state";
|
||||
empty.textContent = message;
|
||||
todayList.appendChild(empty);
|
||||
}
|
||||
|
||||
async function loadToday() {
|
||||
if (!todayList || !token) return;
|
||||
if (!todayList || todayRequestInFlight) return;
|
||||
|
||||
todayRequestInFlight = true;
|
||||
todayList.replaceChildren();
|
||||
const loading = document.createElement("p");
|
||||
loading.className = "empty-state";
|
||||
loading.textContent = "Смотрю, что требует внимания...";
|
||||
todayList.appendChild(loading);
|
||||
renderTodayEmpty("Собираю актуальные напоминания…");
|
||||
|
||||
try {
|
||||
const result = await api("/api/today", {method: "GET"});
|
||||
todayList.replaceChildren();
|
||||
const result = await api("/api/today", { method: "GET" });
|
||||
const items = result.Items || result.items || [];
|
||||
|
||||
todayList.replaceChildren();
|
||||
|
||||
if (!items.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "empty-state";
|
||||
empty.textContent = "Сейчас ничего срочного не требует внимания.";
|
||||
todayList.appendChild(empty);
|
||||
todaySubtitle.textContent = "Сейчас спокойный период. Нечего тревожного";
|
||||
renderTodayEmpty("Сейчас ничего срочного не требует внимания.");
|
||||
return;
|
||||
}
|
||||
|
||||
todaySubtitle.textContent = `Найдено записей: ${items.length}`;
|
||||
|
||||
for (const item of items) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "attention-card";
|
||||
const when = document.createElement("time");
|
||||
const stamp = item.WhenUnixMillis || item.when_unix_millis || 0;
|
||||
when.textContent = stamp ? new Date(stamp).toLocaleString("ru-RU", {day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit"}) : "сейчас";
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = item.Title || item.title || "Событие Домового";
|
||||
const state = document.createElement("p");
|
||||
state.textContent = item.HumanState || item.human_state || "ожидает внимания";
|
||||
const source = document.createElement("small");
|
||||
source.textContent = [item.SourceDescription || item.source_description, item.DeliveryState || item.delivery_state].filter(Boolean).join(" · ");
|
||||
const hintText = item.ResponseHint || item.response_hint || "";
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "attention-actions";
|
||||
for (const [label, text] of [["Готово", "Готово"], ["Отложить", "Отложи на 10 минут"], ["Отменить", "Отмени"]]) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = label;
|
||||
button.addEventListener("click", async () => {
|
||||
await sendChatMessage(text, {silentUser: false});
|
||||
await loadToday();
|
||||
activateTab("chat-panel");
|
||||
});
|
||||
actions.appendChild(button);
|
||||
}
|
||||
if (hintText) {
|
||||
const hint = document.createElement("small");
|
||||
hint.textContent = hintText;
|
||||
card.append(when, title, state, source, hint, actions);
|
||||
} else {
|
||||
card.append(when, title, state, source, actions);
|
||||
}
|
||||
todayList.appendChild(card);
|
||||
todayList.appendChild(createTodayCard(item));
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
todayList.replaceChildren();
|
||||
const error = document.createElement("p");
|
||||
error.className = "empty-state error";
|
||||
error.textContent = "Внимание сейчас недоступно. Чат можно продолжать.";
|
||||
todayList.appendChild(error);
|
||||
const err = document.createElement("p");
|
||||
err.className = "empty-state error";
|
||||
err.textContent = error.message || "Сегодня недоступен на мгновение.";
|
||||
todayList.appendChild(err);
|
||||
todaySubtitle.textContent = "Не удалось обновить список.";
|
||||
} finally {
|
||||
todayRequestInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendChatMessage(text, options = {}) {
|
||||
const clean = text.trim();
|
||||
if (!clean) return;
|
||||
if (!options.silentUser) appendMessage("user", clean);
|
||||
|
||||
if (!options.silentUser) {
|
||||
appendMessage("user", clean);
|
||||
}
|
||||
|
||||
sendButton.disabled = true;
|
||||
messageInput.disabled = true;
|
||||
const originalLabel = sendButton.textContent;
|
||||
sendButton.textContent = "Отправляю…";
|
||||
|
||||
try {
|
||||
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean})});
|
||||
const result = await api("/api/chat/messages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
conversation_id: conversationID,
|
||||
message: clean,
|
||||
}),
|
||||
});
|
||||
|
||||
conversationID = result.conversation_id;
|
||||
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
|
||||
updateActiveFocus(result.active_focus, result.project_provenance);
|
||||
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result));
|
||||
if (conversationID) {
|
||||
sessionStorage.setItem(CONVERSATION_KEY, conversationID);
|
||||
}
|
||||
|
||||
updateActiveFocus(result.active_focus);
|
||||
appendMessage("assistant", result.reply_text || "Принял тебя. Подготовлю ответ.", humanMeta(result));
|
||||
|
||||
if (/что сейчас требует моего внимания/i.test(clean)) {
|
||||
await loadToday();
|
||||
activateTab("today-panel");
|
||||
}
|
||||
} catch {
|
||||
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
|
||||
|
||||
showToast("Сообщение отправлено");
|
||||
} catch (error) {
|
||||
appendMessage("assistant", "Я не могу отправить сообщение в этот момент. Проверьте сеть и авторизацию.");
|
||||
showError(error.message);
|
||||
} finally {
|
||||
sendButton.disabled = false;
|
||||
messageInput.focus();
|
||||
sendButton.textContent = originalLabel;
|
||||
messageInput.disabled = false;
|
||||
if (!options.silentUser) {
|
||||
messageInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activateTab(panelID) {
|
||||
for (const tab of navTabs) tab.classList.toggle("active", tab.dataset.tab === panelID);
|
||||
for (const panel of document.querySelectorAll(".surface-panel")) {
|
||||
let activeSet = false;
|
||||
navTabs.forEach((tab) => {
|
||||
const isActive = tab.dataset.tab === panelID;
|
||||
tab.classList.toggle("active", isActive);
|
||||
tab.setAttribute("aria-selected", String(isActive));
|
||||
tab.tabIndex = isActive ? 0 : -1;
|
||||
if (isActive) activeSet = true;
|
||||
});
|
||||
|
||||
document.querySelectorAll(".surface-panel").forEach((panel) => {
|
||||
const active = panel.id === panelID;
|
||||
panel.hidden = !active;
|
||||
panel.classList.toggle("active-panel", active);
|
||||
});
|
||||
|
||||
if (!activeSet) return;
|
||||
|
||||
localStorage.setItem(SECTION_KEY, panelID);
|
||||
|
||||
if (panelID === "today-panel") {
|
||||
loadToday();
|
||||
}
|
||||
|
||||
if (panelID === "history-panel") {
|
||||
renderHistory();
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTabs(event) {
|
||||
const current = navTabs.findIndex((tab) => tab.classList.contains("active"));
|
||||
if (current === -1) return;
|
||||
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
const index = (current - 1 + navTabs.length) % navTabs.length;
|
||||
activateTab(navTabs[index].dataset.tab);
|
||||
navTabs[index].focus();
|
||||
}
|
||||
|
||||
if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
const index = (current + 1) % navTabs.length;
|
||||
activateTab(navTabs[index].dataset.tab);
|
||||
navTabs[index].focus();
|
||||
}
|
||||
if (panelID === "today-panel") loadToday();
|
||||
if (panelID === "history-panel") renderHistory();
|
||||
}
|
||||
|
||||
loginForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
const loginButton = document.querySelector("#login-button");
|
||||
|
||||
loginError.textContent = "";
|
||||
const username = document.querySelector("#username").value;
|
||||
const password = document.querySelector("#password").value;
|
||||
loginButton.disabled = true;
|
||||
loginButton.textContent = "Проверяю…";
|
||||
|
||||
try {
|
||||
const result = await api("/api/session", {method: "POST", body: JSON.stringify({username, password})});
|
||||
token = result.Token || result.token;
|
||||
sessionStorage.setItem("domovoy.chat.token", token);
|
||||
const result = await api("/api/session", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
token = result.Token || result.token || "";
|
||||
if (!token) {
|
||||
throw new Error("Пустой токен");
|
||||
}
|
||||
|
||||
sessionStorage.setItem(SESSION_KEY, token);
|
||||
conversationID = "";
|
||||
sessionStorage.removeItem(CONVERSATION_KEY);
|
||||
showChat();
|
||||
} catch {
|
||||
loginError.textContent = "Вход не выполнен";
|
||||
loginError.textContent = "Не удалось войти. Проверьте логин и пароль.";
|
||||
showToast("Не удалось войти");
|
||||
} finally {
|
||||
loginButton.disabled = false;
|
||||
loginButton.textContent = "Открыть чат";
|
||||
}
|
||||
});
|
||||
|
||||
togglePassword?.addEventListener("click", () => {
|
||||
const isPassword = passwordInput.type === "password";
|
||||
passwordInput.type = isPassword ? "text" : "password";
|
||||
togglePassword.textContent = isPassword ? "скрыть" : "показать";
|
||||
togglePassword.setAttribute("aria-label", isPassword ? "Скрыть пароль" : "Показать пароль");
|
||||
});
|
||||
|
||||
chatForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const text = messageInput.value.trim();
|
||||
@@ -231,20 +522,31 @@ chatForm.addEventListener("submit", async (event) => {
|
||||
await sendChatMessage(text);
|
||||
});
|
||||
|
||||
messageInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
chatForm.requestSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
for (const tab of navTabs) {
|
||||
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
|
||||
tab.addEventListener("click", () => {
|
||||
activateTab(tab.dataset.tab);
|
||||
});
|
||||
tab.addEventListener("keydown", navigateTabs);
|
||||
}
|
||||
|
||||
if (refreshToday) refreshToday.addEventListener("click", loadToday);
|
||||
refreshToday?.addEventListener("click", loadToday);
|
||||
|
||||
logoutButton.addEventListener("click", () => {
|
||||
token = "";
|
||||
conversationID = "";
|
||||
sessionStorage.removeItem("domovoy.chat.token");
|
||||
sessionStorage.removeItem("domovoy.chat.conversation");
|
||||
clearHistoryButton?.addEventListener("click", () => {
|
||||
localHistory = [];
|
||||
messages.replaceChildren();
|
||||
updateActiveFocus("");
|
||||
showLogin();
|
||||
renderHistory();
|
||||
showToast("История очищена");
|
||||
});
|
||||
|
||||
logoutButton?.addEventListener("click", () => {
|
||||
endSession("Вы вышли из сеанса");
|
||||
});
|
||||
|
||||
if (token) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
:root {
|
||||
--ink: #eef2ec;
|
||||
--muted: #a7b2aa;
|
||||
--faint: #718078;
|
||||
--base: #0f1512;
|
||||
--panel: #17201b;
|
||||
--panel-high: #202b25;
|
||||
--line: #34423a;
|
||||
--line-soft: #26332c;
|
||||
--ink: #e9eee7;
|
||||
--muted: #8c9a91;
|
||||
--faint: #58645d;
|
||||
--base: #0b100e;
|
||||
--panel: #101714;
|
||||
--panel-high: #151e1a;
|
||||
--line: #2a352f;
|
||||
--line-soft: #1d2822;
|
||||
--signal: #c9a85f;
|
||||
--signal-bright: #e7c97b;
|
||||
--good: #78c6a3;
|
||||
@@ -21,15 +21,13 @@
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
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); }
|
||||
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); }
|
||||
button, input, textarea { font: inherit; }
|
||||
button { color: inherit; }
|
||||
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); }
|
||||
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
|
||||
|
||||
.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"); }
|
||||
.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"); }
|
||||
.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; }
|
||||
@@ -65,9 +63,6 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-
|
||||
.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; }
|
||||
@@ -78,55 +73,17 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-
|
||||
.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; 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 { grid-column: 2; grid-row: 2; padding: clamp(28px, 4vw, 58px); min-width: 0; }
|
||||
.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; }
|
||||
.workspace-head h1 { margin: 0; font: 400 clamp(36px, 5vw, 58px)/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); }
|
||||
@@ -137,46 +94,6 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-
|
||||
.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; }
|
||||
@@ -361,9 +278,6 @@ html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before,
|
||||
.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) {
|
||||
@@ -371,25 +285,14 @@ html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before,
|
||||
.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; }
|
||||
|
||||
@@ -7,42 +7,14 @@ 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);
|
||||
@@ -67,18 +39,6 @@ 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}`);
|
||||
@@ -90,9 +50,7 @@ async function request(path, options = {}) {
|
||||
}
|
||||
if (!response.ok) {
|
||||
const detail = (await response.text()).trim();
|
||||
const error = new Error(detail || `Ошибка ${response.status}`);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
throw new Error(detail || `Ошибка ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
@@ -106,57 +64,15 @@ 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);
|
||||
@@ -164,7 +80,6 @@ async function showConsole() {
|
||||
setText("session-expiry", expiry);
|
||||
loginView.hidden = true;
|
||||
consoleView.hidden = false;
|
||||
restoreSettings();
|
||||
restoreContext();
|
||||
await loadAdvisoryPolicy();
|
||||
const context = saveContext();
|
||||
@@ -173,13 +88,10 @@ 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;
|
||||
@@ -216,7 +128,7 @@ loginForm.addEventListener("submit", async (event) => {
|
||||
byId("creator-password").value = "";
|
||||
await showConsole();
|
||||
} catch (error) {
|
||||
loginError.textContent = safeClientError(error);
|
||||
loginError.textContent = error.message === "creator authentication denied" ? "Логин или пароль не принят." : error.message;
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.firstElementChild.textContent = "Открыть консоль";
|
||||
@@ -232,129 +144,14 @@ byId("toggle-secret").addEventListener("click", () => {
|
||||
|
||||
contextForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
const context = saveContext();
|
||||
rememberContextPreset(context);
|
||||
saveContext();
|
||||
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();
|
||||
@@ -388,7 +185,7 @@ advisoryForm.addEventListener("submit", async (event) => {
|
||||
byId("advisory-result").hidden = false;
|
||||
} catch (error) {
|
||||
setText("advisory-state", "ОШИБКА");
|
||||
setText("advisory-text", safeClientError(error));
|
||||
setText("advisory-text", error.message);
|
||||
byId("advisory-result").hidden = false;
|
||||
} finally {
|
||||
byId("advisory-egress-ack").checked = false;
|
||||
@@ -449,57 +246,6 @@ 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");
|
||||
@@ -526,7 +272,7 @@ async function loadLearning() {
|
||||
list.replaceChildren();
|
||||
const failed = document.createElement("p");
|
||||
failed.className = "register-empty activity-error";
|
||||
failed.textContent = safeClientError(error);
|
||||
failed.textContent = error.message;
|
||||
list.append(failed);
|
||||
});
|
||||
setText("learning-count", "ОШИБКА");
|
||||
@@ -625,7 +371,7 @@ async function loadActionHistory() {
|
||||
list.replaceChildren();
|
||||
const failed = document.createElement("p");
|
||||
failed.className = "register-empty activity-error";
|
||||
failed.textContent = safeClientError(error);
|
||||
failed.textContent = error.message;
|
||||
list.append(failed);
|
||||
setText("action-count", "ОШИБКА");
|
||||
} finally {
|
||||
@@ -684,7 +430,7 @@ async function reconcileHistoryAction(actionID, button) {
|
||||
await loadActionHistory();
|
||||
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
|
||||
} catch (error) {
|
||||
notify(safeClientError(error));
|
||||
notify(error.message);
|
||||
button.disabled = false;
|
||||
button.textContent = "Сверить";
|
||||
}
|
||||
@@ -704,7 +450,7 @@ async function loadFocus(query) {
|
||||
} catch (error) {
|
||||
setSignal("focus", "ошибка", "error");
|
||||
setBadge("focus", "НЕДОСТУПЕН", "bad");
|
||||
setText("focus-message", safeClientError(error));
|
||||
setText("focus-message", error.message);
|
||||
byId("connection-state").classList.add("offline");
|
||||
}
|
||||
}
|
||||
@@ -726,7 +472,7 @@ async function loadTask(taskID, query) {
|
||||
currentTask = null;
|
||||
setSignal("task", "отклонена", "error");
|
||||
setBadge("task", "ВНЕ ФОКУСА", "bad");
|
||||
setText("task-message", safeClientError(error));
|
||||
setText("task-message", error.message);
|
||||
updateActionReadiness();
|
||||
}
|
||||
}
|
||||
@@ -765,12 +511,12 @@ async function loadProspectiveRegister(selectedID) {
|
||||
list.replaceChildren();
|
||||
const failed = document.createElement("p");
|
||||
failed.className = "register-empty";
|
||||
failed.textContent = safeClientError(error);
|
||||
failed.textContent = error.message;
|
||||
list.append(failed);
|
||||
setText("prospective-count", "ОШИБКА");
|
||||
setSignal("prospective", "ошибка", "error");
|
||||
setBadge("prospective", "НЕДОСТУПНА", "bad");
|
||||
setText("prospective-message", safeClientError(error));
|
||||
setText("prospective-message", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,7 +572,7 @@ async function loadProspective(prospectiveID) {
|
||||
setSignal("prospective", "ошибка", "error");
|
||||
setBadge("prospective", "НЕДОСТУПНА", "bad");
|
||||
setText("prospective-state", "UNKNOWN");
|
||||
setText("prospective-message", safeClientError(error));
|
||||
setText("prospective-message", error.message);
|
||||
updateActionReadiness();
|
||||
}
|
||||
}
|
||||
@@ -848,7 +594,7 @@ async function loadCapability(capabilityID) {
|
||||
setSignal("capability", "ошибка", "error");
|
||||
setBadge("capability", "НЕДОСТУПНА", "bad");
|
||||
setText("capability-state", "UNKNOWN");
|
||||
setText("capability-reason", safeClientError(error));
|
||||
setText("capability-reason", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,7 +700,7 @@ byId("confirm-notification").addEventListener("click", async () => {
|
||||
await loadActionHistory();
|
||||
notify(result.State === "SUCCEEDED" ? "Receiver принял уведомление." : "Исход отправки требует сверки.");
|
||||
} catch (error) {
|
||||
setText("notification-confirmation-error", safeClientError(error));
|
||||
setText("notification-confirmation-error", error.message);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Подтверждаю отправку";
|
||||
@@ -984,7 +730,7 @@ byId("confirm-satisfaction").addEventListener("click", async () => {
|
||||
notify(result.State === "SUCCEEDED" ? "Исполнение зафиксировано через Commit." : "Исход требует проверки.");
|
||||
await loadProspectiveRegister(command.ProspectiveItemID);
|
||||
} catch (error) {
|
||||
setText("confirmation-error", safeClientError(error));
|
||||
setText("confirmation-error", error.message);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Подтверждаю переход";
|
||||
@@ -1018,7 +764,7 @@ byId("reconcile-action").addEventListener("click", async () => {
|
||||
notify(result.State === "SUCCEEDED" ? "Исход подтверждён." : "Исход пока неизвестен.");
|
||||
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
|
||||
} catch (error) {
|
||||
notify(safeClientError(error));
|
||||
notify(error.message);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Проверить исход";
|
||||
|
||||
@@ -5,60 +5,101 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="theme-color" content="#101714">
|
||||
<title>Домовой - чат</title>
|
||||
<title>Домовой — твой помощник</title>
|
||||
<link rel="stylesheet" href="/assets/chat.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="chat-shell">
|
||||
<section id="login-view" class="login-panel">
|
||||
<div class="brand-mark" aria-hidden="true">Д</div>
|
||||
<h1>Домовой</h1>
|
||||
<form id="login-form" class="login-form">
|
||||
<label for="username">Логин</label>
|
||||
<input id="username" autocomplete="username" required autofocus>
|
||||
<label for="password">Пароль</label>
|
||||
<input id="password" type="password" autocomplete="current-password" required>
|
||||
<button type="submit">Войти</button>
|
||||
<p id="login-error" role="alert"></p>
|
||||
</form>
|
||||
<main class="assistant-shell">
|
||||
<section id="login-view" class="auth-view">
|
||||
<div class="auth-card" aria-live="polite">
|
||||
<div class="brand-block">
|
||||
<span class="brand-mark" aria-hidden="true">Д</span>
|
||||
<div>
|
||||
<p class="brand-kicker">личный диалог</p>
|
||||
<h1>Домовой</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p class="auth-copy">Войдите, чтобы открыть спокойный чат с вашим помощником.</p>
|
||||
<form id="login-form" class="login-form">
|
||||
<label for="username">Логин</label>
|
||||
<input id="username" autocomplete="username" required autofocus>
|
||||
<label for="password">Пароль</label>
|
||||
<div class="input-with-action">
|
||||
<input id="password" type="password" autocomplete="current-password" required>
|
||||
<button id="toggle-password" class="icon-button" type="button" aria-label="Показать пароль">показать</button>
|
||||
</div>
|
||||
<button id="login-button" type="submit">Открыть чат</button>
|
||||
<p id="login-error" role="alert"></p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="chat-view" class="chat-view" hidden>
|
||||
<header class="chat-header">
|
||||
<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="/admin" class="operator-link">Консоль</a>
|
||||
</nav>
|
||||
<span id="active-focus">фокус: не выбран</span>
|
||||
<span id="session-state">сессия активна</span>
|
||||
<button id="logout" type="button" aria-label="Выйти">↗</button>
|
||||
<header class="assistant-header">
|
||||
<div class="header-brand">
|
||||
<span class="brand-mark" aria-hidden="true">Д</span>
|
||||
<div>
|
||||
<p class="brand-kicker">твой спокойный ассистент</p>
|
||||
<h1>Домовой</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-state" aria-live="polite">
|
||||
<span id="connection-state" class="status-pill"></span>
|
||||
<button id="logout" class="ghost-button" type="button" aria-label="Выйти">Выйти</button>
|
||||
</div>
|
||||
</header>
|
||||
<section id="chat-panel" class="surface-panel active-panel">
|
||||
|
||||
<nav class="human-nav" aria-label="Разделы Домового" role="tablist">
|
||||
<button class="nav-tab active" data-tab="chat-panel" role="tab" aria-selected="true" aria-controls="chat-panel" id="tab-chat" type="button">Чат</button>
|
||||
<button class="nav-tab" data-tab="today-panel" role="tab" aria-selected="false" aria-controls="today-panel" id="tab-today" type="button">Сегодня</button>
|
||||
<button class="nav-tab" data-tab="history-panel" role="tab" aria-selected="false" aria-controls="history-panel" id="tab-history" type="button">История</button>
|
||||
</nav>
|
||||
|
||||
<section class="surface-panel active-panel" id="chat-panel" role="tabpanel" aria-labelledby="tab-chat">
|
||||
<div class="panel-toolbar">
|
||||
<div>
|
||||
<h2>Чат</h2>
|
||||
<p>Пишите обычные фразы. Я отвечу спокойно и по делу.</p>
|
||||
</div>
|
||||
<p id="active-focus" class="focus-chip" hidden></p>
|
||||
</div>
|
||||
<div id="messages" class="messages" aria-live="polite"></div>
|
||||
<form id="chat-form" class="composer">
|
||||
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
|
||||
<button id="send" type="submit">Отправить</button>
|
||||
<label class="visually-hidden" for="message">Ваше сообщение</label>
|
||||
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите, чем помочь" required></textarea>
|
||||
<div class="composer-actions">
|
||||
<button id="send" class="primary-button" type="submit">Отправить</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section id="today-panel" class="surface-panel attention-surface" hidden>
|
||||
<div class="surface-heading">
|
||||
<p>Сегодня / внимание</p>
|
||||
<button id="refresh-today" type="button">Обновить</button>
|
||||
|
||||
<section id="today-panel" class="surface-panel attention-surface" hidden role="tabpanel" aria-labelledby="tab-today">
|
||||
<div class="panel-toolbar">
|
||||
<div>
|
||||
<h2>Сегодня</h2>
|
||||
<p id="today-subtitle">Что сейчас требует твоего внимания</p>
|
||||
</div>
|
||||
<button id="refresh-today" class="ghost-button" type="button">Обновить</button>
|
||||
</div>
|
||||
<div id="today-list" class="attention-list"></div>
|
||||
</section>
|
||||
<section id="history-panel" class="surface-panel history-surface" hidden>
|
||||
<div class="surface-heading">
|
||||
<p>История</p>
|
||||
<span>Последние сообщения текущего окна</span>
|
||||
|
||||
<section id="history-panel" class="surface-panel history-surface" hidden role="tabpanel" aria-labelledby="tab-history">
|
||||
<div class="panel-toolbar">
|
||||
<div>
|
||||
<h2>История</h2>
|
||||
<p>Сообщения текущего браузерного сеанса</p>
|
||||
</div>
|
||||
<button id="clear-history" class="ghost-button" type="button">Очистить</button>
|
||||
</div>
|
||||
<div id="history-list" class="history-list"></div>
|
||||
</section>
|
||||
</section>
|
||||
<p id="status-bar" class="status-bar" aria-live="polite"></p>
|
||||
</main>
|
||||
|
||||
<div id="composer-error" class="composer-error" role="status" aria-live="polite"></div>
|
||||
|
||||
<script src="/assets/chat.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="theme-color" content="#101714">
|
||||
<title>Домовой — консоль создателя</title>
|
||||
<link rel="stylesheet" href="/assets/console.css?v=20260821-context">
|
||||
<link rel="stylesheet" href="/assets/console.css">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#overview">К содержимому</a>
|
||||
<div class="grain" aria-hidden="true"></div>
|
||||
|
||||
<main id="login-view" class="login-shell">
|
||||
@@ -45,13 +44,6 @@
|
||||
</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>
|
||||
@@ -64,46 +56,18 @@
|
||||
<p class="rail-copy">Укажите рабочий контекст. Он сохраняется только в этом браузере и не даёт дополнительных полномочий.</p>
|
||||
</div>
|
||||
<form id="context-form" class="context-form">
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
@@ -113,7 +77,7 @@
|
||||
</aside>
|
||||
|
||||
<main class="workspace">
|
||||
<section id="overview" class="workspace-head" data-page="overview">
|
||||
<section class="workspace-head">
|
||||
<div>
|
||||
<p class="eyebrow">операционная картина</p>
|
||||
<h1>Состояние сейчас</h1>
|
||||
@@ -121,28 +85,14 @@
|
||||
<p id="last-update" class="last-update">Данные ещё не запрошены</p>
|
||||
</section>
|
||||
|
||||
<section class="signal-strip" aria-label="Сводка состояния" data-page="overview">
|
||||
<section class="signal-strip" aria-label="Сводка состояния">
|
||||
<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" 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">
|
||||
<section class="advisor-panel" aria-labelledby="advisor-title">
|
||||
<div class="advisor-intro">
|
||||
<p>COGNITIVE / ADVISORY V1</p>
|
||||
<h2 id="advisor-title">Спросить Домового</h2>
|
||||
@@ -170,7 +120,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="operations" class="register-panel" aria-labelledby="register-title" data-page="operations">
|
||||
<section class="register-panel" aria-labelledby="register-title">
|
||||
<header>
|
||||
<div><p>Continuity / ближайшие</p><h2 id="register-title">Реестр ожиданий</h2></div>
|
||||
<span id="prospective-count">0 ЗАПИСЕЙ</span>
|
||||
@@ -180,7 +130,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="activity-panel" aria-labelledby="activity-title" data-page="operations">
|
||||
<section class="activity-panel" aria-labelledby="activity-title">
|
||||
<header>
|
||||
<div><p>Commit / владелец</p><h2 id="activity-title">Журнал действий</h2></div>
|
||||
<div class="activity-tools">
|
||||
@@ -193,7 +143,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="learning" class="learning-panel" aria-labelledby="learning-title" data-page="learning">
|
||||
<section class="learning-panel" aria-labelledby="learning-title">
|
||||
<header>
|
||||
<div><p>Learning / evidence</p><h2 id="learning-title">Опыт и reflection</h2></div>
|
||||
<div class="activity-tools">
|
||||
@@ -217,20 +167,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<section class="data-grid">
|
||||
<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>
|
||||
@@ -251,7 +188,6 @@
|
||||
<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">
|
||||
@@ -269,7 +205,6 @@
|
||||
<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>
|
||||
@@ -305,7 +240,6 @@
|
||||
<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>
|
||||
@@ -352,6 +286,6 @@
|
||||
</dialog>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
||||
<script src="/assets/console.js?v=20260821-context" defer></script>
|
||||
<script src="/assets/console.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000075-creator-chat-primary-surface.json",
|
||||
"task_id": "DEV-000075",
|
||||
"task_file": "dev/tasks/DEV-000072-semantic-interpretation-deferral-boundary.json",
|
||||
"task_id": "DEV-000072",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"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