hub: shared host_detail_body sub-template + customer Host tab (v0.47.0 part 3)

- host_detail_body.html: {{define}}'d body sections extracted from
  host_detail.html; the standalone page is now chrome + the sub-template
- hosts.go: hostDetailData(host, r) view-model builder extracted from
  handleHostDetail (reused by both surfaces)
- store: ListHostsByCustomer (host_id order; the Host tab is a list by
  design - N hosts for a future HA cluster)
- customer Host tab renders one host_detail_body per host + cross-link;
  empty state when no host is enrolled
- tests: TestTemplates_CustomerHostTab(+_Empty), TestListHostsByCustomer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vvz1NCu22p8dGkRCpeX9re
This commit is contained in:
2026-07-11 21:17:44 +02:00
parent 9f29bf34c0
commit ae950e5933
8 changed files with 400 additions and 242 deletions
+27
View File
@@ -86,6 +86,33 @@ func TestGetHostByCustomer(t *testing.T) {
}
}
// v0.47.0 Host tab: ListHostsByCustomer returns ONLY the customer's hosts, ordered by
// host_id, and leaves other customers' hosts out (isolation).
func TestListHostsByCustomer(t *testing.T) {
s := newTestStore(t)
for _, h := range []Host{
{HostID: "c1-bbb", CustomerID: "c1", APIKey: "k2"},
{HostID: "c1-aaa", CustomerID: "c1", APIKey: "k1"},
{HostID: "c2-zzz", CustomerID: "c2", APIKey: "k3"},
} {
h := h
if err := s.UpsertHost(&h); err != nil {
t.Fatal(err)
}
}
got, err := s.ListHostsByCustomer("c1")
if err != nil {
t.Fatalf("ListHostsByCustomer: %v", err)
}
if len(got) != 2 || got[0].HostID != "c1-aaa" || got[1].HostID != "c1-bbb" {
t.Fatalf("c1 hosts = %+v (want [c1-aaa c1-bbb])", got)
}
none, err := s.ListHostsByCustomer("c3")
if err != nil || len(none) != 0 {
t.Fatalf("c3 hosts = %+v / %v (want empty)", none, err)
}
}
func TestSaveHostReport_BumpsRealityPreservesIntent(t *testing.T) {
s := newTestStore(t)
if err := s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"}); err != nil {