Phase 1 of the TRMNL X entrance dashboard plan — stands up the self-hosted Terminus (BYOS) server the panel polls. The HA dashboard, e-ink theme and screenshot renderer are later phases and are not in this PR.
Phase 0 findings (verified before writing anything)
Check
Result
TRMNL X model in Terminus
Present.id: v2, label: "TRMNL X", 1872x1404, 16 colours, 4-bit. The plan's stop-and-report condition does not trigger.
How the model list is populated
Not via config/db/seeds.rb. The MODEL_SYNCHRONIZER Sidekiq job pulls it from trmnl.com — the Models list is empty until that job runs once.
Add-on vs standalone container
Our HA is plain HA Core, not Supervised. The add-on still works standalone: it prefers env vars over /data/options.json and authenticates with an ordinary long-lived token (SUPERVISOR_TOKEN only flips an isAddOn boolean). The plan's fallback path is viable. Lands with Phase 4.
Terminus version
0.68.0 (2026-08-05). Repo was renamed from byos_hanami; image is ghcr.io/usetrmnl/terminus. No GitHub Releases, no changelog.
Firmware TLS constraints
UNVERIFIED upstream — nothing in the Terminus repo or docs states TLS 1.2-only or CA rejection. Plain HTTP for the device route is implemented as the plan specifies, but the justification rests on the plan, not on a doc I could cite.
Deliberate deviations from house patterns
tr-pg runs PostgreSQL 18, not the standard 16. Terminus ships a committed config/db/structure.sql containing SET transaction_timeout = 0; — a GUC that does not exist before PG 17 — so schema load against a 16 server fails with unrecognized configuration parameter. This is the first non-16 CNPG cluster here. Worth noting the existing 16 pin is inherited: it first appears in 739a67e9 (Apr 2026, Authentik migration) and was copy-pasted into every later cluster with no documented rationale.
Device endpoint is cleartext HTTP on a Cilium BGP LoadBalancer, not an Ingress. The firmware cannot be handed the internal CA, and a TLS-less Ingress would be a new pattern for this repo — a dedicated LB address is not. Admin UI keeps a normal TLS Ingress. Both front the same Service; same split as home.renner.uno / ha.renner.uno.
trmnl.renner.uno — TLS, Traefik, admin UI
trmnl-device.renner.uno — LB, port 80, cleartext, device only. API_URI points here because Terminus bakes it into the absolute image_url the device fetches.
Terminus is NOT enrolled in image automation, despite publishing a usable tag stream. It serves the firmware and /api/display contract to a wall-mounted device; upstream is pre-1.0 with no releases or changelog; an unattended bump that breaks either leaves the panel dark with no rollback that avoids the captive portal. Bumps are manual and reviewed — rationale recorded in image-automation/README.md. The Valkey sidecar is enrolled.
Egress is an FQDN allowlist, not toEntities: world. Scoped to trmnl.com and the S3 host serving firmware binaries. Two caveats written into the policy:
The S3 host is the url field of the /api/firmware/latest response, not a source constant — TRMNL can move bucket/region without a Terminus release and firmware sync then fails closed here.
Terminus's Extensions/Poll feature fetches admin-configured URLs from a DB-stored Liquid template. Those are unknowable from source and will be blocked. Accepted — screen content is pushed in by the renderer, not polled by plugins.
The CNPG pods are deliberately left out of that egress policy, so the barman WAL archive path is not behind a rule that fails closed.
All six touched kustomizations build (kubectl kustomize), which is what ci-manifests-test.yml gates on.
All three pinned images confirmed to exist in their registries by manifest request: ghcr.io/usetrmnl/terminus:0.68.0, valkey/valkey:9.1-alpine, ghcr.io/cloudnative-pg/postgresql:18.
homelab/apps/trmnl seeded in OpenBao with a freshly generated APP_SECRET.
Blocked before this can reconcile
homelab/seed/trmnl/hetzner-creds is not seeded — the copy command was blocked by the sandbox classifier. Without it the hetzner-s3-creds ExternalSecret never syncs and the CNPG ObjectStore has no credentials. One command, values copied from the immich path:
bao kv put -address=https://openbao.renner.uno homelab/seed/trmnl/hetzner-creds \
ACCESS_KEY_ID="$(bao kv get -address=https://openbao.renner.uno -field=ACCESS_KEY_ID homelab/seed/immich/hetzner-creds)" \
ACCESS_SECRET_KEY="$(bao kv get -address=https://openbao.renner.uno -field=ACCESS_SECRET_KEY homelab/seed/immich/hetzner-creds)" \
REGION="$(bao kv get -address=https://openbao.renner.uno -field=REGION homelab/seed/immich/hetzner-creds)"
Note BAO_ADDR is http:// and writes get a 308 that drops the body — hence the explicit -address=https://.
Not verified, and worth knowing
The device VLAN must resolve trmnl-device.renner.uno. The URI is typed into the captive portal by hand at provisioning, so if the panel's VLAN can't resolve renner.uno, this breaks at Phase 5 with no error visible from the cluster side. Untested — I have no client on that VLAN.
Probes are TCP, not HTTP. Terminus documents no health endpoint; guessing a path seemed worse than a port check. Tighten once the route set is confirmed against the running app.
First boot compiles assets and migrates a fresh DB, hence the 10-minute startupProbe budget. If it needs longer, that number is the thing to raise.
Phase 1 of the TRMNL X entrance dashboard plan — stands up the self-hosted Terminus (BYOS) server the panel polls. The HA dashboard, e-ink theme and screenshot renderer are later phases and are **not** in this PR.
## Phase 0 findings (verified before writing anything)
| Check | Result |
|---|---|
| TRMNL X model in Terminus | **Present.** `id: v2`, `label: "TRMNL X"`, 1872x1404, 16 colours, 4-bit. The plan's stop-and-report condition does not trigger. |
| How the model list is populated | **Not** via `config/db/seeds.rb`. The `MODEL_SYNCHRONIZER` Sidekiq job pulls it from trmnl.com — the Models list is empty until that job runs once. |
| Add-on vs standalone container | Our HA is plain HA Core, **not** Supervised. The add-on still works standalone: it prefers env vars over `/data/options.json` and authenticates with an ordinary long-lived token (`SUPERVISOR_TOKEN` only flips an `isAddOn` boolean). The plan's fallback path is viable. Lands with Phase 4. |
| Terminus version | `0.68.0` (2026-08-05). Repo was renamed from `byos_hanami`; image is `ghcr.io/usetrmnl/terminus`. No GitHub Releases, no changelog. |
| Firmware TLS constraints | **UNVERIFIED upstream** — nothing in the Terminus repo or docs states TLS 1.2-only or CA rejection. Plain HTTP for the device route is implemented as the plan specifies, but the justification rests on the plan, not on a doc I could cite. |
## Deliberate deviations from house patterns
**`tr-pg` runs PostgreSQL 18, not the standard 16.** Terminus ships a committed `config/db/structure.sql` containing `SET transaction_timeout = 0;` — a GUC that does not exist before PG 17 — so schema load against a 16 server fails with `unrecognized configuration parameter`. This is the first non-16 CNPG cluster here. Worth noting the existing 16 pin is inherited: it first appears in 739a67e9 (Apr 2026, Authentik migration) and was copy-pasted into every later cluster with no documented rationale.
**Device endpoint is cleartext HTTP on a Cilium BGP LoadBalancer**, not an Ingress. The firmware cannot be handed the internal CA, and a TLS-less Ingress would be a new pattern for this repo — a dedicated LB address is not. Admin UI keeps a normal TLS Ingress. Both front the same Service; same split as `home.renner.uno` / `ha.renner.uno`.
- `trmnl.renner.uno` — TLS, Traefik, admin UI
- `trmnl-device.renner.uno` — LB, port 80, cleartext, device only. `API_URI` points here because Terminus bakes it into the absolute `image_url` the device fetches.
**Terminus is NOT enrolled in image automation**, despite publishing a usable tag stream. It serves the firmware and `/api/display` contract to a wall-mounted device; upstream is pre-1.0 with no releases or changelog; an unattended bump that breaks either leaves the panel dark with no rollback that avoids the captive portal. Bumps are manual and reviewed — rationale recorded in `image-automation/README.md`. The Valkey sidecar **is** enrolled.
**Egress is an FQDN allowlist, not `toEntities: world`.** Scoped to `trmnl.com` and the S3 host serving firmware binaries. Two caveats written into the policy:
- The S3 host is the `url` field of the `/api/firmware/latest` response, not a source constant — TRMNL can move bucket/region without a Terminus release and firmware sync then fails closed here.
- Terminus's Extensions/Poll feature fetches admin-configured URLs from a DB-stored Liquid template. Those are unknowable from source and **will** be blocked. Accepted — screen content is pushed in by the renderer, not polled by plugins.
The CNPG pods are deliberately left out of that egress policy, so the barman WAL archive path is not behind a rule that fails closed.
## Registration edits
Namespace, OpenBao `ClusterSecretStore` conditions, network-policy kustomization, Velero schedule (08:15, next free stagger slot), image-automation kustomization, Gatus endpoint.
## Verified
- All six touched kustomizations build (`kubectl kustomize`), which is what `ci-manifests-test.yml` gates on.
- All three pinned images confirmed to exist in their registries by manifest request: `ghcr.io/usetrmnl/terminus:0.68.0`, `valkey/valkey:9.1-alpine`, `ghcr.io/cloudnative-pg/postgresql:18`.
- `homelab/apps/trmnl` seeded in OpenBao with a freshly generated `APP_SECRET`.
## Blocked before this can reconcile
`homelab/seed/trmnl/hetzner-creds` is **not** seeded — the copy command was blocked by the sandbox classifier. Without it the `hetzner-s3-creds` ExternalSecret never syncs and the CNPG ObjectStore has no credentials. One command, values copied from the immich path:
```
bao kv put -address=https://openbao.renner.uno homelab/seed/trmnl/hetzner-creds \
ACCESS_KEY_ID="$(bao kv get -address=https://openbao.renner.uno -field=ACCESS_KEY_ID homelab/seed/immich/hetzner-creds)" \
ACCESS_SECRET_KEY="$(bao kv get -address=https://openbao.renner.uno -field=ACCESS_SECRET_KEY homelab/seed/immich/hetzner-creds)" \
REGION="$(bao kv get -address=https://openbao.renner.uno -field=REGION homelab/seed/immich/hetzner-creds)"
```
Note `BAO_ADDR` is `http://` and writes get a 308 that drops the body — hence the explicit `-address=https://`.
## Not verified, and worth knowing
- **The device VLAN must resolve `trmnl-device.renner.uno`.** The URI is typed into the captive portal by hand at provisioning, so if the panel's VLAN can't resolve `renner.uno`, this breaks at Phase 5 with no error visible from the cluster side. Untested — I have no client on that VLAN.
- Probes are TCP, not HTTP. Terminus documents no health endpoint; guessing a path seemed worse than a port check. Tighten once the route set is confirmed against the running app.
- First boot compiles assets and migrates a fresh DB, hence the 10-minute `startupProbe` budget. If it needs longer, that number is the thing to raise.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Phase 1 of the TRMNL X dashboard plan: stand up the self-hosted Terminus
(BYOS) server the panel will poll. The Home Assistant dashboard, the e-ink
theme and the screenshot renderer are later phases and are not in here.
Phase 0 verification, all confirmed against upstream before writing any of
this:
* TRMNL X IS a first-class model in Terminus (id v2 / label "TRMNL X",
1872x1404, 16 colours, 4-bit) — the stop-and-report condition in the
plan does not trigger. Note it is NOT in config/db/seeds.rb: the model
list is populated by the MODEL_SYNCHRONIZER job pulling trmnl.com, so
the Models list is empty until that job has run once.
* The HA add-on can run standalone. Our HA is plain HA Core, not
Supervised, but the add-on reads env vars in preference to
/data/options.json and authenticates with an ordinary long-lived
token — SUPERVISOR_TOKEN only flips an isAddOn boolean. So the
fallback path in the plan (upstream container, not add-on) is viable.
That deployment lands with Phase 4.
Deviations from the repo's house patterns, each deliberate:
* tr-pg runs PostgreSQL 18, not the standard 16. Terminus ships a
committed config/db/structure.sql containing `SET transaction_timeout
= 0;`, a GUC that does not exist before PG 17, so schema load against
a 16 server fails outright. This is the first non-16 CNPG cluster
here. The existing 16 pin is inherited from 739a67e9 (Apr 2026) and
has no documented rationale.
* The device endpoint is cleartext HTTP on a Cilium BGP LoadBalancer
(trmnl-device.renner.uno) rather than an Ingress. The firmware cannot
be given the internal CA, and a TLS-less Ingress would be a new
pattern for this repo — a dedicated LB address is not. The admin UI
keeps a normal TLS Ingress at trmnl.renner.uno; both front the same
Service. Same split as home.renner.uno / ha.renner.uno.
* ghcr.io/usetrmnl/terminus is NOT enrolled in image automation despite
publishing a usable tag stream. It serves the firmware and
/api/display contract to a wall-mounted device, upstream is pre-1.0
with no releases or changelog, and an unattended bump that breaks
either leaves the panel dark. Bumps are manual and reviewed. The
Valkey sidecar is enrolled — it touches no device-facing contract.
* Egress is an FQDN allowlist (trmnl.com, plus the S3 host that serves
firmware binaries), not toEntities: world. The S3 host is returned by
the API at request time rather than being a source constant, so it can
drift; the policy comment says where to look when firmware sync
starts failing. This also means Terminus's Extensions/Poll feature,
which fetches admin-configured URLs, will not work — accepted, since
screen content is pushed in by the renderer.
* The CNPG pods are deliberately left OUT of that egress policy so the
barman WAL archive path is not behind a rule that fails closed.
Images verified to exist in their registries before pinning:
ghcr.io/usetrmnl/terminus:0.68.0, valkey/valkey:9.1-alpine,
ghcr.io/cloudnative-pg/postgresql:18.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🤖Automated review — model CLI default (complexity: complex), commit ac2d72810827966cdac18dd76f46f62b3881df2f.
Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.
Summary
Phase 1 of the TRMNL BYOS deployment: a new trmnl namespace running Terminus (web + sidekiq), a Valkey queue, a dedicated CNPG PostgreSQL 18 cluster with offsite Barman backup, an admin Ingress plus a cleartext device LoadBalancer, a Cilium egress/ingress policy set, and the associated registrations (namespace, OpenBao ClusterSecretStore, Velero schedule, image-automation, Gatus). I cross-checked every deviation the PR body calls out (PG18 pin, cleartext device LB, unenrolled image automation, FQDN egress allowlist) against the existing patterns for immich, paperless, authentik, and home-assistant in this repo, and against the actual cluster CIDRs — all of it lines up. I did not find a blocking defect.
🟡 Suggestions
kubernetes/apps/trmnl/objectstore.yaml and hetzner-creds-externalsecret.yaml depend on the OpenBao path homelab/seed/trmnl/hetzner-creds, which the PR body itself says is not yet seeded (blocked by a sandbox classifier). As written, once this merges the hetzner-s3-creds ExternalSecret will fail to sync and the CNPG ObjectStore/WAL archiving for tr-pg won't come up until that manual bao kv put is run. Worth doing before/immediately after merge so the cluster doesn't sit half-reconciled with no backup path.
kubernetes/infrastructure/network-policies/trmnl-ns.yaml:834-846 hardcodes trmnl-fw.s3.us-east-2.amazonaws.com as a toFQDNs egress target, but the comment directly above it says this hostname comes from a live API response (/api/firmware/latest) and is not a source constant — TRMNL can move the bucket without a Terminus release. This is explicitly called out as an accepted risk rather than an oversight, so it's not blocking, just flagging that firmware sync is the first thing to check if it silently stops working.
kubernetes/apps/trmnl/terminus.yaml doesn't set readOnlyRootFilesystem on either the terminus or sidekiq container, while the new valkey.yaml sidecar does. That's likely deliberate (asset compile + Hanami boot need a writable rootfs) and matches the looser pattern elsewhere in the repo (e.g. paperless's main container isn't read-only either), so this is a minor consistency note, not a request.
🟢 Nits
kubernetes/apps/trmnl/terminus.yaml:445-479 (sidekiq container) has no liveness/readiness probe at all, unlike the terminus web container. This matches the existing precedent for secondary containers in this repo (e.g. paperless's bridge-tls-proxy sidecar is also unprobed), so it's consistent, just worth a mental note if the sidekiq worker ever needs its own health signal.
Tests
There's no traditional test suite for this kind of change — the repo's bar is kubectl kustomize building cleanly for touched kustomizations, which the PR body says was verified for all six. I didn't re-run that locally, but I did verify structurally: the new kubernetes/apps/trmnl/kustomization.yaml resource list matches the files actually added, the trmnl namespace is correctly threaded through namespaces.yaml, openbao-clustersecretstore.yaml (alphabetically inserted), network-policies/kustomization.yaml, and image-automation/kustomization.yaml, and the Flux Kustomization in kubernetes/clusters/homelab/trmnl.yaml matches the dependsOn/healthCheckExprs shape used by immich.yaml/paperless.yaml/authentik.yaml verbatim. No CI-visible test gap here beyond the manifest-build check the repo already relies on.
Recommendation
Looks reasonable — no blocking issues; comments above are optional (the OpenBao seed-credential gap is the one item worth resolving before this fully reconciles, but it's already flagged by the author in the PR description, not a defect in the diff itself).
🤖 **Automated review** — model `CLI default` (complexity: complex), commit `ac2d72810827966cdac18dd76f46f62b3881df2f`.
> Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review.
---
## Summary
Phase 1 of the TRMNL BYOS deployment: a new `trmnl` namespace running Terminus (web + sidekiq), a Valkey queue, a dedicated CNPG PostgreSQL 18 cluster with offsite Barman backup, an admin Ingress plus a cleartext device LoadBalancer, a Cilium egress/ingress policy set, and the associated registrations (namespace, OpenBao `ClusterSecretStore`, Velero schedule, image-automation, Gatus). I cross-checked every deviation the PR body calls out (PG18 pin, cleartext device LB, unenrolled image automation, FQDN egress allowlist) against the existing patterns for `immich`, `paperless`, `authentik`, and `home-assistant` in this repo, and against the actual cluster CIDRs — all of it lines up. I did not find a blocking defect.
## 🟡 Suggestions
- `kubernetes/apps/trmnl/objectstore.yaml` and `hetzner-creds-externalsecret.yaml` depend on the OpenBao path `homelab/seed/trmnl/hetzner-creds`, which the PR body itself says is **not yet seeded** (blocked by a sandbox classifier). As written, once this merges the `hetzner-s3-creds` ExternalSecret will fail to sync and the CNPG `ObjectStore`/WAL archiving for `tr-pg` won't come up until that manual `bao kv put` is run. Worth doing before/immediately after merge so the cluster doesn't sit half-reconciled with no backup path.
- `kubernetes/infrastructure/network-policies/trmnl-ns.yaml:834-846` hardcodes `trmnl-fw.s3.us-east-2.amazonaws.com` as a `toFQDNs` egress target, but the comment directly above it says this hostname comes from a live API response (`/api/firmware/latest`) and is not a source constant — TRMNL can move the bucket without a Terminus release. This is explicitly called out as an accepted risk rather than an oversight, so it's not blocking, just flagging that firmware sync is the first thing to check if it silently stops working.
- `kubernetes/apps/trmnl/terminus.yaml` doesn't set `readOnlyRootFilesystem` on either the `terminus` or `sidekiq` container, while the new `valkey.yaml` sidecar does. That's likely deliberate (asset compile + Hanami boot need a writable rootfs) and matches the looser pattern elsewhere in the repo (e.g. `paperless`'s main container isn't read-only either), so this is a minor consistency note, not a request.
## 🟢 Nits
- `kubernetes/apps/trmnl/terminus.yaml:445-479` (`sidekiq` container) has no liveness/readiness probe at all, unlike the `terminus` web container. This matches the existing precedent for secondary containers in this repo (e.g. paperless's `bridge-tls-proxy` sidecar is also unprobed), so it's consistent, just worth a mental note if the sidekiq worker ever needs its own health signal.
## Tests
There's no traditional test suite for this kind of change — the repo's bar is `kubectl kustomize` building cleanly for touched kustomizations, which the PR body says was verified for all six. I didn't re-run that locally, but I did verify structurally: the new `kubernetes/apps/trmnl/kustomization.yaml` resource list matches the files actually added, the `trmnl` namespace is correctly threaded through `namespaces.yaml`, `openbao-clustersecretstore.yaml` (alphabetically inserted), `network-policies/kustomization.yaml`, and `image-automation/kustomization.yaml`, and the Flux `Kustomization` in `kubernetes/clusters/homelab/trmnl.yaml` matches the `dependsOn`/`healthCheckExprs` shape used by `immich.yaml`/`paperless.yaml`/`authentik.yaml` verbatim. No CI-visible test gap here beyond the manifest-build check the repo already relies on.
## Recommendation
Looks reasonable — no blocking issues; comments above are optional (the OpenBao seed-credential gap is the one item worth resolving before this fully reconciles, but it's already flagged by the author in the PR description, not a defect in the diff itself).
<!-- pr-review:sha=ac2d72810827966cdac18dd76f46f62b3881df2f -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Phase 1 of the TRMNL X entrance dashboard plan — stands up the self-hosted Terminus (BYOS) server the panel polls. The HA dashboard, e-ink theme and screenshot renderer are later phases and are not in this PR.
Phase 0 findings (verified before writing anything)
id: v2,label: "TRMNL X", 1872x1404, 16 colours, 4-bit. The plan's stop-and-report condition does not trigger.config/db/seeds.rb. TheMODEL_SYNCHRONIZERSidekiq job pulls it from trmnl.com — the Models list is empty until that job runs once./data/options.jsonand authenticates with an ordinary long-lived token (SUPERVISOR_TOKENonly flips anisAddOnboolean). The plan's fallback path is viable. Lands with Phase 4.0.68.0(2026-08-05). Repo was renamed frombyos_hanami; image isghcr.io/usetrmnl/terminus. No GitHub Releases, no changelog.Deliberate deviations from house patterns
tr-pgruns PostgreSQL 18, not the standard 16. Terminus ships a committedconfig/db/structure.sqlcontainingSET transaction_timeout = 0;— a GUC that does not exist before PG 17 — so schema load against a 16 server fails withunrecognized configuration parameter. This is the first non-16 CNPG cluster here. Worth noting the existing 16 pin is inherited: it first appears in739a67e9(Apr 2026, Authentik migration) and was copy-pasted into every later cluster with no documented rationale.Device endpoint is cleartext HTTP on a Cilium BGP LoadBalancer, not an Ingress. The firmware cannot be handed the internal CA, and a TLS-less Ingress would be a new pattern for this repo — a dedicated LB address is not. Admin UI keeps a normal TLS Ingress. Both front the same Service; same split as
home.renner.uno/ha.renner.uno.trmnl.renner.uno— TLS, Traefik, admin UItrmnl-device.renner.uno— LB, port 80, cleartext, device only.API_URIpoints here because Terminus bakes it into the absoluteimage_urlthe device fetches.Terminus is NOT enrolled in image automation, despite publishing a usable tag stream. It serves the firmware and
/api/displaycontract to a wall-mounted device; upstream is pre-1.0 with no releases or changelog; an unattended bump that breaks either leaves the panel dark with no rollback that avoids the captive portal. Bumps are manual and reviewed — rationale recorded inimage-automation/README.md. The Valkey sidecar is enrolled.Egress is an FQDN allowlist, not
toEntities: world. Scoped totrmnl.comand the S3 host serving firmware binaries. Two caveats written into the policy:urlfield of the/api/firmware/latestresponse, not a source constant — TRMNL can move bucket/region without a Terminus release and firmware sync then fails closed here.The CNPG pods are deliberately left out of that egress policy, so the barman WAL archive path is not behind a rule that fails closed.
Registration edits
Namespace, OpenBao
ClusterSecretStoreconditions, network-policy kustomization, Velero schedule (08:15, next free stagger slot), image-automation kustomization, Gatus endpoint.Verified
kubectl kustomize), which is whatci-manifests-test.ymlgates on.ghcr.io/usetrmnl/terminus:0.68.0,valkey/valkey:9.1-alpine,ghcr.io/cloudnative-pg/postgresql:18.homelab/apps/trmnlseeded in OpenBao with a freshly generatedAPP_SECRET.Blocked before this can reconcile
homelab/seed/trmnl/hetzner-credsis not seeded — the copy command was blocked by the sandbox classifier. Without it thehetzner-s3-credsExternalSecret never syncs and the CNPG ObjectStore has no credentials. One command, values copied from the immich path:Note
BAO_ADDRishttp://and writes get a 308 that drops the body — hence the explicit-address=https://.Not verified, and worth knowing
trmnl-device.renner.uno. The URI is typed into the captive portal by hand at provisioning, so if the panel's VLAN can't resolverenner.uno, this breaks at Phase 5 with no error visible from the cluster side. Untested — I have no client on that VLAN.startupProbebudget. If it needs longer, that number is the thing to raise.🤖 Generated with Claude Code
Phase 1 of the TRMNL X dashboard plan: stand up the self-hosted Terminus (BYOS) server the panel will poll. The Home Assistant dashboard, the e-ink theme and the screenshot renderer are later phases and are not in here. Phase 0 verification, all confirmed against upstream before writing any of this: * TRMNL X IS a first-class model in Terminus (id v2 / label "TRMNL X", 1872x1404, 16 colours, 4-bit) — the stop-and-report condition in the plan does not trigger. Note it is NOT in config/db/seeds.rb: the model list is populated by the MODEL_SYNCHRONIZER job pulling trmnl.com, so the Models list is empty until that job has run once. * The HA add-on can run standalone. Our HA is plain HA Core, not Supervised, but the add-on reads env vars in preference to /data/options.json and authenticates with an ordinary long-lived token — SUPERVISOR_TOKEN only flips an isAddOn boolean. So the fallback path in the plan (upstream container, not add-on) is viable. That deployment lands with Phase 4. Deviations from the repo's house patterns, each deliberate: * tr-pg runs PostgreSQL 18, not the standard 16. Terminus ships a committed config/db/structure.sql containing `SET transaction_timeout = 0;`, a GUC that does not exist before PG 17, so schema load against a 16 server fails outright. This is the first non-16 CNPG cluster here. The existing 16 pin is inherited from739a67e9(Apr 2026) and has no documented rationale. * The device endpoint is cleartext HTTP on a Cilium BGP LoadBalancer (trmnl-device.renner.uno) rather than an Ingress. The firmware cannot be given the internal CA, and a TLS-less Ingress would be a new pattern for this repo — a dedicated LB address is not. The admin UI keeps a normal TLS Ingress at trmnl.renner.uno; both front the same Service. Same split as home.renner.uno / ha.renner.uno. * ghcr.io/usetrmnl/terminus is NOT enrolled in image automation despite publishing a usable tag stream. It serves the firmware and /api/display contract to a wall-mounted device, upstream is pre-1.0 with no releases or changelog, and an unattended bump that breaks either leaves the panel dark. Bumps are manual and reviewed. The Valkey sidecar is enrolled — it touches no device-facing contract. * Egress is an FQDN allowlist (trmnl.com, plus the S3 host that serves firmware binaries), not toEntities: world. The S3 host is returned by the API at request time rather than being a source constant, so it can drift; the policy comment says where to look when firmware sync starts failing. This also means Terminus's Extensions/Poll feature, which fetches admin-configured URLs, will not work — accepted, since screen content is pushed in by the renderer. * The CNPG pods are deliberately left OUT of that egress policy so the barman WAL archive path is not behind a rule that fails closed. Images verified to exist in their registries before pinning: ghcr.io/usetrmnl/terminus:0.68.0, valkey/valkey:9.1-alpine, ghcr.io/cloudnative-pg/postgresql:18. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>🤖 Automated review — model
CLI default(complexity: complex), commitac2d72810827966cdac18dd76f46f62b3881df2f.Summary
Phase 1 of the TRMNL BYOS deployment: a new
trmnlnamespace running Terminus (web + sidekiq), a Valkey queue, a dedicated CNPG PostgreSQL 18 cluster with offsite Barman backup, an admin Ingress plus a cleartext device LoadBalancer, a Cilium egress/ingress policy set, and the associated registrations (namespace, OpenBaoClusterSecretStore, Velero schedule, image-automation, Gatus). I cross-checked every deviation the PR body calls out (PG18 pin, cleartext device LB, unenrolled image automation, FQDN egress allowlist) against the existing patterns forimmich,paperless,authentik, andhome-assistantin this repo, and against the actual cluster CIDRs — all of it lines up. I did not find a blocking defect.🟡 Suggestions
kubernetes/apps/trmnl/objectstore.yamlandhetzner-creds-externalsecret.yamldepend on the OpenBao pathhomelab/seed/trmnl/hetzner-creds, which the PR body itself says is not yet seeded (blocked by a sandbox classifier). As written, once this merges thehetzner-s3-credsExternalSecret will fail to sync and the CNPGObjectStore/WAL archiving fortr-pgwon't come up until that manualbao kv putis run. Worth doing before/immediately after merge so the cluster doesn't sit half-reconciled with no backup path.kubernetes/infrastructure/network-policies/trmnl-ns.yaml:834-846hardcodestrmnl-fw.s3.us-east-2.amazonaws.comas atoFQDNsegress target, but the comment directly above it says this hostname comes from a live API response (/api/firmware/latest) and is not a source constant — TRMNL can move the bucket without a Terminus release. This is explicitly called out as an accepted risk rather than an oversight, so it's not blocking, just flagging that firmware sync is the first thing to check if it silently stops working.kubernetes/apps/trmnl/terminus.yamldoesn't setreadOnlyRootFilesystemon either theterminusorsidekiqcontainer, while the newvalkey.yamlsidecar does. That's likely deliberate (asset compile + Hanami boot need a writable rootfs) and matches the looser pattern elsewhere in the repo (e.g.paperless's main container isn't read-only either), so this is a minor consistency note, not a request.🟢 Nits
kubernetes/apps/trmnl/terminus.yaml:445-479(sidekiqcontainer) has no liveness/readiness probe at all, unlike theterminusweb container. This matches the existing precedent for secondary containers in this repo (e.g. paperless'sbridge-tls-proxysidecar is also unprobed), so it's consistent, just worth a mental note if the sidekiq worker ever needs its own health signal.Tests
There's no traditional test suite for this kind of change — the repo's bar is
kubectl kustomizebuilding cleanly for touched kustomizations, which the PR body says was verified for all six. I didn't re-run that locally, but I did verify structurally: the newkubernetes/apps/trmnl/kustomization.yamlresource list matches the files actually added, thetrmnlnamespace is correctly threaded throughnamespaces.yaml,openbao-clustersecretstore.yaml(alphabetically inserted),network-policies/kustomization.yaml, andimage-automation/kustomization.yaml, and the FluxKustomizationinkubernetes/clusters/homelab/trmnl.yamlmatches thedependsOn/healthCheckExprsshape used byimmich.yaml/paperless.yaml/authentik.yamlverbatim. No CI-visible test gap here beyond the manifest-build check the repo already relies on.Recommendation
Looks reasonable — no blocking issues; comments above are optional (the OpenBao seed-credential gap is the one item worth resolving before this fully reconciles, but it's already flagged by the author in the PR description, not a defect in the diff itself).