diff --git a/cmd/creator-web/chat_test.go b/cmd/creator-web/chat_test.go index 0216595..85c9e85 100644 --- a/cmd/creator-web/chat_test.go +++ b/cmd/creator-web/chat_test.go @@ -25,7 +25,7 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) { t.Fatalf("expected /chat status 200, got %d", recorder.Code) } body := recorder.Body.String() - for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли", "версия 1.0.0", "обновлено 21.08.2026", "datetime=\"2026-08-21\""} { + for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли", "build-meta", "версия ", "обновлено ", "datetime=\""} { if !strings.Contains(body, required) { t.Fatalf("/chat is missing human status marker %q", required) } diff --git a/cmd/creator-web/server.go b/cmd/creator-web/server.go index e064bce..2584303 100644 --- a/cmd/creator-web/server.go +++ b/cmd/creator-web/server.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "embed" "encoding/json" "fmt" @@ -8,7 +9,9 @@ import ( "io/fs" "net/http" "net/url" + "os" "strings" + "time" "unicode/utf8" "git.cin.su/m/Assistant/contracts/capability" @@ -36,6 +39,12 @@ type gateway struct { cognitive *url.URL client *http.Client static http.Handler + build buildMetadata +} + +type buildMetadata struct { + revision string + updated time.Time } type routeTarget uint8 @@ -74,7 +83,20 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client, c if client == nil { client = http.DefaultClient } - return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets))}), nil + updated := time.Now().UTC() + if raw := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_TIME")); raw != "" { + if parsed, parseErr := time.Parse(time.RFC3339, raw); parseErr == nil { + updated = parsed.UTC() + } + } + revision := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_REVISION")) + if revision == "" { + revision = "development" + } + if len(revision) > 12 { + revision = revision[:12] + } + return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets)), build: buildMetadata{revision: revision, updated: updated}}), nil } func parseRuntimeURL(rawURL, name string) (*url.URL, error) { @@ -112,11 +134,11 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } if r.URL.Path == "/" { - serveEmbeddedHTML(w, "chat.html") + g.serveEmbeddedHTML(w, "chat.html") return } if r.URL.Path == "/admin" { - serveEmbeddedHTML(w, "index.html") + g.serveEmbeddedHTML(w, "index.html") return } if r.URL.Path == "/chat" { @@ -127,7 +149,7 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { g.static.ServeHTTP(w, r) } -func serveEmbeddedHTML(w http.ResponseWriter, name string) { +func (g *gateway) serveEmbeddedHTML(w http.ResponseWriter, name string) { body, err := staticFiles.ReadFile("static/" + name) if err != nil { http.Error(w, "page unavailable", http.StatusNotFound) @@ -135,6 +157,9 @@ func serveEmbeddedHTML(w http.ResponseWriter, name string) { } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "text/html; charset=utf-8") + body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_REVISION__"), []byte(g.build.revision)) + body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_TIME__"), []byte(g.build.updated.Format("02.01.2006 15:04 UTC"))) + body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_DATETIME__"), []byte(g.build.updated.Format(time.RFC3339))) _, _ = w.Write(body) } diff --git a/cmd/creator-web/static/chat.html b/cmd/creator-web/static/chat.html index 7f380fa..d1b6c5d 100644 --- a/cmd/creator-web/static/chat.html +++ b/cmd/creator-web/static/chat.html @@ -29,8 +29,8 @@
- версия 1.0.0 - + версия __ASSISTANT_BUILD_REVISION__ +
Консоль diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index cbf70c8..493795d 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -172,6 +172,7 @@ services: CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081 COMMIT_RUNTIME_URL: http://commit-runtime:8082 COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083 + ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown} ports: ["${CREATOR_WEB_PORT:-18080}:8084"] depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker] healthcheck: diff --git a/dev/tasks/DEV-000088-runtime-build-status.json b/dev/tasks/DEV-000088-runtime-build-status.json new file mode 100644 index 0000000..12fd0a9 --- /dev/null +++ b/dev/tasks/DEV-000088-runtime-build-status.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000088", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877", + "risk_tier": "B", + "intent": "Expose self-describing build revision and runtime update time in the Domovoy chat without requiring a human status query.", + "requirement_ids": ["Human can identify the running build", "Displayed update time reflects the running container", "No internal identifiers are exposed in the chat UI"], + "allowed_change_scope": {"modules": ["creator-web", "deployment"], "paths": ["cmd/creator-web/server.go", "cmd/creator-web/chat_test.go", "cmd/creator-web/static/chat.html", "deploy/docker-compose.prod.yml", "dev/tasks/DEV-000088-runtime-build-status.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**"]}, + "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 source revision build argument"], "may_change": []}, + "semantic_owner": "creator-web", + "canonical_state": [], + "state_transitions": ["Container startup -> build metadata visible in chat"], + "side_effects_and_privileges": ["No new privilege", "No user or provider secrets displayed"], + "persistence_and_migrations": ["No persistence or migration"], + "failure_retry_restart": ["Missing revision is displayed as development/unknown; startup time remains observable"], + "assumptions": ["The production compose runner supplies ASSISTANT_SOURCE_REVISION for the running revision"], + "acceptance_scenarios": ["Chat shows revision and update time without a manual question", "HTML contains no internal provider or session identifiers", "Tests remain deterministic outside production"], + "required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "git diff --check", "production page verification"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +}