Compare commits

...
Author SHA1 Message Date
claude-botandClaude Opus 5 2f25145cd1 fix(trmnl): drop the timestamp overlay, the dashboard has a clock card
The dashboard now carries a type: clock card, which the browser draws
at capture time — so the time in the image is already the time the
screenshot was taken, and the renderer's overlay would print it twice.

Supersedes the overlay added a commit ago, which was itself replacing a
Home Assistant template sensor that fired a state-changed event and
wrote a recorder row every minute. The card does the same job with no
entity, no event and no row.

Pairs with homeassistant#221.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:33:55 +02:00
claude-botandClaude Opus 5 e2fa885fa3 fix(trmnl): burn the render time into the image, not into an entity
Sets the schedule's `timestamp` option, which the renderer already
supports and which I had written out as false without asking what it
did — the same mistake as leaving `lang` null.

The freshness stamp was a Home Assistant template sensor formatting
now(). That fired a state-changed event, woke every listener and wrote
a recorder row EVERY MINUTE, forever, so the panel could draw a line of
text. Pairs with homeassistant#221, which deletes it.

Baking it into the PNG is also more correct, not just cheaper. The
panel is poll-only and shows the last image indefinitely if the
renderer dies, so the stamp exists to prove the image is fresh. A time
read from an entity proves Home Assistant was alive when the shot was
taken; a time drawn by the renderer proves the RENDER happened, which
is the failure actually being guarded against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:31:18 +02:00
claude-botandClaude Opus 5 507e86cb83 fix(trmnl): let Reloader restart the renderer on config and secret changes
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The renderer's init container writes /data/schedules.json ONCE, at pod
start, and nothing re-reads either source afterwards. So a ConfigMap or
Secret change lands in git, updates the object, and the running
renderer keeps the old values indefinitely.

The app re-reads schedules.json every 60 seconds, which makes it look
self-updating. It is not — it re-reads the FILE, and only the init
container writes the file.

This was hit three times in practice, each needing `kubectl delete pod`
by hand:

  * switching the theme to Graphite E-ink Light — the pod kept
    rendering with a theme name that no longer existed
  * setting the schedule's lang field to "de"
  * every re-mint of the BYOS tokens, which is the Secret half

The theme case is the instructive one: nothing errored. HA silently
served the default theme for a name that did not resolve, so the panel
just looked wrong, and I was about to debug the design.

Annotates both sources. Named per-resource rather than the blanket
reloader.stakater.com/auto: "true", matching terminus.yaml and
mosquitto-deployment.yaml.

Terminus got this annotation earlier; the renderer did not, despite
having the same init-container pattern and hitting the same failure in
the same session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:22:04 +02:00
k.renner b2ed2beae4 Merge pull request 'fix(trmnl): render the panel in German via the schedule's lang field' (#488) from fix-trmnl-render-lang into master
Reviewed-on: k.renner/homelab#488
2026-08-12 20:18:58 +00:00
claude-botandClaude Opus 5 f8c968c5a7 fix(trmnl): render the panel in German via the schedule's lang field
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
lang was null, so the screenshot session rendered in English — and the
dashboard grew a German weekday array, a German month array and a
fifteen-entry weather-condition dict to translate around it by hand.

The field is documented as "UI language code (HA mode only)" and calls
Home Assistant's own _selectLanguage() on the page before capture;
upstream's tests use lang: 'de'. So localisation is HA's job and the
field was sitting in the schedule template, already written out, set to
null because I copied the shape without asking what it did.

Pairs with homeassistant#220, which deletes both hand-written mappings:
the weather badge points at weather.garten directly and the date is a
device_class: timestamp the frontend formats.

The instance stays language "en". This affects only the render session,
so the panel is German without changing anyone's UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:16:43 +02:00
k.renner 595e5eb7a3 Merge pull request 'fix(trmnl): disable Terminus session expiry, the other half of the token fix' (#487) from fix-trmnl-session-expiry into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#487
2026-08-12 20:02:38 +00:00
claude-botandClaude Opus 5 7e293d3de7 fix(trmnl): disable Terminus session expiry, the other half of the token fix
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The renderer stopped pushing and the panel preview froze. Every webhook:

  HTTP 401: Unauthorized - This session has expired, please login again

Terminus has TWO independent expiries and the earlier fix only addressed
one. API_ACCESS_TOKEN_PERIOD governs the JWT's own lifetime and is now
10 years. SESSION_EXPIRATION_ENABLED / SESSION_INACTIVITY_LIMIT govern
the SESSION BEHIND that token and were left at their defaults, so a
valid, decade-long token stopped being accepted after about half an
hour of session inactivity.

Worth being precise, because the two look alike and are not: the first
failure was 400 "expired JWT access token", the token itself. This is
401 "This session has expired", the session. Different mechanism,
different error, same symptom — a panel that silently stops updating.

I noticed both expiries when diagnosing the first one, wrote that
Terminus "has two independent expiries", and then fixed one and moved
on. That is the actual mistake here, not the missing variable.

Disabling session expiry is the state the renderer's own code already
assumes ("stock Terminus disables session expiration"), which is false
by default and has to be made true here — the same shape as the token
period. And it is the deciding constraint for an unattended appliance:
a session that expires needs a human with a password to restore it, and
a hallway panel has neither.

Reloader will roll the pod; the ConfigMap annotation added earlier
covers this. Existing tokens must be re-minted afterwards — the current
session is already dead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:01:34 +02:00
k.renner 9423381102 Merge pull request 'fix(trmnl): point the renderer at Graphite E-ink Light' (#486) from fix-trmnl-graphite-theme into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#486
2026-08-12 19:36:24 +00:00
2 changed files with 58 additions and 1 deletions
+35 -1
View File
@@ -29,6 +29,19 @@ data:
# The app re-reads schedules.json every 60 s, so edits here take
# effect without a restart once the init container has rewritten it.
#
# timestamp stays false: the dashboard carries a clock CARD, which
# the browser draws at capture time — so the time in the image is
# already the time the screenshot was taken, and the renderer's own
# overlay would just print it twice.
#
# lang is "de", not null. It calls Home Assistant's own
# _selectLanguage() for the screenshot session, so the rendered page
# is localised by HA rather than by hand. With it left null the
# dashboard grew a German weekday array, a German month array and a
# fifteen-entry weather-condition dict — reimplementing translations
# HA already ships. The instance itself stays language "en"; this
# only affects the render session.
#
# model_id is 1 — TRMNL X (name "v2", 1872x1404, 16 colours) — and it
# is an INTEGER, not a name. Confirmed against the running server:
# GET /api/screens returns model_id as a number, and upstream's
@@ -61,7 +74,7 @@ data:
"zoom": 1,
"wait": null,
"theme": "Graphite E-ink Light",
"lang": null,
"lang": "de",
"dark": false,
"invert": false,
"timestamp": false,
@@ -107,6 +120,27 @@ metadata:
namespace: trmnl
labels:
app.kubernetes.io/name: trmnl-renderer
annotations:
# BOTH sources need this, and for the same reason: the init
# container writes /data/schedules.json ONCE, at pod start. Nothing
# re-reads either source afterwards.
#
# The app re-reads schedules.json every 60s, which makes it look
# self-updating — but only the FILE, and the file is only rewritten
# by the init container. So a ConfigMap or Secret change lands in
# git, updates the object, and the running renderer keeps the old
# values indefinitely.
#
# Both halves were hit repeatedly in practice and each time needed
# `kubectl delete pod` by hand: the theme name, the schedule's lang
# field, and every re-mint of the BYOS tokens. A fix that depends on
# someone remembering to restart a pod is not deployed.
#
# Named per-resource rather than the blanket
# reloader.stakater.com/auto: "true", matching terminus.yaml and
# mosquitto-deployment.yaml.
configmap.reloader.stakater.com/reload: trmnl-renderer-schedule
secret.reloader.stakater.com/reload: trmnl-renderer-secrets
spec:
replicas: 1
# RWO PVC, and two renderers would push conflicting screens for the
+23
View File
@@ -53,6 +53,29 @@ data:
# NOTE: the period is fixed at mint time. Changing this does not extend
# tokens that already exist — they must be re-minted afterwards.
API_ACCESS_TOKEN_PERIOD: "315360000"
# The OTHER half of the same problem, and the reason the first fix
# did not hold. Terminus has TWO independent expiries: the JWT's own
# lifetime (above) and the SESSION behind it. Raising only the token
# period left the session at its defaults, so pushes kept working for
# a while and then failed with
# HTTP 401 {"error":"This session has expired, please login again"}
# which is a different error, from a different mechanism, than the
# "expired JWT access token" the first fix addressed.
#
# Documented in doc/configuration.adoc:
# SESSION_EXPIRATION_ENABLED — "Determines if your browser session
# or JWT API token is expired or not." Default: true.
# SESSION_INACTIVITY_LIMIT 1800s, SESSION_LIFETIME_LIMIT 86400s.
#
# Disabling it is exactly the state the renderer's own code assumes
# ("stock Terminus disables session expiration and issues ~100-year
# tokens") — an assumption that is false by default and has to be
# made true here, same as the token period.
#
# Same trade as above, and it is the deciding one for an unattended
# appliance: a session that expires needs a human with a password to
# restore it, and this panel has neither.
SESSION_EXPIRATION_ENABLED: "false"
---
apiVersion: apps/v1
kind: Deployment