5 Commits
Author SHA1 Message Date
tfaour fdb5dc7ab3 Bump firmware to 1.1.2a (test build)
Build and release firmware / build-and-release (push) Successful in 1m49s
2026-07-21 22:22:04 -04:00
tfaour 0e50529343 Bump firmware to 1.1.2
Build and release firmware / build-and-release (push) Successful in 1m49s
2026-07-21 22:17:12 -04:00
tfaour b988799fb3 Make the firmware repo URL Edit button smaller/inline-sized
Build and push server image / build-and-push (push) Successful in 36s
2026-07-21 22:14:55 -04:00
tfaour b8fd8f4ad4 Trim firmware card verbosity; Gitea repo URL becomes text+Edit once set
Build and push server image / build-and-push (push) Successful in 35s
Dropped the paragraph explaining the release workflow builds the
binaries -- not something the web UI needs to narrate. The repo URL
field now shows as plain text with an Edit button once a value is
saved, instead of always being an open input.
2026-07-21 22:10:04 -04:00
tfaour 7ea5c9a4fb Consolidate firmware UI; server learns board from the device, not a picker
Build and push server image / build-and-push (push) Successful in 37s
All firmware-related controls (manual upload, Gitea repo URL,
auto-update checkbox, detected board, Update frame button) now live in
one "Firmware update" card instead of being split across the main
Settings form and a separate card.

The board variant used to pick a Gitea release asset was a dropdown
the user had to set by hand and could get wrong. The device now
reports it itself via a new X-Frame-Board header (CONFIG_FRAME_BOARD_NAME,
"devkit" by default, "xiao" in sdkconfig.xiao) on every /frame/config
poll, stored as device_board_variant -- the server learns it instead.
Update checks/applies are gated on the board being known, since there's
nothing to fetch until a device has checked in at least once.
2026-07-21 21:58:59 -04:00
9 changed files with 149 additions and 61 deletions
+15
View File
@@ -1,5 +1,20 @@
menu "ESPresso Frame Configuration" menu "ESPresso Frame Configuration"
config FRAME_BOARD_NAME
string "Board variant name, reported to the server"
default "devkit"
help
Sent as the X-Frame-Board request header on every
GET /frame/config poll, so the server can learn which board
this device is and automatically fetch the right OTA build
from a configured Gitea repo's releases -- no manual "which
board" picker in the web UI. Must match one of the asset
names .gitea/workflows/firmware-release-build.yml publishes
(firmware-<name>.bin): "devkit" (this default, for the
plain ESP32-C6-DevKitC-1 build) or "xiao" (set via
sdkconfig.xiao for the Seeed XIAO ESP32-C6 build -- see
build_for_board.sh).
config FRAME_XIAO_ANTENNA_INIT config FRAME_XIAO_ANTENNA_INIT
bool "Select onboard antenna on Seeed XIAO ESP32-C6 (RF switch init)" bool "Select onboard antenna on Seeed XIAO ESP32-C6 (RF switch init)"
default n default n
+8 -3
View File
@@ -342,9 +342,13 @@ static bool json_extract_string(const char *json, const char *key, char *out, si
/* GETs the server's /frame/config -- doubles as both the reachability /* GETs the server's /frame/config -- doubles as both the reachability
* check (any completed HTTP response means the socket-level connection * check (any completed HTTP response means the socket-level connection
* succeeded), the source of the server-configurable refresh interval, * succeeded), the source of the server-configurable refresh interval,
* and (via the X-Frame-Version request header / firmware_version * and (via the X-Frame-Version/X-Frame-Board request headers and
* response field) the device's OTA update check -- piggybacked on a * firmware_version response field) the device's OTA update check --
* request already made every wake, no extra round trip. */ * piggybacked on a request already made every wake, no extra round
* trip. X-Frame-Board lets the server learn which board this device is
* (CONFIG_FRAME_BOARD_NAME) so it can pick the right Gitea release
* asset itself, instead of a user manually selecting a board in the
* web UI. */
static frame_server_config_t fetch_frame_config(const frame_config_t *cfg) static frame_server_config_t fetch_frame_config(const frame_config_t *cfg)
{ {
frame_server_config_t result = { frame_server_config_t result = {
@@ -364,6 +368,7 @@ static frame_server_config_t fetch_frame_config(const frame_config_t *cfg)
}; };
esp_http_client_handle_t client = esp_http_client_init(&config); esp_http_client_handle_t client = esp_http_client_init(&config);
esp_http_client_set_header(client, "X-Frame-Version", esp_app_get_description()->version); esp_http_client_set_header(client, "X-Frame-Version", esp_app_get_description()->version);
esp_http_client_set_header(client, "X-Frame-Board", CONFIG_FRAME_BOARD_NAME);
esp_err_t err = esp_http_client_open(client, 0); esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) { if (err != ESP_OK) {
+2
View File
@@ -8,6 +8,8 @@ CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_xiao.csv" CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_xiao.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions_xiao.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions_xiao.csv"
CONFIG_FRAME_BOARD_NAME="xiao"
# Powers the XIAO's RF switch and selects its onboard antenna -- without # Powers the XIAO's RF switch and selects its onboard antenna -- without
# this the softAP/STA radio doesn't reliably reach the antenna at all. # this the softAP/STA radio doesn't reliably reach the antenna at all.
# See the Kconfig help text (FRAME_XIAO_ANTENNA_INIT) for why. # See the Kconfig help text (FRAME_XIAO_ANTENNA_INIT) for why.
+1 -1
View File
@@ -1 +1 @@
1.1.1 1.1.2a
+28 -17
View File
@@ -48,14 +48,17 @@ algorithm itself -- it just streams the response straight to the panel.
pushing this repo to a Gitea instance, `.gitea/workflows/firmware-release-build.yml` pushing this repo to a Gitea instance, `.gitea/workflows/firmware-release-build.yml`
builds both supported boards and publishes them as release assets builds both supported boards and publishes them as release assets
(`firmware-xiao.bin`/`firmware-devkit.bin`) whenever `firmware/version.txt` (`firmware-xiao.bin`/`firmware-devkit.bin`) whenever `firmware/version.txt`
changes on `main`. In the web UI's Settings form, set **Firmware Gitea changes on `main`. In the web UI's "Firmware update" card, set the
repo URL** to that repo (e.g. `https://git.example.com/owner/repo`) and **Gitea repo URL** to that repo (e.g. `https://git.example.com/owner/repo`);
pick the frame's **board**; if the repo is private, also set if the repo is private, also set `GITEA_FIRMWARE_TOKEN` (a read-only
`GITEA_FIRMWARE_TOKEN` (a read-only PAT) in `docker-compose.yml`. The PAT) in `docker-compose.yml`. Which board's build to fetch is learned
server then periodically checks for a newer release and either shows from the frame itself (its `X-Frame-Board` header, `CONFIG_FRAME_BOARD_NAME`
an "Update frame" button or, with **Automatically apply updates** on the firmware side) -- nothing to pick by hand, though the frame
checked, stages it itself -- either way the frame only actually does need to have checked in at least once first. The server then
updates on its own next wake (see `POST /api/firmware` above). periodically checks for a newer release and either shows an "Update
frame" button or, with **Automatically apply updates** checked,
stages it itself -- either way the frame only actually updates on its
own next wake (see `POST /api/firmware` above).
## Endpoints ## Endpoints
@@ -117,7 +120,11 @@ algorithm itself -- it just streams the response straight to the panel.
device compares it against its own running version device compares it against its own running version
(`esp_app_get_description()->version`, sent as an `X-Frame-Version` (`esp_app_get_description()->version`, sent as an `X-Frame-Version`
request header, stored as `device_firmware_version`) to decide whether request header, stored as `device_firmware_version`) to decide whether
to OTA to OTA. The device also sends an `X-Frame-Board` header
(`CONFIG_FRAME_BOARD_NAME`, e.g. `"xiao"`), stored as
`device_board_variant` -- how the Gitea auto-update feature below
learns which board to fetch a release for, instead of a user picking
it
- `GET /frame/photo-info` -- `{"asset_id": ..., "location_line1": ... | - `GET /frame/photo-info` -- `{"asset_id": ..., "location_line1": ... |
null, "location_line2": ... | null, "taken_at": ... | null}` for the null, "location_line2": ... | null, "taken_at": ... | null}` for the
current photo (same idempotent current-photo semantics as current photo (same idempotent current-photo semantics as
@@ -170,16 +177,20 @@ algorithm itself -- it just streams the response straight to the panel.
been uploaded yet been uploaded yet
- `GET /api/firmware/check` -- throttled (`gitea_releases.UPDATE_CHECK_INTERVAL_S`, - `GET /api/firmware/check` -- throttled (`gitea_releases.UPDATE_CHECK_INTERVAL_S`,
15 min) check of the configured Gitea repo's latest release for the 15 min) check of the configured Gitea repo's latest release for the
frame's board variant. `{"enabled": false}` if no repo URL is frame's board variant (learned from the device, see `device_board_variant`
configured; otherwise `{"enabled": true, "latest_version": "1.2.3" | null, below -- not user-configured). `{"enabled": false}` if no repo URL is
"staged_version": "1.2.2" | null, "update_available": bool}`. If configured; otherwise `{"enabled": true, "board": "xiao" | null,
"Automatically apply updates" is on and a newer release is found, this "latest_version": "1.2.3" | null, "staged_version": "1.2.2" | null,
call also stages it immediately (same effect as a manual upload) -- "update_available": bool}`. `update_available` stays false until the
otherwise the web UI shows an "Update frame" button board is known, regardless of what Gitea has. If "Automatically apply
updates" is on and a newer release is found, this call also stages it
immediately (same effect as a manual upload) -- otherwise the web UI
shows an "Update frame" button
- `POST /api/firmware/apply-latest` -- the "Update frame" button: pulls - `POST /api/firmware/apply-latest` -- the "Update frame" button: pulls
and stages the latest Gitea release right now, bypassing the check and stages the latest Gitea release right now, bypassing the check
throttle. 404 if no repo is configured, has no releases, or the latest throttle. 400 if no repo is configured or no device has checked in
release has no asset for the configured board variant yet (board unknown); 404 if the repo has no releases, or the latest
release has no asset for the frame's board
- `GET /api/queue` -- `{"current": {...} | null, "upcoming": [...], - `GET /api/queue` -- `{"current": {...} | null, "upcoming": [...],
"device": {"last_seen": ts | null, "overdue": bool, "device": {"last_seen": ts | null, "overdue": bool,
"firmware_version": "1.2.3" | null, "firmware_available": "1.2.4" | null, "firmware_version": "1.2.3" | null, "firmware_available": "1.2.4" | null,
+9 -6
View File
@@ -91,21 +91,24 @@ class FrameConfig(BaseModel):
battery_log: list = [] battery_log: list = []
# Device liveness/telemetry: last_seen is touched by every /frame/* # Device liveness/telemetry: last_seen is touched by every /frame/*
# request; device_firmware_version comes from the X-Frame-Version # request; device_firmware_version/device_board_variant come from the
# header the device sends with its config poll. # X-Frame-Version/X-Frame-Board headers the device sends with its
# config poll (CONFIG_FRAME_BOARD_NAME on the firmware side).
last_seen: float = 0.0 last_seen: float = 0.0
device_firmware_version: str = "" device_firmware_version: str = ""
device_board_variant: str = "" # "" until a device has ever checked in
# Version parsed out of the most recently uploaded OTA image # Version parsed out of the most recently uploaded OTA image
# (POST /api/firmware); "" = none uploaded yet. # (POST /api/firmware); "" = none uploaded yet.
firmware_available_version: str = "" firmware_available_version: str = ""
# Gitea-hosted firmware auto-update (see app/gitea_releases.py). # Gitea-hosted firmware auto-update (see app/gitea_releases.py).
# repo_url empty = feature off, no Gitea calls made at all. # repo_url empty = feature off, no Gitea calls made at all. Which
# board_variant picks which release asset to pull -- must match one of # release asset to pull is learned from the device itself
# the names .gitea/workflows/firmware-release-build.yml publishes # (device_board_variant below, from its X-Frame-Board header) rather
# than picked by the user -- must match one of the names
# .gitea/workflows/firmware-release-build.yml publishes
# (firmware-<board_variant>.bin). # (firmware-<board_variant>.bin).
firmware_update_repo_url: str = "" # e.g. "https://git.example.com/owner/repo" firmware_update_repo_url: str = "" # e.g. "https://git.example.com/owner/repo"
firmware_board_variant: str = "xiao" # "xiao" or "devkit"
firmware_auto_update: bool = False # pull+stage a newer release with no button click firmware_auto_update: bool = False # pull+stage a newer release with no button click
# Optional Gitea PAT (read-only access is enough) for a private repo's # Optional Gitea PAT (read-only access is enough) for a private repo's
# releases; blank is fine for a public repo. GITEA_FIRMWARE_TOKEN env # releases; blank is fine for a public repo. GITEA_FIRMWARE_TOKEN env
+30 -13
View File
@@ -207,10 +207,14 @@ def frame_config(request: Request):
reachability check. Always returns 200 with current settings reachability check. Always returns 200 with current settings
(defaults if nothing's been saved yet) -- no Immich-configured gate, (defaults if nothing's been saved yet) -- no Immich-configured gate,
since this doubles as the "is the server up" signal. Also captures since this doubles as the "is the server up" signal. Also captures
the device's running firmware version (X-Frame-Version header) and the device's running firmware version and board variant (X-Frame-
advertises the uploaded OTA image's version, so the device's update Version/X-Frame-Board headers -- the latter is how the Gitea
check costs zero extra round trips.""" auto-update feature learns which release asset to fetch, instead of
a user picking it in the web UI) and advertises the uploaded OTA
image's version, so the device's update check costs zero extra
round trips."""
reported_version = request.headers.get("X-Frame-Version", "") reported_version = request.headers.get("X-Frame-Version", "")
reported_board = request.headers.get("X-Frame-Board", "")
with config.locked(): with config.locked():
cfg = config.load() cfg = config.load()
cfg.last_seen = time.time() cfg.last_seen = time.time()
@@ -221,6 +225,8 @@ def frame_config(request: Request):
if cfg.device_firmware_version and reported_version != cfg.device_firmware_version: if cfg.device_firmware_version and reported_version != cfg.device_firmware_version:
cfg.stats.ota_updates_applied += 1 cfg.stats.ota_updates_applied += 1
cfg.device_firmware_version = reported_version cfg.device_firmware_version = reported_version
if reported_board:
cfg.device_board_variant = reported_board
config.save(cfg) config.save(cfg)
return { return {
"refresh_interval_s": _effective_refresh_interval_s(cfg), "refresh_interval_s": _effective_refresh_interval_s(cfg),
@@ -276,7 +282,6 @@ def api_config_save(
quiet_hours_end: str = Form("07:00"), quiet_hours_end: str = Form("07:00"),
timezone: str = Form("UTC"), timezone: str = Form("UTC"),
firmware_update_repo_url: str = Form(""), firmware_update_repo_url: str = Form(""),
firmware_board_variant: str = Form("xiao"),
firmware_auto_update: bool = Form(False), firmware_auto_update: bool = Form(False),
): ):
# Immich URL/API key/Gitea token are env-var only (IMMICH_URL/ # Immich URL/API key/Gitea token are env-var only (IMMICH_URL/
@@ -309,7 +314,6 @@ def api_config_save(
if timezone in ALL_TIMEZONES: if timezone in ALL_TIMEZONES:
cfg.timezone = timezone cfg.timezone = timezone
cfg.firmware_update_repo_url = firmware_update_repo_url.strip() cfg.firmware_update_repo_url = firmware_update_repo_url.strip()
cfg.firmware_board_variant = firmware_board_variant if firmware_board_variant in ("xiao", "devkit") else "xiao"
cfg.firmware_auto_update = firmware_auto_update cfg.firmware_auto_update = firmware_auto_update
cfg.stats.config_saves += 1 cfg.stats.config_saves += 1
config.save(cfg) config.save(cfg)
@@ -493,13 +497,19 @@ def _fetch_latest_release(cfg: config.FrameConfig) -> dict | None:
def _apply_gitea_update(cfg: config.FrameConfig) -> str: def _apply_gitea_update(cfg: config.FrameConfig) -> str:
"""Downloads the configured Gitea repo's latest release asset for this """Downloads the configured Gitea repo's latest release asset for this
frame's board variant and stages it exactly like a manual frame's board variant and stages it exactly like a manual
POST /api/firmware upload would. Network I/O happens before the lock POST /api/firmware upload would. The board comes from the device
is taken, matching the load/mutate/save concurrency pattern used itself (device_board_variant, learned from its X-Frame-Board header
elsewhere (see config.locked()).""" on GET /frame/config -- see frame_config()), not a user picker, so
there's nothing to fetch until a device has checked in at least
once. Network I/O happens before the lock is taken, matching the
load/mutate/save concurrency pattern used elsewhere (see
config.locked())."""
if not cfg.device_board_variant:
raise HTTPException(400, "No frame has checked in yet -- can't tell which board's build to fetch")
release = _fetch_latest_release(cfg) release = _fetch_latest_release(cfg)
if not release: if not release:
raise HTTPException(404, "No releases found in the configured Gitea repo") raise HTTPException(404, "No releases found in the configured Gitea repo")
asset_name = gitea_releases.asset_name_for_board(cfg.firmware_board_variant) asset_name = gitea_releases.asset_name_for_board(cfg.device_board_variant)
asset_url = release["assets"].get(asset_name) asset_url = release["assets"].get(asset_name)
if not asset_url: if not asset_url:
raise HTTPException(404, f"Latest release has no '{asset_name}' asset") raise HTTPException(404, f"Latest release has no '{asset_name}' asset")
@@ -524,7 +534,11 @@ def api_firmware_check():
(gitea_releases.UPDATE_CHECK_INTERVAL_S) -- cheap, since it only reads (gitea_releases.UPDATE_CHECK_INTERVAL_S) -- cheap, since it only reads
the release's tag name, not its binaries. If firmware_auto_update is the release's tag name, not its binaries. If firmware_auto_update is
on and a newer version is found, applies it immediately; otherwise on and a newer version is found, applies it immediately; otherwise
just reports it so the web UI can offer the "Update frame" button.""" just reports it so the web UI can offer the "Update frame" button.
Applying (auto or manual) needs to know the frame's board, which is
learned from the device's own X-Frame-Board header rather than
picked by the user -- update_available stays false until a device
has checked in at least once, regardless of what Gitea has."""
cfg = config.load() cfg = config.load()
if not cfg.firmware_update_repo_url: if not cfg.firmware_update_repo_url:
return {"enabled": False} return {"enabled": False}
@@ -543,9 +557,11 @@ def api_firmware_check():
config.save(cfg) config.save(cfg)
cfg = config.load() cfg = config.load()
update_available = bool( update_available = (
cfg.firmware_gitea_latest_version bool(cfg.firmware_gitea_latest_version)
) and cfg.firmware_gitea_latest_version != cfg.firmware_available_version and cfg.firmware_gitea_latest_version != cfg.firmware_available_version
and bool(cfg.device_board_variant)
)
if update_available and cfg.firmware_auto_update: if update_available and cfg.firmware_auto_update:
_apply_gitea_update(cfg) _apply_gitea_update(cfg)
cfg = config.load() cfg = config.load()
@@ -553,6 +569,7 @@ def api_firmware_check():
return { return {
"enabled": True, "enabled": True,
"board": cfg.device_board_variant or None,
"latest_version": cfg.firmware_gitea_latest_version or None, "latest_version": cfg.firmware_gitea_latest_version or None,
"staged_version": cfg.firmware_available_version or None, "staged_version": cfg.firmware_available_version or None,
"update_available": update_available, "update_available": update_available,
+6
View File
@@ -207,6 +207,12 @@
} }
button:hover { background: var(--accent-hover); } button:hover { background: var(--accent-hover); }
button:active { transform: translateY(1px); } button:active { transform: translateY(1px); }
button.btn-inline {
margin-top: 0;
padding: 3px 10px;
font-size: 12px;
vertical-align: middle;
}
button.secondary { button.secondary {
background: transparent; background: transparent;
color: var(--text); color: var(--text);
+50 -21
View File
@@ -75,25 +75,6 @@
{% endfor %} {% endfor %}
</select> </select>
</label> </label>
<label>Firmware Gitea repo URL
<input type="text" id="firmware_update_repo_url" placeholder="https://git.example.com/owner/repo"
value="{{ cfg.firmware_update_repo_url }}">
</label>
<label>Frame's board
<select id="firmware_board_variant">
<option value="xiao" {% if cfg.firmware_board_variant == "xiao" %}selected{% endif %}>Seeed XIAO ESP32-C6</option>
<option value="devkit" {% if cfg.firmware_board_variant == "devkit" %}selected{% endif %}>ESP32-C6-DevKitC-1</option>
</select>
</label>
<div class="checkbox-row">
<input type="checkbox" id="firmware_auto_update" {% if cfg.firmware_auto_update %}checked{% endif %}>
<label for="firmware_auto_update">Automatically apply updates</label>
</div>
<p class="sub" style="margin-top: 8px;">When a Gitea repo URL is set, the
server periodically checks its releases for a newer build for the
board above. With auto-apply off, an "Update frame" button appears
below when one's found; with it on, the server stages the new
build itself -- the frame still only updates on its own next wake.</p>
<button type="button" class="secondary" id="load-albums">Load Albums</button> <button type="button" class="secondary" id="load-albums">Load Albums</button>
<button type="submit">Save</button> <button type="submit">Save</button>
</form> </form>
@@ -124,12 +105,31 @@
<section class="card"> <section class="card">
<h2 class="card-title">Firmware update</h2> <h2 class="card-title">Firmware update</h2>
<p class="sub" id="firmware-board">
{% if cfg.device_board_variant %}Detected board: {{ cfg.device_board_variant }}
{% else %}Board not detected yet -- the frame reports it on its next check-in.{% endif %}
</p>
<p class="sub" id="firmware-available"> <p class="sub" id="firmware-available">
{% if cfg.firmware_available_version %}Uploaded: v{{ cfg.firmware_available_version }} -- the frame {% if cfg.firmware_available_version %}Uploaded: v{{ cfg.firmware_available_version }} -- the frame
updates itself on its next wake if it's running something else.{% else %}No firmware uploaded yet.{% endif %} updates itself on its next wake if it's running something else.{% else %}No firmware uploaded yet.{% endif %}
</p> </p>
<input type="file" id="firmware-file" accept=".bin"> <input type="file" id="firmware-file" accept=".bin">
<button type="button" class="secondary" id="firmware-upload">Upload firmware</button> <button type="button" class="secondary" id="firmware-upload">Upload firmware</button>
<div id="firmware-repo-display" style="margin-top: 16px; {% if not cfg.firmware_update_repo_url %}display: none;{% endif %}">
<p class="sub">Gitea repo: <code id="firmware-repo-text">{{ cfg.firmware_update_repo_url }}</code>
<button type="button" class="secondary btn-inline" id="firmware-repo-edit-btn">Edit</button>
</p>
</div>
<label id="firmware-repo-edit" style="{% if cfg.firmware_update_repo_url %}display: none;{% endif %}">Gitea repo URL
<input type="text" id="firmware_update_repo_url" placeholder="https://git.example.com/owner/repo"
value="{{ cfg.firmware_update_repo_url }}">
</label>
<div class="checkbox-row">
<input type="checkbox" id="firmware_auto_update" {% if cfg.firmware_auto_update %}checked{% endif %}>
<label for="firmware_auto_update">Automatically apply updates</label>
</div>
<button type="button" class="secondary" id="firmware-settings-save">Save</button>
<p class="sub" id="firmware-gitea-status" style="display: none; margin-top: 10px;"></p> <p class="sub" id="firmware-gitea-status" style="display: none; margin-top: 10px;"></p>
<button type="button" id="firmware-update-btn" style="display: none;">Update frame</button> <button type="button" id="firmware-update-btn" style="display: none;">Update frame</button>
</section> </section>
@@ -167,7 +167,6 @@
quiet_hours_end: document.getElementById('quiet_hours_end').value || '07:00', quiet_hours_end: document.getElementById('quiet_hours_end').value || '07:00',
timezone: document.getElementById('timezone').value || 'UTC', timezone: document.getElementById('timezone').value || 'UTC',
firmware_update_repo_url: document.getElementById('firmware_update_repo_url').value || '', firmware_update_repo_url: document.getElementById('firmware_update_repo_url').value || '',
firmware_board_variant: document.getElementById('firmware_board_variant').value,
firmware_auto_update: String(document.getElementById('firmware_auto_update').checked), firmware_auto_update: String(document.getElementById('firmware_auto_update').checked),
}); });
const resp = await fetch('/api/config', { const resp = await fetch('/api/config', {
@@ -215,6 +214,29 @@
} }
}); });
function showRepoDisplayMode(url) {
document.getElementById('firmware-repo-text').textContent = url;
document.getElementById('firmware-repo-display').style.display = url ? 'block' : 'none';
document.getElementById('firmware-repo-edit').style.display = url ? 'none' : 'block';
}
document.getElementById('firmware-repo-edit-btn').addEventListener('click', () => {
document.getElementById('firmware-repo-display').style.display = 'none';
document.getElementById('firmware-repo-edit').style.display = 'block';
document.getElementById('firmware_update_repo_url').focus();
});
document.getElementById('firmware-settings-save').addEventListener('click', async () => {
try {
await saveConfig();
showStatus(true, 'Saved.');
showRepoDisplayMode(document.getElementById('firmware_update_repo_url').value.trim());
loadFirmwareCheck();
} catch (e) {
showStatus(false, e.message);
}
});
let upcomingItems = []; let upcomingItems = [];
// Pointer Events (not the native HTML5 Drag-and-Drop API) so the same // Pointer Events (not the native HTML5 Drag-and-Drop API) so the same
@@ -522,19 +544,26 @@
async function loadFirmwareCheck() { async function loadFirmwareCheck() {
const statusEl = document.getElementById('firmware-gitea-status'); const statusEl = document.getElementById('firmware-gitea-status');
const btn = document.getElementById('firmware-update-btn'); const btn = document.getElementById('firmware-update-btn');
const boardEl = document.getElementById('firmware-board');
try { try {
const resp = await fetch('/api/firmware/check'); const resp = await fetch('/api/firmware/check');
if (!resp.ok) { if (!resp.ok) {
return; return;
} }
const data = await resp.json(); const data = await resp.json();
if (data.board) {
boardEl.textContent = `Detected board: ${data.board}`;
}
if (!data.enabled) { if (!data.enabled) {
statusEl.style.display = 'none'; statusEl.style.display = 'none';
btn.style.display = 'none'; btn.style.display = 'none';
return; return;
} }
statusEl.style.display = 'block'; statusEl.style.display = 'block';
if (data.update_available) { if (!data.board) {
statusEl.textContent = "Waiting for the frame to check in before it can look up the right build.";
btn.style.display = 'none';
} else if (data.update_available) {
statusEl.textContent = `Update available: v${data.latest_version}.`; statusEl.textContent = `Update available: v${data.latest_version}.`;
btn.style.display = 'inline-block'; btn.style.display = 'inline-block';
} else if (data.latest_version) { } else if (data.latest_version) {