hub v0.154.0: the host page shows each storage place's physical disk(s) and whether the agent proved them (16 §3)
gates / gates (push) Successful in 6m43s
gates / gates (push) Successful in 6m43s
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
## v0.154.0 — the host page shows which physical disk each storage place sits on (2026-10-15)
|
||||
|
||||
Storage identity build A (`documentation/architecture/16-storage-physical-identity.md` §3; agent v0.157.0).
|
||||
|
||||
- The host report's `storage_targets` carry `kind`, `physical_disks`, `identity`, `identity_reason` (agent-computed). The hub's
|
||||
contract mirror and its copy of `host-report.golden.json` match the agent's byte for byte; the key-set test was seen failing
|
||||
without the mirror fields.
|
||||
- The host page's Storage Targets table gains two columns: the physical disk key(s), and „proven" / „unknown (reason)" · kind.
|
||||
An older agent shows „—". Shown only — no alarm logic (build A).
|
||||
|
||||
## v0.153.0 — no token sends, mints or reveals a household credential or link (R-1055) (2026-10-15)
|
||||
|
||||
Operator ruling 2026-10-15 13:20 (`09` §3 decision 252, R-1055 = A). `documentation/architecture/05-hub-architecture.md` §17.
|
||||
|
||||
@@ -847,7 +847,13 @@ type hostStorageTarget struct {
|
||||
BackingDevice string `json:"backing_device"`
|
||||
ClassHint string `json:"class_hint"`
|
||||
Role string `json:"role"`
|
||||
ThinPool *struct {
|
||||
// Physical-disk identity (agent v0.157.0, felhom.eu architecture/16-storage-physical-identity.md §3):
|
||||
// agent-computed; the hub stores it with the report and shows it on the host page. No alarm (build A).
|
||||
Kind string `json:"kind,omitempty"`
|
||||
PhysicalDisks []string `json:"physical_disks,omitempty"`
|
||||
Identity string `json:"identity,omitempty"`
|
||||
IdentityReason string `json:"identity_reason,omitempty"`
|
||||
ThinPool *struct {
|
||||
DataUsedFraction float64 `json:"data_used_fraction"`
|
||||
MetadataUsedFraction *float64 `json:"metadata_used_fraction"`
|
||||
} `json:"thin_pool,omitempty"`
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
"backing_device": "",
|
||||
"class_hint": "fast",
|
||||
"role": "",
|
||||
"kind": "system",
|
||||
"physical_disks": ["serial:ata:SanDisk_X600:182195804614"],
|
||||
"identity": "proven",
|
||||
"thin_pool": { "data_used_fraction": 0.42, "metadata_used_fraction": null },
|
||||
"smart": {
|
||||
"health": "UNKNOWN",
|
||||
@@ -74,6 +77,10 @@
|
||||
"backing_device": "/dev/sdb1",
|
||||
"class_hint": "slow",
|
||||
"role": "",
|
||||
"kind": "disk",
|
||||
"physical_disks": ["serial:usb:External:000000000000"],
|
||||
"identity": "unknown",
|
||||
"identity_reason": "usb_bridge",
|
||||
"smart": {
|
||||
"health": "UNKNOWN",
|
||||
"temperature_c": null,
|
||||
|
||||
@@ -480,6 +480,12 @@ type storageTargetView struct {
|
||||
SmartHealth string
|
||||
TempC *int
|
||||
WearPct *int // NVMe percentage_used
|
||||
// Physical-disk identity (agent v0.157.0, `16` §3): the disk keys under the place and whether the agent
|
||||
// can prove them. Empty from an older agent.
|
||||
Kind string
|
||||
PhysicalDisks []string
|
||||
Identity string
|
||||
IdentityReason string
|
||||
}
|
||||
|
||||
// parseHostStorageTargets extracts the rich storage-target rows from a report body. A
|
||||
@@ -491,14 +497,18 @@ func parseHostStorageTargets(reportJSON string) []storageTargetView {
|
||||
}
|
||||
var body struct {
|
||||
StorageTargets []struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
State string `json:"state"`
|
||||
MountPath string `json:"mount_path"`
|
||||
Reachable bool `json:"reachable"`
|
||||
UsedFraction float64 `json:"used_fraction"`
|
||||
ThinPool *struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
State string `json:"state"`
|
||||
MountPath string `json:"mount_path"`
|
||||
Reachable bool `json:"reachable"`
|
||||
UsedFraction float64 `json:"used_fraction"`
|
||||
Kind string `json:"kind"`
|
||||
PhysicalDisks []string `json:"physical_disks"`
|
||||
Identity string `json:"identity"`
|
||||
IdentityReason string `json:"identity_reason"`
|
||||
ThinPool *struct {
|
||||
DataUsedFraction float64 `json:"data_used_fraction"`
|
||||
} `json:"thin_pool"`
|
||||
Smart struct {
|
||||
@@ -517,6 +527,7 @@ func parseHostStorageTargets(reportJSON string) []storageTargetView {
|
||||
MountPath: t.MountPath, Reachable: t.Reachable,
|
||||
FillPct: t.UsedFraction * 100,
|
||||
SmartHealth: t.Smart.Health, TempC: t.Smart.TemperatureC, WearPct: t.Smart.PercentageUsed,
|
||||
Kind: t.Kind, PhysicalDisks: t.PhysicalDisks, Identity: t.Identity, IdentityReason: t.IdentityReason,
|
||||
}
|
||||
if t.ThinPool != nil {
|
||||
v.HasThin = true
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
)
|
||||
|
||||
// felhom.eu architecture/16-storage-physical-identity.md §3 (agent v0.157.0): the host page shows, per
|
||||
// storage place, the physical disk keys and whether the agent proved them. Shown only — no alarm (build A).
|
||||
func TestHostDetail_ShowsPhysicalDiskIdentity(t *testing.T) {
|
||||
s, st := newTestServer(t)
|
||||
if err := st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "k"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
report := `{"host_id":"h1","agent_version":"0.157.0","storage_targets":[` +
|
||||
`{"name":"local-lvm","type":"lvmthin","state":"attached","kind":"system","physical_disks":["serial:ata:SanDisk_X600:182195804614"],"identity":"proven","smart":{"health":"UNKNOWN"}},` +
|
||||
`{"name":"felhom-usb","type":"usb","state":"attached","kind":"disk","physical_disks":["serial:scsi:QEMU_HARDDISK:drive-scsi1"],"identity":"unknown","identity_reason":"vm","smart":{"health":"UNKNOWN"}},` +
|
||||
`{"name":"old-agent","type":"local","state":"attached","smart":{"health":"UNKNOWN"}}]}`
|
||||
if err := st.SaveHostReport("h1", "c1", []byte(report), store.HostReportDenorm{AgentVersion: "0.157.0"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
s.handleHostDetail(rr, httptest.NewRequest(http.MethodGet, "/hosts/h1", nil), "h1")
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d", rr.Code)
|
||||
}
|
||||
body := rr.Body.String()
|
||||
for _, want := range []string{"Physical disk", "serial:ata:SanDisk_X600:182195804614", ">proven<", "unknown (vm)", "· system", "· disk"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("host page missing %q", want)
|
||||
}
|
||||
}
|
||||
v := parseHostStorageTargets(report)
|
||||
if len(v) != 3 || v[0].Identity != "proven" || v[1].IdentityReason != "vm" || v[2].Identity != "" {
|
||||
t.Errorf("parsed identity rows wrong: %+v", v)
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,8 @@
|
||||
<th>SMART</th>
|
||||
<th>Temp</th>
|
||||
<th>Wear</th>
|
||||
<th>Physical disk</th>
|
||||
<th>Identity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -176,6 +178,8 @@
|
||||
<td>{{if .SmartHealth}}{{if eq .SmartHealth "PASSED"}}<span style="color: var(--green)">{{.SmartHealth}}</span>{{else if eq .SmartHealth "FAILED"}}<span style="color: var(--red)">{{.SmartHealth}}</span>{{else}}{{.SmartHealth}}{{end}}{{else}}—{{end}}</td>
|
||||
<td>{{if .TempC}}{{.TempC}}°C{{else}}—{{end}}</td>
|
||||
<td>{{if .WearPct}}{{.WearPct}}%{{else}}—{{end}}</td>
|
||||
<td class="mono" style="font-size:.8rem">{{if .PhysicalDisks}}{{range $i, $d := .PhysicalDisks}}{{if $i}}<br>{{end}}{{$d}}{{end}}{{else}}—{{end}}</td>
|
||||
<td>{{if eq .Identity "proven"}}<span style="color: var(--green)">proven</span>{{else if .Identity}}unknown{{if .IdentityReason}} ({{.IdentityReason}}){{end}}{{else}}—{{end}}{{if .Kind}} · {{.Kind}}{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user