Document the api layer's operational models, and fix two headers that had gone stale against the code

This commit is contained in:
Gmer4Lfe
2026-08-25 16:25:55 -04:00
parent 63e68740b6
commit 4cc50889d6
6 changed files with 88 additions and 18 deletions
+7
View File
@@ -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.
+16 -11
View File
@@ -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()
+29 -6
View File
@@ -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=<id> that channel's messages
// POST action=send channel=<id> text=… [color=#rrggbb] [font=mono|sans|serif]
// POST action=delete channel=<id> id=<msgid> local only
// POST action=read channel=<id> 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=<id> that channel's messages
// POST action=send channel=<id> text=… [color=#rrggbb] [font=mono|sans|serif]
// POST action=delete channel=<id> id=<msgid> local only
// POST action=read channel=<id> mark seen up to now
//
// RESPONSE
// {"ok":true,"me":<host>,"channels":[{…,"unread":N}],"hostnames":{id:name}} channel list
// {"ok":true,"me":<host>,"channel":<id>,"messages":[…],"last_read":<ts>} 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
// ═══════════════════════════════════════════════════════════════════════════════════════════════
+10
View File
@@ -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.
+13
View File
@@ -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
+13 -1
View File
@@ -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,