so right now disk 10 is gone, i have a replacement coming sorta. but in monitor page parity is shown as invalid. parity is still valid and disk is being emulated. we need to turn the bar red on disks that fail. parity is valid, we can state enumerating disk*, or somthing but not invalid


- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- verify: Verify that a code change actually does what it's supposed to by running the app and observing behavior. Use when asked to verify a PR, confirm a fix works, test a change manually, check that a feature works, or validate local changes before pushing.
- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch

update-config keybindings-help verify code-review simplify fewer-permission-prompts loop schedule claude-api run init review security-review


Fix parity status display for failed disks


Let me look at the monitor page to find where disk status and parity are displayed.


Bash

find /boot/config/plugins/varaverk/Plugin/unraid -type f | sort

List plugin files


1	<?php
2	require_once __DIR__ . '/common.php';
3
4	// Monitor-page-specific helpers — partner state, fallback state, watchdog summary, scripts status.
5
6	function vv_partner_state(): array {
7	    $vars   = vv_conf_vars();
8	    $myName = trim(shell_exec('hostname -s') ?: '');
9
10	    // Parse Tailscale peer status once
11	    $tsData  = json_decode(shell_exec('tailscale status --json 2>/dev/null') ?: '{}', true) ?? [];
12	    $tsPeers = [];
13	    foreach ($tsData['Peer'] ?? [] as $peer) {
14	        // DNSName is "hostname.tailnet.ts.net." — take the first label (full, not truncated)
15	        $dns = $peer['DNSName'] ?? '';
16	        $h   = $dns ? strtolower(explode('.', $dns)[0]) : strtolower($peer['HostName'] ?? '');
17	        if ($h) $tsPeers[$h] = (bool)($peer['Online'] ?? false);
18	    }
19
20	    $hostIds = array_filter(array_keys($vars), fn($k) => preg_match('/^HOST\d+$/', $k) && ($vars[$k] ?? '') !== '');
21	    sort($hostIds);
22
23	    $setupDb = vv_setup_state_read();
24
25	    $hosts = [];
26	    foreach ($hostIds as $id) {
27	        $hostname = $vars[$id] ?? '';
28	        if (!$hostname) continue;
29	        $isMe    = strcasecmp($hostname, $myName) === 0;
30	        $isOwner = strcasecmp($id, $vars['PARTNERSHIP_OWNER_HOST'] ?? '') === 0;
31	        $online  = $isMe ? true : ($tsPeers[strtolower($hostname)] ?? null);
32	        $onboardPhase = $isMe ? null
33	            : (($setupDb[$id . '_PHASE2_DONE'] ?? '') === 'true' ? 2
34	              : (($setupDb[$id . '_PHASE1_DONE'] ?? '') === 'true' ? 1 : 0));
35	        $hosts[] = [
36	            'id'            => $id,
37	            'hostname'      => $hostname,
38	            'owner'         => $vars[$id . '_OWNER'] ?? '',
39	            'is_me'         => $isMe,
40	            'is_owner'      => $isOwner,
41	            'online'        => $online,
42	            'onboard_phase' => $onboardPhase,
43	        ];
44	    }
45
46	    return [
47	        'enabled'    => ($vars['PARTNERSHIP_ENABLED'] ?? 'false') === 'true',
48	        'owner_host' => $vars['PARTNERSHIP_OWNER_HOST'] ?? '',
49	        'sync_min'   => (int)($vars['PARTNERSHIP_SYNC_INTERVAL'] ?? 15),
50	        'hosts'      => $hosts,
51	    ];
52	}
53
54	function vv_fallback_state(): array {
55	    $vars      = vv_conf_vars();
56	    $enabled   = ($vars['FALLBACK_ENABLED']           ?? 'false') === 'true';
57	    $interval  = (int)($vars['FALLBACK_CHECK_INTERVAL']    ?? 30);
58	    $reqStrikes = (int)($vars['FALLBACK_HANDBACK_STRIKES'] ?? 3);
59	    $ptRequired = ($vars['FALLBACK_PARTNERSHIP_REQUIRED']  ?? 'true') === 'true';
60	    $suspendAfter = (int)($vars['FALLBACK_PARTNERSHIP_SUSPEND_AFTER'] ?? 120);
61
62	    $stateFile = '/tmp/fallback_state.db';
63	    if (!file_exists($stateFile)) {
64	        return ['state' => 'UNKNOWN', 'enabled' => $enabled, 'check_interval' => $interval,
65	                'handback_strikes' => 0, 'handback_strikes_required' => $reqStrikes,
66	                'partnership_required' => $ptRequired, 'partnership_suspended' => false,
67	                'partner_lost_at' => 0, 'partnership_suspend_after' => $suspendAfter];
68	    }
69	    $raw = [];
70	    foreach (file($stateFile) ?: [] as $line) {
71	        [$k, $v] = array_pad(explode('=', trim($line), 2), 2, '');
72	        $raw[trim($k)] = trim($v);
73	    }
74	    return [
75	        'state'                      => $raw['state']               ?? 'UNKNOWN',
76	        'failover_start'             => $raw['failover_start']       ?? '0',
77	        'tier2_started'              => $raw['tier2_started']        ?? 'false',
78	        'tier3_started'              => $raw['tier3_started']        ?? 'false',
79	        'tier4_started'              => $raw['tier4_started']        ?? 'false',
80	        'handback_strikes'           => (int)($raw['handback_strikes']    ?? 0),
81	        'partnership_suspended'      => ($raw['partnership_suspended']    ?? 'false') === 'true',
82	        'partner_lost_at'            => (int)($raw['partner_lost_at']     ?? 0),
83	        'enabled'                    => $enabled,
84	        'check_interval'             => $interval,
85	        'handback_strikes_required'  => $reqStrikes,
86	        'partnership_required'       => $ptRequired,
87	        'partnership_suspend_after'  => $suspendAfter,
88	    ];
89	}
90
91	function vv_fallback_active(): array {
92	    $vars   = vv_conf_vars();
93	    $myName = trim(shell_exec('hostname -s') ?: '');
94
95	    // Identify which HOST id we are
96	    $allHostIds = array_filter(array_keys($vars), fn($k) => preg_match('/^HOST\d+$/', $k) && ($vars[$k] ?? '') !== '');
97	    sort($allHostIds);
98	    $myId = null;
99	    foreach ($allHostIds as $id) {
100	        if (strcasecmp($vars[$id] ?? '', $myName) === 0) { $myId = $id; break; }
101	    }
102	    if (!$myId) return [];
103
104	    // Running containers: name → image
105	    $running = [];
106	    $psOut = shell_exec("docker ps --format '{\"n\":\"{{.Names}}\",\"i\":\"{{.Image}}\"}' 2>/dev/null") ?: '';
107	    foreach (explode("\n", trim($psOut)) as $line) {
108	        $c = json_decode($line, true);
109	        if ($c) $running[strtolower($c['n'])] = $c['i'];
110	    }
111
112	    // Parse FALLBACK arrays from this host's conf
113	    $confFile = strtolower($myId) . '.conf';
114	    $rawConf  = vv_read_conf_raw($confFile);
115
116	    $result = [];
117	    foreach ($allHostIds as $covered) {
118	        if ($covered === $myId) continue;
119	        $coveredHostname = $vars[$covered] ?? '';
120	        if (!$coveredHostname) continue;
121
122	        $names = [];
123	        for ($tier = 1; $tier <= 4; $tier++)
124	            $names = array_merge($names, vv_parse_bash_array($rawConf, "FALLBACK_{$myId}_COVERS_{$covered}_TIER{$tier}"));
125
126	        $active = [];
127	        foreach ($names as $name) {
128	            if (isset($running[strtolower($name)]))
129	                $active[] = ['name' => $name, 'image' => $running[strtolower($name)]];
130	        }
131
132	        if ($active) $result[] = ['host_id' => $covered, 'hostname' => $coveredHostname, 'containers' => $active];
133	    }
134
135	    return $result;
136	}
137
138	function vv_watchdog_summary(): array {
139	    $parseKv = function(string $raw): array {
140	        $out = [];
141	        foreach (explode("\n", $raw) as $line) {
142	            $line = trim($line);
143	            if (str_contains($line, ':'))      { [$k, $v] = explode(':', $line, 2); $out[trim($k)] = trim($v); }
144	            elseif (str_contains($line, '='))  { [$k, $v] = explode('=', $line, 2); $out[trim($k)] = trim($v, '"\''); }
145	        }
146	        return $out;
147	    };
148
149	    $dock = $parseKv(@file_get_contents('/tmp/container_watchdog_state.db')  ?: '');
150	    $rw   = $parseKv(@file_get_contents('/tmp/resource_watchdog_state.db')   ?: '');
151
152	    $ctrStrikes = [];
153	    foreach ($dock as $k => $v) {
154	        if ($k !== 'daemon_strikes' && $k !== 'daemon_restarted_flag' && (int)$v > 0)
155	            $ctrStrikes[$k] = (int)$v;
156	    }
157
158	    // Recent restarts (24 h)
159	    $restartLog = DATA_DIR . '/container_restart_history.db';
160	    $restartRaw = @file_get_contents($restartLog) ?: '';
161	    $cutoff     = time() - 86400;
162	    $restarts   = [];
163	    foreach (explode("\n", trim($restartRaw)) as $line) {
164	        if (!$line || !str_contains($line, '|')) continue;
165	        [$name, $ts] = explode('|', $line, 2);
166	        if ((int)$ts >= $cutoff) $restarts[] = ['name' => trim($name), 'ts' => (int)$ts];
167	    }
168	    usort($restarts, fn($a, $b) => $b['ts'] - $a['ts']);
169
170	    // Reboots (12 h)
171	    $rebootRaw = @file_get_contents('/boot/config/system_watchdog_reboots.db') ?: '';
172	    $rbootCutoff = time() - 43200;
173	    $reboots = 0;
174	    foreach (explode("\n", trim($rebootRaw)) as $line) {
175	        if ((int)trim($line) >= $rbootCutoff) $reboots++;
176	    }
177
178	    $rwLevel       = (int)($rw['rm_action_level']  ?? 0);
179	    $daemonStrikes = (int)($dock['daemon_strikes']  ?? 0);
180	    $oomCount      = (int)trim(@file_get_contents('/tmp/system_watchdog_oom.db') ?: '0');
181
182	    // ── Stability watchdog strikes (/tmp/system_watchdog_state.db) ───────────
183	    $stabRaw = @file_get_contents('/tmp/system_watchdog_state.db') ?: '';
184	    $stabStrikes = [];
185	    foreach (explode("\n", $stabRaw) as $line) {
186	        $line = trim($line);
187	        if (!$line || !str_contains($line, ':')) continue;
188	        [$k, $v] = explode(':', $line, 2);
189	        $count = (int)trim($v);
190	        if ($count > 0) $stabStrikes[trim($k)] = $count;
191	    }
192
193	    // ── Storage watchdog strikes (/tmp/storage_watchdog_state.db) ────────────
194	    $storRaw = @file_get_contents('/tmp/storage_watchdog_state.db') ?: '';
195	    $growthStrikes = []; $logStrikes = [];
196	    foreach (explode("\n", $storRaw) as $line) {
197	        $line = trim($line);
198	        if (!$line || !str_contains($line, ':')) continue;
199	        [$k, $v] = explode(':', $line, 2);
200	        $count = (int)trim($v);
201	        if ($count <= 0) continue;
202	        $key = trim($k);
203	        if (str_starts_with($key, 'appdata_growth_'))
204	            $growthStrikes[substr($key, strlen('appdata_growth_'))] = $count;
205	        elseif (str_starts_with($key, 'appdata_log_'))
206	            $logStrikes[substr($key, strlen('appdata_log_'))] = $count;
207	    }
208
209	    // ── Network watchdog NPM strikes (/tmp/network_watchdog_state.db) ────────
210	    $netRaw = @file_get_contents('/tmp/network_watchdog_state.db') ?: '';
211	    $npmStrikes = 0;
212	    foreach (explode("\n", $netRaw) as $line) {
213	        $line = trim($line);
214	        if (str_starts_with($line, 'npm:')) $npmStrikes = (int)trim(substr($line, 4));
215	    }
216
217	    // ── Stability live stats ──────────────────────────────────────────────────
218	    $dfPct = function(string $path): int {
219	        $out = shell_exec("df " . escapeshellarg($path) . " --output=pcent 2>/dev/null | tail -1") ?: '';
220	        return (int)trim(str_replace('%', '', $out));
221	    };
222
223	    $memRaw   = @file_get_contents('/proc/meminfo') ?: '';
224	    $memAvail = 0;
225	    if (preg_match('/^MemAvailable:\s+(\d+)/m', $memRaw, $m)) $memAvail = (int)$m[1];
226
227	    $loadRaw = @file_get_contents('/proc/loadavg') ?: '0';
228	    $load1   = (float)explode(' ', trim($loadRaw))[0];
229
230	    $cpuTemp = null;
231	    $sensorsOut = shell_exec("sensors 2>/dev/null | grep -E 'Core 0|Package id 0|Tdie|Tctl|CPU Temp' | grep -oE '[0-9]+\\.[0-9]+' | sort -n | tail -1") ?: '';
232	    if ($sensorsOut && is_numeric(trim($sensorsOut))) $cpuTemp = (int)round((float)trim($sensorsOut));
233
234	    $zombies = (int)trim(shell_exec("ps -eo stat 2>/dev/null | grep -c '^Z'") ?: '0');
235
236	    $nic      = trim(shell_exec("ip route show default 2>/dev/null | awk 'NR==1{print \$5}'") ?: 'eth0') ?: 'eth0';
237	    $nicState = trim(@file_get_contents("/sys/class/net/$nic/operstate") ?: 'unknown');
238	    $sshdOk   = (int)trim(shell_exec('pgrep -c sshd 2>/dev/null') ?: '0') > 0;
239
240	    $healthy = empty($ctrStrikes) && empty($stabStrikes) && empty($growthStrikes) && empty($logStrikes)
241	        && $rwLevel === 0 && $daemonStrikes === 0 && $oomCount === 0 && $reboots === 0
242	        && $npmStrikes === 0 && $nicState === 'up' && $sshdOk;
243
244	    return [
245	        'healthy'        => $healthy,
246	        'ctr_strikes'    => $ctrStrikes,
247	        'rw_level'       => $rwLevel,
248	        'rw_paused'      => array_values(array_filter(explode(',', $rw['rm_paused_containers'] ?? ''))),
249	        'rw_stopped'     => array_values(array_filter(explode(',', $rw['rm_stopped_containers'] ?? ''))),
250	        'daemon_strikes' => $daemonStrikes,
251	        'oom_count'      => $oomCount,
252	        'reboots_12h'    => $reboots,
253	        'restarts_24h'   => array_slice($restarts, 0, 6),
254	        'restart_count'  => count($restarts),
255	        'stability'      => [
256	            'rootfs_pct'  => $dfPct('/'),
257	            'log_pct'     => $dfPct('/var/log'),
258	            'tmp_pct'     => $dfPct('/tmp'),
259	            'ram_free_gb' => round($memAvail / 1048576, 1),
260	            'load_1min'   => $load1,
261	            'cpu_temp'    => $cpuTemp,
262	            'zombies'     => $zombies,
263	            'nic'         => $nic,
264	            'nic_state'   => $nicState,
265	            'sshd_ok'     => $sshdOk,
266	            'strikes'     => $stabStrikes,
267	        ],
268	        'storage_wd'     => [
269	            'growth_strikes' => $growthStrikes,
270	            'log_strikes'    => $logStrikes,
271	        ],
272	        'network_wd'     => [
273	            'npm_strikes' => $npmStrikes,
274	        ],
275	    ];
276	}
277
278	function vv_scripts_status(): array {
279	    $logDir    = LOG_DIR;
280	    $statFiles = array_merge(
281	        glob("$logDir/*.json")   ?: [],
282	        glob("$logDir/*/*.json") ?: []
283	    );
284
285	    $scripts = [];
286	    foreach ($statFiles as $statFile) {
287	        $stat   = json_decode(@file_get_contents($statFile) ?: '{}', true) ?: [];
288	        $status = $stat['status'] ?? 'unknown';
289
290	        // Stale running — PID gone (crash or reboot with no cleanup)
291	        if ($status === 'running' && !empty($stat['pid'])) {
292	            if (!file_exists("/proc/{$stat['pid']}")) $status = 'error';
293	        }
294
295	        $id   = $stat['id'] ?? basename($statFile, '.json');
296	        $name = basename(preg_replace('/\.sh$/', '', $id));
297	        $ts   = (int)($stat['end'] ?? $stat['start'] ?? @filemtime($statFile) ?: 0);
298
299	        $scripts[] = [
300	            'id'       => $id,
301	            'name'     => $name,
302	            'last_ts'  => $ts,
303	            'status'   => $status,
304	            'running'  => $status === 'running',
305	            'exit'     => $stat['exit'] ?? null,
306	            'duration' => isset($stat['start'], $stat['end'])
307	                ? (int)$stat['end'] - (int)$stat['start'] : null,
308	        ];
309	    }
310
311	    usort($scripts, fn($a, $b) => ($b['last_ts'] ?? 0) <=> ($a['last_ts'] ?? 0));
312	    $scripts = array_slice($scripts, 0, 12);
313
314	    return [
315	        'scripts'       => $scripts,
316	        'running_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'running')),
317	        'ok_count'      => count(array_filter($scripts, fn($s) => $s['status'] === 'ok')),
318	        'warn_count'    => count(array_filter($scripts, fn($s) => $s['status'] === 'warn')),
319	        'error_count'   => count(array_filter($scripts, fn($s) => $s['status'] === 'error')),
320	    ];
321	}
322
323	function vv_rsync_status(): array {
324	    $vars = vv_conf_vars();
325
326	    $enabled = ($vars['RSYNC_ENABLED'] ?? 'true') !== 'false';
327
328	    $windows = [
329	        'critical'     => ($vars['CRITICAL_RSYNC_ENABLED']     ?? 'false') !== 'false',
330	        'daily'        => ($vars['DAILY_RSYNC_ENABLED']        ?? 'false') !== 'false',
331	        'intermediate' => ($vars['INTERMEDIATE_RSYNC_ENABLED'] ?? 'true')  !== 'false',
332	        'weekly'       => ($vars['WEEKLY_RSYNC_ENABLED']       ?? 'false') !== 'false',
333	    ];
334
335	    // Active rsync profiles — from lock files
336	    $lockDir = '/tmp/unraid_locks';
337	    $active  = [];
338	    foreach (glob("$lockDir/rsync_*.lock") ?: [] as $lf) {
339	        $content = trim(@file_get_contents($lf) ?: '');
340	        [$pid, $locked_name] = array_pad(explode(':', $content, 2), 2, '');
341	        if (!$pid || !file_exists("/proc/$pid")) continue;
342	        $profile  = preg_replace('/^rsync_/', '', $locked_name ?: basename($lf, '.lock'));
343	        $active[] = [
344	            'profile' => $profile,
345	            'pid'     => (int)$pid,
346	            'elapsed' => time() - (int)filemtime($lf),
347	        ];
348	    }
349
350	    // Last completed run per window (orchestrator log files)
351	    $scriptMap = [
352	        'critical'     => 'critical_sync_maintenance',
353	        'daily'        => 'daily_sync_maintenance',
354	        'intermediate' => 'intermediate_sync_maintenance',
355	        'weekly'       => 'weekly_sync_maintenance',
356	    ];
357	    $lastSync = [];
358	    foreach ($scriptMap as $key => $scriptName) {
359	        $logFile = LOG_DIR . "/$scriptName.json";
360	        if (!file_exists($logFile)) continue;
361	        $stat = json_decode(@file_get_contents($logFile) ?: '{}', true) ?: [];
362	        $lastSync[$key] = [
363	            'ts'       => (int)($stat['end'] ?? $stat['start'] ?? 0),
364	            'status'   => $stat['status'] ?? 'unknown',
365	            'duration' => isset($stat['start'], $stat['end'])
366	                ? (int)$stat['end'] - (int)$stat['start'] : null,
367	        ];
368	    }
369
370	    // Profile activity — last 7 days, aggregated per profile
371	    $bwLog   = DATA_DIR . '/bandwidth_history.db';
372	    $cutoff7 = date('Y-m-d', strtotime('-7 days'));
373	    $profiles = [];
374	    if (file_exists($bwLog)) {
375	        foreach (file($bwLog, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
376	            $p = explode('|', $line);
377	            if (count($p) < 4 || ($p[0] ?? '') < $cutoff7) continue;
378	            $name = $p[2] ?? '';
379	            if (!$name) continue;
380	            if (!isset($profiles[$name])) $profiles[$name] = ['runs' => 0, 'dur' => 0, 'bytes' => 0];
381	            $profiles[$name]['runs']++;
382	            $profiles[$name]['dur']   += (int)($p[3] ?? 0);
383	            $profiles[$name]['bytes'] += (int)($p[5] ?? 0);
384	        }
385	    }
386	    arsort($profiles); // sort by run count descending
387	    $bwSummary = array_slice($profiles, 0, 6, true);
388
389	    return [
390	        'enabled'    => $enabled,
391	        'windows'    => $windows,
392	        'active'     => $active,
393	        'last_sync'  => $lastSync,
394	        'bw_summary' => $bwSummary,
395	    ];
396	}
397

<?php
require_once __DIR__ . '/common.php';

// Monitor-page-specific helpers — partner state, fallback state, watchdog summary, scripts status.

function vv_partner_state(): array {
    $vars   = vv_conf_vars();
    $myName = trim(shell_exec('hostname -s') ?: '');

    // Parse Tailscale peer status once
    $tsData  = json_decode(shell_exec('tailscale status --json 2>/dev/null') ?: '{}', true) ?? [];
    $tsPeers = [];
    foreach ($tsData['Peer'] ?? [] as $peer) {
        // DNSName is "hostname.tailnet.ts.net." — take the first label (full, not truncated)
        $dns = $peer['DNSName'] ?? '';
        $h   = $dns ? strtolower(explode('.', $dns)[0]) : strtolower($peer['HostName'] ?? '');
        if ($h) $tsPeers[$h] = (bool)($peer['Online'] ?? false);
    }

    $hostIds = array_filter(array_keys($vars), fn($k) => preg_match('/^HOST\d+$/', $k) && ($vars[$k] ?? '') !== '');
    sort($hostIds);

    $setupDb = vv_setup_state_read();

    $hosts = [];
    foreach ($hostIds as $id) {
        $hostname = $vars[$id] ?? '';
        if (!$hostname) continue;
        $isMe    = strcasecmp($hostname, $myName) === 0;
        $isOwner = strcasecmp($id, $vars['PARTNERSHIP_OWNER_HOST'] ?? '') === 0;
        $online  = $isMe ? true : ($tsPeers[strtolower($hostname)] ?? null);
        $onboardPhase = $isMe ? null
            : (($setupDb[$id . '_PHASE2_DONE'] ?? '') === 'true' ? 2
              : (($setupDb[$id . '_PHASE1_DONE'] ?? '') === 'true' ? 1 : 0));
        $hosts[] = [
            'id'            => $id,
            'hostname'      => $hostname,
            'owner'         => $vars[$id . '_OWNER'] ?? '',
            'is_me'         => $isMe,
            'is_owner'      => $isOwner,
            'online'        => $online,
            'onboard_phase' => $onboardPhase,
        ];
    }

    return [
        'enabled'    => ($vars['PARTNERSHIP_ENABLED'] ?? 'false') === 'true',
        'owner_host' => $vars['PARTNERSHIP_OWNER_HOST'] ?? '',
        'sync_min'   => (int)($vars['PARTNERSHIP_SYNC_INTERVAL'] ?? 15),
        'hosts'      => $hosts,
    ];
}

function vv_fallback_state(): array {
    $vars      = vv_conf_vars();
    $enabled   = ($vars['FALLBACK_ENABLED']           ?? 'false') === 'true';
    $interval  = (int)($vars['FALLBACK_CHECK_INTERVAL']    ?? 30);
    $reqStrikes = (int)($vars['FALLBACK_HANDBACK_STRIKES'] ?? 3);
    $ptRequired = ($vars['FALLBACK_PARTNERSHIP_REQUIRED']  ?? 'true') === 'true';
    $suspendAfter = (int)($vars['FALLBACK_PARTNERSHIP_SUSPEND_AFTER'] ?? 120);

    $stateFile = '/tmp/fallback_state.db';
    if (!file_exists($stateFile)) {
        return ['state' => 'UNKNOWN', 'enabled' => $enabled, 'check_interval' => $interval,
                'handback_strikes' => 0, 'handback_strikes_required' => $reqStrikes,
                'partnership_required' => $ptRequired, 'partnership_suspended' => false,
                'partner_lost_at' => 0, 'partnership_suspend_after' => $suspendAfter];
    }
    $raw = [];
    foreach (file($stateFile) ?: [] as $line) {
        [$k, $v] = array_pad(explode('=', trim($line), 2), 2, '');
        $raw[trim($k)] = trim($v);
    }
    return [
        'state'                      => $raw['state']               ?? 'UNKNOWN',
        'failover_start'             => $raw['failover_start']       ?? '0',
        'tier2_started'              => $raw['tier2_started']        ?? 'false',
        'tier3_started'              => $raw['tier3_started']        ?? 'false',
        'tier4_started'              => $raw['tier4_started']        ?? 'false',
        'handback_strikes'           => (int)($raw['handback_strikes']    ?? 0),
        'partnership_suspended'      => ($raw['partnership_suspended']    ?? 'false') === 'true',
        'partner_lost_at'            => (int)($raw['partner_lost_at']     ?? 0),
        'enabled'                    => $enabled,
        'check_interval'             => $interval,
        'handback_strikes_required'  => $reqStrikes,
        'partnership_required'       => $ptRequired,
        'partnership_suspend_after'  => $suspendAfter,
    ];
}

function vv_fallback_active(): array {
    $vars   = vv_conf_vars();
    $myName = trim(shell_exec('hostname -s') ?: '');

    // Identify which HOST id we are
    $allHostIds = array_filter(array_keys($vars), fn($k) => preg_match('/^HOST\d+$/', $k) && ($vars[$k] ?? '') !== '');
    sort($allHostIds);
    $myId = null;
    foreach ($allHostIds as $id) {
        if (strcasecmp($vars[$id] ?? '', $myName) === 0) { $myId = $id; break; }
    }
    if (!$myId) return [];

    // Running containers: name → image
    $running = [];
    $psOut = shell_exec("docker ps --format '{\"n\":\"{{.Names}}\",\"i\":\"{{.Image}}\"}' 2>/dev/null") ?: '';
    foreach (explode("\n", trim($psOut)) as $line) {
        $c = json_decode($line, true);
        if ($c) $running[strtolower($c['n'])] = $c['i'];
    }

    // Parse FALLBACK arrays from this host's conf
    $confFile = strtolower($myId) . '.conf';
    $rawConf  = vv_read_conf_raw($confFile);

    $result = [];
    foreach ($allHostIds as $covered) {
        if ($covered === $myId) continue;
        $coveredHostname = $vars[$covered] ?? '';
        if (!$coveredHostname) continue;

        $names = [];
        for ($tier = 1; $tier <= 4; $tier++)
            $names = array_merge($names, vv_parse_bash_array($rawConf, "FALLBACK_{$myId}_COVERS_{$covered}_TIER{$tier}"));

        $active = [];
        foreach ($names as $name) {
            if (isset($running[strtolower($name)]))
                $active[] = ['name' => $name, 'image' => $running[strtolower($name)]];
        }

        if ($active) $result[] = ['host_id' => $covered, 'hostname' => $coveredHostname, 'containers' => $active];
    }

    return $result;
}

function vv_watchdog_summary(): array {
    $parseKv = function(string $raw): array {
        $out = [];
        foreach (explode("\n", $raw) as $line) {
            $line = trim($line);
            if (str_contains($line, ':'))      { [$k, $v] = explode(':', $line, 2); $out[trim($k)] = trim($v); }
            elseif (str_contains($line, '='))  { [$k, $v] = explode('=', $line, 2); $out[trim($k)] = trim($v, '"\''); }
        }
        return $out;
    };

    $dock = $parseKv(@file_get_contents('/tmp/container_watchdog_state.db')  ?: '');
    $rw   = $parseKv(@file_get_contents('/tmp/resource_watchdog_state.db')   ?: '');

    $ctrStrikes = [];
    foreach ($dock as $k => $v) {
        if ($k !== 'daemon_strikes' && $k !== 'daemon_restarted_flag' && (int)$v > 0)
            $ctrStrikes[$k] = (int)$v;
    }

    // Recent restarts (24 h)
    $restartLog = DATA_DIR . '/container_restart_history.db';
    $restartRaw = @file_get_contents($restartLog) ?: '';
    $cutoff     = time() - 86400;
    $restarts   = [];
    foreach (explode("\n", trim($restartRaw)) as $line) {
        if (!$line || !str_contains($line, '|')) continue;
        [$name, $ts] = explode('|', $line, 2);
        if ((int)$ts >= $cutoff) $restarts[] = ['name' => trim($name), 'ts' => (int)$ts];
    }
    usort($restarts, fn($a, $b) => $b['ts'] - $a['ts']);

    // Reboots (12 h)
    $rebootRaw = @file_get_contents('/boot/config/system_watchdog_reboots.db') ?: '';
    $rbootCutoff = time() - 43200;
    $reboots = 0;
    foreach (explode("\n", trim($rebootRaw)) as $line) {
        if ((int)trim($line) >= $rbootCutoff) $reboots++;
    }

    $rwLevel       = (int)($rw['rm_action_level']  ?? 0);
    $daemonStrikes = (int)($dock['daemon_strikes']  ?? 0);
    $oomCount      = (int)trim(@file_get_contents('/tmp/system_watchdog_oom.db') ?: '0');

    // ── Stability watchdog strikes (/tmp/system_watchdog_state.db) ───────────
    $stabRaw = @file_get_contents('/tmp/system_watchdog_state.db') ?: '';
    $stabStrikes = [];
    foreach (explode("\n", $stabRaw) as $line) {
        $line = trim($line);
        if (!$line || !str_contains($line, ':')) continue;
        [$k, $v] = explode(':', $line, 2);
        $count = (int)trim($v);
        if ($count > 0) $stabStrikes[trim($k)] = $count;
    }

    // ── Storage watchdog strikes (/tmp/storage_watchdog_state.db) ────────────
    $storRaw = @file_get_contents('/tmp/storage_watchdog_state.db') ?: '';
    $growthStrikes = []; $logStrikes = [];
    foreach (explode("\n", $storRaw) as $line) {
        $line = trim($line);
        if (!$line || !str_contains($line, ':')) continue;
        [$k, $v] = explode(':', $line, 2);
        $count = (int)trim($v);
        if ($count <= 0) continue;
        $key = trim($k);
        if (str_starts_with($key, 'appdata_growth_'))
            $growthStrikes[substr($key, strlen('appdata_growth_'))] = $count;
        elseif (str_starts_with($key, 'appdata_log_'))
            $logStrikes[substr($key, strlen('appdata_log_'))] = $count;
    }

    // ── Network watchdog NPM strikes (/tmp/network_watchdog_state.db) ────────
    $netRaw = @file_get_contents('/tmp/network_watchdog_state.db') ?: '';
    $npmStrikes = 0;
    foreach (explode("\n", $netRaw) as $line) {
        $line = trim($line);
        if (str_starts_with($line, 'npm:')) $npmStrikes = (int)trim(substr($line, 4));
    }

    // ── Stability live stats ──────────────────────────────────────────────────
    $dfPct = function(string $path): int {
        $out = shell_exec("df " . escapeshellarg($path) . " --output=pcent 2>/dev/null | tail -1") ?: '';
        return (int)trim(str_replace('%', '', $out));
    };

    $memRaw   = @file_get_contents('/proc/meminfo') ?: '';
    $memAvail = 0;
    if (preg_match('/^MemAvailable:\s+(\d+)/m', $memRaw, $m)) $memAvail = (int)$m[1];

    $loadRaw = @file_get_contents('/proc/loadavg') ?: '0';
    $load1   = (float)explode(' ', trim($loadRaw))[0];

    $cpuTemp = null;
    $sensorsOut = shell_exec("sensors 2>/dev/null | grep -E 'Core 0|Package id 0|Tdie|Tctl|CPU Temp' | grep -oE '[0-9]+\\.[0-9]+' | sort -n | tail -1") ?: '';
    if ($sensorsOut && is_numeric(trim($sensorsOut))) $cpuTemp = (int)round((float)trim($sensorsOut));

    $zombies = (int)trim(shell_exec("ps -eo stat 2>/dev/null | grep -c '^Z'") ?: '0');

    $nic      = trim(shell_exec("ip route show default 2>/dev/null | awk 'NR==1{print \$5}'") ?: 'eth0') ?: 'eth0';
    $nicState = trim(@file_get_contents("/sys/class/net/$nic/operstate") ?: 'unknown');
    $sshdOk   = (int)trim(shell_exec('pgrep -c sshd 2>/dev/null') ?: '0') > 0;

    $healthy = empty($ctrStrikes) && empty($stabStrikes) && empty($growthStrikes) && empty($logStrikes)
        && $rwLevel === 0 && $daemonStrikes === 0 && $oomCount === 0 && $reboots === 0
        && $npmStrikes === 0 && $nicState === 'up' && $sshdOk;

    return [
        'healthy'        => $healthy,
        'ctr_strikes'    => $ctrStrikes,
        'rw_level'       => $rwLevel,
        'rw_paused'      => array_values(array_filter(explode(',', $rw['rm_paused_containers'] ?? ''))),
        'rw_stopped'     => array_values(array_filter(explode(',', $rw['rm_stopped_containers'] ?? ''))),
        'daemon_strikes' => $daemonStrikes,
        'oom_count'      => $oomCount,
        'reboots_12h'    => $reboots,
        'restarts_24h'   => array_slice($restarts, 0, 6),
        'restart_count'  => count($restarts),
        'stability'      => [
            'rootfs_pct'  => $dfPct('/'),
            'log_pct'     => $dfPct('/var/log'),
            'tmp_pct'     => $dfPct('/tmp'),
            'ram_free_gb' => round($memAvail / 1048576, 1),
            'load_1min'   => $load1,
            'cpu_temp'    => $cpuTemp,
            'zombies'     => $zombies,
            'nic'         => $nic,
            'nic_state'   => $nicState,
            'sshd_ok'     => $sshdOk,
            'strikes'     => $stabStrikes,
        ],
        'storage_wd'     => [
            'growth_strikes' => $growthStrikes,
            'log_strikes'    => $logStrikes,
        ],
        'network_wd'     => [
            'npm_strikes' => $npmStrikes,
        ],
    ];
}

function vv_scripts_status(): array {
    $logDir    = LOG_DIR;
    $statFiles = array_merge(
        glob("$logDir/*.json")   ?: [],
        glob("$logDir/*/*.json") ?: []
    );

    $scripts = [];
    foreach ($statFiles as $statFile) {
        $stat   = json_decode(@file_get_contents($statFile) ?: '{}', true) ?: [];
        $status = $stat['status'] ?? 'unknown';

        // Stale running — PID gone (crash or reboot with no cleanup)
        if ($status === 'running' && !empty($stat['pid'])) {
            if (!file_exists("/proc/{$stat['pid']}")) $status = 'error';
        }

        $id   = $stat['id'] ?? basename($statFile, '.json');
        $name = basename(preg_replace('/\.sh$/', '', $id));
        $ts   = (int)($stat['end'] ?? $stat['start'] ?? @filemtime($statFile) ?: 0);

        $scripts[] = [
            'id'       => $id,
            'name'     => $name,
            'last_ts'  => $ts,
            'status'   => $status,
            'running'  => $status === 'running',
            'exit'     => $stat['exit'] ?? null,
            'duration' => isset($stat['start'], $stat['end'])
                ? (int)$stat['end'] - (int)$stat['start'] : null,
        ];
    }

    usort($scripts, fn($a, $b) => ($b['last_ts'] ?? 0) <=> ($a['last_ts'] ?? 0));
    $scripts = array_slice($scripts, 0, 12);

    return [
        'scripts'       => $scripts,
        'running_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'running')),
        'ok_count'      => count(array_filter($scripts, fn($s) => $s['status'] === 'ok')),
        'warn_count'    => count(array_filter($scripts, fn($s) => $s['status'] === 'warn')),
        'error_count'   => count(array_filter($scripts, fn($s) => $s['status'] === 'error')),
    ];
}

function vv_rsync_status(): array {
    $vars = vv_conf_vars();

    $enabled = ($vars['RSYNC_ENABLED'] ?? 'true') !== 'false';

    $windows = [
        'critical'     => ($vars['CRITICAL_RSYNC_ENABLED']     ?? 'false') !== 'false',
        'daily'        => ($vars['DAILY_RSYNC_ENABLED']        ?? 'false') !== 'false',
        'intermediate' => ($vars['INTERMEDIATE_RSYNC_ENABLED'] ?? 'true')  !== 'false',
        'weekly'       => ($vars['WEEKLY_RSYNC_ENABLED']       ?? 'false') !== 'false',
    ];

    // Active rsync profiles — from lock files
    $lockDir = '/tmp/unraid_locks';
    $active  = [];
    foreach (glob("$lockDir/rsync_*.lock") ?: [] as $lf) {
        $content = trim(@file_get_contents($lf) ?: '');
        [$pid, $locked_name] = array_pad(explode(':', $content, 2), 2, '');
        if (!$pid || !file_exists("/proc/$pid")) continue;
        $profile  = preg_replace('/^rsync_/', '', $locked_name ?: basename($lf, '.lock'));
        $active[] = [
            'profile' => $profile,
            'pid'     => (int)$pid,
            'elapsed' => time() - (int)filemtime($lf),
        ];
    }

    // Last completed run per window (orchestrator log files)
    $scriptMap = [
        'critical'     => 'critical_sync_maintenance',
        'daily'        => 'daily_sync_maintenance',
        'intermediate' => 'intermediate_sync_maintenance',
        'weekly'       => 'weekly_sync_maintenance',
    ];
    $lastSync = [];
    foreach ($scriptMap as $key => $scriptName) {
        $logFile = LOG_DIR . "/$scriptName.json";
        if (!file_exists($logFile)) continue;
        $stat = json_decode(@file_get_contents($logFile) ?: '{}', true) ?: [];
        $lastSync[$key] = [
            'ts'       => (int)($stat['end'] ?? $stat['start'] ?? 0),
            'status'   => $stat['status'] ?? 'unknown',
            'duration' => isset($stat['start'], $stat['end'])
                ? (int)$stat['end'] - (int)$stat['start'] : null,
        ];
    }

    // Profile activity — last 7 days, aggregated per profile
    $bwLog   = DATA_DIR . '/bandwidth_history.db';
    $cutoff7 = date('Y-m-d', strtotime('-7 days'));
    $profiles = [];
    if (file_exists($bwLog)) {
        foreach (file($bwLog, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
            $p = explode('|', $line);
            if (count($p) < 4 || ($p[0] ?? '') < $cutoff7) continue;
            $name = $p[2] ?? '';
            if (!$name) continue;
            if (!isset($profiles[$name])) $profiles[$name] = ['runs' => 0, 'dur' => 0, 'bytes' => 0];
            $profiles[$name]['runs']++;
            $profiles[$name]['dur']   += (int)($p[3] ?? 0);
            $profiles[$name]['bytes'] += (int)($p[5] ?? 0);
        }
    }
    arsort($profiles); // sort by run count descending
    $bwSummary = array_slice($profiles, 0, 6, true);

    return [
        'enabled'    => $enabled,
        'windows'    => $windows,
        'active'     => $active,
        'last_sync'  => $lastSync,
        'bw_summary' => $bwSummary,
    ];
}


1	<?php
2	header('Content-Type: application/json');
3	require_once dirname(__DIR__) . '/include/config.php';
4
5	if (!isset($_GET['live'])) {
6	    $_vv_cached = vv_cache_read('monitor', 300);
7	    if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; }
8	    unset($_vv_cached);
9	}
10
11	require_once dirname(__DIR__) . '/include/monitor.php';
12	require_once dirname(__DIR__) . '/include/vms.php';
13	require_once dirname(__DIR__) . '/include/docker_folders.php';
14
15	// Pre-warm the API cache with one request (shared by all API-first functions below).
16	vv_api_data();
17
18	echo json_encode([
19	    'system'          => vv_system_info(),
20	    'fallback'        => vv_fallback_state(),
21	    'fallback_active' => vv_fallback_active(),
22	    'partner'         => vv_partner_state(),
23	    'resources'   => vv_system_resources(),
24	    'cpu'         => vv_cpu_per_core(),
25	    'mem'         => vv_memory_breakdown(),
26	    'net'         => vv_network_stats(),
27	    'gpu'         => vv_gpu_stats(),
28	    'gpu_procs'   => vv_gpu_processes(),
29	    'containers'  => vv_docker_containers(),
30	    'stopped'     => vv_docker_stopped(),
31	    'transcode'   => vv_transcode_sessions(),
32	    'ups'         => vv_ups_stats(),
33	    'parity'      => vv_parity_status(),
34	    'storage'     => vv_storage_pools(),
35	    'array_disks' => vv_array_disks(),
36	    'disk_io'     => vv_disk_io_rates(),
37	    'watchdog'       => vv_watchdog_summary(),
38	    'scripts'        => vv_scripts_status(),
39	    'rsync'          => vv_rsync_status(),
40	    'thresholds'     => vv_disk_thresholds(),
41	    'vms'            => vv_get_vms(),
42	    'docker_folders' => vv_get_docker_folders(),
43	    'remote_hosts'   => vv_remote_hosts_stats(),
44	    '_api_status'    => vv_api_get_status(),
45	    'ts'             => time(),
46	]);
47

<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

if (!isset($_GET['live'])) {
    $_vv_cached = vv_cache_read('monitor', 300);
    if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; }
    unset($_vv_cached);
}

require_once dirname(__DIR__) . '/include/monitor.php';
require_once dirname(__DIR__) . '/include/vms.php';
require_once dirname(__DIR__) . '/include/docker_folders.php';

// Pre-warm the API cache with one request (shared by all API-first functions below).
vv_api_data();

echo json_encode([
    'system'          => vv_system_info(),
    'fallback'        => vv_fallback_state(),
    'fallback_active' => vv_fallback_active(),
    'partner'         => vv_partner_state(),
    'resources'   => vv_system_resources(),
    'cpu'         => vv_cpu_per_core(),
    'mem'         => vv_memory_breakdown(),
    'net'         => vv_network_stats(),
    'gpu'         => vv_gpu_stats(),
    'gpu_procs'   => vv_gpu_processes(),
    'containers'  => vv_docker_containers(),
    'stopped'     => vv_docker_stopped(),
    'transcode'   => vv_transcode_sessions(),
    'ups'         => vv_ups_stats(),
    'parity'      => vv_parity_status(),
    'storage'     => vv_storage_pools(),
    'array_disks' => vv_array_disks(),
    'disk_io'     => vv_disk_io_rates(),
    'watchdog'       => vv_watchdog_summary(),
    'scripts'        => vv_scripts_status(),
    'rsync'          => vv_rsync_status(),
    'thresholds'     => vv_disk_thresholds(),
    'vms'            => vv_get_vms(),
    'docker_folders' => vv_get_docker_folders(),
    'remote_hosts'   => vv_remote_hosts_stats(),
    '_api_status'    => vv_api_get_status(),
    'ts'             => time(),
]);


so right now disk 10 is gone, i have a replacement coming sorta. but in monitor page parity is shown as invalid. parity is still valid and disk is being emulated. we need to turn the bar red on disks…


Now let me find where `vv_parity_status()` and `vv_array_disks()` are defined.


grep -n "function vv_parity_status\|function vv_array_disks\|function vv_storage_pools" /boot/config/plugins/varaverk/Plugin/unraid/include/*.php

Find parity and disk functions


/boot/config/plugins/varaverk/Plugin/unraid/include/common.php:401:function vv_parity_status(): array {
/boot/config/plugins/varaverk/Plugin/unraid/include/common.php:468:function vv_storage_pools(): array {
/boot/config/plugins/varaverk/Plugin/unraid/include/common.php:497:function vv_array_disks(): array {


395	        'num_xfers'  => (int)($fields['NUMXFERS'] ?? 0),
396	        'on_batt_s'  => $parse_num('CUMONBATT'),
397	        'selftest'   => $fields['SELFTEST'] ?? '',
398	    ];
399	}
400
401	function vv_parity_status(): array {
402	    $var = [];
403	    foreach (@file('/var/local/emhttp/var.ini') ?: [] as $line) {
404	        if (preg_match('/^(\w+)="([^"]*)"/', $line, $m)) $var[$m[1]] = $m[2];
405	    }
406
407	    $isValid    = ($var['mdNumInvalid'] ?? '0') === '0';
408	    $exitCode   = (int)($var['sbSyncExit'] ?? 0);
409	    $errors     = (int)($var['sbSyncErrs'] ?? 0);
410	    $inProgress = ($var['mdResync'] ?? '0') !== '0';
411	    $resyncPos  = (int)($var['mdResyncPos'] ?? 0);
412	    $resyncSize = (int)($var['mdResyncSize'] ?? 1);
413	    $resyncPct  = $resyncSize > 0 ? round($resyncPos / $resyncSize * 100, 1) : 0;
414
415	    // Last check from log
416	    $lastDate = null; $lastDuration = 0; $lastSpeed = 0; $lastErrors = 0; $lastExit = 0;
417	    $logFile  = '/boot/config/parity-checks.log';
418	    if (file_exists($logFile)) {
419	        $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
420	        if ($lines) {
421	            $p = explode('|', trim(end($lines)));
422	            $lastDate     = trim($p[0] ?? '');
423	            $lastDuration = (int)($p[1] ?? 0);
424	            $lastSpeed    = (int)($p[2] ?? 0);
425	            $lastExit     = (int)($p[3] ?? 0);
426	            $lastErrors   = (int)($p[4] ?? 0);
427	        }
428	    }
429
430	    // Parse last date string to timestamp
431	    $lastTs = $lastDate ? strtotime($lastDate) : null;
432
433	    // Next scheduled check from cron
434	    $nextTs   = null;
435	    $cronFile = '/boot/config/plugins/dynamix/parity-check.cron';
436	    foreach (@file($cronFile) ?: [] as $line) {
437	        $line = trim($line);
438	        if ($line === '' || $line[0] === '#') continue;
439	        if (!str_contains($line, 'mdcmd')) continue;
440	        $p = preg_split('/\s+/', $line);
441	        // cron: min hour dom month dow command...
442	        if (count($p) >= 5 && is_numeric($p[0]) && is_numeric($p[1]) && is_numeric($p[2])) {
443	            $next = new DateTime('now');
444	            $next->setTime((int)$p[1], (int)$p[0], 0);
445	            $next->setDate((int)$next->format('Y'), (int)$next->format('n'), (int)$p[2]);
446	            if ($next->getTimestamp() <= time()) $next->modify('+1 month');
447	            $nextTs = $next->getTimestamp();
448	        }
449	        break;
450	    }
451
452	    $exitMap = ['0' => 'Completed', '-4' => 'Aborted', '-5' => 'Cancelled'];
453	    return [
454	        'valid'         => $isValid,
455	        'in_progress'   => $inProgress,
456	        'resync_pct'    => $resyncPct,
457	        'exit_code'     => $exitCode,
458	        'exit_label'    => $exitMap[(string)$lastExit] ?? 'Unknown',
459	        'errors'        => $lastErrors,
460	        'last_date'     => $lastDate,
461	        'last_ts'       => $lastTs,
462	        'last_duration' => $lastDuration,
463	        'last_speed_mb' => $lastSpeed > 0 ? round($lastSpeed / 1048576, 1) : null,
464	        'next_ts'       => $nextTs,
465	    ];
466	}
467
468	function vv_storage_pools(): array {
469	    // ── API path ──────────────────────────────────────────────────────────────
470	    $api = vv_api_data();
471	    if ($api && isset($api['array']['caches'])) {
472	        $out = [];
473	        foreach ($api['array']['caches'] as $d) {
474	            $entry = vv_api_disk_entry($d, 'data');
475	            if ($entry) $out[] = $entry;
476	        }
477	        if (!empty($out)) {
478	            usort($out, fn($a, $b) => strcmp($a['name'], $b['name']));
479	            return $out;
480	        }
481	    }
482
483	    // ── Local fallback ────────────────────────────────────────────────────────
484	    vv_api_record_fallback('storage_pools');
485	    $ini = @parse_ini_file('/var/local/emhttp/disks.ini', true) ?: [];
486	    $out = [];
487	    foreach ($ini as $key => $d) {
488	        if (($d['type'] ?? '') !== 'Cache') continue;
489	        if (($d['fsStatus'] ?? '') !== 'Mounted') continue;
490	        $entry = vv_disk_entry($d, $key);
491	        if ($entry) $out[] = $entry;
492	    }
493	    usort($out, fn($a, $b) => strcmp($a['name'], $b['name']));
494	    return $out;
495	}
496
497	function vv_array_disks(): array {
498	    // ── API path ──────────────────────────────────────────────────────────────
499	    $api = vv_api_data();
500	    if ($api && (isset($api['array']['parities']) || isset($api['array']['disks']))) {
501	        $parity = [];
502	        $data   = [];
503	        foreach ($api['array']['parities'] ?? [] as $d) {
504	            $entry = vv_api_disk_entry($d, 'parity');
505	            if ($entry) $parity[] = $entry;
506	        }
507	        foreach ($api['array']['disks'] ?? [] as $d) {
508	            $entry = vv_api_disk_entry($d, 'data');
509	            if ($entry) $data[] = $entry;
510	        }
511	        if (!empty($parity) || !empty($data)) {
512	            usort($parity, fn($a, $b) => strnatcmp($a['name'], $b['name']));
513	            usort($data,   fn($a, $b) => strnatcmp($a['name'], $b['name']));
514	            return array_merge($parity, $data);
515	        }
516	    }
517
518	    // ── Local fallback ────────────────────────────────────────────────────────
519	    vv_api_record_fallback('array_disks');
520	    $ini     = @parse_ini_file('/var/local/emhttp/disks.ini', true) ?: [];
521	    $parity  = [];
522	    $data    = [];
523	    foreach ($ini as $key => $d) {
524	        $type = $d['type'] ?? '';
525	        if ($type === 'Parity') {
526	            $entry = vv_disk_entry($d, $key, 'parity');
527	            if ($entry) $parity[] = $entry;
528	        } elseif ($type === 'Data') {
529	            $entry = vv_disk_entry($d, $key, 'data');
530	            if ($entry) $data[] = $entry;
531	        }
532	    }
533	    usort($parity, fn($a, $b) => strnatcmp($a['name'], $b['name']));
534	    usort($data,   fn($a, $b) => strnatcmp($a['name'], $b['name']));
535	    return array_merge($parity, $data);
536	}
537
538	function vv_disk_io_rates(): array {
539	    $snapFile = VV_CACHE_DIR . '/vv_diskio_snap.json';
540	    $now      = microtime(true);
541
542	    // Read current whole-disk stats from /proc/diskstats
543	    $current = [];
544	    foreach (@file('/proc/diskstats', FILE_IGNORE_NEW_LINES) ?: [] as $line) {
545	        $p = preg_split('/\s+/', trim($line));
546	        if (count($p) < 14) continue;
547	        $dev = $p[2];
548	        // Keep only whole disks: sda/sdb, nvme0n1, md*, not sda1/nvme0n1p1
549	        if (!preg_match('/^(sd[a-z]+|nvme\d+n\d+|md\d+)$/', $dev)) continue;
550	        $current[$dev] = [(int)$p[5], (int)$p[9]]; // [sectors_read, sectors_written]
551	    }
552
553	    // Load previous snapshot
554	    $snap     = @json_decode(@file_get_contents($snapFile) ?: '', true) ?: [];

        'num_xfers'  => (int)($fields['NUMXFERS'] ?? 0),
        'on_batt_s'  => $parse_num('CUMONBATT'),
        'selftest'   => $fields['SELFTEST'] ?? '',
    ];
}

function vv_parity_status(): array {
    $var = [];
    foreach (@file('/var/local/emhttp/var.ini') ?: [] as $line) {
        if (preg_match('/^(\w+)="([^"]*)"/', $line, $m)) $var[$m[1]] = $m[2];
    }

    $isValid    = ($var['mdNumInvalid'] ?? '0') === '0';
    $exitCode   = (int)($var['sbSyncExit'] ?? 0);
    $errors     = (int)($var['sbSyncErrs'] ?? 0);
    $inProgress = ($var['mdResync'] ?? '0') !== '0';
    $resyncPos  = (int)($var['mdResyncPos'] ?? 0);
    $resyncSize = (int)($var['mdResyncSize'] ?? 1);
    $resyncPct  = $resyncSize > 0 ? round($resyncPos / $resyncSize * 100, 1) : 0;

    // Last check from log
    $lastDate = null; $lastDuration = 0; $lastSpeed = 0; $lastErrors = 0; $lastExit = 0;
    $logFile  = '/boot/config/parity-checks.log';
    if (file_exists($logFile)) {
        $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
        if ($lines) {
            $p = explode('|', trim(end($lines)));
            $lastDate     = trim($p[0] ?? '');
            $lastDuration = (int)($p[1] ?? 0);
            $lastSpeed    = (int)($p[2] ?? 0);
            $lastExit     = (int)($p[3] ?? 0);
            $lastErrors   = (int)($p[4] ?? 0);
        }
    }

    // Parse last date string to timestamp
    $lastTs = $lastDate ? strtotime($lastDate) : null;

    // Next scheduled check from cron
    $nextTs   = null;
    $cronFile = '/boot/config/plugins/dynamix/parity-check.cron';
    foreach (@file($cronFile) ?: [] as $line) {
        $line = trim($line);
        if ($line === '' || $line[0] === '#') continue;
        if (!str_contains($line, 'mdcmd')) continue;
        $p = preg_split('/\s+/', $line);
        // cron: min hour dom month dow command...
        if (count($p) >= 5 && is_numeric($p[0]) && is_numeric($p[1]) && is_numeric($p[2])) {
            $next = new DateTime('now');
            $next->setTime((int)$p[1], (int)$p[0], 0);
            $next->setDate((int)$next->format('Y'), (int)$next->format('n'), (int)$p[2]);
            if ($next->getTimestamp() <= time()) $next->modify('+1 month');
            $nextTs = $next->getTimestamp();
        }
        break;
    }

    $exitMap = ['0' => 'Completed', '-4' => 'Aborted', '-5' => 'Cancelled'];
    return [
        'valid'         => $isValid,
        'in_progress'   => $inProgress,
        'resync_pct'    => $resyncPct,
        'exit_code'     => $exitCode,
        'exit_label'    => $exitMap[(string)$lastExit] ?? 'Unknown',
        'errors'        => $lastErrors,
        'last_date'     => $lastDate,
        'last_ts'       => $lastTs,
        'last_duration' => $lastDuration,
        'last_speed_mb' => $lastSpeed > 0 ? round($lastSpeed / 1048576, 1) : null,
        'next_ts'       => $nextTs,
    ];
}

function vv_storage_pools(): array {
    // ── API path ──────────────────────────────────────────────────────────────
    $api = vv_api_data();
    if ($api && isset($api['array']['caches'])) {
        $out = [];
        foreach ($api['array']['caches'] as $d) {
            $entry = vv_api_disk_entry($d, 'data');
            if ($entry) $out[] = $entry;
        }
        if (!empty($out)) {
            usort($out, fn($a, $b) => strcmp($a['name'], $b['name']));
            return $out;
        }
    }

    // ── Local fallback ────────────────────────────────────────────────────────
    vv_api_record_fallback('storage_pools');
    $ini = @parse_ini_file('/var/local/emhttp/disks.ini', true) ?: [];
    $out = [];
    foreach ($ini as $key => $d) {
        if (($d['type'] ?? '') !== 'Cache') continue;
        if (($d['fsStatus'] ?? '') !== 'Mounted') continue;
        $entry = vv_disk_entry($d, $key);
        if ($entry) $out[] = $entry;
    }
    usort($out, fn($a, $b) => strcmp($a['name'], $b['name']));
    return $out;
}

function vv_array_disks(): array {
    // ── API path ──────────────────────────────────────────────────────────────
    $api = vv_api_data();
    if ($api && (isset($api['array']['parities']) || isset($api['array']['disks']))) {
        $parity = [];
        $data   = [];
        foreach ($api['array']['parities'] ?? [] as $d) {
            $entry = vv_api_disk_entry($d, 'parity');
            if ($entry) $parity[] = $entry;
        }
        foreach ($api['array']['disks'] ?? [] as $d) {
            $entry = vv_api_disk_entry($d, 'data');
            if ($entry) $data[] = $entry;
        }
        if (!empty($parity) || !empty($data)) {
            usort($parity, fn($a, $b) => strnatcmp($a['name'], $b['name']));
            usort($data,   fn($a, $b) => strnatcmp($a['name'], $b['name']));
            return array_merge($parity, $data);
        }
    }

    // ── Local fallback ────────────────────────────────────────────────────────
    vv_api_record_fallback('array_disks');
    $ini     = @parse_ini_file('/var/local/emhttp/disks.ini', true) ?: [];
    $parity  = [];
    $data    = [];
    foreach ($ini as $key => $d) {
        $type = $d['type'] ?? '';
        if ($type === 'Parity') {
            $entry = vv_disk_entry($d, $key, 'parity');
            if ($entry) $parity[] = $entry;
        } elseif ($type === 'Data') {
            $entry = vv_disk_entry($d, $key, 'data');
            if ($entry) $data[] = $entry;
        }
    }
    usort($parity, fn($a, $b) => strnatcmp($a['name'], $b['name']));
    usort($data,   fn($a, $b) => strnatcmp($a['name'], $b['name']));
    return array_merge($parity, $data);
}

function vv_disk_io_rates(): array {
    $snapFile = VV_CACHE_DIR . '/vv_diskio_snap.json';
    $now      = microtime(true);

    // Read current whole-disk stats from /proc/diskstats
    $current = [];
    foreach (@file('/proc/diskstats', FILE_IGNORE_NEW_LINES) ?: [] as $line) {
        $p = preg_split('/\s+/', trim($line));
        if (count($p) < 14) continue;
        $dev = $p[2];
        // Keep only whole disks: sda/sdb, nvme0n1, md*, not sda1/nvme0n1p1
        if (!preg_match('/^(sd[a-z]+|nvme\d+n\d+|md\d+)$/', $dev)) continue;
        $current[$dev] = [(int)$p[5], (int)$p[9]]; // [sectors_read, sectors_written]
    }

    // Load previous snapshot
    $snap     = @json_decode(@file_get_contents($snapFile) ?: '', true) ?: [];


Now let me find the disk entry functions and the frontend rendering of the disk/parity sections.


grep -n "function vv_disk_entry\|function vv_api_disk_entry" /boot/config/plugins/varaverk/Plugin/unraid/include/common.php

Find disk entry functions


343:function vv_disk_entry(array $d, string $key, string $role = 'data'): ?array {


grep -n "function vv_api_disk_entry" /boot/config/plugins/varaverk/Plugin/unraid/include/*.php

Find vv_api_disk_entry across all includes


/boot/config/plugins/varaverk/Plugin/unraid/include/unraid_api.php:114:function vv_api_disk_entry(array $d, string $role = ''): ?array {


343	function vv_disk_entry(array $d, string $key, string $role = 'data'): ?array {
344	    $name      = $d['name'] ?? $key;
345	    $isParity  = $role === 'parity';
346	    $mounted   = ($d['fsStatus'] ?? '') === 'Mounted';
347	    // Parity has no filesystem — use raw size only
348	    $size_kb   = (int)($isParity ? ($d['size'] ?? 0) : ($mounted ? ($d['fsSize'] ?? 0) : ($d['size'] ?? 0)));
349	    $used_kb   = (int)($isParity ? 0             : ($mounted ? ($d['fsUsed'] ?? 0) : 0));
350	    if ($size_kb <= 0) return null;
351	    $tempRaw   = trim($d['temp'] ?? '');
352	    return [
353	        'name'      => $name,
354	        'device'    => $d['device'] ?? $key,
355	        'role'      => $role,
356	        'size_gb'   => round($size_kb / 1048576, 1),
357	        'used_gb'   => round($used_kb / 1048576, 1),
358	        'pct'       => (!$isParity && $size_kb > 0) ? round($used_kb / $size_kb * 100, 1) : null,
359	        'temp'      => is_numeric($tempRaw) ? (int)$tempRaw : null,
360	        'transport' => $d['transport'] ?? 'ata',
361	        'mounted'   => $mounted,
362	        'status'    => $d['status'] ?? '',
363	    ];
364	}
365
366	function vv_ups_stats(): array {
367	    $raw = shell_exec('apcaccess 2>/dev/null') ?: '';
368	    if (!$raw) return ['available' => false];
369
370	    $fields = [];
371	    foreach (explode("\n", $raw) as $line) {
372	        if (preg_match('/^(\w+)\s*:\s*(.+)$/', trim($line), $m)) {
373	            $fields[trim($m[1])] = trim($m[2]);
374	        }
375	    }
376	    if (empty($fields)) return ['available' => false];
377
378	    $parse_num = fn(string $k) => isset($fields[$k]) ? (float)$fields[$k] : null;
379
380	    $loadPct  = $parse_num('LOADPCT');
381	    $nomPower = $parse_num('NOMPOWER');
382	    $watts    = ($loadPct !== null && $nomPower !== null) ? round($loadPct / 100 * $nomPower) : null;
383
384	    return [
385	        'available'  => true,
386	        'model'      => $fields['MODEL']   ?? '',
387	        'status'     => trim(explode(' ', $fields['STATUS'] ?? 'UNKNOWN')[0]),
388	        'line_v'     => $parse_num('LINEV'),
389	        'output_v'   => $parse_num('OUTPUTV'),
390	        'load_pct'   => $loadPct,
391	        'nom_power'  => $nomPower,
392	        'watts'      => $watts,
393	        'bcharge'    => $parse_num('BCHARGE'),
394	        'timeleft'   => $parse_num('TIMELEFT'),
395	        'num_xfers'  => (int)($fields['NUMXFERS'] ?? 0),
396	        'on_batt_s'  => $parse_num('CUMONBATT'),
397	        'selftest'   => $fields['SELFTEST'] ?? '',
398	    ];
399	}
400
401	function vv_parity_status(): array {
402	    $var = [];

function vv_disk_entry(array $d, string $key, string $role = 'data'): ?array {
    $name      = $d['name'] ?? $key;
    $isParity  = $role === 'parity';
    $mounted   = ($d['fsStatus'] ?? '') === 'Mounted';
    // Parity has no filesystem — use raw size only
    $size_kb   = (int)($isParity ? ($d['size'] ?? 0) : ($mounted ? ($d['fsSize'] ?? 0) : ($d['size'] ?? 0)));
    $used_kb   = (int)($isParity ? 0             : ($mounted ? ($d['fsUsed'] ?? 0) : 0));
    if ($size_kb <= 0) return null;
    $tempRaw   = trim($d['temp'] ?? '');
    return [
        'name'      => $name,
        'device'    => $d['device'] ?? $key,
        'role'      => $role,
        'size_gb'   => round($size_kb / 1048576, 1),
        'used_gb'   => round($used_kb / 1048576, 1),
        'pct'       => (!$isParity && $size_kb > 0) ? round($used_kb / $size_kb * 100, 1) : null,
        'temp'      => is_numeric($tempRaw) ? (int)$tempRaw : null,
        'transport' => $d['transport'] ?? 'ata',
        'mounted'   => $mounted,
        'status'    => $d['status'] ?? '',
    ];
}

function vv_ups_stats(): array {
    $raw = shell_exec('apcaccess 2>/dev/null') ?: '';
    if (!$raw) return ['available' => false];

    $fields = [];
    foreach (explode("\n", $raw) as $line) {
        if (preg_match('/^(\w+)\s*:\s*(.+)$/', trim($line), $m)) {
            $fields[trim($m[1])] = trim($m[2]);
        }
    }
    if (empty($fields)) return ['available' => false];

    $parse_num = fn(string $k) => isset($fields[$k]) ? (float)$fields[$k] : null;

    $loadPct  = $parse_num('LOADPCT');
    $nomPower = $parse_num('NOMPOWER');
    $watts    = ($loadPct !== null && $nomPower !== null) ? round($loadPct / 100 * $nomPower) : null;

    return [
        'available'  => true,
        'model'      => $fields['MODEL']   ?? '',
        'status'     => trim(explode(' ', $fields['STATUS'] ?? 'UNKNOWN')[0]),
        'line_v'     => $parse_num('LINEV'),
        'output_v'   => $parse_num('OUTPUTV'),
        'load_pct'   => $loadPct,
        'nom_power'  => $nomPower,
        'watts'      => $watts,
        'bcharge'    => $parse_num('BCHARGE'),
        'timeleft'   => $parse_num('TIMELEFT'),
        'num_xfers'  => (int)($fields['NUMXFERS'] ?? 0),
        'on_batt_s'  => $parse_num('CUMONBATT'),
        'selftest'   => $fields['SELFTEST'] ?? '',
    ];
}

function vv_parity_status(): array {
    $var = [];


114	function vv_api_disk_entry(array $d, string $role = ''): ?array {
115	    if (!$role) $role = _vv_api_disk_role((string)($d['type'] ?? 'DATA'));
116
117	    if ($role === 'parity') {
118	        // Parity disks have no filesystem — use raw size only.
119	        $sizeRaw = (float)($d['size'] ?? 0);
120	        if ($sizeRaw <= 0) return null;
121	        $sizeGb  = _vv_api_bytes_to_gb($sizeRaw);
122	        $usedGb  = 0.0;
123	        $pct     = null;
124	    } else {
125	        // Data/cache disks: prefer fsSize/fsUsed; fall back to size if unmounted.
126	        $sizeRaw = (float)($d['fsSize'] ?? $d['size'] ?? 0);
127	        if ($sizeRaw <= 0) return null;
128	        $usedRaw = (float)($d['fsUsed'] ?? 0);
129	        $sizeGb  = _vv_api_bytes_to_gb($sizeRaw);
130	        $usedGb  = _vv_api_bytes_to_gb($usedRaw);
131	        $pct     = $sizeGb > 0 ? round($usedGb / $sizeGb * 100, 1) : null;
132	    }
133
134	    $temp      = isset($d['temp']) && is_numeric($d['temp']) ? (int)$d['temp'] : null;
135	    $spinning  = (bool)($d['isSpinning'] ?? true);
136	    $transport = $d['transport'] ?? (str_contains(strtolower($d['device'] ?? ''), 'nvme') ? 'nvme' : 'ata');
137
138	    return [
139	        'name'      => $d['name']   ?? '',
140	        'device'    => $d['device'] ?? '',
141	        'role'      => $role,
142	        'size_gb'   => $sizeGb,
143	        'used_gb'   => $usedGb,
144	        'pct'       => $pct,
145	        'temp'      => $temp,
146	        'transport' => strtolower($transport),
147	        'mounted'   => $spinning,
148	        'status'    => $d['status'] ?? 'DISK_OK',
149	    ];
150	}
151
152	// ── Node metrics extractor ────────────────────────────────────────────────────
153
154	// Parse CPU%, RAM, array storage, disk temps, and VM count from a raw API response.
155	// Used by vv_remote_hosts_stats() and the local vv_api_data() path — one parser, no duplication.
156	// GQL must include: metrics.cpu.percentTotal, metrics.memory.{total,used},
157	//   array.{disks,caches,parities}.{fsSize,fsUsed,temp}, vms.domains.
158	function vv_api_node_metrics(?array $d): array {
159	    if (!$d) return [];
160	    $cpu = (int)round((float)($d['metrics']['cpu']['percentTotal'] ?? 0));
161	    $mem = $d['metrics']['memory'] ?? [];
162	    $ramUsed = isset($mem['used'])  ? _vv_api_bytes_to_gb((float)$mem['used'])  : null;
163	    $ramTot  = isset($mem['total']) ? _vv_api_bytes_to_gb((float)$mem['total']) : null;
164
165	    $disks  = $d['array']['disks']    ?? [];
166	    $caches = $d['array']['caches']   ?? [];
167	    $pars   = $d['array']['parities'] ?? [];
168	    $usedGb = 0.0; $totGb = 0.0;
169	    foreach (array_merge($disks, $caches) as $dk) {
170	        $sz = (float)($dk['fsSize'] ?? 0);
171	        if ($sz <= 0) continue;
172	        $totGb  += _vv_api_bytes_to_gb($sz);
173	        $usedGb += _vv_api_bytes_to_gb((float)($dk['fsUsed'] ?? 0));
174	    }
175	    $temps = array_filter(
176	        array_merge(array_column($disks,'temp'), array_column($caches,'temp'), array_column($pars,'temp')),
177	        fn($t) => is_numeric($t) && $t > 0
178	    );
179	    return [
180	        'cpu_pct'        => $cpu,
181	        'ram_used_gb'    => $ramUsed !== null ? round($ramUsed, 1) : null,
182	        'ram_total_gb'   => $ramTot  !== null ? round($ramTot,  1) : null,
183	        'array_used_tb'  => $totGb > 0 ? round($usedGb / 1000, 1) : null,
184	        'array_total_tb' => $totGb > 0 ? round($totGb  / 1000, 1) : null,
185	        'max_disk_temp'  => $temps ? (int)max($temps) : null,
186	        'vm_count'       => count($d['vms']['domains'] ?? []),
187	    ];
188	}
189
190	// ── Local host stats snapshot — same shape as one entry from vv_remote_hosts_stats() ─────────
191	// Called locally and via SSH by remote_arr_cache_writer.sh on remote hosts.
192	function vv_local_host_stats(): array {
193	    $host = vv_detect_host();

function vv_api_disk_entry(array $d, string $role = ''): ?array {
    if (!$role) $role = _vv_api_disk_role((string)($d['type'] ?? 'DATA'));

    if ($role === 'parity') {
        // Parity disks have no filesystem — use raw size only.
        $sizeRaw = (float)($d['size'] ?? 0);
        if ($sizeRaw <= 0) return null;
        $sizeGb  = _vv_api_bytes_to_gb($sizeRaw);
        $usedGb  = 0.0;
        $pct     = null;
    } else {
        // Data/cache disks: prefer fsSize/fsUsed; fall back to size if unmounted.
        $sizeRaw = (float)($d['fsSize'] ?? $d['size'] ?? 0);
        if ($sizeRaw <= 0) return null;
        $usedRaw = (float)($d['fsUsed'] ?? 0);
        $sizeGb  = _vv_api_bytes_to_gb($sizeRaw);
        $usedGb  = _vv_api_bytes_to_gb($usedRaw);
        $pct     = $sizeGb > 0 ? round($usedGb / $sizeGb * 100, 1) : null;
    }

    $temp      = isset($d['temp']) && is_numeric($d['temp']) ? (int)$d['temp'] : null;
    $spinning  = (bool)($d['isSpinning'] ?? true);
    $transport = $d['transport'] ?? (str_contains(strtolower($d['device'] ?? ''), 'nvme') ? 'nvme' : 'ata');

    return [
        'name'      => $d['name']   ?? '',
        'device'    => $d['device'] ?? '',
        'role'      => $role,
        'size_gb'   => $sizeGb,
        'used_gb'   => $usedGb,
        'pct'       => $pct,
        'temp'      => $temp,
        'transport' => strtolower($transport),
        'mounted'   => $spinning,
        'status'    => $d['status'] ?? 'DISK_OK',
    ];
}

// ── Node metrics extractor ────────────────────────────────────────────────────

// Parse CPU%, RAM, array storage, disk temps, and VM count from a raw API response.
// Used by vv_remote_hosts_stats() and the local vv_api_data() path — one parser, no duplication.
// GQL must include: metrics.cpu.percentTotal, metrics.memory.{total,used},
//   array.{disks,caches,parities}.{fsSize,fsUsed,temp}, vms.domains.
function vv_api_node_metrics(?array $d): array {
    if (!$d) return [];
    $cpu = (int)round((float)($d['metrics']['cpu']['percentTotal'] ?? 0));
    $mem = $d['metrics']['memory'] ?? [];
    $ramUsed = isset($mem['used'])  ? _vv_api_bytes_to_gb((float)$mem['used'])  : null;
    $ramTot  = isset($mem['total']) ? _vv_api_bytes_to_gb((float)$mem['total']) : null;

    $disks  = $d['array']['disks']    ?? [];
    $caches = $d['array']['caches']   ?? [];
    $pars   = $d['array']['parities'] ?? [];
    $usedGb = 0.0; $totGb = 0.0;
    foreach (array_merge($disks, $caches) as $dk) {
        $sz = (float)($dk['fsSize'] ?? 0);
        if ($sz <= 0) continue;
        $totGb  += _vv_api_bytes_to_gb($sz);
        $usedGb += _vv_api_bytes_to_gb((float)($dk['fsUsed'] ?? 0));
    }
    $temps = array_filter(
        array_merge(array_column($disks,'temp'), array_column($caches,'temp'), array_column($pars,'temp')),
        fn($t) => is_numeric($t) && $t > 0
    );
    return [
        'cpu_pct'        => $cpu,
        'ram_used_gb'    => $ramUsed !== null ? round($ramUsed, 1) : null,
        'ram_total_gb'   => $ramTot  !== null ? round($ramTot,  1) : null,
        'array_used_tb'  => $totGb > 0 ? round($usedGb / 1000, 1) : null,
        'array_total_tb' => $totGb > 0 ? round($totGb  / 1000, 1) : null,
        'max_disk_temp'  => $temps ? (int)max($temps) : null,
        'vm_count'       => count($d['vms']['domains'] ?? []),
    ];
}

// ── Local host stats snapshot — same shape as one entry from vv_remote_hosts_stats() ─────────
// Called locally and via SSH by remote_arr_cache_writer.sh on remote hosts.
function vv_local_host_stats(): array {
    $host = vv_detect_host();


Now let me look at the frontend JS for how disk status and parity are rendered.


grep -n "parity\|array_disks\|DISK_OK\|emulat\|invalid\|disk.*red\|status.*disk\|disk.*status\|bar.*red\|red.*bar" /boot/config/plugins/varaverk/Plugin/unraid/js/varaverk.js | head -80

Find disk/parity rendering in JS


(Bash completed with no output)


grep -rn "parity\|array_disks\|DISK_OK\|emulat\|invalid" /boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php | head -60

Find parity/disk rendering in monitor page


/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php:160:

/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php:168:
Loading...

/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php:563:  const isParity  = disk.role === 'parity';
/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php:969:      const par = d.parity ?? {};
/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php:1035:        document.getElementById('vv-parity-body').innerHTML = html;
/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php:1191:          const okDisks   = disks.filter(d => d.status === 'DISK_OK' || !d.status).length;
/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php:1222:      const arrayDisks = d.array_disks ?? [];
/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php:1225:        const okDisks     = dataDisks.filter(d => d.status === 'DISK_OK').length;
/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php:1931:    const allOk       = drives.every(d => d.status === 'DISK_OK' || !d.status);


550	  devices.forEach(dev => { const io = vvDiskIo[dev]; if (io) { tr += io.tr ?? 0; tw += io.tw ?? 0; } });
551	  return [tr, tw];
552	}
553	function vvFmtGb(gb) {
554	  if (gb >= 1024) return (gb / 1024).toFixed(1) + ' TB';
555	  if (gb >= 1)    return gb.toFixed(1) + ' GB';
556	  return (gb * 1024).toFixed(0) + ' MB';
557	}
558
559	function vvDiskRow(disk) {
560	  const tempC     = disk.temp;
561	  const tempColor = vvTempColor(tempC, disk.transport);
562	  const tempStr   = tempC !== null ? `${tempC}°` : '—';
563	  const isParity  = disk.role === 'parity';
564	  const nameColor = isParity ? '#6a8faf' : '#aaa';
565	  const pct       = disk.pct ?? 0;
566	  const barColor  = isParity ? '#1e3a5a'
567	    : pct >= vvThresholds.util_crit ? '#f44336'
568	    : pct >= vvThresholds.util_warn ? '#ff9800'
569	    : '#4caf50';
570	  const barWidth  = isParity ? '100' : pct;
571	  const spinLabel = (!isParity && !disk.mounted) ? `<span style="color:#555;font-size:9px;margin-left:4px;">↓</span>` : '';
572	  const right     = isParity
573	    ? `<span style="color:#444;font-size:10px;">${vvFmt(disk.size_gb)}</span>`
574	    : `<span style="color:#555;font-size:10px;">${vvFmt(disk.used_gb)} / ${vvFmt(disk.size_gb)}</span>`;
575	  return `

576

577	      ${disk.name}${spinLabel}${vvIoChip(disk.device)}
578	      ${right}
579	      ${tempStr}
580

581

582

583

584
`;
585	}
586
587	function vvDiskCol(disks) {
588	  return `<div style="flex:1;min-width:0;">${disks.map(vvDiskRow).join('')}</div>`;
589	}
590
591	// ── Poll ──────────────────────────────────────────────────────────────────────
592
593	let _vvFirstPoll = true;
594
595	function vvPollMonitor() {
596	  const _url = _vvFirstPoll ? '/plugins/varaverk/api/monitor.php' : '/plugins/varaverk/api/monitor.php?live=1';
597	  _vvFirstPoll = false;
598	  fetch(_url)
599	    .then(r => r.json())
600	    .then(d => {
601
602	      // ── API status banner ────────────────────────────────────────────────────
603	      const apiStatus = d._api_status ?? {};
604	      const banner    = document.getElementById('vv-api-banner');
605	      if (banner) {
606	        const fallbacks = apiStatus.fallbacks ?? [];
607	        if (fallbacks.length === 0) {
608	          banner.style.display = 'none';
609	        } else if (apiStatus.key_missing) {
610	          // Key not configured — informational, not alarming
611	          Object.assign(banner.style, {display:'', background:'#111', border:'1px solid #2a2a2a', color:'#555'});
612	          banner.textContent = 'API key not configured — add HOST' +
613	            (typeof vvLocalHostConf !== 'undefined' ? vvLocalHostConf.replace(/\D/g,'') : '1') +
614	            '_UNRAID_API_KEY in Scheduler → host conf to enable enhanced monitoring.';
615	        } else {
616	          // Key present but API unreachable — genuine problem
617	          Object.assign(banner.style, {display:'', background:'#1a1200', border:'1px solid #3a2800', color:'#ff9800'});
618	          banner.innerHTML = '⚠ Unraid API unreachable — using local reads. Check API key in host conf.' +
619	            (fallbacks.length ? ' (' + fallbacks.join(', ') + ')' : '');
620	        }
621	      }
622
623	      // ── Thresholds (from dynamix.cfg via backend) ────────────────────────────
624	      if (d.thresholds) vvThresholds = d.thresholds;
625
626	      // ── System ──────────────────────────────────────────────────────────────
627	      const sys = d.system ?? {};
628	      const now = new Date();
629	      const timeStr = now.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});

devices.forEach(dev => { const io = vvDiskIo[dev]; if (io) { tr += io.tr ?? 0; tw += io.tw ?? 0; } });
  return [tr, tw];
}
function vvFmtGb(gb) {
  if (gb >= 1024) return (gb / 1024).toFixed(1) + ' TB';
  if (gb >= 1)    return gb.toFixed(1) + ' GB';
  return (gb * 1024).toFixed(0) + ' MB';
}

function vvDiskRow(disk) {
  const tempC     = disk.temp;
  const tempColor = vvTempColor(tempC, disk.transport);
  const tempStr   = tempC !== null ? `${tempC}°` : '—';
  const isParity  = disk.role === 'parity';
  const nameColor = isParity ? '#6a8faf' : '#aaa';
  const pct       = disk.pct ?? 0;
  const barColor  = isParity ? '#1e3a5a'
    : pct >= vvThresholds.util_crit ? '#f44336'
    : pct >= vvThresholds.util_warn ? '#ff9800'
    : '#4caf50';
  const barWidth  = isParity ? '100' : pct;
  const spinLabel = (!isParity && !disk.mounted) ? `<span style="color:#555;font-size:9px;margin-left:4px;">↓</span>` : '';
  const right     = isParity
    ? `<span style="color:#444;font-size:10px;">${vvFmt(disk.size_gb)}</span>`
    : `<span style="color:#555;font-size:10px;">${vvFmt(disk.used_gb)} / ${vvFmt(disk.size_gb)}</span>`;
  return `

      ${disk.name}${spinLabel}${vvIoChip(disk.device)}
      ${right}
      ${tempStr}

`;
}

function vvDiskCol(disks) {
  return `<div style="flex:1;min-width:0;">${disks.map(vvDiskRow).join('')}</div>`;
}

// ── Poll ──────────────────────────────────────────────────────────────────────

let _vvFirstPoll = true;

function vvPollMonitor() {
  const _url = _vvFirstPoll ? '/plugins/varaverk/api/monitor.php' : '/plugins/varaverk/api/monitor.php?live=1';
  _vvFirstPoll = false;
  fetch(_url)
    .then(r => r.json())
    .then(d => {

      // ── API status banner ────────────────────────────────────────────────────
      const apiStatus = d._api_status ?? {};
      const banner    = document.getElementById('vv-api-banner');
      if (banner) {
        const fallbacks = apiStatus.fallbacks ?? [];
        if (fallbacks.length === 0) {
          banner.style.display = 'none';
        } else if (apiStatus.key_missing) {
          // Key not configured — informational, not alarming
          Object.assign(banner.style, {display:'', background:'#111', border:'1px solid #2a2a2a', color:'#555'});
          banner.textContent = 'API key not configured — add HOST' +
            (typeof vvLocalHostConf !== 'undefined' ? vvLocalHostConf.replace(/\D/g,'') : '1') +
            '_UNRAID_API_KEY in Scheduler → host conf to enable enhanced monitoring.';
        } else {
          // Key present but API unreachable — genuine problem
          Object.assign(banner.style, {display:'', background:'#1a1200', border:'1px solid #3a2800', color:'#ff9800'});
          banner.innerHTML = '⚠ Unraid API unreachable — using local reads. Check API key in host conf.' +
            (fallbacks.length ? ' (' + fallbacks.join(', ') + ')' : '');
        }
      }

      // ── Thresholds (from dynamix.cfg via backend) ────────────────────────────
      if (d.thresholds) vvThresholds = d.thresholds;

      // ── System ──────────────────────────────────────────────────────────────
      const sys = d.system ?? {};
      const now = new Date();
      const timeStr = now.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});


960	             Out   ${outV}
961	             Xfers  0 ? '#ff9800' : '#444'};">${xfers}
962	             Test  ${ups.selftest || '—'}
963
`;
964	      } else {
965	        document.getElementById('vv-ups-body').innerHTML = '
No UPS detected
';
966	      }
967
968	      // ── Parity ──────────────────────────────────────────────────────────────
969	      const par = d.parity ?? {};
970	      (function() {
971	        const valid      = par.valid !== false;
972	        const inProg     = par.in_progress;
973	        const validColor = valid ? '#4caf50' : '#f44336';
974	        const validLabel = valid ? 'Parity is valid' : 'Parity is INVALID';
975
976	        function vvRelTime(ts) {
977	          if (!ts) return '';
978	          const diff = Math.floor(Date.now() / 1000) - ts;
979	          const d = Math.floor(diff / 86400), h = Math.floor((diff % 86400) / 3600), m = Math.floor((diff % 3600) / 60);
980	          const parts = [];
981	          if (d) parts.push(d + ' day' + (d !== 1 ? 's' : ''));
982	          if (h) parts.push(h + ' hour' + (h !== 1 ? 's' : ''));
983	          if (!d && m) parts.push(m + ' minute' + (m !== 1 ? 's' : ''));
984	          return parts.join(', ') + ' ago';
985	        }
986	        function vvDueIn(ts) {
987	          if (!ts) return '—';
988	          const diff = ts - Math.floor(Date.now() / 1000);
989	          if (diff <= 0) return 'overdue';
990	          const d = Math.floor(diff / 86400), h = Math.floor((diff % 86400) / 3600), m = Math.floor((diff % 3600) / 60);
991	          const parts = [];
992	          if (d) parts.push(d + ' day' + (d !== 1 ? 's' : ''));
993	          if (h) parts.push(h + ' hour' + (h !== 1 ? 's' : ''));
994	          if (!d && m) parts.push(m + ' minute' + (m !== 1 ? 's' : ''));
995	          return 'Due in: ' + parts.join(', ');
996	        }
997	        function vvFmtDate(ts) {
998	          if (!ts) return '—';
999	          return new Date(ts * 1000).toLocaleString([], {weekday:'short',day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'});
1000	        }
1001
1002	        const exitColor  = par.exit_label === 'Completed' ? '#4caf50' : par.exit_label === 'Aborted' ? '#ff9800' : '#f44336';
1003	        const errColor   = (par.errors ?? 0) > 0 ? '#f44336' : '#444';
1004	        const speedStr   = par.last_speed_mb ? ` · ${par.last_speed_mb} MB/s` : '';
1005	        const nextDate   = vvFmtDate(par.next_ts);
1006	        const dueIn      = vvDueIn(par.next_ts);
1007	        const parBannerCls = !valid ? 'vv-banner-err' : (par.errors ?? 0) > 0 ? 'vv-banner-warn' : 'vv-banner-ok';
1008
1009	        let html = `

1010	          ${valid ? '✓ Valid' : '✗ INVALID'}
1011	          ${(par.errors ?? 0) > 0 ? `<span style="font-size:11px;">${par.errors} error${par.errors !== 1 ? 's' : ''}</span>` : ''}
1012
`;
1013
1014	        if (inProg) {
1015	          const pct = par.resync_pct ?? 0;
1016	          html += `
Check in progress — ${pct}%

1017

1018

1019
`;
1020	        }
1021
1022	        html += `
1023

1024	            Last check
1025	            ${par.last_date ?? '—'}${speedStr}
1026	            Status
1027	            ${par.exit_label ?? '—'}
1028	            Errors
1029	            ${par.errors ?? 0}
1030	            Next check
1031	            ${nextDate}
1032

1033
${dueIn}
`;
1034
1035	        document.getElementById('vv-parity-body').innerHTML = html;
1036	      })();
1037
1038	      // ── Watchdog ─────────────────────────────────────────────────────────────
1039	      (function() {
1040	        const wd  = d.watchdog ?? {};
1041	        const el  = document.getElementById('vv-watchdog-body');
1042	        if (!el) return;
1043
1044	        const healthy      = wd.healthy ?? true;
1045	        const ctrStrikes   = wd.ctr_strikes ?? {};
1046	        const ctrNames     = Object.keys(ctrStrikes);
1047	        const rwLevel      = wd.rw_level ?? 0;
1048	        const rwPaused     = wd.rw_paused ?? [];
1049	        const rwStopped    = wd.rw_stopped ?? [];
1050	        const daemonHit    = (wd.daemon_strikes ?? 0) > 0;
1051	        const oom          = wd.oom_count ?? 0;
1052	        const reboots      = wd.reboots_12h ?? 0;
1053	        const restartCount = wd.restart_count ?? 0;
1054	        const restarts     = wd.restarts_24h ?? [];
1055
1056	        const stab         = wd.stability ?? {};
1057	        const stabStrikes  = stab.strikes ?? {};
1058	        const stabNames    = Object.keys(stabStrikes);
1059

Out   ${outV}
             Xfers  0 ? '#ff9800' : '#444'};">${xfers}
             Test  ${ups.selftest || '—'}

`;
      } else {
        document.getElementById('vv-ups-body').innerHTML = '
No UPS detected
';
      }

      // ── Parity ──────────────────────────────────────────────────────────────
      const par = d.parity ?? {};
      (function() {
        const valid      = par.valid !== false;
        const inProg     = par.in_progress;
        const validColor = valid ? '#4caf50' : '#f44336';
        const validLabel = valid ? 'Parity is valid' : 'Parity is INVALID';

        function vvRelTime(ts) {
          if (!ts) return '';
          const diff = Math.floor(Date.now() / 1000) - ts;
          const d = Math.floor(diff / 86400), h = Math.floor((diff % 86400) / 3600), m = Math.floor((diff % 3600) / 60);
          const parts = [];
          if (d) parts.push(d + ' day' + (d !== 1 ? 's' : ''));
          if (h) parts.push(h + ' hour' + (h !== 1 ? 's' : ''));
          if (!d && m) parts.push(m + ' minute' + (m !== 1 ? 's' : ''));
          return parts.join(', ') + ' ago';
        }
        function vvDueIn(ts) {
          if (!ts) return '—';
          const diff = ts - Math.floor(Date.now() / 1000);
          if (diff <= 0) return 'overdue';
          const d = Math.floor(diff / 86400), h = Math.floor((diff % 86400) / 3600), m = Math.floor((diff % 3600) / 60);
          const parts = [];
          if (d) parts.push(d + ' day' + (d !== 1 ? 's' : ''));
          if (h) parts.push(h + ' hour' + (h !== 1 ? 's' : ''));
          if (!d && m) parts.push(m + ' minute' + (m !== 1 ? 's' : ''));
          return 'Due in: ' + parts.join(', ');
        }
        function vvFmtDate(ts) {
          if (!ts) return '—';
          return new Date(ts * 1000).toLocaleString([], {weekday:'short',day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'});
        }

        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' : (par.errors ?? 0) > 0 ? 'vv-banner-warn' : 'vv-banner-ok';

        let html = `

          ${valid ? '✓ Valid' : '✗ INVALID'}
          ${(par.errors ?? 0) > 0 ? `<span style="font-size:11px;">${par.errors} error${par.errors !== 1 ? 's' : ''}</span>` : ''}

`;

        if (inProg) {
          const pct = par.resync_pct ?? 0;
          html += `
Check in progress — ${pct}%

`;
        }

        html += `

            Last check
            ${par.last_date ?? '—'}${speedStr}
            Status
            ${par.exit_label ?? '—'}
            Errors
            ${par.errors ?? 0}
            Next check
            ${nextDate}

${dueIn}
`;

        document.getElementById('vv-parity-body').innerHTML = html;
      })();

      // ── Watchdog ─────────────────────────────────────────────────────────────
      (function() {
        const wd  = d.watchdog ?? {};
        const el  = document.getElementById('vv-watchdog-body');
        if (!el) return;

        const healthy      = wd.healthy ?? true;
        const ctrStrikes   = wd.ctr_strikes ?? {};
        const ctrNames     = Object.keys(ctrStrikes);
        const rwLevel      = wd.rw_level ?? 0;
        const rwPaused     = wd.rw_paused ?? [];
        const rwStopped    = wd.rw_stopped ?? [];
        const daemonHit    = (wd.daemon_strikes ?? 0) > 0;
        const oom          = wd.oom_count ?? 0;
        const reboots      = wd.reboots_12h ?? 0;
        const restartCount = wd.restart_count ?? 0;
        const restarts     = wd.restarts_24h ?? [];

        const stab         = wd.stability ?? {};
        const stabStrikes  = stab.strikes ?? {};
        const stabNames    = Object.keys(stabStrikes);


Let me check what `var.ini` fields relate to emulated disks, and look at the array disks rendering section more.


grep -E "mdNum|emulat|Invalid|DISK_" /var/local/emhttp/var.ini 2>/dev/null | head -30

Check var.ini for disk state fields


1185	      vvDiskIo = d.disk_io ?? {};
1186	      vvLastStorageDisks = d.storage ?? [];
1187	      (() => {
1188	        const disks     = vvLastStorageDisks;
1189	        const titleEl   = document.getElementById('vv-pools-title');
1190	        if (titleEl && disks.length) {
1191	          const okDisks   = disks.filter(d => d.status === 'DISK_OK' || !d.status).length;
1192	          const totalGb   = disks.reduce((s, d) => s + (d.size_gb ?? 0), 0);
1193	          const usedGb    = disks.reduce((s, d) => s + (d.used_gb ?? 0), 0);
1194	          const pct       = totalGb > 0 ? Math.round(usedGb / totalGb * 100) : 0;
1195	          const pctColor  = pct >= (vvThresholds.util_crit ?? 90) ? '#f44336' : pct >= (vvThresholds.util_warn ?? 70) ? '#ff9800' : '#4caf50';
1196	          const temps     = disks.map(d => d.temp).filter(t => t != null);
1197	          const maxTemp   = temps.length ? Math.max(...temps) : null;
1198	          const maxTempColor = maxTemp == null ? '#444'
1199	            : maxTemp >= (vvThresholds.ssd_crit ?? 70) ? '#f44336'
1200	            : maxTemp >= (vvThresholds.ssd_warn ?? 60) ? '#ff9800' : '#4caf50';
1201	          const [pIor, pIow] = vvIoSum(disks.map(d => d.device));
1202	          const ioParts = [];
1203	          if (pIor >= 0.05) ioParts.push(`<span style="color:#3a7a3a;">↓${vvFmtRate(pIor)}</span>`);
1204	          if (pIow >= 0.05) ioParts.push(`<span style="color:#7a4a1a;">↑${vvFmtRate(pIow)}</span>`);
1205	          const ioHtml = ioParts.length ? `· <span style="font-weight:400;">${ioParts.join(' ')}</span>` : '';
1206	          titleEl.innerHTML = `Pools
1207
1208	              ${okDisks}/${disks.length} ok
1209	              · ${pct}% used
1210	              ${maxTemp != null ? `· <span style="color:${maxTempColor};">max ${maxTemp}°</span>` : ''}
1211	              ${ioHtml}
1212	            `;
1213	          const [ptr, ptw] = vvIoTotalSum(disks.map(d => d.device));
1214	          const totEl = document.getElementById('vv-pools-io-total');
1215	          if (totEl && (ptr > 0.001 || ptw > 0.001))
1216	            totEl.innerHTML = `<span style="color:#3a5a3a;">↓${vvFmtGb(ptr)}</span> <span style="color:#5a3a1a;">↑${vvFmtGb(ptw)}</span>`;
1217	        }
1218	      })();
1219	      document.getElementById('vv-storage-body').innerHTML = vvRenderPools(vvLastStorageDisks);
1220
1221	      // ── Array disks — summary header + min 3 columns ─────────────────────────
1222	      const arrayDisks = d.array_disks ?? [];
1223	      if (arrayDisks.length) {
1224	        const dataDisks   = arrayDisks.filter(d => d.role === 'data');
1225	        const okDisks     = dataDisks.filter(d => d.status === 'DISK_OK').length;
1226	        const errDisks    = dataDisks.length - okDisks;
1227	        const temps       = arrayDisks.map(d => d.temp).filter(t => t != null);
1228	        const maxTemp     = temps.length ? Math.max(...temps) : null;
1229	        const maxTempColor = maxTemp == null ? '#444' : maxTemp >= (vvThresholds.hdd_crit ?? 55) ? '#f44336' : maxTemp >= (vvThresholds.hdd_warn ?? 45) ? '#ff9800' : '#4caf50';
1230	        const totalUsedGb = dataDisks.reduce((s, d) => s + (d.used_gb ?? 0), 0);
1231	        const totalSizeGb = dataDisks.reduce((s, d) => s + (d.size_gb ?? 0), 0);
1232	        const arrPct      = totalSizeGb > 0 ? Math.round(totalUsedGb / totalSizeGb * 100) : 0;
1233	        const arrPctColor = arrPct >= (vvThresholds.util_crit ?? 90) ? '#f44336' : arrPct >= (vvThresholds.util_warn ?? 70) ? '#ff9800' : '#4caf50';
1234
1235	        const [arrIor, arrIow] = vvIoSum(arrayDisks.map(d => d.device));
1236	        const arrIoParts = [];
1237	        if (arrIor >= 0.05) arrIoParts.push(`<span style="color:#3a7a3a;">↓${vvFmtRate(arrIor)}</span>`);
1238	        if (arrIow >= 0.05) arrIoParts.push(`<span style="color:#7a4a1a;">↑${vvFmtRate(arrIow)}</span>`);
1239	        const arrIoHtml = arrIoParts.length
1240	          ? `· <span style="font-weight:400;">${arrIoParts.join(' ')}</span>` : '';
1241
1242	        const titleEl = document.getElementById('vv-array-title');
1243	        if (titleEl) titleEl.innerHTML = `Array
1244
1245	            ${okDisks}/${dataDisks.length} ok
1246	            ${errDisks > 0 ? `<span style="color:#f44336;margin-left:4px;">· ${errDisks} err</span>` : ''}
1247	            · ${arrPct}% used
1248	            ${maxTemp != null ? `· <span style="color:${maxTempColor};">max ${maxTemp}°</span>` : ''}
1249	            ${arrIoHtml}
1250	          `;
1251
1252	        const [atr, atw] = vvIoTotalSum(arrayDisks.map(d => d.device));
1253	        const arrTotEl = document.getElementById('vv-array-io-total');
1254	        if (arrTotEl && (atr > 0.001 || atw > 0.001))
1255	          arrTotEl.innerHTML = `<span style="color:#3a5a3a;">↓${vvFmtGb(atr)}</span> <span style="color:#5a3a1a;">↑${vvFmtGb(atw)}</span>`;
1256
1257	        const numCols = window.innerWidth <= 1024 ? 2 : Math.max(3, Math.ceil(arrayDisks.length / 6));
1258	        const perCol  = Math.ceil(arrayDisks.length / numCols);
1259	        const cols    = [];
1260	        for (let i = 0; i < numCols; i++) cols.push(arrayDisks.slice(i * perCol, (i + 1) * perCol));
1261	        document.getElementById('vv-array-body').innerHTML =
1262	          `<div style="display:flex;gap:10px;">${cols.map(vvDiskCol).join('')}</div>`;
1263	      } else {
1264	        document.getElementById('vv-array-body').innerHTML = '
No array disks
';

vvDiskIo = d.disk_io ?? {};
      vvLastStorageDisks = d.storage ?? [];
      (() => {
        const disks     = vvLastStorageDisks;
        const titleEl   = document.getElementById('vv-pools-title');
        if (titleEl && disks.length) {
          const okDisks   = disks.filter(d => d.status === 'DISK_OK' || !d.status).length;
          const totalGb   = disks.reduce((s, d) => s + (d.size_gb ?? 0), 0);
          const usedGb    = disks.reduce((s, d) => s + (d.used_gb ?? 0), 0);
          const pct       = totalGb > 0 ? Math.round(usedGb / totalGb * 100) : 0;
          const pctColor  = pct >= (vvThresholds.util_crit ?? 90) ? '#f44336' : pct >= (vvThresholds.util_warn ?? 70) ? '#ff9800' : '#4caf50';
          const temps     = disks.map(d => d.temp).filter(t => t != null);
          const maxTemp   = temps.length ? Math.max(...temps) : null;
          const maxTempColor = maxTemp == null ? '#444'
            : maxTemp >= (vvThresholds.ssd_crit ?? 70) ? '#f44336'
            : maxTemp >= (vvThresholds.ssd_warn ?? 60) ? '#ff9800' : '#4caf50';
          const [pIor, pIow] = vvIoSum(disks.map(d => d.device));
          const ioParts = [];
          if (pIor >= 0.05) ioParts.push(`<span style="color:#3a7a3a;">↓${vvFmtRate(pIor)}</span>`);
          if (pIow >= 0.05) ioParts.push(`<span style="color:#7a4a1a;">↑${vvFmtRate(pIow)}</span>`);
          const ioHtml = ioParts.length ? `· <span style="font-weight:400;">${ioParts.join(' ')}</span>` : '';
          titleEl.innerHTML = `Pools

              ${okDisks}/${disks.length} ok
              · ${pct}% used
              ${maxTemp != null ? `· <span style="color:${maxTempColor};">max ${maxTemp}°</span>` : ''}
              ${ioHtml}
            `;
          const [ptr, ptw] = vvIoTotalSum(disks.map(d => d.device));
          const totEl = document.getElementById('vv-pools-io-total');
          if (totEl && (ptr > 0.001 || ptw > 0.001))
            totEl.innerHTML = `<span style="color:#3a5a3a;">↓${vvFmtGb(ptr)}</span> <span style="color:#5a3a1a;">↑${vvFmtGb(ptw)}</span>`;
        }
      })();
      document.getElementById('vv-storage-body').innerHTML = vvRenderPools(vvLastStorageDisks);

      // ── Array disks — summary header + min 3 columns ─────────────────────────
      const arrayDisks = d.array_disks ?? [];
      if (arrayDisks.length) {
        const dataDisks   = arrayDisks.filter(d => d.role === 'data');
        const okDisks     = dataDisks.filter(d => d.status === 'DISK_OK').length;
        const errDisks    = dataDisks.length - okDisks;
        const temps       = arrayDisks.map(d => d.temp).filter(t => t != null);
        const maxTemp     = temps.length ? Math.max(...temps) : null;
        const maxTempColor = maxTemp == null ? '#444' : maxTemp >= (vvThresholds.hdd_crit ?? 55) ? '#f44336' : maxTemp >= (vvThresholds.hdd_warn ?? 45) ? '#ff9800' : '#4caf50';
        const totalUsedGb = dataDisks.reduce((s, d) => s + (d.used_gb ?? 0), 0);
        const totalSizeGb = dataDisks.reduce((s, d) => s + (d.size_gb ?? 0), 0);
        const arrPct      = totalSizeGb > 0 ? Math.round(totalUsedGb / totalSizeGb * 100) : 0;
        const arrPctColor = arrPct >= (vvThresholds.util_crit ?? 90) ? '#f44336' : arrPct >= (vvThresholds.util_warn ?? 70) ? '#ff9800' : '#4caf50';

        const [arrIor, arrIow] = vvIoSum(arrayDisks.map(d => d.device));
        const arrIoParts = [];
        if (arrIor >= 0.05) arrIoParts.push(`<span style="color:#3a7a3a;">↓${vvFmtRate(arrIor)}</span>`);
        if (arrIow >= 0.05) arrIoParts.push(`<span style="color:#7a4a1a;">↑${vvFmtRate(arrIow)}</span>`);
        const arrIoHtml = arrIoParts.length
          ? `· <span style="font-weight:400;">${arrIoParts.join(' ')}</span>` : '';

        const titleEl = document.getElementById('vv-array-title');
        if (titleEl) titleEl.innerHTML = `Array

            ${okDisks}/${dataDisks.length} ok
            ${errDisks > 0 ? `<span style="color:#f44336;margin-left:4px;">· ${errDisks} err</span>` : ''}
            · ${arrPct}% used
            ${maxTemp != null ? `· <span style="color:${maxTempColor};">max ${maxTemp}°</span>` : ''}
            ${arrIoHtml}
          `;

        const [atr, atw] = vvIoTotalSum(arrayDisks.map(d => d.device));
        const arrTotEl = document.getElementById('vv-array-io-total');
        if (arrTotEl && (atr > 0.001 || atw > 0.001))
          arrTotEl.innerHTML = `<span style="color:#3a5a3a;">↓${vvFmtGb(atr)}</span> <span style="color:#5a3a1a;">↑${vvFmtGb(atw)}</span>`;

        const numCols = window.innerWidth <= 1024 ? 2 : Math.max(3, Math.ceil(arrayDisks.length / 6));
        const perCol  = Math.ceil(arrayDisks.length / numCols);
        const cols    = [];
        for (let i = 0; i < numCols; i++) cols.push(arrayDisks.slice(i * perCol, (i + 1) * perCol));
        document.getElementById('vv-array-body').innerHTML =
          `<div style="display:flex;gap:10px;">${cols.map(vvDiskCol).join('')}</div>`;
      } else {
        document.getElementById('vv-array-body').innerHTML = '
No array disks
';


mdNumDisks="12"
mdNumDisabled="1"
mdNumInvalid="1"
mdNumMissing="0"
mdNumNew="0"
mdNumErased="0"


Let me check what status values disk 10 currently has, and look at the full parity logic.


grep -A 15 '^\[disk10\]' /var/local/emhttp/disks.ini 2>/dev/null

Check disk10 status in disks.ini


grep -E "mdNum|mdState|mdResync|sbSync" /var/local/emhttp/var.ini 2>/dev/null

Check array state fields in var.ini


sbSynced="1780286401"
sbSyncErrs="0"
sbSynced2="1780439227"
sbSyncExit="0"
mdNumDisks="12"
mdNumDisabled="1"
mdNumInvalid="1"
mdNumMissing="0"
mdNumNew="0"
mdNumErased="0"
mdResync="0"
mdResyncCorr="0"
mdResyncPos="0"
mdResyncDb="0"
mdResyncDt="0"
mdResyncAction="check P Q"
mdResyncSize="11718885324"
mdState="STARTED"


grep -E "^\[disk10\]|status|numDisabled" /var/local/emhttp/disks.ini 2>/dev/null | head -30

Check disk10 and disabled status in disks.ini


status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_DSBL"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"
status="DISK_OK"


php -r "
\$ini = parse_ini_file('/var/local/emhttp/disks.ini', true);
foreach (\$ini as \$k => \$v) {
  if (strpos(\$k, 'disk10') !== false || (\$v['status'] ?? '') !== 'DISK_OK') {
    echo \"[\$k]\\n\";
    foreach (\$v as \$fk => \$fv) echo \"  \$fk=\$fv\\n\";
  }
}
" 2>/dev/null

Check non-OK disks in disks.ini