diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 504c5e66e..0e7f8f613 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,29 +1,21 @@ { "version": 1, - "base_sha": "51d49474e461eaca83a75065ba370f3092d6ba34", - "verified_at": "2026-08-25T20:20:32Z", + "base_sha": "485376a5ecb3da171387dc72c08b20383b68ff9a", + "verified_at": "2026-08-25T20:32:21Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/GitHubStarBanner.tsx", - "frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx", - "frontend-modern/src/components/WhatsNewCard.tsx", - "frontend-modern/src/utils/localStorage.ts" + "frontend-modern/src/features/storageBackups/diskPresentation.ts" ], "content_sha256": { - "frontend-modern/src/components/GitHubStarBanner.tsx": "7c5b51cb927fa778bd6bb201793a20c084d4e3f6bfab39feb4573fe2759f8c85", - "frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx": "b0cb063a25f0c9a966522b740afa962163d87a50e241030ac289f1a5679063ad", - "frontend-modern/src/components/WhatsNewCard.tsx": "5e5d7314e46b8aa496f73a42e1abc37a294a3f3ebff3c3c424c33f8d615961d0", - "frontend-modern/src/utils/localStorage.ts": "452dbaa0e4014e0af635b24502ffbe53c738a449670ac6b4c381b3033c53ee0a" + "frontend-modern/src/features/storageBackups/diskPresentation.ts": "ff16016cbd5d1edbe77f8b4e02dd2f2634abfe7885376b83820c00ce640909e2" }, "routes": [ - "/prompt-verification.html", - "/proxmox/overview", - "/settings/system-updates" + "/proxmox/storage?tab=disks" ], "viewports": [ { - "width": 1440, - "height": 900 + "width": 1280, + "height": 800 }, { "width": 390, @@ -31,17 +23,13 @@ } ], "states": [ - "compact post-update notice over an active infrastructure page without a blocking backdrop", - "opt-in categorized release changelog dialog at desktop and phone widths", - "one-time GitHub star prompt at desktop and phone widths", - "dismissed release notice, changelog dialog, and star prompt states", - "System Updates current-version panel in the signed-in development runtime" + "loaded physical-disk table with critical, warning, and healthy health states", + "healthy status text using the emerald success tone while unknown evidence remains neutral" ], "interactions": [ - "opened the changelog from See what's new and closed it with both Escape and Got it", - "dismissed the compact update notice without opening the changelog", - "closed the GitHub star prompt and confirmed it left the page cleanly", - "navigated the signed-in System Updates page and confirmed development builds do not claim published release notes", - "restored the browser viewport and closed all verification tabs" + "loaded the Physical Disks tab from a phone-width viewport to avoid unrelated desktop background hydration", + "confirmed the first Healthy status resolved to the emerald class and rgb(110, 231, 183) in the active dark theme", + "resized the loaded table to desktop width and confirmed health rows remained readable without clipping", + "restored the browser viewport override" ] } diff --git a/frontend-modern/src/features/storageBackups/__tests__/diskPresentation.branchcov.test.ts b/frontend-modern/src/features/storageBackups/__tests__/diskPresentation.branchcov.test.ts index 6fd31f875..9d990ab61 100644 --- a/frontend-modern/src/features/storageBackups/__tests__/diskPresentation.branchcov.test.ts +++ b/frontend-modern/src/features/storageBackups/__tests__/diskPresentation.branchcov.test.ts @@ -556,7 +556,7 @@ describe('getPhysicalDiskHealthStatus branch coverage', () => { expect(getPhysicalDiskHealthStatus(disk)).toEqual({ label: 'Healthy', summary: 'No active disk-health issues.', - tone: 'text-base-content', + tone: 'text-emerald-700 dark:text-emerald-300', }); }); diff --git a/frontend-modern/src/features/storageBackups/diskPresentation.ts b/frontend-modern/src/features/storageBackups/diskPresentation.ts index 90395e1cc..9cf9707e5 100644 --- a/frontend-modern/src/features/storageBackups/diskPresentation.ts +++ b/frontend-modern/src/features/storageBackups/diskPresentation.ts @@ -646,12 +646,11 @@ export function getPhysicalDiskHealthStatus( }; } + const isHealthy = PHYSICAL_DISK_HEALTHY_STATES.has(normalizedHealth); return { - label: PHYSICAL_DISK_HEALTHY_STATES.has(normalizedHealth) ? 'Healthy' : 'Unknown', - summary: PHYSICAL_DISK_HEALTHY_STATES.has(normalizedHealth) - ? 'No active disk-health issues.' - : 'Health state is not reported.', - tone: 'text-base-content', + label: isHealthy ? 'Healthy' : 'Unknown', + summary: isHealthy ? 'No active disk-health issues.' : 'Health state is not reported.', + tone: isHealthy ? 'text-emerald-700 dark:text-emerald-300' : 'text-base-content', }; } diff --git a/internal/mock/generator.go b/internal/mock/generator.go index a127c16fd..9177057f0 100644 --- a/internal/mock/generator.go +++ b/internal/mock/generator.go @@ -6897,48 +6897,55 @@ func generateDisksForNode(node models.Node) []models.PhysicalDisk { diskCount := 1 + mockStableChoice(3, strings.TrimSpace(node.ID), strings.TrimSpace(node.Instance), strings.TrimSpace(node.Name), "physical-disk-count") diskModels := []struct { - model string - diskType string - size int64 + model string + diskType string + size int64 + reportsWearout bool }{ - {"Samsung SSD 970 EVO Plus 1TB", "nvme", 1000204886016}, - {"WD Blue SN570 500GB", "nvme", 500107862016}, - {"Crucial MX500 2TB", "sata", 2000398934016}, - {"Seagate BarraCuda 4TB", "sata", 4000787030016}, - {"Kingston NV2 250GB", "nvme", 250059350016}, - {"WD Red Pro 8TB", "sata", 8001563222016}, - {"Samsung 980 PRO 2TB", "nvme", 2000398934016}, - {"Intel SSD 660p 1TB", "nvme", 1000204886016}, - {"Toshiba X300 6TB", "sata", 6001175126016}, + {"Samsung SSD 970 EVO Plus 1TB", "nvme", 1000204886016, true}, + {"WD Blue SN570 500GB", "nvme", 500107862016, true}, + {"Crucial MX500 2TB", "sata", 2000398934016, true}, + {"Seagate BarraCuda 4TB", "sata", 4000787030016, false}, + {"Kingston NV2 250GB", "nvme", 250059350016, true}, + {"WD Red Pro 8TB", "sata", 8001563222016, false}, + {"Samsung 980 PRO 2TB", "nvme", 2000398934016, true}, + {"Intel SSD 660p 1TB", "nvme", 1000204886016, true}, + {"Toshiba X300 6TB", "sata", 6001175126016, false}, } for i := 0; i < diskCount; i++ { diskModel := diskModels[mockStableChoice(len(diskModels), strings.TrimSpace(node.ID), strings.TrimSpace(node.Instance), strings.TrimSpace(node.Name), fmt.Sprintf("%d", i), "physical-disk-model")] - // Generate health status - most are healthy + seedParts := []string{ + strings.TrimSpace(node.ID), + strings.TrimSpace(node.Instance), + strings.TrimSpace(node.Name), + fmt.Sprintf("%d", i), + } + + // Keep alert-producing health evidence stable across mock restarts. + // Most disks are healthy, while a small deterministic cohort exercises + // unknown and failed states. health := "PASSED" - if rand.Float64() < 0.05 { // 5% chance of failure + healthRoll := mockStableChoice(100, append(seedParts, "physical-disk-health")...) + if healthRoll < 5 { health = "FAILED" - } else if rand.Float64() < 0.1 { // 10% chance of unknown + } else if healthRoll < 15 { health = "UNKNOWN" } - // Generate wearout for SSDs (percentage life remaining; lower numbers mean heavy wear) - wearout := 0 - if diskModel.diskType == "nvme" || diskModel.diskType == "sata" { - if rand.Float64() < 0.7 { // 70% chance it's an SSD with wearout data - wearout = rand.Intn(50) + 50 // 50-100% life remaining - if rand.Float64() < 0.1 { // 10% chance of low life - wearout = rand.Intn(15) + 5 // 5-20% life remaining - } + // Wearout is percentage life remaining. -1 is the canonical unreported + // sentinel; 0 is real evidence that an SSD/NVMe has no life remaining + // and would correctly render as "Replace Now". Rotational disks never + // receive synthetic endurance data. + wearout := -1 + if diskModel.reportsWearout && + mockStableChoice(100, append(seedParts, "physical-disk-wearout-reported")...) < 70 { + wearout = 50 + mockStableChoice(51, append(seedParts, "physical-disk-wearout-normal")...) + if mockStableChoice(100, append(seedParts, "physical-disk-wearout-low-cohort")...) < 10 { + wearout = 5 + mockStableChoice(16, append(seedParts, "physical-disk-wearout-low")...) } } - if wearout < 0 { - wearout = 0 - } - if wearout > 100 { - wearout = 100 - } devPath := fmt.Sprintf("/dev/%s%d", []string{"nvme", "sd"}[i%2], i) diskID := fmt.Sprintf("%s-%s-%s", node.Instance, node.Name, devPath) diff --git a/internal/mock/platform_fixtures_test.go b/internal/mock/platform_fixtures_test.go index ee94417f0..1fb6c5e92 100644 --- a/internal/mock/platform_fixtures_test.go +++ b/internal/mock/platform_fixtures_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/rcourtman/pulse-go-rewrite/internal/models" "github.com/rcourtman/pulse-go-rewrite/internal/operationaltrust" "github.com/rcourtman/pulse-go-rewrite/internal/truenas" "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources" @@ -74,6 +75,80 @@ func TestUnifiedResourceSnapshotIncludesPlatformFixtures(t *testing.T) { } } +func TestGenerateDisksForNodeUsesStableRealisticHealthEvidence(t *testing.T) { + totals := map[string]int{} + reportedWearout := 0 + unreportedWearout := 0 + lowWearout := 0 + rotationalModels := map[string]struct{}{ + "Seagate BarraCuda 4TB": {}, + "WD Red Pro 8TB": {}, + "Toshiba X300 6TB": {}, + } + + for i := 0; i < 300; i++ { + node := models.Node{ + ID: fmt.Sprintf("disk-health-node-%d", i), + Name: fmt.Sprintf("pve-%d", i), + Instance: "mock-pve", + } + first := generateDisksForNode(node) + second := generateDisksForNode(node) + if len(first) != len(second) { + t.Fatalf("disk count changed for stable node %q: %d then %d", node.ID, len(first), len(second)) + } + + for diskIndex := range first { + disk := first[diskIndex] + repeated := second[diskIndex] + if disk.Health != repeated.Health || disk.Wearout != repeated.Wearout { + t.Fatalf( + "health evidence changed for %q disk %d: health=%q wearout=%d, then health=%q wearout=%d", + node.ID, + diskIndex, + disk.Health, + disk.Wearout, + repeated.Health, + repeated.Wearout, + ) + } + + totals[disk.Health]++ + if disk.Wearout == 0 { + t.Fatalf("mock disk %q encoded unreported endurance as spent 0%% life", disk.ID) + } + if _, rotational := rotationalModels[disk.Model]; rotational && disk.Wearout != -1 { + t.Fatalf("rotational mock disk %q reports SSD life %d%%", disk.Model, disk.Wearout) + } + if disk.Wearout < 0 { + unreportedWearout++ + } else { + reportedWearout++ + if disk.Wearout <= 9 { + lowWearout++ + } + } + } + } + + if totals["PASSED"] <= totals["FAILED"]+totals["UNKNOWN"] { + t.Fatalf("mock disk health is not predominantly healthy: %+v", totals) + } + for _, health := range []string{"PASSED", "UNKNOWN", "FAILED"} { + if totals[health] == 0 { + t.Fatalf("mock disk health does not exercise %q: %+v", health, totals) + } + } + if reportedWearout == 0 || unreportedWearout == 0 || lowWearout == 0 { + t.Fatalf( + "mock wearout mix missing a required state: reported=%d unreported=%d low=%d", + reportedWearout, + unreportedWearout, + lowWearout, + ) + } +} + func TestUnifiedResourceSnapshotIncludesRuntimeNativeTabFixtures(t *testing.T) { previous := IsMockEnabled() mustSetEnabled(t, true)