Clarify config identity in preview UI

This commit is contained in:
tian 2026-04-19 14:14:56 +08:00
parent 03ccac2230
commit 468db800cd
3 changed files with 72 additions and 0 deletions

View File

@ -121,6 +121,13 @@ func NewUI(discovery *service.DiscoveryService, registry *service.RegistryServic
}
return false
},
"shortHash": func(v string) string {
v = strings.TrimSpace(v)
if len(v) > 8 {
return v[:8]
}
return v
},
"ago": func(ms int64) string {
if ms <= 0 {
return "-"

View File

@ -13,6 +13,9 @@
<div class="card">
<h2>生成配置预览</h2>
{{if .ConfigSources.Root}}<div class="muted small">Media 仓库:<span class="mono">{{.ConfigSources.Root}}</span></div>{{end}}
<div class="muted small" style="margin-top:8px">
<span class="mono">config_id</span> 是配置名,默认会带上 <span class="mono">device_id</span>(当前设备:<span class="mono">{{.Device.DeviceID}}</span><span class="mono">config_version</span> 表示本次生成版本;<span class="mono">SHA256</span> 是最终文件内容指纹。
</div>
<form method="post" action="/ui/devices/{{.Device.DeviceID}}/config-preview" style="margin-top:12px">
<div class="row">
<div>
@ -74,6 +77,10 @@
<div><div class="muted small">Profile</div><div class="mono">{{index .ConfigPreview.Metadata "profile"}}</div></div>
<div><div class="muted small">大小</div><div class="mono">{{.ConfigPreview.Size}} bytes</div></div>
</div>
<div style="margin-top:10px">
<div class="muted small">Overlay</div>
<div class="mono">{{if index .ConfigPreview.Metadata "overlays"}}{{range $i, $name := index .ConfigPreview.Metadata "overlays"}}{{if $i}}, {{end}}{{$name}}{{end}}{{else}}-{{end}}</div>
</div>
<div style="margin-top:10px">
<div class="muted small">SHA256</div>
<div class="mono small">{{.ConfigPreview.Sha256}}</div>
@ -109,10 +116,14 @@
<div>
<div class="muted small">当前运行</div>
<div class="mono">{{if .ConfigStatus.Metadata.ConfigID}}{{.ConfigStatus.Metadata.ConfigID}} / {{if .ConfigStatus.Metadata.ConfigVersion}}{{.ConfigStatus.Metadata.ConfigVersion}}{{else}}未标记{{end}}{{else}}未标记{{end}}</div>
<div class="muted small mono" style="margin-top:6px">{{if .ConfigStatus.Metadata.Overlays}}{{range $i, $name := .ConfigStatus.Metadata.Overlays}}{{if $i}}, {{end}}{{$name}}{{end}}{{else}}-{{end}}</div>
<div class="muted small mono" style="margin-top:6px">sha: {{shortHash .ConfigStatus.Sha256}}</div>
</div>
<div>
<div class="muted small">last_good</div>
<div class="mono">{{if and .ConfigStatus.LastGood .ConfigStatus.LastGood.Exists .ConfigStatus.LastGood.Metadata.ConfigID}}{{.ConfigStatus.LastGood.Metadata.ConfigID}} / {{if .ConfigStatus.LastGood.Metadata.ConfigVersion}}{{.ConfigStatus.LastGood.Metadata.ConfigVersion}}{{else}}未标记{{end}}{{else}}-{{end}}</div>
<div class="muted small mono" style="margin-top:6px">{{if and .ConfigStatus.LastGood .ConfigStatus.LastGood.Metadata.Overlays}}{{range $i, $name := .ConfigStatus.LastGood.Metadata.Overlays}}{{if $i}}, {{end}}{{$name}}{{end}}{{else}}-{{end}}</div>
<div class="muted small mono" style="margin-top:6px">sha: {{if .ConfigStatus.LastGood}}{{shortHash .ConfigStatus.LastGood.Sha256}}{{end}}</div>
</div>
<div>
<div class="muted small">candidate</div>
@ -123,6 +134,9 @@
<div>{{if .ConfigStatus.MediaServer.Running}}<span class="pill ok">运行中</span>{{else}}<span class="pill bad">未运行</span>{{end}}</div>
</div>
</div>
{{if and .ConfigStatus.LastGood .ConfigStatus.Sha256 .ConfigStatus.LastGood.Sha256 (eq .ConfigStatus.Metadata.ConfigID .ConfigStatus.LastGood.Metadata.ConfigID) (eq .ConfigStatus.Metadata.ConfigVersion .ConfigStatus.LastGood.Metadata.ConfigVersion) (ne .ConfigStatus.Sha256 .ConfigStatus.LastGood.Sha256)}}
<div class="muted small" style="margin-top:10px">当前运行与 last_good 的 <span class="mono">config_id/config_version</span> 相同,但文件内容不同,请以 <span class="mono">overlay</span><span class="mono">sha</span> 为准。</div>
{{end}}
</div>
{{end}}

View File

@ -522,7 +522,9 @@ func TestUI_ConfigPreviewShowsApplySummaryAfterApplyResult(t *testing.T) {
Metadata: map[string]any{
"config_id": "preview_edge-01",
"config_version": "v2",
"overlays": []any{"face_test_sensitive", "production_quiet"},
},
Sha256: "eecdf8d422705f3affa0f892199604f037f60ea8fe578fe2a65527e1800044c5",
Size: 64,
},
ConfigStatus: &ConfigStatusView{
@ -530,6 +532,7 @@ func TestUI_ConfigPreviewShowsApplySummaryAfterApplyResult(t *testing.T) {
Metadata: ConfigStatusMetadata{
ConfigID: "preview_edge-01",
ConfigVersion: "v2",
Overlays: []string{"face_test_sensitive", "production_quiet"},
},
Candidate: &ConfigStatusLastGoodFile{
Exists: false,
@ -538,9 +541,11 @@ func TestUI_ConfigPreviewShowsApplySummaryAfterApplyResult(t *testing.T) {
LastGood: &ConfigStatusLastGoodFile{
Exists: true,
Path: "/opt/rk3588-media-server/etc/media-server.json.last_good.json",
Sha256: "07a37fabd73e98c1c131d31ddfa7b6c0e0949be854225bf2a6e990a09e60ddd3",
Metadata: ConfigStatusMetadata{
ConfigID: "local_3588_face_debug",
ConfigVersion: "20260419.120246",
Overlays: []string{"face_debug"},
},
},
MediaServer: ConfigStatusMediaServer{
@ -559,6 +564,10 @@ func TestUI_ConfigPreviewShowsApplySummaryAfterApplyResult(t *testing.T) {
"preview_edge-01 / v2",
"last_good",
"local_3588_face_debug / 20260419.120246",
"face_test_sensitive, production_quiet",
"face_debug",
"eecdf8d4",
"07a37fab",
"candidate",
"已清空",
"media-server",
@ -570,6 +579,48 @@ func TestUI_ConfigPreviewShowsApplySummaryAfterApplyResult(t *testing.T) {
}
}
func TestUI_ConfigPreviewExplainsConfigIdentityFields(t *testing.T) {
ui := newTestUI(t)
req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil)
rr := httptest.NewRecorder()
ui.render(rr, req, "config_preview", PageData{
Title: "配置预览",
Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100},
SelectedTemplate: "workshop_face_shoe_alarm",
SelectedProfile: "local_3588_test",
SelectedOverlays: []string{"face_debug"},
SelectedConfigID: "preview_edge-01",
ConfigPreview: &service.ConfigPreviewResult{
JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v2","overlays":["face_debug"]}}`,
Metadata: map[string]any{
"config_id": "preview_edge-01",
"config_version": "v2",
"template": "workshop_face_shoe_alarm",
"profile": "local_3588_test",
"overlays": []any{"face_debug"},
},
Sha256: "eecdf8d422705f3affa0f892199604f037f60ea8fe578fe2a65527e1800044c5",
Size: 64,
},
})
body := rr.Body.String()
for _, want := range []string{
"config_id</span> 是配置名,默认会带上 <span class=\"mono\">device_id",
"config_version</span> 表示本次生成版本",
"device_id",
"edge-01",
"SHA256</span> 是最终文件内容指纹",
"Overlay",
"face_debug",
} {
if !strings.Contains(body, want) {
t.Fatalf("expected preview explanation HTML to contain %q, got:\n%s", want, body)
}
}
}
func TestUI_ActionDeviceConfigCandidateApplyReloadsStatusAfterApply(t *testing.T) {
statusCalls := 0
agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {