docs(open-notebook): runbook, episode-destination interim, and the log entry

Page registered in the vault index (the docs-completeness seam fails an
unregistered page) and logged in the bundle change log.

What the page owes the reader: the provider contract (frank-zealot for scripts
with a /v1 base URL, frank-tts for voices with 1-4 speakers mapped to the
narrator1/2/3 voice packs typed as free text), why Test Connection and Discover
Models fail cosmetically against frank-tts rather than being a fault, the
encryption key as the precondition for saving any credential, and the honest
blocker — frank-tts is not deployed and nothing listens on 7772, so a generation
run writes its script and fails on the first voice.

Episode destination is stated as interim and cites Gitea issue #7 (human-only,
agents must not work it): the app keeps its named volume, one bind mount puts
finished MP3s under /home/dank/media/podcasts, and deleting that one line is the
whole rollback. Rollback section also covers the full undo, including that the
podman network only disappears once the units are gone.
This commit is contained in:
Frank
2026-10-18 16:27:03 -07:00
parent ff11c37ee0
commit e2393bacad
3 changed files with 212 additions and 0 deletions
+79
View File
@@ -2311,3 +2311,82 @@ every restart since the cutover.
Kokoro-enum-only — so "the agent talks in my voice" needs that provider row or
a `pi.registerProvider` extension, decided in phase 2.
## 2026-10-18 — open-notebook: the podcast generator's config lands (nothing deployed)
- Goal: **open-notebook** as the fleet's podcast generator on Dankclaw — notes
and sources in, a multi-speaker episode out, script written by
**frank-zealot**, voices rendered by **frank-tts**, no cloud provider
anywhere in the path. Repo-side only: no host touched, no container pulled or
started. Runbook: [[Open Notebook]].
- **Two containers, not one.** `lfnovo/open_notebook:v1-latest` plus
`surrealdb/surrealdb:v2`, both **digest-pinned** with `Pull=never` and a
documented one-time `podman pull` — frank-dragoon's pinning style, because
`:latest` moving underneath a rootless service is how you get an unbootable
pod after an unrelated `systemctl-daemon-reload`. The registry round-trip is
what proved `v2` is SurrealDB **2.7.0** and that the app image is Debian
trixie with **curl and ffmpeg already inside** — which is how both health
probes can be argument-only: `curl -fsS …/health` (route is upstream's,
`api/main.py:239`, and it sits on the auth exclude list) and
`/surreal is-ready`, which I ran against the digest here: green in 2 s, and
it refuses to pass without `SURREAL_PASS` in the environment — a credential
that must never ride the command line, so `Exec=` carries flags only.
- **Host 5055 was already spoken for.** seerr has held `127.0.0.1:5055` since
2026-10-14 (live `ss -ltn` agrees today), so the API publishes
`0.0.0.0:5056:5055`. open-notebook's frontend derives its API URL from the
window origin when `API_URL` is unset — the upstream env reference spells the
reverse-proxy symptom — so a silent remap would hang the UI on a port nobody
listens on, and `API_URL=http://192.168.0.143:5056` is set accordingly. 8502
publishes on `0.0.0.0` because this service carries its own password (the
episode-audio route is unauthenticated), unlike the loopback-and-proxied
observability stack; **8000 is published nowhere**, where upstream exposes it
on loopback with root credentials, and the seam forbids it.
- **The CPU guard, pre-armed.** `frank-tts` spawns one ~8 GiB, 8-thread engine
per request on a box that also serves two LLMs, while open-notebook defaults
to `TTS_BATCH_SIZE=5` × `OPEN_NOTEBOOK_WORKER_MAX_TASKS=5` — a five-way
self-DoS. Both are pinned to `1` in `scripts/open-notebook.env`, and the
worker knob is only real because the image's own supervisord expands
`${OPEN_NOTEBOOK_WORKER_MAX_TASKS:-5}` (`supervisord.conf:25`). Both
`ESPERANTO_*` timeouts go above upstream's 300 s for CPU synthesis.
- **What a credential needs in order to exist:** `OPEN_NOTEBOOK_ENCRYPTION_KEY`
must be set before the UI can save *any* provider credential, and
`OPEN_NOTEBOOK_PASSWORD` must stay **absent** to remain auth-off — the app
reads `os.environ.get(…) is not None`, so `PASSWORD=` boots a login form
asking for the empty string. The repo env file carries both keys empty; the
deployer seeds `/home/dank/Containers/open-notebook/env` (600) from the host
vault and **refuses to start the pair while either secret is blank**, because
a half-configured open-notebook stores no credentials and fails quietly.
- **Episode destination is interim, by decision.** The destination is **Gitea
issue #7, `human-only`** — agents must not work it. Until Frank settles it,
the app keeps its own named volume and `/home/dank/media/podcasts` is
bind-mounted over `/app/data/podcasts` (`DATA_FOLDER` is hard-coded `./data`),
so finished MP3s are ordinary files in his home directory instead of volume
contents needing `podman unshare` — the database volume demonstrably lands on
a mapped subuid, measured with the pinned image. One mount line; one deletion
undoes it.
- **Two UI buttons will fail, and that is cosmetic.** Test Connection does
`GET <base>/models` and the TTS probe synthesises voice `alloy`; frank-tts
serves `/health` and `/v1/audio/speech` only. Model rows are created *by
name* and the voice field is free text, so `narrator1/2/3` is typed in and
never discovered. Documented, not worked around.
- **Blocked on frank-tts, stated plainly:** phase 1 landed without deployer or
homepage registration and nothing listens on 7772 today, so a generation run
writes its script and then fails on the first voice. That is upstream's
creation-podcasts warning in fleet clothing.
- Suite green: **403/403** (`./tests/run.sh`, wall 67 s) — 19 tests in the new
`tests/open_notebook.bats`, structure only: pins, boot persistence, the
unpublishable database port, the 5055→5056 remap plus the `API_URL` that has
to match it, the env-file read order, the guard knobs, the health gate naming
`/health`, planner routing, the homepage card hidden while undeployed.
- Three deployer seams cost time and are now law-shaped: a service must be
registered in **five** places — `SERVICE_ORDER`, `DANKCLAW_SERVICES`, the
`deploy-remote.sh` dispatch case, its pre-deploy requirement, and
`deploy.sh`'s dispatch regex, where an omission silently plans an empty set
for `--only`; a `dankclaw/<svc>/*` case must precede the `dankclaw/*.container`
glob because that `*` crosses `/`; and the changed-path routing lives *inside*
the per-path loop, after the `scripts/*.env)` case, not at the top of the file.
- Deliberately NOT done: the deploy (pulling 1.27 GiB to Dankclaw was not
asked for), any Caddy vhost or public exposure, audiobook/EPUB (issue #5), any
`frank-tts` code change, and the harbor templates on Dankserver —
`~/.harbor/compose.opennotebook.yml` pins the deprecated `-single` image,
carries no encryption key and injects the whole harbor env — left untouched,
read-only, as the record of what not to copy.