feat: expose runtime build status in chat
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 15s

This commit is contained in:
m
2026-08-22 01:36:48 +03:00
parent 4f06720e3f
commit 2cf1fb5c5e
5 changed files with 56 additions and 7 deletions
+1 -1
View File
@@ -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)
}
+29 -4
View File
@@ -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)
}
+2 -2
View File
@@ -29,8 +29,8 @@
<button id="cognitive-status" class="cognitive-status" data-state="checking" type="button" role="status" aria-live="polite" title="Проверить канал ещё раз">проверяю канал мышления</button>
<a id="cognitive-console-link" class="cognitive-console-link" href="/admin" hidden>настроить в консоли</a>
<div class="build-meta" aria-label="Версия и время обновления">
<span>версия 1.0.0</span>
<time datetime="2026-08-21">обновлено 21.08.2026</time>
<span>версия __ASSISTANT_BUILD_REVISION__</span>
<time datetime="__ASSISTANT_BUILD_DATETIME__">обновлено __ASSISTANT_BUILD_TIME__</time>
</div>
<a href="/admin" class="operator-link">Консоль</a>
<button id="logout" type="button" aria-label="Выйти">↗</button>
+1
View File
@@ -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:
@@ -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"]
}