Firmware CI releases + Gitea auto-update in the server
Build and push server image / build-and-push (push) Successful in 39s

New Gitea Actions workflow builds both board variants and publishes
them as release assets whenever firmware/version.txt is bumped. The
server can now poll that repo's releases (next to the existing manual
upload) and either surface an "Update frame" button or, with
"Automatically apply updates" checked, stage the new build itself --
the frame still only updates on its own next wake either way.
This commit is contained in:
2026-07-21 19:25:08 -04:00
parent 4cf75b4b11
commit 94d67f7767
8 changed files with 493 additions and 42 deletions
+81
View File
@@ -75,6 +75,25 @@
{% endfor %}
</select>
</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="submit">Save</button>
</form>
@@ -111,6 +130,8 @@
</p>
<input type="file" id="firmware-file" accept=".bin">
<button type="button" class="secondary" id="firmware-upload">Upload firmware</button>
<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>
</section>
</div>
</div>
@@ -145,6 +166,9 @@
quiet_hours_start: document.getElementById('quiet_hours_start').value || '22:00',
quiet_hours_end: document.getElementById('quiet_hours_end').value || '07:00',
timezone: document.getElementById('timezone').value || 'UTC',
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),
});
const resp = await fetch('/api/config', {
method: 'POST',
@@ -495,6 +519,57 @@
}
});
async function loadFirmwareCheck() {
const statusEl = document.getElementById('firmware-gitea-status');
const btn = document.getElementById('firmware-update-btn');
try {
const resp = await fetch('/api/firmware/check');
if (!resp.ok) {
return;
}
const data = await resp.json();
if (!data.enabled) {
statusEl.style.display = 'none';
btn.style.display = 'none';
return;
}
statusEl.style.display = 'block';
if (data.update_available) {
statusEl.textContent = `Update available: v${data.latest_version}.`;
btn.style.display = 'inline-block';
} else if (data.latest_version) {
statusEl.textContent = `Up to date (v${data.latest_version}).`;
btn.style.display = 'none';
} else {
statusEl.textContent = 'No releases found yet.';
btn.style.display = 'none';
}
} catch (e) {
// A failed check is silent -- the manual upload path still works
// regardless, and this just retries on the next poll.
}
}
document.getElementById('firmware-update-btn').addEventListener('click', async () => {
const btn = document.getElementById('firmware-update-btn');
btn.disabled = true;
try {
const resp = await fetch('/api/firmware/apply-latest', { method: 'POST' });
if (!resp.ok) {
throw new Error(await resp.text());
}
const result = await resp.json();
document.getElementById('firmware-available').textContent =
`Uploaded: v${result.version} -- the frame updates itself on its next wake if it's running something else.`;
showStatus(true, `Firmware v${result.version} staged from Gitea.`);
loadFirmwareCheck();
} catch (e) {
showStatus(false, e.message);
} finally {
btn.disabled = false;
}
});
let lastDevice = null;
async function loadQueue() {
@@ -672,6 +747,7 @@
loadQueue();
loadBatteryLog();
loadStats();
loadFirmwareCheck();
// Redraw the canvas chart (and the "Last seen" alert color) with the
// new theme's colors as soon as the toggle in the header is used --
@@ -698,5 +774,10 @@
// from elsewhere, battery report, firmware version) without a manual
// refresh. Skipped mid-drag (see loadQueue above).
setInterval(loadQueue, 10000);
// Separate, slower poll for the Gitea release check -- cheap either
// way since the server itself throttles actual Gitea API calls to
// once per gitea_releases.UPDATE_CHECK_INTERVAL_S.
setInterval(loadFirmwareCheck, 60000);
</script>
{% endblock %}