Files
Varaverk/Plugin/unraid/README-unraid.md
T
Gmer4Lfe 987313e7dc Document the PHP api layer and fix what documenting it exposed
Writing down what each endpoint actually guarantees made the places it
didn't obvious — shell arguments reaching a crontab or a bash -c
unescaped, master.conf written without tmp+rename, and conf edits that
could be saved without ever being parsed.
2026-08-02 10:11:39 -04:00

10 KiB

━━━━━ PLUGIN / UNRAID ━━━━━

The Unraid platform layer. Everything in this folder exists to translate one specific operating system into the vocabulary Varaverk speaks — and to put a face on it in the Unraid WebGUI.

Varaverk itself knows nothing about Unraid. No script outside this folder references /etc/rc.d, /boot/config/plugins/dynamix, emhttp, the mover, or a .page file. They call platform_*() and read $STATE_DIR; this folder decides what those mean on Unraid.

  adapter.sh              24 platform_*() functions — the entire OS contract
  Varaverk.page           WebGUI menu entry (Tasks:95)
  VaraverkSettings.page   WebGUI settings entry
  event/                  Unraid array lifecycle hooks
  pages/ api/ include/    the three-layer web UI
  System_Essentials/      Unraid-specific system scripts
  Tools/                  Unraid-specific operator tools
  Watchdogs/System/       Unraid-specific watchdog (WebGUI health)
  Partnership/            Unraid-specific container deploy/cleanup

Swapping platforms means writing Plugin/truenas/adapter.sh implementing the same 24 function names. load_config.sh sources Plugin/$PLATFORM/adapter.sh — nothing else in the codebase changes.


━━━ THE PROBLEM THAT BUILT THIS ━━━

Every NAS OS Answers the Same Questions Differently "Is the array mounted?" "Restart this service." "What are the disk temperature thresholds?" "Send a notification." Every platform has an answer, and every answer is different — a different path, a different init system, a different config format.

Scattering those differences through 115 scripts would mean every script carrying an if unraid ... elif truenas branch, and every new platform touching all of them.

Varaverk Vars Are Not OS Vars Varaverk works in terms of STATE_DIR, SCRIPTS_DIR, DOCKER_APPDATA_BASE, WATCHDOG_CONTAINERS. Unraid works in terms of /boot/config/plugins/, emhttp, /mnt/user, dockerMan XML templates, and dynamix.cfg. Something has to map one onto the other, in exactly one place, or the two vocabularies leak into each other.

A Headless Ecosystem Is Hard to Trust 115 scripts running unattended produce a lot of state nobody can see. Without a UI the only way to know what the watchdogs did last night is to read logs. The WebGUI exists so the system can be inspected without SSH.


━━━ THE ADAPTER — WHERE OS MEETS VARAVERK ━━━

adapter.sh is the whole contract. 24 functions, sourced by load_config.sh after common.sh, so every script in the ecosystem has them.

The most-called ones, by usage across the repo:

Function Calls What it hides
platform_require_cmd 28 Verifying a platform binary exists and is executable
platform_restart_service 13 /etc/rc.d/rc.<name> init scripts
platform_is_service_running 13 Unraid's service process conventions
platform_is_mover_running 10 The Unraid mover — no other OS has one
platform_get_os_version 10 Where the version string lives
platform_setup_db_path 9 Where wizard/setup state persists
platform_storage_path 7 /mnt/user as the user-share root

Plus: disk states and temperature thresholds from dynamix.cfg, parity/maintenance detection, native notifications through the dynamix notify script, container rebuild from dockerMan XML templates, and the WebGUI install path.

The contract is strict, and it is what makes the layer work:

  • Every function returns 0/1 and never calls exit. A missing capability is information the caller needs, not a decision the adapter makes — a watchdog may want to skip where an installer wants to abort.
  • Value-producing functions write to stdout, captured with $(). Status travels in the exit code. Keeping those separate is what lets callers use them in conditionals.
  • It reports; it does not remediate. No retries, no escalation, no notification policy. Burying those here would make identical calls behave differently per platform.

━━━ THE WEB UI — THREE LAYERS ━━━

  pages/*.php      what you look at        ── polls ─────►  api/
  api/*.php        what the page fetches   ── requires ──►  include/
  include/*.php    the actual logic — vv_*() functions

Most pages hold no logic at all. Eight of the eleven are pure view — markup, CSS and a poll loop, with every value arriving as JSON from api/. Only auth.php, monitor.php and scheduler.php require an include/ file directly, and then only to server-render their initial state; their mutations still go through endpoints.

That is why the layer split holds: a page and its endpoint cannot disagree about what a value means, because the page does not compute it. Adding a metric means adding it in include/ once, and the endpoint, the cache writer, and the page all pick it up together.

Layer Files Role
pages/ 11 One per WebGUI tab — monitor, docker, arrs, fallback, watchdog, rsync, partnership, scheduler, auth, settings, setup
api/ 50 JSON endpoints the pages poll, plus action endpoints (run a script, stop a job, toggle a flag)
include/ 16 Shared builders and helpers — vv_monitor_*, vv_arrs_*, vv_docker_*, config read/write, auth

Caching. Several endpoints serve from /tmp/vv_cache (tmpfs) rather than hitting live APIs on every page view, refreshed by Tools/api_cache_writer.sh. ?live=1 bypasses the cache. A missing or unparseable cache always falls back to a live call, so the cache can never be the reason a page fails to load.


━━━ WHAT GUARDS THE API LAYER ━━━

Every endpoint under api/ is protected by exactly one thing: the Unraid WebGUI session. Anything that can reach /plugins/varaverk/api/*.php with a valid session can do everything this plugin can do — stop the array, power off the host, write master.conf and push it to every partner, create an lldap user, write and schedule a root-run script.

That is the same trust level as the rest of the WebGUI, and it is the intended model. It is written down here because two things about it are easy to assume and wrong.

No endpoint validates a CSRF token. pages/partnership.php and pages/scheduler.php send Unraid's csrf_token with their POSTs, which reads like the token is checked somewhere. It is not — no file in api/ looks at it. A request that arrives with a logged-in session cookie is honoured whatever caused the browser to send it. Adding validation is a worthwhile hardening pass, but it is a real change: every caller has to send the token before any endpoint requires it, or the UI breaks silently on whichever page was missed. Do it deliberately, in one pass, with the pages open — not opportunistically while touching one endpoint.

api/webhook.php is the exception that authenticates nothing at all. It exists to receive Sonarr/Radarr/Lidarr download events, which arrive from a container rather than a browser. master.conf carries a WEBHOOK_SECRET and the standalone Node listener on WEBHOOK_PORT validates it — this WebGUI-hosted path does not. Injection is not the risk (the path is validated and escaped); triggering work is. Closing it means adding a secret check here and updating the webhook URL in each arr's settings, in that order.

Two further endpoints are worth knowing about because they read wider than the rest: api/api_test.php returns an API key prefix and the live GraphQL schema, and api/import_script.php's browse action and api/manual_sync.php's browse actions list directory names anywhere on either host. All three are read-only and none return file contents, but they are the ones to look at first if the session boundary ever moves.


━━━ ARRAY LIFECYCLE HOOKS ━━━

event/ plugs Varaverk into Unraid's own array lifecycle. These are how the ecosystem starts and stops with the array rather than with the OS.

Hook Fires Runs
disks_mounted/array_start_jobs Array started Orchestrators/array_started.shARRAY_START_SCRIPTS
disks_mounted/rebuild_cron Array started Regenerates varaverk.cron from the scheduler config
unmounting_disks/array_stop_jobs Array stopping Orchestrators/array_stopping.shARRAY_STOP_SCRIPTS

Array-start rather than boot is the correct trigger: in flash storage mode SCRIPTS_DIR lives in appdata, which does not exist until the array mounts.


━━━ THE PLATFORM-SPECIFIC SCRIPT MIRRORS ━━━

These folders mirror the top-level Varaverk structure, but hold work that cannot be platform-agnostic. A script lives here rather than in the root folder when it manipulates Unraid itself.

Folder Contents Why it is platform-specific
System_Essentials/ 4 scripts Mover, User Scripts plugin, PHP-FPM pool, unraid-api registry — all Unraid subsystems
Tools/ 5 files Share .cfg recreation, storage-mode migration, API cache writers
Watchdogs/System/ webgui_watchdog.sh emhttp / nginx / php-fpm health — there is no generic "WebGUI"
Partnership/ containers.sh Container deploy from dockerMan CA XML templates

Each has its own README where the group is large enough to need one; single-script folders are documented in the script header.


━━━ HOW THIS FOLDER IS INSTALLED ━━━

Two paths, deliberately different:

DevelopmentPlugin/plugin_setup.sh symlinks this directory into the WebGUI plugin location. Every edit is live immediately. This is the normal working mode.

ReleasePlugin/build.sh packages this directory into a Slackware .txz and rewrites the version and checksum in varaverk.plg. That is what Unraid reinstalls from flash on every boot.

In flash storage mode git_pull_execute.sh syncs Plugin/ back to /boot/ after each pull, so the WebGUI always serves current PHP even though SCRIPTS_DIR points at appdata.


━━━ ADDING A NEW PLATFORM ━━━

  1. Create Plugin/<platform>/adapter.sh implementing all 24 platform_*() functions.
  2. Set PLATFORM=<platform> in varaverk.cfg.

That is the whole contract for the script ecosystem. load_config.sh sources Plugin/$PLATFORM/adapter.sh and every script keeps working unchanged.

The WebGUI is separate and optional — it is Unraid-specific by nature (.page files, emhttp conventions). A new platform can run the entire ecosystem headless with only the adapter, and add a UI later in whatever form that platform expects.