Per-frame palette calibration + sidebar battery indicator
Build and push server image / build-and-push (push) Successful in 40s

Advanced configuration (Configuration tab, collapsed <details> section):
a color picker per ink color (black/white/yellow/red/blue/green),
overriding image_pipeline.DEFAULT_PALETTE_RGB for that frame's actual
panel -- different units can vary enough from the documented
approximations to be worth calibrating once you can compare a rendered
photo against the real hardware. Stored as Frame.palette_rgb (NULL =
default, schema migration v4), threaded through render_frame/
render_placeholder/_quantize_and_pack (which now builds the PIL palette
image per call instead of once at import) so both photos and the
unclaimed/unconfigured placeholder screen respect it. "Reset to
defaults" clears back to NULL. Config-save validates exactly 6 #rrggbb
values, rejecting anything else with a 400.

Also: each frame's sidebar entry now shows its last-reported battery
percent (🔋NN%) next to the name, using the frame_dot's existing
recently-seen indicator conventions -- silent when never reported
(mains-only frames, or before the first report), matching how battery
is hidden everywhere else it's not applicable.

Verified against the same live-shaped database as the SMTP work: the
v3->v4 migration, save/reload/reset round trip through the real HTTP
route, an actual rendered image using a custom palette (confirmed via
its packed panel-code bytes), input validation, and the sidebar badge
against real battery data -- plus the standing legacy-device curl suite.
This commit is contained in:
2026-07-22 01:19:06 -04:00
parent c1c803b497
commit 5b11f2accb
12 changed files with 185 additions and 38 deletions
+24 -1
View File
@@ -112,6 +112,26 @@
disable. Needs SMTP configured by an admin.</p>
<button type="button" class="secondary" id="battery-alert-save">Save</button>
</section>
<details class="card">
<summary class="card-title">Advanced configuration</summary>
<p class="sub">Color-quantization values used when dithering photos
for this panel -- approximations by default, since exact primaries
aren't published. Tune them by comparing a rendered photo against
the physical panel; different panel units can vary enough to be
worth calibrating per frame.</p>
<div class="palette-grid">
{% set current_hex = palette_to_hex(frame.palette_rgb or default_palette_rgb) %}
{% for label in palette_labels %}
<label class="palette-swatch">
<input type="color" id="palette_{{ loop.index0 }}" value="{{ current_hex[loop.index0] }}">
{{ label }}
</label>
{% endfor %}
</div>
<button type="button" class="secondary" id="palette-save">Save</button>
<button type="button" class="secondary" id="palette-reset">Reset to defaults</button>
</details>
</div>
</div>
@@ -119,6 +139,9 @@
{% endblock %}
{% block scripts %}
<script>window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};</script>
<script>
window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};
window.DEFAULT_PALETTE_HEX = {{ palette_to_hex(default_palette_rgb) | tojson }};
</script>
<script src="/static/frame_config.js"></script>
{% endblock %}