diff --git a/Plugin/unraid/include/common.php b/Plugin/unraid/include/common.php index 9d93afc..819b376 100644 --- a/Plugin/unraid/include/common.php +++ b/Plugin/unraid/include/common.php @@ -517,6 +517,48 @@ function vv_parity_status(): array { $numMissing = (int)($var['mdNumMissing'] ?? 0); $exitCode = (int)($var['sbSyncExit'] ?? 0); $errors = (int)($var['sbSyncErrs'] ?? 0); + + // How many disks are actually being emulated, which is not mdNumDisabled. + // + // mdNumDisabled counts every slot the array marks disabled, including a parity slot that was + // never populated. HOST2 reports mdNumDisabled=1 with mdNumMissing=0, and the slot is parity2 + // at DISK_NP_DSBL — an empty second-parity slot. Unraid's own Main page shows nothing for it, + // while this card said "Emulating 1 disk" and turned the banner amber. + // + // Parity is never emulated: reconstructing a parity disk from parity is not a thing. Only data + // slots are, so the count is taken from disks.ini and restricted to them. A data disk that is + // disabled *is* emulated whether or not it is physically present — a pulled failed drive reads + // DISK_NP_DSBL and is genuinely being served from parity — so presence is not the test here, + // role is. + // + // A disabled parity slot is still worth knowing about when a real parity disk fails, so it is + // counted separately rather than discarded. + // + // Both counts require the slot to have an assigned identity. A slot that was never populated + // reads id="" device="" size="0" — parity2 on HOST2 is exactly that — while a disk that failed + // and was pulled keeps its id, because the array remembers the assignment it is emulating. + // Presence is therefore the wrong test and identity is the right one: it separates "no disk was + // ever here" from "the disk that belongs here is gone", which look identical in the status + // field alone and mean opposite things. + $numEmulated = 0; + $numParityDisabled = 0; + $slot = ''; $slotId = ''; $slotStatus = ''; + $tally = function () use (&$slot, &$slotId, &$slotStatus, &$numEmulated, &$numParityDisabled) { + if ($slot === '' || $slotId === '') return; + if (strpos($slotStatus, 'DSBL') === false) return; + if (preg_match('/^disk\d+$/', $slot)) $numEmulated++; + elseif (preg_match('/^parity\d*$/', $slot)) $numParityDisabled++; + }; + foreach (@file('/var/local/emhttp/disks.ini') ?: [] as $line) { + if (preg_match('/^\["([^"]+)"\]/', $line, $m)) { + $tally(); // close the previous slot + $slot = $m[1]; $slotId = ''; $slotStatus = ''; + continue; + } + if (preg_match('/^id="([^"]*)"/', $line, $m)) $slotId = trim($m[1]); + if (preg_match('/^status="([^"]*)"/', $line, $m)) $slotStatus = $m[1]; + } + $tally(); // and the last one, which has no header after it // Emulated (DISK_DSBL) disks are protected by parity and don't make parity invalid. // True invalidity: sync errors on the last check, or unprotectable missing slots. $isValid = $errors === 0 && $numMissing === 0; @@ -566,7 +608,12 @@ function vv_parity_status(): array { $exitMap = ['0' => 'Completed', '-4' => 'Aborted', '-5' => 'Cancelled']; return [ 'valid' => $isValid, + // mdNumDisabled as the array reports it, kept because it is Unraid's own number and + // anything comparing against the WebGUI wants it. Not what the card labels, though — + // num_emulated is the one that means "data is being served from parity". 'num_disabled' => $numDisabled, + 'num_emulated' => $numEmulated, + 'num_parity_disabled' => $numParityDisabled, 'num_missing' => $numMissing, 'in_progress' => $inProgress, 'resync_action' => $resyncAction, diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index 4626e58..baf1900 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -1290,21 +1290,28 @@ function vvPollMonitor(live) { return new Date(ts * 1000).toLocaleString([], {weekday:'short',day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}); } - const numDisabled = par.num_disabled ?? 0; + // num_emulated, not num_disabled: the array counts an empty parity slot as disabled, and + // labelling that "Emulating 1 disk" described a failure that was not happening. See + // vv_parity_status() for why parity slots are counted separately. + const numEmulated = par.num_emulated ?? 0; + const numParityDsbl = par.num_parity_disabled ?? 0; const numMissing = par.num_missing ?? 0; - const degraded = numDisabled > 0 || numMissing > 0; + const degraded = numEmulated > 0 || numMissing > 0; const exitColor = par.exit_label === 'Completed' ? '#4caf50' : par.exit_label === 'Aborted' ? '#ff9800' : '#f44336'; const errColor = (par.errors ?? 0) > 0 ? '#f44336' : '#444'; const speedStr = par.last_speed_mb ? ` · ${par.last_speed_mb} MB/s` : ''; const nextDate = vvFmtDate(par.next_ts); const dueIn = vvDueIn(par.next_ts); const parBannerCls = !valid ? 'vv-banner-err' : degraded || (par.errors ?? 0) > 0 ? 'vv-banner-warn' : 'vv-banner-ok'; - const emulLabel = numDisabled > 0 ? `Emulating ${numDisabled} disk${numDisabled !== 1 ? 's' : ''}` : ''; + const emulLabel = numEmulated > 0 ? `Emulating ${numEmulated} disk${numEmulated !== 1 ? 's' : ''}` : ''; const missingLabel = numMissing > 0 ? `${numMissing} slot${numMissing !== 1 ? 's' : ''} missing` : ''; + // A failed parity disk is a real fault and still has to surface — it just is not emulation, + // and it does not mean data is being reconstructed. + const parDsblLabel = numParityDsbl > 0 ? `${numParityDsbl} parity disk${numParityDsbl !== 1 ? 's' : ''} disabled` : ''; let html = `
${valid ? '✓ Valid' : '✗ INVALID'} - ${emulLabel}${missingLabel} + ${emulLabel}${missingLabel}${parDsblLabel} ${(par.errors ?? 0) > 0 ? `${par.errors} error${par.errors !== 1 ? 's' : ''}` : ''}
`;