The page multiplies its core count by the RW_LOAD_* multipliers to colour the
load bar, and resource_watchdog.sh computes its real thresholds from nproc. The
page preferred the API's physical core count — 16 against 32 threads — so it
turned amber at load 32 and red at 48 while the watchdog did not reach soft
pressure until 64 or medium until 96. A page whose whole job is reporting what
the watchdogs think was reporting a crisis about a machine they considered idle.
The remote path already sent the partner's own nproc, so both nodes now agree.
docker stats sums CPU across every logical processor, so Jellyfin's "227%" is
2.3 threads busy — 7% of a 32-thread host, which is what Unraid shows and what
the operator was reading. The card was printing the raw figure and colouring it
against thresholds meant for a percentage, so ordinary background work looked
like an emergency; I twice flagged Jellyfin as pegged on that basis and was
wrong both times. Divides by nproc, not the watchdog's `cores` — that field is
the physical 16 because it compares load averages, and borrowing it here would
have looked reasonable and doubled every reading.
A partner's Emby URL is http://localhost:8096 — true there, meaningless here —
so nobody queries a partner's media server directly. Each host reports its own
over the SSH call remote_arr_cache_writer.sh already makes, which keeps its API
keys on it and costs no extra connection. Local is the default: opening the tab
to check your own server should not first make you read past a partner's.
A partner with no cache is stated as not collected rather than omitted, because
an empty mesh view and a partner that is down look identical otherwise.
The counts need the entire library downloaded to compute six numbers — 16.0 MB
from Radarr and 5.5 MB from Sonarr, measured — and they were on the same
one-minute clock as queue depth and health, which pulled ~30 GB a day out of the
arrs to re-count records that had not changed. They now cache for fifteen
minutes against how often the numbers actually move: 7.6s to 3.0s per refresh.
Emby and Jellyfin get a card each: version, CPU, memory, uptime, streams, users
and transcodes, with update/restart flags. Two halves of one question — a server
answering happily at 145% CPU is a different situation from one at 8%, and
neither the app nor the container says so alone.
The media jobs row I just added formats durations with _dur(), which the rsync
page defines locally and js/varaverk.js does not export at all. The reference
threw inside _render(), the fetch's catch reported it as "Error loading arrs
status", and the whole page went blank — nothing to do with the API, which was
returning valid JSON throughout.
play_state_sync, media_shares_permissions and media_cleaner work on the same
files the arrs manage and were visible only by opening the Scheduler and reading
an orchestrator's log. Readable at all because run_orch_child() now writes a run
record and a per-script log for its children — this could not have been written
yesterday. Each card shows the sentence the script itself ended on rather than a
count re-derived here, since the three word their outcome differently and the
wording is the part worth reading.
Everything a child printed was interleaved into the parent's log with forty
other scripts, separated only by human headings that map to no script id — so
scoped_log could answer for ten orchestrators and nothing else. tee rather than
a redirect, so the parent keeps receiving exactly what it did before and this
only adds a destination; PIPESTATUS[0] preserves the child's exit code through
the pipe. Trimmed to 1000 lines like run_job.sh does, because LOG_DIR is tmpfs
and an untrimmed chatty script spends RAM. Falls back to the original unpiped
call when the log cannot be opened — a child must never be denied a run because
somewhere to write about it could not be created.
run_job.sh records the ten jobs cron starts; the seventy entries inside the
*_SCRIPTS lists had none, and nothing said so. Consumers quietly got less — arr
cleanup stats fell through to a daily aggregate with no end time, and
troubleshoot answered "I cannot see a run record" for every script that is not
an orchestrator, which is nearly all of them and exactly the ones a question is
about. run_orch_child() already had the start time, exit code and severity
ladder; it just never wrote them down. Not run_job.sh itself: that also locks
and interval-gates, which would let a child skip itself or block on its parent.
The arr cleanups are entries in DAILY_MAINTENANCE_SCRIPTS, which the orchestrator
invokes with plain bash, so they never get the run record the primary parse needs
— every payload has come from the daily aggregate db. That path read every column
except the byte counts, leaving orphans_sz at its "0B" default, which is
invisible at zero orphans and would have read "12 orphans (0B)" the first time
there were any. 'total' had no source there and no reader anywhere, so it is gone.
Arr Sync now reads "disabled" rather than "never run" when its switch is off.
Health messages go straight from each arr's API into innerHTML and quote things
nobody here controls — indexer names from Prowlarr, import-list names, release
titles. The page used no escaping at all. _sz() was also binary while labelling
its output TB, so /tv read 86.9 TB here and 95.6 TB on every other tab; it has
one caller and that caller is disk capacity, so it now matches the rest.
Retyping two paths and nine checkboxes correctly every time is where the
mistakes come from, and "one-off" described how a transfer is scheduled rather
than how often it is run. Pinned entries are exempt from the rotation — the
command used twice a year is both the most worth keeping and the first that ten
ordinary runs evict. Stored server-side, so the list is there from any screen,
and loading one fills the form and stops rather than running it.
--stats prints "Total transferred file size: 1,234,567 bytes", so $NF is the
unit word; stripping non-digits from it left an empty string and ""+0 is 0.
Every transfer ever logged recorded zero. Nothing looked broken — the history
filled with 1,423 correct rows all reading 0, the graph drew a flat line, and
DIGEST_SMART_ON_BANDWIDTH could never fire because 0 is never above 50GB.
Both parse sites now share one program, since fixing either alone would have
left the other silently wrong.
The preview ended in the literal text "…src host:dst", so the one thing worth
checking before pressing Run — the real source and destination, and which of
them has the trailing slash — was the one thing never shown. --delete sat
unmarked among nine checkboxes with no confirmation on a card that can remove
data on the far host. Confirm fires only for --delete without --dry-run, so it
stays worth reading.
last_sync reads the maintenance orchestrators' run records, and those do git
pull, permissions, cleaners and docker updates on schedule whether or not the
global gate is open — so the page reported a healthy 56-minute "daily sync"
while nothing had transferred since 16 July. Both cards also compared status
against 'success' while being fed 'ok', so every clean run rendered in the
failure colour. Adds the assistant, scoped to the tab.
Every existing exemption lasts until someone remembers to undo it, and nobody
does — Healarr has sat in a pressure list since it was uninstalled and seven
ignore entries name containers that are gone. A mute states when it ends and
then ends, capped by WATCHDOG_MUTE_MAX_HOURS so temporary is enforced rather
than intended. Applied where IGNORE_MAP is built, so all five check sites
inherit it, and shown with its countdown because an invisible suppression is
the thing being fixed.
Without it the page renders zeros, all-clear pills and green dots when the
orchestrator has stopped entirely — its healthiest appearance describing a
machine with no watchdogs, against a header promising absence is never drawn as
an all-clear. The heartbeat already existed; overdue is measured against the
cron schedule rather than an assumed fifteen minutes.
Half the hard-pressure stop list is containers that are not installed, so at
level 3 it sheds half the memory the card implies. Badged on the lists where a
dead entry costs something and deliberately not on Scan ignore, where ignoring
something already gone is the point — a card of dashed pills by design would
teach the eye to skip the badge where it has to be believed.
A fourth candidate source beside the arrs, the system log and container logs:
skip-listed containers, strikes past their limit, repeated restarts, unattended
reboots and sustained pressure become findings. The kind is deliberately not
conf-bound, so it cannot autofix by construction rather than by a switch — and
WATCHDOG_SCAN_IGNORE suppresses it, so a knowingly broken container stays quiet.
First consumer AI_ASSIST_WATCHDOG has ever had.
Both the reader and the writer ran to the first closing paren, which is only the
array's own close if nothing inside contains one. HOST1_WATCHDOG_SCAN_IGNORE has
carried "(exit 127 — bad image)" for weeks: writes spliced into the middle of it
and were refused by bash -n with a bare false, and reads returned the entries
above it — the Rsync tab showed no intermediate scripts at all and 10 of 17
daily. Arrays now close on a ) that starts its own line, as confform already did.
It is the setting people arrive looking for — the one that takes a container out
of the watchdog's hands rather than tuning what it does with it. Its hint says
so, because it silences a real fault as readily as a noisy one.
Tier 1 limits, both pressure lists and Required get add, remove and adjust on
the page that shows what they did. Per-entry comments are carried through the
rebuild rather than regenerated — they hold the reason for every number.
The tab collected over SSH on every 30s poll — 8.3s a load with HOST2 down.
vv_parse_conf_scalar() captured to end of line, so a commented toggle parsed as
"true # HOST2 back online": every threshold read right because (int) stops at
the first non-digit, and 38 booleans read wrong. The Fallback tab has been
showing failover disabled while it was on.
The page states what struck but never why, and the log that would say is the
orchestrator's, not one per watchdog. Why? opens troubleshoot against it —
read-only, because the page's contract is that it changes nothing.
.vv-ai-chat transitions its height, so measuring the dock straight after setting
it returns the height it is animating away from — the view was cut short for a
dock that had already shrunk, leaving the difference as dead panel underneath.
The API mixes units per field — memory in bytes, disk size in KiB, fsSize and
fsUsed in kB — and one helper was guessing which from the magnitude of the
number, which also read any memory total under 100 GB as KB.
Three # rules open most docs here, so the title became a section too small to
survive and the paragraph defining the project got a chunk with no heading.
The detector answers no to "why is my array so slow", so claiming that meant a
question about the wider world would have answered it from general web pages.
A paragraph beside the rule did not shift it; the ready-made refusal was simply
the nearest thing to hand, so the correction goes next to the question instead.
Chat was told it could not look anything up, which is true of this machine and
false the moment a search succeeds, so it deflected while holding six sources.
The map documents 447 settings, so without this every question about where a
control lives earns a proposal that costs a dismissal and teaches nothing.
Only the array name was taken from the orchestrator; the enabled flag still came
from the first-match map, so every orchestrator showed the first list's state.
docker_update.sh runs bare in daily, --weekly in weekly and --remainder in
monthly, so switching it off in monthly disabled the daily run and reported
success.
An undefined variable reaching a by-reference array parameter is a TypeError
under PHP 8, so every save through this endpoint died before writing anything.
The comments above a setting already state its choices, units and bounds, so the
form can read them instead of asking for the file to be annotated first.
The pattern used # as its delimiter and excluded a literal # inside the character class, so PCRE
ended the pattern early and the match never fired. It read as the filter doing nothing. The unit
test missed it by asserting against a retyped copy of the regex rather than the one in the file,
which is the only version that matters.
Its Reddit engine returns a community's landing page for any query at all — a question about ZFS
scrubs came back with Rocket League Esports and Dividend Investing, interleaved one in two, so
half of what reached the model was noise. Threads from that same engine are frequently the best
result there is, so the engine stays and only the URL shape goes: /r/<name> with nothing after it
is a front page by definition. That is a fact about the URL, not an opinion about how well it
matches, and this file does no similarity scoring of its own.
The operator already runs it, so this is the one provider that costs nothing to adopt: no key,
no card, no third party, and no new container. It aggregates several engines and returns them
merged, and its /api/search reply is already the shape everything downstream expects — title,
url, snippet, with content as a fallback for the roughly one result in six that has no snippet.
It is also now the only provider whose response has been read from a live service rather than
from documentation, which is why it is the default.
Search is the one capability that fits the profile holding none: every other capability either
reads this installation or changes it, and chat's whole contract is that it can do neither. The
assistant deliberately does not get it — its contract is that answers come from this install's
own documents, and a web result there is an answer that looks sourced and is not. A chat question
about this machine hands off to the assistant before the search would run, so it never reaches
the internet even with the box ticked.
Off by default, and not because it is dangerous: searching sends the operator's question outside
the house, which is theirs to decide. Asked for per turn as well as enabled in conf.
Provider-agnostic, as asked — searxng, brave, tavily. Only whichever is configured here can be
verified; all three read every field by name, so a shape that changes underneath yields no
results rather than wrong ones.
The explain fixtures asserted chat holds no capabilities at all, which is exactly the guarantee
worth keeping. caps=only: now states the set rather than its emptiness, so the check still fails
the day something else is granted there.
The evidence of a syslog or container finding carries a line count and the timestamp of the
first matching line, so it differs on every pass even when nothing about the fault has changed.
Both the acknowledgement and the announcement were pinned to it, which meant acking one expired
fifteen minutes later and the notification repeated every sweep, forever — worst exactly where
it matters least, on a fault that has been there since the machine was built. Those kinds now
pin to their identity. A fault that genuinely worsens has a different class and so is a
different finding, and still announces on its own.
Triage could see what Varaverk wrote about itself and what the arrs said about themselves, and
nothing else — so a disk throwing I/O errors, a filesystem going read-only or a PCIe link
retraining every two minutes was invisible to the thing whose job is noticing. Container state
was already watched; no line any container actually wrote ever was.
Container patterns match the environment rather than the application. Fifty containers are fifty
programs with no shared vocabulary for malfunctioning, but an exact shared one for a full disk or
a corrupt SQLite file, because those strings come from libc and SQLite rather than from the app.
Both halves are checked by Tools/ai_log_check.sh, which is two tests because the failure modes
are opposite: fixtures for recall on faults this host has never had, and a replay of its real
logs for precision — 74,519 syslog lines and 79,193 container lines, matching only the PCIe
errors it genuinely has.
Severity was being graded from a hand-picked three fields, so every one of these would have been
filed as a warning however bad it was, and notified as one.
A finding nobody is told about is a finding nobody has, and the card added earlier only shows
them to someone who opens the tab. Only needs_operator is announced — an open finding may still
be repaired by the next pass — one notification for all of them, and each is announced once and
stays quiet until the fault changes or gets worse.
vv_notify() hands the message to common.sh's notify() rather than reimplementing the channels,
and calls detect_hosts() explicitly because load_config.sh deliberately does not: without it the
Unraid notification arrives and Discord silently never does. It also reports false when no
channel is switched on at all, since notify() exits 0 either way and a caller believing that
would mark a finding as told and never mention it again.
Notification text is folded to ASCII. Unraid's notifier dropped an em dash outright and left the
double space behind, which was found by sending one and reading what arrived.
The 55 alert() calls carried the same suppression as the confirms, and go wrong in the worse
direction: a silenced confirm makes a button do nothing, while a silenced alert lets the action
run and says nothing about it failing. vvAlert returns a promise nobody has to await, so these
converted by rename with no caller becoming async. The icon is inferred from the message rather
than asked of fifty call sites, and an explicit type still wins.
Every confirm() and prompt() in the plugin could be switched off from inside itself — one tick of
"prevent this page from creating additional dialogs" and all 32 of them returned false while
drawing nothing, across every tab, until a full reload. swal is already global on every webGUI
page and core uses it 370 times without a single confirm(), so this costs no new dependency.
vvConfirmRun() is the one that mattered: it returned a boolean to three callers testing !it, and
an unawaited promise is always truthy, so leaving those alone would have run every job without
asking. The wrapper's callback is a classic function expression on purpose — SweetAlert only
calls back on cancel when the callback's own source declares a parameter, and an arrow would
have hung the promise forever.
Both browsers offer "prevent this page from creating additional dialogs" inside the dialog
itself, and once it is ticked every later confirm() returns false without drawing anything — so
Fix and Never a problem silently declined and read as dead buttons. Unraid swaps tabs by AJAX
without reloading, so the suppression outlived leaving the tab and only clearing history got it
back. Both now arm on the first press and act on the second, and say what the press will do
while they wait for it.
Several scripts delete inside a conf path — the orphan cleaner runs rm -rf under a download dir
and rsync runs --delete against a destination — so /mnt/user/Movies becoming /mnt/user is the
edit that turns a cleanup into a sweep. Depth cannot be the test, because /tv and /movies are
real container-internal values here; direction can. Clearing a path, making it relative and
'..' segments go with it, and autofix additionally requires a proposed path to exist, since
every probe it has is a network probe and proves nothing about a directory. Refusals now reach
the caller: a save whose only change was refused answered ok with no explanation.
Seven findings had been sitting in needs_operator with no way to reach the operator: the only
way to read one was the sweep tool over SSH. The card states the gate as well as the list,
because an empty list means nothing found when repair is on and nothing is looking when it is
off. Buttons come from the actions the endpoint returned for that row, so a tab left open
overnight cannot offer a choice the store has moved past.
An ack is supposed to hold only while the thing acknowledged is still true, but a finding with
no key compared '' against '' and so was silenced permanently — dismiss wearing ack's label,
which is every arr health finding on this host. Those pin to the shape of the fault instead.
Dismiss and reopen become real answers rather than states only reachable by editing JSON, and
apply_action now refuses anything the finding did not offer.
Conf writing is granted to this profile alone and it is not offered as a button — making it
selectable would put a write one click from any question. Its prompt leads on what it does
not decide, because the resolver picks the key and the probe picks the value.
Learned alias, then the literal key, then the keys and script ids whose own words contain
every word said. No similarity scoring anywhere: a near-match fails silently and confidently,
and here it would write to a key nobody named. Several matches is a question, not a ranking.
A term alias decides what gets written to conf and has to be earned by repetition; a spelling
decides how a sentence is read, and the same typo is rarely made three times.
A term that resolved the same way three times stops being an inference and becomes a lookup;
the corrections are the rows worth having, since a term that meant two things is still a
guess and is never promoted.
They already publish what they believe is wrong, structured and graded, so there is nothing
to parse and no severity to second-guess. Findings widen from naming a conf key to naming
anything specific — Radarr's import lists being down is actionable in Radarr, not here.
Nine entries named paths nothing lives at — a directory that was renamed to System_Essentials,
and docker_update_remaining.sh which has never existed — and the API key renewal was absent
from both arrays it runs from on this host. A fresh install would have skipped all of it.
vv_conf_vars() read and regex-parsed ~1,900 lines per call and reached a shell fork through
vv_detect_host(); the repair sweep asks it for a key per log line. Keyed on a hash of the
contents rather than mtime and size, which missed a same-second rewrite to the same length.
Nothing in Varaverk fires when a job ends, so this picks up run records that completed
since the last pass rather than adding a hook to forty scripts. Ahead of stability in the
cycle on purpose: a wrong port is not fixed by rebooting the machine.
The reply to "shall I fix it" ends in a conf write, so the three actions are matched by
pattern with an ambiguity refusal instead of being inferred — and a phrase that supports
two readings asks again rather than picking one.
A model can be confident a port should be 8686; a probe can report that 8686 answered, and
only the second is a fact. Detecting and repairing are also separate things to trust, so
the feature runs read-only until the write switch is turned on under it.