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.
85 lines
3.2 KiB
HTML
85 lines
3.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>{% block title %}ESPresso Frame{% endblock %}</title>
|
|
{% if csrf_token %}<meta name="csrf-token" content="{{ csrf_token }}">{% endif %}
|
|
<script>
|
|
// Applied before first paint so there's no flash of the wrong theme.
|
|
(function () {
|
|
try {
|
|
var stored = localStorage.getItem('theme');
|
|
if (stored === 'light' || stored === 'dark') {
|
|
document.documentElement.setAttribute('data-theme', stored);
|
|
}
|
|
} catch (e) { /* localStorage unavailable (private mode, etc.) */ }
|
|
})();
|
|
</script>
|
|
<link rel="stylesheet" href="/static/theme.css">
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<div class="sidebar-backdrop"></div>
|
|
<aside class="sidebar">
|
|
<div class="brand">
|
|
<span class="brand-mark" aria-hidden="true">☕</span>
|
|
<h1>ESPresso Frame</h1>
|
|
</div>
|
|
|
|
<div class="sidebar-section">Frames</div>
|
|
{% for f in sidebar_frames %}
|
|
<a class="nav-item {% if active_frame and active_frame.id == f.id %}active{% endif %} {% if f.recently_seen %}online{% endif %}"
|
|
href="/frames/{{ f.id }}">
|
|
<span class="frame-dot" aria-hidden="true"></span>
|
|
{{ f.name or ("Frame " ~ f.id) }}
|
|
{% if f.owner_user_id is none %}
|
|
<span class="nav-sub">unclaimed</span>
|
|
{% elif f.battery_percent >= 0 %}
|
|
<span class="nav-sub battery-badge" title="Battery">🔋{{ f.battery_percent }}%</span>
|
|
{% endif %}
|
|
</a>
|
|
{% endfor %}
|
|
{% if not sidebar_frames %}
|
|
<p class="sub" style="padding: 0 10px;">No frames yet.</p>
|
|
{% endif %}
|
|
|
|
<div class="sidebar-footer">
|
|
<a class="nav-item {% if active_nav == 'settings' %}active{% endif %}" href="/settings">Settings</a>
|
|
{% if user.is_admin %}
|
|
<a class="nav-item {% if active_nav == 'admin' %}active{% endif %}" href="/admin">Admin</a>
|
|
{% endif %}
|
|
<form method="post" action="/logout">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<button type="submit" class="linklike">Log out</button>
|
|
</form>
|
|
</div>
|
|
</aside>
|
|
|
|
<div class="main-wrap" style="flex: 1; min-width: 0;">
|
|
<div class="mobile-bar">
|
|
<button type="button" id="sidebar-toggle" class="icon-btn" title="Menu" aria-label="Open menu">☰</button>
|
|
<div class="brand"><span aria-hidden="true">☕</span><h1>ESPresso Frame</h1></div>
|
|
<div class="spacer"></div>
|
|
</div>
|
|
|
|
<main class="main">
|
|
<div class="page-head">
|
|
<h1>{% block page_title %}{% endblock %}</h1>
|
|
<div class="head-actions">
|
|
{% block head_actions %}{% endblock %}
|
|
<button type="button" id="theme-toggle" class="icon-btn" title="Toggle dark mode" aria-label="Toggle dark mode">🌓</button>
|
|
</div>
|
|
</div>
|
|
{% block tabs %}{% endblock %}
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/static/common.js"></script>
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|