diff --git a/Plugin/unraid/api/fallback.php b/Plugin/unraid/api/fallback.php index 144e3a9..a876fd6 100644 --- a/Plugin/unraid/api/fallback.php +++ b/Plugin/unraid/api/fallback.php @@ -5,6 +5,13 @@ // state, tier activation, handback strikes, covered container status — for the fallback // tab's 30s poll. // +// OPERATIONAL MODEL +// Computed fresh on every request, deliberately uncached. The inputs are small local state +// files that fallback.sh rewrites as it moves between states, so assembling them costs about +// nothing — and a cached fallback picture is the one kind of stale this tab must never serve. +// A page showing NORMAL because the answer was cached before the switch is worse than a page +// that took an extra moment to load. +// // DESIGN PRINCIPLES // Thin transport. Every judgement about what a state file means lives in // include/fallback.php. This file exists to give the browser a URL. diff --git a/Plugin/unraid/api/media.php b/Plugin/unraid/api/media.php index a8004c7..60e103c 100644 --- a/Plugin/unraid/api/media.php +++ b/Plugin/unraid/api/media.php @@ -4,12 +4,19 @@ // Active media sessions endpoint. Normalised now-playing across every Emby, Jellyfin and // Plex instance configured for this host, for the monitor page's session panel. // +// OPERATIONAL MODEL +// Local is the default and stays the cheap path: one call per media server configured on this +// host. Mesh adds one bounded SSH hop per partner and is only requested while the operator is +// looking at the mesh view, so a dashboard left open on the default costs exactly what it did +// before the scope existed. +// // DESIGN PRINCIPLES // Thin transport. Discovery, per-server API dialects and normalisation all live in // include/media.php; this file only sets the content type and encodes the result. // -// No parameters. Which servers to ask is derived from conf, not from the request, so the -// browser cannot point this endpoint at an arbitrary URL. +// The request chooses a scope, never a target. scope= selects local or mesh; which servers +// are asked, and which partners the mesh hop reaches, are both derived from conf. The browser +// can widen what it asks for, but it cannot point this endpoint at an arbitrary URL. // // OPERATIONAL SAFEGUARDS // Bounded by the library's 3s per-request timeout. @@ -25,18 +32,16 @@ // messages a client. // // REQUEST -// GET, no parameters -// -// RESPONSE -// vv_media_sessions() verbatim — a flat list of normalised sessions across all servers -// -// REQUEST // GET this host's sessions // GET ?scope=mesh every node's sessions, each row tagged with the host it is playing on // -// Local is the default and stays the cheap path: one call per configured media server here. -// Mesh adds one bounded SSH hop per partner and is only requested while the operator is looking -// at the mesh view, so a dashboard left open on the default costs exactly what it did before. +// RESPONSE +// local {"scope":"local","sessions":[…],"server_names":[…],"server_count":N} +// mesh {"scope":"mesh","nodes":[…],"sessions":[…],"server_names":[…],"server_count":N} +// +// sessions is the normalised list; in the mesh scope each row also carries the host it is +// playing on. server_count counts media servers, not sessions. There is no error shape — an +// unreachable server contributes nothing; see OPERATIONAL SAFEGUARDS. // // DEPENDS ON // include/media.php vv_media_sessions(), vv_media_sessions_mesh() diff --git a/Plugin/unraid/api/node_chat.php b/Plugin/unraid/api/node_chat.php index 86800dc..166aba2 100644 --- a/Plugin/unraid/api/node_chat.php +++ b/Plugin/unraid/api/node_chat.php @@ -4,12 +4,19 @@ // The Partnership tab's mesh chat: read a channel, post to it, forget a message on this // machine, and mark a channel read. // -// REQUEST -// GET channels + this host's id + unread counts -// GET ?channel= that channel's messages -// POST action=send channel= text=… [color=#rrggbb] [font=mono|sans|serif] -// POST action=delete channel= id= local only -// POST action=read channel= mark seen up to now +// OPERATIONAL MODEL +// The method is the routing. Anything that is not a POST is a read — channel list or one +// channel's messages — and every POST carries an action. That keeps the CSRF boundary and the +// read/write boundary on the same line, so a mutation cannot arrive un-covered by being +// spelled as a GET. +// +// Reads are computed per request, not cached. Unread counts come from walking the last 200 +// messages of each channel against this host's read mark; the card polls on a slow cycle and +// the store is a small append log, so a cache would add a staleness class for no gain. +// +// Delivery is not part of the response's success. vv_nc_send() writes locally and spools for +// any partner that could not be reached, and the reply carries that spool depth as `queued`. +// A sleeping partner is a pending message, not a failed one. // // DESIGN PRINCIPLES // Read marks are local and per channel. "Unread" is a fact about this operator at this @@ -28,6 +35,22 @@ // locally and spooled for retry; saying "failed" over something that is stored and queued // would be the wrong claim. // +// REQUEST +// GET channels + this host's id + unread counts +// GET ?channel= that channel's messages +// POST action=send channel= text=… [color=#rrggbb] [font=mono|sans|serif] +// POST action=delete channel= id= local only +// POST action=read channel= mark seen up to now +// +// RESPONSE +// {"ok":true,"me":,"channels":[{…,"unread":N}],"hostnames":{id:name}} channel list +// {"ok":true,"me":,"channel":,"messages":[…],"last_read":} one channel +// {"ok":true,"msg":{…},"queued":N} send; N = spooled +// {"ok":true|false} delete +// {"ok":true} read +// {"ok":false,"error":"Unknown channel"|"Nothing to send"|"Could not store message" +// |"No message id"|"Unknown action"} +// // DEPENDS ON // include/node_chat.php // ═══════════════════════════════════════════════════════════════════════════════════════════════ diff --git a/Plugin/unraid/api/partnership.php b/Plugin/unraid/api/partnership.php index f99fef3..08c7446 100644 --- a/Plugin/unraid/api/partnership.php +++ b/Plugin/unraid/api/partnership.php @@ -5,6 +5,16 @@ // reachability, SSH trust, conf-sync state and the shared-service inventory — for the // partnership tab's poll. // +// OPERATIONAL MODEL +// One call, one document. The endpoint holds no logic and adds no cache of its own — it +// encodes whatever vv_partnership_all() returns, so any freshness policy is the library's and +// there is only one place it can be changed. +// +// The whole picture is assembled per request rather than exposed as separate endpoints for +// hosts, trust and services. Those answers are read together and judged against each other — +// a host that is reachable but has lost SSH trust is a different state from either fact alone, +// and splitting them would let the tab render a combination that never existed at one moment. +// // DESIGN PRINCIPLES // Thin transport. Host enumeration, SSH probing and trust evaluation live in // include/partnership.php; this file only sets the content type and encodes. diff --git a/Plugin/unraid/api/readscript.php b/Plugin/unraid/api/readscript.php index f2bd62f..c0e9bcb 100644 --- a/Plugin/unraid/api/readscript.php +++ b/Plugin/unraid/api/readscript.php @@ -4,6 +4,19 @@ // Script and document reader. Returns the full text of one .sh or .md file inside // SCRIPTS_DIR — the source view behind the scheduler page's script viewer and the docs tab. // +// OPERATIONAL MODEL +// Validate, resolve, read. The id is checked against a character class and an extension list +// before it is joined to SCRIPTS_DIR, so nothing reaches the filesystem that did not already +// look like a repo-relative path. +// +// The whole file is returned in one response — no ranges, no pagination. These are scripts and +// documents, not logs; the largest is a few hundred kilobytes, and a viewer that had to stitch +// pages together would be more machinery than the thing it displays. +// +// Every failure is a JSON body with ok:false, never an HTTP error code. The scheduler's viewer +// and the docs tab both parse the response before looking at anything else, so a 404 would +// surface as a parse failure rather than as "that file is not there". +// // DESIGN PRINCIPLES // Two extensions, one endpoint. // Scripts and their READMEs are read the same way because they are read for the same diff --git a/Plugin/unraid/api/watchdog.php b/Plugin/unraid/api/watchdog.php index 47aafc8..63cd2da 100644 --- a/Plugin/unraid/api/watchdog.php +++ b/Plugin/unraid/api/watchdog.php @@ -5,6 +5,16 @@ // resource, docker, system, storage, network and stability — together with the thresholds // each one is judging against, for the watchdog tab's poll. // +// OPERATIONAL MODEL +// Served from a 5-minute cache unless ?live is present. Assembling this payload reads every +// watchdog's state files and resolves every threshold out of master.conf, which is far more +// work than the tab's poll needs — the watchdogs themselves only run every 15 minutes, so a +// fresher answer would describe the same cycle. +// +// The cache is consulted before include/watchdog.php is even loaded, so a cache hit costs one +// file read and nothing else. ?live skips the read, recomputes, and writes the result back, so +// an explicit refresh also benefits the next visitor rather than being discarded. +// // DESIGN PRINCIPLES // Thin transport. State-file parsing and threshold resolution live in // include/watchdog.php; this file only sets the content type and encodes. @@ -15,7 +25,9 @@ // against, so the page never has to fetch the two independently and risk mismatching // them across a conf edit. // -// No parameters. Which watchdogs exist is fixed by the codebase, not by the request. +// The only parameter is a freshness override. ?live decides how old an answer may be, never +// what is in it — which watchdogs exist is fixed by the codebase, and no request can select, +// filter or widen the set. // // OPERATIONAL SAFEGUARDS // Read-only. Nothing here clears a strike, lifts a skip-list entry, restarts a container,