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:
+24
-17
@@ -276,22 +276,13 @@ func (s *Server) handleHostsList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleHostDetail renders the read-only per-host detail page (audit F-M1). GET only.
|
||||
func (s *Server) handleHostDetail(w http.ResponseWriter, r *http.Request, hostID string) {
|
||||
host, err := s.store.GetHost(hostID)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] Host detail %s: %v", hostID, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if host == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// hostDetailData assembles the view-model map the shared host_detail_body sub-template
|
||||
// renders — used by BOTH the standalone /hosts/{id} page and the customer page's Host tab
|
||||
// (v0.47.0). Booleans/counts only for DR/escrow; never api_key or blob contents.
|
||||
func (s *Server) hostDetailData(host *store.Host, r *http.Request) map[string]interface{} {
|
||||
status := s.hostStatus(host.LastReportAt)
|
||||
|
||||
guests, _ := s.store.ListGuestsForHost(hostID)
|
||||
guests, _ := s.store.ListGuestsForHost(host.HostID)
|
||||
guestRunning := 0
|
||||
for _, g := range guests {
|
||||
if g.Status == "running" {
|
||||
@@ -305,10 +296,10 @@ func (s *Server) handleHostDetail(w http.ResponseWriter, r *http.Request, hostID
|
||||
sort.Slice(storageTargets, func(i, j int) bool { return storageTargets[i].Name < storageTargets[j].Name })
|
||||
|
||||
// DR / backup presence — booleans only, never the opaque blobs.
|
||||
drBundle, _ := s.store.GetHostDRBundle(hostID)
|
||||
escrow, _ := s.store.GetHostEscrow(hostID)
|
||||
drBundle, _ := s.store.GetHostDRBundle(host.HostID)
|
||||
escrow, _ := s.store.GetHostEscrow(host.HostID)
|
||||
|
||||
data := map[string]interface{}{
|
||||
return map[string]interface{}{
|
||||
"HostID": host.HostID,
|
||||
"CustomerID": host.CustomerID,
|
||||
"CustomerName": s.customerName(host.CustomerID),
|
||||
@@ -333,6 +324,22 @@ func (s *Server) handleHostDetail(w http.ResponseWriter, r *http.Request, hostID
|
||||
"LogBundles": s.hostLogBundleRows(host),
|
||||
"CSRFToken": s.getCSRFToken(r),
|
||||
}
|
||||
}
|
||||
|
||||
// handleHostDetail renders the read-only per-host detail page (audit F-M1). GET only.
|
||||
func (s *Server) handleHostDetail(w http.ResponseWriter, r *http.Request, hostID string) {
|
||||
host, err := s.store.GetHost(hostID)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] Host detail %s: %v", hostID, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if host == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
data := s.hostDetailData(host, r)
|
||||
if err := s.templates.ExecuteTemplate(w, "host_detail.html", data); err != nil {
|
||||
s.logger.Printf("[ERROR] host_detail.html template: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user