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
This commit is contained in:
pulse-triage[bot]
2026-10-14 20:00:33 +01:00
parent a3e6594f17
commit 1c9374fb0c
3 changed files with 159 additions and 0 deletions
+40
View File
@@ -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
@@ -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
@@ -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()