From 1c9374fb0caa39fd3921269c512909707f127b64 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:00:33 +0100 Subject: [PATCH] Explain Unraid empty slots without hiding missing members Distinguish configured placeholders from assigned failures, explain online parity counts, and preserve existing evidence without array changes or another retest. Contract-Neutral: Documentation-only guidance for existing Unraid collection and alert semantics; no runtime, wire, access or trust contract changes. Change-source: pulse-maintainer --- docs/TROUBLESHOOTING.md | 40 ++++++++++ .../public/docs/TROUBLESHOOTING.md | 40 ++++++++++ .../tests/test_unraid_troubleshooting_docs.py | 79 +++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 scripts/tests/test_unraid_troubleshooting_docs.py diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index f6350d4fb3..4f2a547564 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -51,6 +51,10 @@ establish safe recovery. [required members, active members and spares](TROUBLESHOOTING.md#raid-array-is-degraded-with-a-spare). A spare-inclusive total is not the required member count; do not change the array just to clear a Pulse alert. +- **Unraid reports missing disks in empty slots:** distinguish + [unassigned slots, missing members and parity counts](TROUBLESHOOTING.md#unraid-reports-missing-disks-in-empty-slots). + Unraid parity is not Linux MD RAID; do not rebuild or change disk assignments + just to make Pulse's count agree. - **Pulse itself uses more memory, CPU or disk writes:** use [memory baseline and growth checks](TROUBLESHOOTING.md#memory-use-keeps-growing) or [CPU and write-rate checks](TROUBLESHOOTING.md#excessive-cpu-writes-or-database-growth). @@ -415,6 +419,42 @@ restore, not by overwriting today's history. Follow ### Monitoring Data +#### Unraid reports missing disks in empty slots + +Compare the same host's **existing** Unraid status with Pulse's disk inventory, +storage-topology alert and observation time. Keep the Pulse server and Unraid +Unified Agent versions separate: updating the server does not establish that +the agent updated. No new command, Diagnostics run or array operation is needed +to report a disagreement. + +| Existing observation | Meaning | +| --- | --- | +| `DISK_NP`, no device or size, and only `fsType=auto` or a transport-only identity such as `ata-_` | A configured-but-unassigned slot, not evidence of a missing assigned disk. A slot name such as `disk6` or `parity2` alone is not assignment evidence. | +| `DISK_NP_MISSING`, even without a device or identity | Unraid explicitly reports an assigned-but-absent member. Do not discard it as an empty slot. | +| A missing/disabled slot with a real device, model/serial, filesystem or size | Assignment evidence remains; do not dismiss the failure because other slots are empty. `auto` alone is not a real filesystem. | +| Single online parity disk and five data disks | Pulse's `numProtected` counts online parity devices: **1**, not five protected data disks. That count alone does not prove complete protection or healthy disks. | +| No matching native observation, an old reading or an unknown version | The comparison is unavailable, not a healthy result. `STARTED` alone does not establish array health. | + +For the healthy partially populated topology in [#1788](https://github.com/rcourtman/Pulse/issues/1788), +seventeen empty data slots wrongly contributed to `numMissing`. The final +correction treats `fsType=auto` as a placeholder at agent collection, server +ingestion and storage-health assessment. **Pulse v6.5.0 contains this correction +in the server and Unified Agent**; an earlier preview retest failed before +this final correction. This does not establish that a current alert has the +same cause or that the reporter's installation has recovered. + +If existing native status shows no missing assigned members but Pulse disagrees, +preserve the mismatch rather than suppressing the check or changing the array. +Do not unassign a disk, rebuild, start a parity check, restart or re-enrol the +agent, or enable Debug just to diagnose the alert. A genuine missing, disabled, +invalid or unavailable parity member remains a separate safety concern. + +For a report, share only already available versions, the original time, relevant +status/counts and redacted alert text. Keep disk serials, paths and raw agent +payloads private; use consistent aliases and leave unknowns unknown. Follow +[Getting Help](TROUBLESHOOTING.md#-getting-help), without repeating a failed +update or array operation to supply evidence. + #### RAID array is degraded with a spare For a Linux MD RAID alert, compare the same array's **existing** native status diff --git a/frontend-modern/public/docs/TROUBLESHOOTING.md b/frontend-modern/public/docs/TROUBLESHOOTING.md index f6350d4fb3..4f2a547564 100644 --- a/frontend-modern/public/docs/TROUBLESHOOTING.md +++ b/frontend-modern/public/docs/TROUBLESHOOTING.md @@ -51,6 +51,10 @@ establish safe recovery. [required members, active members and spares](TROUBLESHOOTING.md#raid-array-is-degraded-with-a-spare). A spare-inclusive total is not the required member count; do not change the array just to clear a Pulse alert. +- **Unraid reports missing disks in empty slots:** distinguish + [unassigned slots, missing members and parity counts](TROUBLESHOOTING.md#unraid-reports-missing-disks-in-empty-slots). + Unraid parity is not Linux MD RAID; do not rebuild or change disk assignments + just to make Pulse's count agree. - **Pulse itself uses more memory, CPU or disk writes:** use [memory baseline and growth checks](TROUBLESHOOTING.md#memory-use-keeps-growing) or [CPU and write-rate checks](TROUBLESHOOTING.md#excessive-cpu-writes-or-database-growth). @@ -415,6 +419,42 @@ restore, not by overwriting today's history. Follow ### Monitoring Data +#### Unraid reports missing disks in empty slots + +Compare the same host's **existing** Unraid status with Pulse's disk inventory, +storage-topology alert and observation time. Keep the Pulse server and Unraid +Unified Agent versions separate: updating the server does not establish that +the agent updated. No new command, Diagnostics run or array operation is needed +to report a disagreement. + +| Existing observation | Meaning | +| --- | --- | +| `DISK_NP`, no device or size, and only `fsType=auto` or a transport-only identity such as `ata-_` | A configured-but-unassigned slot, not evidence of a missing assigned disk. A slot name such as `disk6` or `parity2` alone is not assignment evidence. | +| `DISK_NP_MISSING`, even without a device or identity | Unraid explicitly reports an assigned-but-absent member. Do not discard it as an empty slot. | +| A missing/disabled slot with a real device, model/serial, filesystem or size | Assignment evidence remains; do not dismiss the failure because other slots are empty. `auto` alone is not a real filesystem. | +| Single online parity disk and five data disks | Pulse's `numProtected` counts online parity devices: **1**, not five protected data disks. That count alone does not prove complete protection or healthy disks. | +| No matching native observation, an old reading or an unknown version | The comparison is unavailable, not a healthy result. `STARTED` alone does not establish array health. | + +For the healthy partially populated topology in [#1788](https://github.com/rcourtman/Pulse/issues/1788), +seventeen empty data slots wrongly contributed to `numMissing`. The final +correction treats `fsType=auto` as a placeholder at agent collection, server +ingestion and storage-health assessment. **Pulse v6.5.0 contains this correction +in the server and Unified Agent**; an earlier preview retest failed before +this final correction. This does not establish that a current alert has the +same cause or that the reporter's installation has recovered. + +If existing native status shows no missing assigned members but Pulse disagrees, +preserve the mismatch rather than suppressing the check or changing the array. +Do not unassign a disk, rebuild, start a parity check, restart or re-enrol the +agent, or enable Debug just to diagnose the alert. A genuine missing, disabled, +invalid or unavailable parity member remains a separate safety concern. + +For a report, share only already available versions, the original time, relevant +status/counts and redacted alert text. Keep disk serials, paths and raw agent +payloads private; use consistent aliases and leave unknowns unknown. Follow +[Getting Help](TROUBLESHOOTING.md#-getting-help), without repeating a failed +update or array operation to supply evidence. + #### RAID array is degraded with a spare For a Linux MD RAID alert, compare the same array's **existing** native status diff --git a/scripts/tests/test_unraid_troubleshooting_docs.py b/scripts/tests/test_unraid_troubleshooting_docs.py new file mode 100644 index 0000000000..813f81fc16 --- /dev/null +++ b/scripts/tests/test_unraid_troubleshooting_docs.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Keep Unraid help distinct from MD spares and unsafe array-repair advice. + +These checks guard the instructions and shipped mirror, not an installed array's +recovery. Source-bound collector/assessment fixtures and browser proof are retained +separately when the guidance changes. +""" + +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +HEADING = "#### Unraid reports missing disks in empty slots\n" +ANCHOR = "TROUBLESHOOTING.md#unraid-reports-missing-disks-in-empty-slots" + + +def section(text): + return " ".join(text.split(HEADING, 1)[1].split("\n#### ", 1)[0].split()) + + +class UnraidTroubleshootingDocsTest(unittest.TestCase): + def setUp(self): + self.source = (ROOT / "docs/TROUBLESHOOTING.md").read_text() + self.guide = section(self.source) + + def test_existing_symptom_entry_reaches_the_distinct_unraid_workflow(self): + self.assertIn(ANCHOR, self.source.split("### Monitoring Data", 1)[0]) + self.assertIn("Unraid parity is not Linux MD RAID", self.source) + self.assertIn("do not rebuild or change disk assignments", self.source) + + def test_empty_slot_evidence_does_not_hide_assigned_missing_members(self): + for phrase in ( + "`DISK_NP`, no device or size", "`fsType=auto`", + "transport-only identity such as `ata-_`", "not assignment evidence", + "`DISK_NP_MISSING`, even without a device or identity", + "assigned-but-absent member", "Do not discard it as an empty slot", + "real device, model/serial, filesystem or size", + "Assignment evidence remains", "`auto` alone is not a real filesystem", + ): + with self.subTest(phrase=phrase): + self.assertIn(phrase, self.guide) + + def test_parity_and_unknowns_are_not_misrepresented_as_recovery(self): + for phrase in ( + "Single online parity disk and five data disks", + "`numProtected` counts online parity devices: **1**", + "does not prove complete protection", "not a healthy result", + "`STARTED` alone does not establish array health", + "Pulse server and Unraid Unified Agent versions separate", + "updating the server does not establish that the agent updated", + "agent collection, server ingestion and storage-health assessment", + "an earlier preview retest failed", "installation has recovered", + ): + with self.subTest(phrase=phrase): + self.assertIn(phrase, self.guide) + + def test_no_replay_array_change_or_private_payload_is_requested(self): + self.assertNotIn("```", self.source.split(HEADING, 1)[1].split("\n#### ", 1)[0]) + for phrase in ( + "existing", "No new command, Diagnostics run or array operation", + "rather than suppressing the check or changing the array", + "Do not unassign a disk, rebuild, start a parity check", + "restart or re-enrol the agent", "enable Debug just to diagnose", + "genuine missing, disabled, invalid or unavailable parity member", + "already available versions", "original time", "redacted alert text", + "Keep disk serials, paths and raw agent payloads private", + "consistent aliases", "leave unknowns unknown", "without repeating a failed", + ): + with self.subTest(phrase=phrase): + self.assertIn(phrase, self.guide) + + def test_shipped_help_matches_the_source(self): + self.assertEqual((ROOT / "docs/TROUBLESHOOTING.md").read_bytes(), + (ROOT / "frontend-modern/public/docs/TROUBLESHOOTING.md").read_bytes()) + + +if __name__ == "__main__": + unittest.main()