feat: expose runtime build status in chat
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user