docs: correct what a pre-release audit found across the shipped markdown

Checked every tracked .md against the code rather than for plausibility. The
API table, the metric names, the dashboard payload, the permissions
cheat-sheet, every config field and SECURITY's numbers all already matched.
Four things did not.

ARCHITECTURE contradicted itself on the power lease: keying it on the host
updated the description a few lines up but left a later paragraph still saying
the lease is per device, complete with the extra sleep/wake cycle that no
longer happens. Replaced with the consequence that is now true -- every device
on a held machine reads busy, which is what disables the power button on its
siblings.

The startup catch-up was documented nowhere despite sending notifications.
ARCHITECTURE's scheduler section now covers it and the heartbeat that decides
what counts as downtime.

frontend/README pointed at design/joulenap-remix/, which .gitignore excludes --
a path no clone has. It also filed the setup wizard under the settings panels,
where it has not lived since it moved to src/wizard/, and said npm install
where the lockfile wants npm ci.

Two wordings tightened: the WoL interface is a dropdown now, not free text with
an override; and "stores a copy needed to verify requests" read as though the
API key were hashed, when it sits in config.yaml like every other secret.
This commit is contained in:
Catubba
2026-08-07 15:35:28 +02:00
parent 0170773f03
commit 6478adc845
4 changed files with 12 additions and 10 deletions
+3 -2
View File
@@ -13,7 +13,8 @@
- **Web UI** (frontend): single-page app. Talks to the backend over the REST API below.
- **Backend / API**: serves the UI, exposes the REST API, holds the scheduler, runs the route cycles, manages config.
- **Scheduler**: in-process (APScheduler). **One cron trigger per enabled route**, plus a daily history-prune job (armed independently, so history is trimmed even while every route is paused). Re-armed whenever config changes. GC and verify have no triggers of their own — they are options of a route, or a manual action on a box.
- **Scheduler**: in-process (APScheduler). **One cron trigger per enabled route**, plus a daily history-prune job and a one-minute liveness heartbeat (both armed independently, so history is trimmed and liveness recorded even while every route is paused). Re-armed whenever config changes. GC and verify have no triggers of their own — they are options of a route, or a manual action on a box.
- **Startup catch-up**: the jobstore is in memory, so a fire due while the container was stopped is simply lost. At startup each armed route is checked for a slot that came due *while the process was not running*, and one is reported as a missed run (logged, and notified under `on_failure` — never auto-run: a restart must not kick off a heavy PBS-waking backup). "Not running" is a fact, not an inference: the heartbeat touches `data/.heartbeat` every minute and its mtime bounds the window, so a schedule edited to earlier in the day — or a route disabled and re-enabled, or the kill-switch — cannot be mistaken for downtime. No heartbeat on record (first boot, unwritable data dir) reports nothing.
- **Run queue + power lease**: one run is ever in flight; the rest wait in a FIFO queue. Each PBS a run needs is held under a refcounted lease that wakes it on first acquire and powers it off on last release. See below.
- **Connectors**:
- `pve` — PVE API client (list guests, trigger `vzdump`, read task status).
@@ -72,7 +73,7 @@ Only the last of those is worth a warning; the other three are the correct outco
`managed_power: false` describes an always-on PBS. The lease is the single place that knows: acquiring degrades to a reachability check and releasing does nothing.
**The lease is keyed per device, not per machine.** A backup server serving two datastores is two devices, so each holds its own lease and neither knows about the other: a run that finishes with the first can power the machine off while a queued run on the second still wants it, and that run then wakes it again. Runs are serialised by the single-run lock, so this costs one extra sleep/wake cycle rather than correctness — but on one physical box, prefer a single datastore, or expect the extra cycle.
Because the key is the machine, a run holding one datastore of a box reports every device on that box as busy — which is what disables the ⏻ button on its siblings, since an SSH power-off would take the running server down with them.
## What each kind does
+1 -1
View File
@@ -40,7 +40,7 @@ For each field: **auto** = discovered/derived, **manual** = entered.
| PBS host/IP, port | auto | read from the PVE storage config (manual in flow B) |
| PBS datastore | auto | from the storage config |
| PBS fingerprint | auto | from the storage config, or read from the PBS certificate on connect |
| WoL broadcast interface | auto | the NIC with the route to the PBS subnet; override allowed |
| WoL broadcast interface | auto | the NIC with the route to the PBS subnet; a dropdown lists the host's own NICs if you want to pin one |
| PBS MAC | auto | connect + read ARP, with the PBS powered on ("Detect MAC") |
| PBS API token | manual, or auto in root mode | see the privileges below |
| PBS SSH host key | auto | scanned, shown, saved to `data/known_hosts` on your confirmation |
+4 -3
View File
@@ -39,9 +39,10 @@ Integrations**; enabling the integration enables both.
1. Open Joulenap → **Settings → Integrations**.
2. Click **Generate API key**. The key is shown once — copy it somewhere
safe (a password manager, your dashboard's secret store, etc.). Joulenap
only stores a copy needed to verify requests; it won't show you the key
again.
safe (a password manager, your dashboard's secret store, etc.). The
interface will not show it again: like every other secret it lives in
`config.yaml` and is redacted from every API response, so recovering it
means reading that file on the host — or generating a new one.
3. Pick your dashboard in the picker on that page to get a ready-to-paste
config snippet with the key and endpoint URL already filled in.
4. Disabling the integration (the **Disable** button) clears the key, and
+4 -4
View File
@@ -1,13 +1,12 @@
# Joulenap frontend
React + TypeScript + Vite SPA for Joulenap, recreated from the prototypes in
`design/joulenap-remix/` and wired to the backend REST API.
React + TypeScript + Vite SPA for Joulenap, wired to the backend REST API.
## Develop
```bash
cd frontend
npm install
npm ci
npm run dev # Vite dev server on :5173, proxies /api -> http://localhost:8080
```
@@ -27,7 +26,8 @@ needs the built output present. `dist/` is git-ignored.
- `src/api/` — typed client + response types for `/api/*`
- `src/auth/`, `src/config/` — auth + config React contexts
- `src/pages/``Login`, `Dashboard` (+ `dashboard/` panels), `Settings` (+ `settings/` panels incl. the setup wizard)
- `src/pages/``Login`, `Dashboard` (+ `dashboard/` panels), `Settings` (+ `settings/` panels, incl. the device editor)
- `src/wizard/` — the two device wizards (add a PVE, add a PBS) and the PBS steps they share
- `src/shell/` — header + authenticated app shell
- `src/components/`, `src/hooks/`, `src/utils/` — shared widgets/helpers
- `src/i18n/``react-i18next` setup; `en.json` is the base language, `it.json` the translation