Skip to content

Commit 14ffa1e

Browse files
cdibonaclaude
andcommitted
Add per-board layout templates and a pre-sleep lead time
Two Vestaboard features, both configurable from the board editor without rebuilding the app: - Custom board layouts. An "Advanced" section takes a sandboxed Jinja template that replaces the built-in layout: one rendered line per board row, `LEFT | RIGHT` splits a row, everything else centers. `{% if %}` works, so the bottom row can show the delay when there is one and the next ship's name when there isn't. trim_blocks/lstrip_blocks keep a conditional on its own line from emitting a blank row; a failing template shows TEMPLATE ERROR on the preview instead of pushing a blank board. Example templates are per-model so a 6-row flagship layout doesn't lose its conditional line when truncated onto a 3-row Note. Full variable list documented in-UI and in the README. - Pre-sleep lead time. The sleep message now goes out quiet.sleep_lead_min minutes (default 3) before the quiet window's start, and ferry pushes stop at that same early moment. A board's own Quiet Hours drop incoming messages and aren't reachable from the Read/Write API, so pushing at the exact start can leave the ferry layout up all night. The board editor links to web2.vestaboard.com for that device-side setting. Verified against the live data dir via a wip build driven through the real /api/vestaboard preview endpoint (delay -> "DELAYED 11:30A", no delay -> next vessel name) on both Note and flagship. 24 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7e4c6e4 commit 14ffa1e

4 files changed

Lines changed: 480 additions & 21 deletions

File tree

CLAUDE.md

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@ TRMNL e-ink devices and Vestaboard split-flap displays. Public, MIT,
1717

1818
## Layout
1919

20-
Almost everything is `web/app.py` (~3.3k lines), in this order:
20+
Almost everything is `web/app.py` (~3.5k lines), in this order:
2121

2222
| Lines (approx) | What |
2323
|---|---|
2424
| 1–310 | config, `APP_VERSION`, timezone helpers, Discord notify |
25-
| 313–520 | `TRMNL_MK_*` Liquid markup constants, one per device layout |
26-
| 520–1630 | the control-panel HTML/CSS/JS, inline as Python strings |
27-
| 1634–1970 | WSDOT fetch, route/direction logic, delay parsing |
28-
| 1973–2290 | Vestaboard encoding, board capture history |
29-
| 2287–2400 | TRMNL webhook push, merge variables |
30-
| 2400–2980 | settings normalization/persistence, Flask routes |
31-
| 2980–3290 | push scheduler, quiet hours, startup persistence check |
25+
| 313–525 | `TRMNL_MK_*` Liquid markup constants, one per device layout |
26+
| 530–1855 | the control-panel HTML/CSS/JS, inline as Python strings |
27+
| 1860–2070 | WSDOT fetch, route/direction logic, delay parsing |
28+
| 2075–2210 | Vestaboard encoding, board capture history |
29+
| 2214–2340 | per-board layout templates (sandboxed Jinja) |
30+
| 2340–2610 | Vestaboard/TRMNL push, merge variables |
31+
| 2610–3210 | settings normalization/persistence, Flask routes |
32+
| 3213–3530 | push scheduler, quiet hours, startup persistence check |
3233

3334
`assets/trmnl-markup.liquid` is a standalone copy of the markup and must be kept in sync
3435
with the `TRMNL_MK_*` constants when they change.
@@ -89,6 +90,23 @@ Also: **in Liquid an empty string is truthy** (only `nil`/`false` are falsy), so
8990
Vestaboard: check `vb_dimensions(model)` before assuming grid size — Note is 3 rows × 15 cols,
9091
Flagship is 6 × 22, and markup that fits one clips on the other.
9192

93+
## Vestaboard quiet hours and board templates
94+
95+
A board's own Quiet Hours (set in the Vestaboard app / web2.vestaboard.com) **drop incoming
96+
messages**, and the Read/Write API cannot read or set them — it only does read message, send
97+
message, get/set transition. So the pre-sleep message is pushed `quiet.sleep_lead_min`
98+
minutes (default 3) *before* our quiet window's start time, and ferry pushes stop at that
99+
same early moment so nothing overwrites the goodnight. `_in_quiet_hours()` implements the
100+
shift; the wake time is not shifted.
101+
102+
A board can override the built-in layout with `board['template']` — a sandboxed Jinja
103+
template, one rendered line per board row (`LEFT | RIGHT` splits a row, anything else
104+
centers). Two gotchas: the env uses `trim_blocks`/`lstrip_blocks` so an `{% if %}` on its
105+
own line doesn't emit a blank row, and `BOARD_TEMPLATE_EXAMPLES` is **per model** because a
106+
6-row flagship example loses its conditional line when truncated to a 3-row Note. Anything
107+
new added to `board_template_context()` should be top-level and pre-formatted, and any new
108+
call site of `format_vestaboard_message()` must pass `template=`.
109+
92110
## Releases
93111

94112
Use `/release`. The short version: tests → commit → push `main``gh release create vX.Y.Z`

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,37 @@ message once — a line of text or a captured layout. Click **Read current board
107107
grab whatever is on the board and save it to that board's capture history, then pick
108108
any capture as the sleep message.
109109

110+
The sleep message goes out a few minutes *before* the start time (3 by default,
111+
adjustable per board). A Vestaboard's own Quiet Hours — set in the Vestaboard app, and
112+
not reachable from the Read/Write API — drop incoming messages, so a goodnight pushed at
113+
the exact start time can be swallowed and leave the ferry layout up all night. Ferry
114+
pushes stop at the same early moment, so nothing overwrites the goodnight. Set the lead
115+
to 0 to fire exactly at the start time.
116+
117+
**Custom board layouts.** Under **Advanced** in the board editor, a template can replace
118+
the built-in Vestaboard layout without touching the code. One rendered line per board row;
119+
a line containing `|` splits into left- and right-aligned halves, everything else is
120+
centered. `{% if %}` / `{% else %}` / `{% endif %}` on their own lines don't emit blank
121+
rows, so the bottom row can show the delay when there is one and the next ship's name when
122+
there isn't:
123+
124+
```
125+
{{ origin }}-{{ dest }} {{ time_short }}
126+
SPACES: {{ spaces }}
127+
{% if delay %}
128+
DELAYED {{ time_short }}
129+
{% else %}
130+
{{ vessel }}
131+
{% endif %}
132+
```
133+
134+
Available variables — `route`, `route_name`, `origin`, `dest`, `origin_full`, `dest_full`,
135+
`time`, `time_short`, `vessel`, `spaces`, `delay`, `alert`, `clock`, `clock12`, `date`,
136+
`docked`, `error`, `has_status`, `rows`, `cols` — are listed with examples in the UI, and
137+
the board preview updates as you type. Templates render in a sandbox; one that fails shows
138+
`TEMPLATE ERROR` on the preview instead of pushing a blank board. Leave the box empty for
139+
the built-in layout.
140+
110141
**Persistence.** All settings are saved on the server as JSON (default
111142
`/app/data/settings.json`), so they survive restarts and are shared across browsers.
112143
Mount a volume at `/app/data` to keep them (the Docker quick-start does this).

0 commit comments

Comments
 (0)