Add storage block geometry to audit and viewer
This commit is contained in:
@@ -572,6 +572,7 @@ func (h *handler) handleExportIndex(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *handler) handleViewer(w http.ResponseWriter, r *http.Request) {
|
||||
snapshot, _ := loadSnapshot(h.opts.AuditPath)
|
||||
snapshot = enrichSnapshotForViewer(snapshot)
|
||||
body, err := viewer.RenderHTML(snapshot, h.opts.Title)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -1016,6 +1016,39 @@ func TestViewerRendersLatestSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewerRendersDerivedStorageBlockFormat(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "audit.json")
|
||||
body := `{
|
||||
"collected_at":"2026-04-29T00:05:00Z",
|
||||
"hardware":{
|
||||
"board":{"serial_number":"SERIAL-NEW"},
|
||||
"storage":[
|
||||
{
|
||||
"serial_number":"DISK-1",
|
||||
"model":"Test NVMe",
|
||||
"logical_block_size_bytes":512,
|
||||
"physical_block_size_bytes":4096,
|
||||
"metadata_bytes_per_block":8
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(path, []byte(body), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler := NewHandler(HandlerOptions{AuditPath: path})
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/viewer", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d", rec.Code)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "512+8") {
|
||||
t.Fatalf("viewer body missing derived block format: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditJSONServesLatestSnapshot(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "audit.json")
|
||||
@@ -1038,6 +1071,36 @@ func TestAuditJSONServesLatestSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditJSONDoesNotInjectDerivedStorageBlockFormat(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "audit.json")
|
||||
body := `{
|
||||
"hardware":{
|
||||
"board":{"serial_number":"SERIAL-API"},
|
||||
"storage":[
|
||||
{
|
||||
"serial_number":"DISK-1",
|
||||
"logical_block_size_bytes":512,
|
||||
"metadata_bytes_per_block":8
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(path, []byte(body), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler := NewHandler(HandlerOptions{AuditPath: path})
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/audit.json", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d", rec.Code)
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "block_format") {
|
||||
t.Fatalf("audit.json should remain contract-only: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingAuditJSONReturnsNotFound(t *testing.T) {
|
||||
handler := NewHandler(HandlerOptions{AuditPath: "/missing/audit.json"})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
62
audit/internal/webui/viewer_snapshot.go
Normal file
62
audit/internal/webui/viewer_snapshot.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func enrichSnapshotForViewer(snapshot []byte) []byte {
|
||||
if len(snapshot) == 0 {
|
||||
return snapshot
|
||||
}
|
||||
var root map[string]any
|
||||
if err := json.Unmarshal(snapshot, &root); err != nil {
|
||||
return snapshot
|
||||
}
|
||||
hardware, _ := root["hardware"].(map[string]any)
|
||||
if len(hardware) == 0 {
|
||||
return snapshot
|
||||
}
|
||||
storage, _ := hardware["storage"].([]any)
|
||||
if len(storage) == 0 {
|
||||
return snapshot
|
||||
}
|
||||
changed := false
|
||||
for _, item := range storage {
|
||||
row, _ := item.(map[string]any)
|
||||
if len(row) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := row["block_format"]; exists {
|
||||
continue
|
||||
}
|
||||
logical, okLogical := jsonNumberToInt64(row["logical_block_size_bytes"])
|
||||
metadata, okMetadata := jsonNumberToInt64(row["metadata_bytes_per_block"])
|
||||
if !okLogical || !okMetadata || logical <= 0 || metadata < 0 {
|
||||
continue
|
||||
}
|
||||
row["block_format"] = strconv.FormatInt(logical, 10) + "+" + strconv.FormatInt(metadata, 10)
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
return snapshot
|
||||
}
|
||||
out, err := json.Marshal(root)
|
||||
if err != nil {
|
||||
return snapshot
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func jsonNumberToInt64(v any) (int64, bool) {
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
return int64(x), true
|
||||
case int64:
|
||||
return x, true
|
||||
case int:
|
||||
return int64(x), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user