Server: Frame.panel_type (new column + migration) is auto-derived from the device's reported board (X-Frame-Board), never user-set -- the panel is a property of the hardware, not a picker in the UI. image_pipeline's packing/render pipeline is parameterized by panel geometry instead of hardcoded 800x480 globals, with the real confirmed 13.3in geometry (1600x1200) registered alongside the original 7.3in panel. Existing 7.3in frames are unaffected (column default + board mapping both resolve to the original panel). Board identifiers are also renamed (devkit/xiao -> devkit_esp32c6/ xiao_esp32c6, plus new "ee02") since the EE02 board also carries a XIAO module -- "xiao" alone stopped disambiguating hardware. The server keeps accepting the legacy bare names indefinitely for already-flashed devices. Firmware: scaffolds a third build target (ee02, ESP32-S3 -- a real chip-target change, not just a same-chip Kconfig variant like xiao) and a new epd13in3e driver component skeleton. The actual panel init/LUT/ refresh register sequence isn't ported from vendor demo code yet (none was available), so that component deliberately fails to compile (#error) rather than risk sending unverified register values to real hardware -- devkit/xiao are unaffected and build identically to before. CI's ee02 build step is continue-on-error for the same reason.
131 lines
6.4 KiB
HTML
131 lines
6.4 KiB
HTML
{% extends "app_base.html" %}
|
|
|
|
{% block title %}Admin{% endblock %}
|
|
{% block page_title %}Administration{% endblock %}
|
|
|
|
{% block content %}
|
|
{% include "_admin_tabs.html" %}
|
|
|
|
{% if notice %}<div class="status ok">{{ notice }}</div>{% endif %}
|
|
{% if error %}<div class="status err">{{ error }}</div>{% endif %}
|
|
|
|
<div class="layout">
|
|
<div class="main-col">
|
|
<section class="card">
|
|
<h2 class="card-title">Users</h2>
|
|
<table class="admin-table">
|
|
<thead><tr><th>Username</th><th>Display name</th><th>Role</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for u in users %}
|
|
<tr>
|
|
<td>{{ u.username }}</td>
|
|
<td>{{ u.display_name }}</td>
|
|
<td>{{ "admin" if u.is_admin else "user" }}</td>
|
|
<td class="admin-actions">
|
|
<details>
|
|
<summary>Reset password</summary>
|
|
<form method="post" action="/admin/users/{{ u.id }}/reset-password">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<input type="password" name="password" minlength="8" placeholder="New password" required>
|
|
<button type="submit" class="secondary btn-inline">Reset</button>
|
|
</form>
|
|
</details>
|
|
{% if u.id != user.id %}
|
|
<form method="post" action="/admin/users/{{ u.id }}/delete"
|
|
onsubmit="return confirm('Delete user {{ u.username }}?');">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<button type="submit" class="secondary btn-inline">Delete</button>
|
|
</form>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h2 class="card-title" style="margin-top: 24px;">Email (SMTP)</h2>
|
|
<p class="sub">Used for "forgot password" links and battery-low
|
|
alerts (set per frame in its Configuration tab). Each user needs
|
|
an email set in their own Settings for either to reach them.</p>
|
|
<form method="post" action="/admin/smtp">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<label>SMTP server
|
|
<input type="text" name="smtp_host" placeholder="smtp.example.com" value="{{ smtp.smtp_host }}">
|
|
</label>
|
|
<label>Port
|
|
<input type="number" name="smtp_port" min="1" max="65535" value="{{ smtp.smtp_port }}">
|
|
</label>
|
|
<label>Encryption
|
|
<select name="smtp_encryption">
|
|
<option value="starttls" {% if smtp.smtp_encryption == "starttls" %}selected{% endif %}>STARTTLS (usually port 587)</option>
|
|
<option value="ssl" {% if smtp.smtp_encryption == "ssl" %}selected{% endif %}>SSL/TLS (usually port 465)</option>
|
|
<option value="none" {% if smtp.smtp_encryption == "none" %}selected{% endif %}>None (usually port 25)</option>
|
|
</select>
|
|
</label>
|
|
<label>Username
|
|
<input type="text" name="smtp_username" autocomplete="off" value="{{ smtp.smtp_username }}">
|
|
</label>
|
|
<label>Password
|
|
<input type="password" name="smtp_password" autocomplete="off"
|
|
placeholder="{% if smtp.smtp_password %}(unchanged -- enter a new one to replace){% else %}smtp password{% endif %}">
|
|
</label>
|
|
<label>From address
|
|
<input type="text" name="smtp_from_address" placeholder="[email protected]" value="{{ smtp.smtp_from_address }}">
|
|
</label>
|
|
<button type="submit">Save SMTP settings</button>
|
|
</form>
|
|
<form method="post" action="/admin/smtp/test" style="margin-top: 8px;">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<button type="submit" class="secondary">Send test email to myself</button>
|
|
</form>
|
|
|
|
<h2 class="card-title" style="margin-top: 24px;">Enroll a user</h2>
|
|
<form method="post" action="/admin/users">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<label>Username
|
|
<input type="text" name="username" maxlength="64" required>
|
|
</label>
|
|
<label>Password
|
|
<input type="password" name="password" minlength="8" required autocomplete="new-password">
|
|
</label>
|
|
<div class="checkbox-row">
|
|
<input type="checkbox" id="is_admin" name="is_admin" value="true">
|
|
<label for="is_admin">Administrator</label>
|
|
</div>
|
|
<button type="submit">Create user</button>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
|
|
<div class="side-col">
|
|
<section class="card">
|
|
<h2 class="card-title">Frames</h2>
|
|
{% for f in frames %}
|
|
<div class="admin-frame">
|
|
<p class="sub">
|
|
<strong>#{{ f.id }} {{ f.name }}</strong><br>
|
|
device: <code>{{ f.device_id or "not yet reported" }}</code><br>
|
|
owner: {{ (f.owner.username if f.owner else none) or "UNCLAIMED" }}
|
|
· linked: {{ links_by_frame.get(f.id, []) | map(attribute="username") | join(", ") or "nobody" }}<br>
|
|
firmware: {{ f.device_firmware_version or "?" }} ({{ f.device_board_variant or "board unknown" }})
|
|
· panel: {{ panel_labels.get(f.panel_type, f.panel_type) }}
|
|
· token ack: {{ "yes" if f.device_token_ack else "no" }}
|
|
</p>
|
|
<form method="post" action="/admin/frames/{{ f.id }}/link-user" class="admin-inline-form">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<input type="text" name="username" placeholder="Link user by name" required>
|
|
<button type="submit" class="secondary btn-inline">Link</button>
|
|
</form>
|
|
<form method="post" action="/admin/frames/{{ f.id }}/delete" class="admin-inline-form"
|
|
onsubmit="return confirm('Delete frame #{{ f.id }} and all its history?');">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<button type="submit" class="secondary btn-inline">Delete</button>
|
|
</form>
|
|
</div>
|
|
{% endfor %}
|
|
{% if not frames %}<p class="sub">No frames yet.</p>{% endif %}
|
|
</section>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|