Files
oikos/plans/2026-06-24-trmnl-plugins-lxc.md
dtoro 8a6422bd7d docs: move narrative wiki under knowledge/wiki/ (phase 3)
Problem: node and cross-cutting narratives lived at the repo root
(containers/, vms/, infrastructure/, host .md files), interleaved with the
machine-readable substrate.

Change:
- Move containers/ -> knowledge/wiki/containers/, vms/ -> knowledge/wiki/vms/,
  infrastructure/ -> knowledge/wiki/infrastructure/, hosts/{hubris,strong}.md ->
  knowledge/wiki/hosts/, infrastructure/references/ -> knowledge/sources/references/,
  GLOSSARY.md -> knowledge/GLOSSARY.md.
- Add knowledge/{index.md,log.md,sources/index.md} scaffolding.
- Rewrite all relative links repo-wide via a path-resolving mapper (inbound +
  outbound + between-moved-files), including .hermes/, runbooks, operations,
  investigations, plans, README, AGENTS.
- Repoint inventory.yaml doc_page fields and regenerate hosts/*.yaml (which
  embed doc_page); update oikos/gen-topology.py output path, candidate doc
  paths, and footer links; update code-comment doc paths.

Substrate untouched in place: inventory.yaml, hosts/*.yaml (regenerated,
idempotent), oikos/ code, mcp/, secrets/, bin/.

Verification:
- Logical broken-link set identical to pre-move baseline (net 128 -> 127; the
  topology regen fixed one, introduced none). Remaining are pre-existing refs
  to destroyed/archived nodes, out of scope for this move.
- gen-topology.py --check exit 0 (in sync); cards carry knowledge/wiki/ doc paths.
- build_host_files.py idempotent; all inventory doc_page targets resolve.
- MCP contract verified: get_page/search_docs/get_changelog resolve moved pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:35:23 +02:00

5.8 KiB
Raw Blame History

2026-06-24 — TRMNL plugins LXC (128) + middleware deploy pipeline

Goal

Stand up a dedicated LXC to host self-hosted middleware for TRMNL e-ink plugins. TRMNL cloud polls https://trmnl.hubris.network/<plugin>/dashboard every 15 min; the middleware fetches/shapes live data and returns JSON that TRMNL merges into the plugin's Liquid template. First consumer: the Munich Home Dashboard (/munich-home/dashboard). One LXC + one FastAPI service hosts all current and future plugins (router per plugin).

Source repo: gitea dtoro/terminalito (app code). This repo only documents the fabric wiring, same split as Artifacto/Plato.

Current state

  • No TRMNL middleware in the lab. Highest LXC id is 127 (see containers/index.md).
  • Public hostnames terminate at the VPS netbird traefik → netbird mesh → caddy (121) → backend LXC. Cert obtained by Caddy (IONOS DNS-01) and mirrored to the VPS by the daily cert-sync timer on the host.
  • Auto-deploy pipelines are gitea-webhook driven, two shapes (see auto-deploy).

Target state

TRMNL cloud  --GET 15m, Bearer token-->  https://trmnl.hubris.network/munich-home/dashboard
   VPS traefik (public TLS) --netbird--> caddy (121) --> trmnl (128) :9851  trmnl-plugins.service
                                                          ├ Open-Meteo (weather)
                                                          ├ MVG departures (transit)
                                                          └ Google Calendar (OAuth, SOPS)
  • LXC 128 trmnl: Debian, unprivileged, ~1 core / 512 MiB1 GiB / 8 GiB rootfs. No mounts.
  • Service trmnl-plugins.serviceuvicorn server.app:app --host 0.0.0.0 --port 9851, EnvironmentFile=/etc/trmnl-plugins/env. Auth: every path except /health requires Authorization: Bearer $TRMNL_POLL_TOKEN.
  • Auto-deploy (Shape B): /opt/terminalito working tree, sibling /opt/terminalito-deploy/.
  • Public hostname trmnl.hubris.network.

Pre-flight checklist

  • Confirm next free LXC id is 128 (homelab list, containers/index.md).
  • Decide IP on 192.168.8.0/16 LAN (e.g. 192.168.8.211) — pick a free one.
  • Have Google OAuth client + refresh token, MVG stop globalIds, and a generated trmnl_poll_token ready for the secret (see dtoro/terminalito README).

Step-by-step procedure

  1. Provision + enroll

    # create LXC 128 trmnl on hubris (Debian), then enroll it:
    homelab client add trmnl                 # joins netbird, provisions /etc/age/key.txt, edits inventory.yaml
    homelab client add --finalize-pubkey <age_pubkey>   # commits the age pubkey
    ssh trmnl 'apt-get install -y python3-venv git'
    
  2. Secret (trmnl-oauth): create secrets/trmnl-oauth.yaml with google_client_id/secret/refresh_token, MVG stop ids, and trmnl_poll_token; add a path_regex rule in .sops.yaml granting trmnl's age pubkey; sops updatekeys. The service reads it at deploy time via homelab secret trmnl-oauth/etc/trmnl-plugins/env.

  3. App + service on LXC 128 (clone uses the internal gitea URL — git.hubris.network resets from inside LXCs):

    git clone http://192.168.8.121:3000/dtoro/terminalito.git /opt/terminalito
    python3 -m venv /opt/terminalito/server/.venv
    /opt/terminalito/server/.venv/bin/pip install -r /opt/terminalito/server/requirements.txt
    # install /etc/systemd/system/trmnl-plugins.service, enable --now
    
  4. Deploy pipeline (Shape B, mirrors homelab-mcp): create /opt/terminalito-deploy/ {webhook.py,deploy.sh} (HMAC vs /etc/terminalito-deploy/secret, filter refs/heads/main, git pullpip install -r server/requirements.txt → rebuild env from homelab secretsystemctl restart trmnl-plugins). Receiver :9797. Git creds at /etc/terminalito-deploy/git-credentials (mode 600). Register a gitea webhook on dtoro/terminalito; add 192.168.8.<128-ip> to gitea app.ini ALLOWED_HOST_LIST.

  5. DNS: add trmnl.hubris.network A → 192.168.8.175 (caddy) on Technitium (107).

  6. Caddy (dtoro/caddy-conf, commit+push auto-deploys):

    trmnl.hubris.network { reverse_proxy 192.168.8.<128-ip>:9851 }
    
  7. Public exposure on the VPS: add traefik router+service for Host(\trmnl.hubris.network`)http://192.168.8.<128-ip>:9851`; add the host to the cert-sync map so the LE cert mirrors over.

  8. TRMNL cloud: create a Polling private plugin, URL …/munich-home/dashboard, header Authorization: Bearer <trmnl_poll_token>, refresh 15 min; paste full.liquid; add to a playlist.

Verification

  • ssh trmnl systemctl is-active trmnl-pluginsactive; curl -s localhost:9851/healthok.
  • LAN: curl -s -H "Authorization: Bearer <tok>" https://trmnl.hubris.network/munich-home/dashboard → 200 JSON; without the header → 401; /health → 200.
  • Public: same curl from off-mesh resolves via VPS, 200.
  • Push a no-op commit → /opt/terminalito-deploy logs show pull+restart; webhook 202.
  • homelab mcp get_host trmnl and search_docs trmnl agree with containers/128-trmnl.md.

Post-migration

When executed, write changelog entries (same date) on: containers/128-trmnl.md (new page), containers/index.md (row), inventory.yaml (services.trmnl), infrastructure/auto-deploy.md (pipeline row + ALLOWED_HOST_LIST + changelog), containers/104-gitea.md (webhook + allowed host), containers/121-caddy.md (new proxied host), containers/107-dns.md (A record), hosts/netbird-vps.md (public route + cert-sync). Then set this plan's status to Done in plans/index.md.