Live-update whiteboard/tasks source display; add firmware CI build check
Build and push server image / test (push) Successful in 15s
Firmware build check / build-check (push) Successful in 2m6s
Build and push server image / build-and-push (push) Successful in 2m26s

Whiteboard and tasks-source save/clear used to just tell the user to
reload the page to see the change. Both endpoints always assign a
successful "set" to the calling user, so the new state is fully known
client-side already -- rewrite the "Currently using/showing ..." block
in place instead, no server round trip or reload needed.

Also adds .gitea/workflows/firmware-build-check.yml: builds both board
variants (devkit, xiao) on every push touching firmware/**, unlike
firmware-release-build.yml which only builds on a version.txt bump.
Verified both builds succeed locally against the actual ESP-IDF
toolchain before wiring this in.
This commit is contained in:
2026-07-23 19:08:01 -04:00
parent 31adc34a19
commit 1100580c2c
5 changed files with 173 additions and 48 deletions
+51
View File
@@ -0,0 +1,51 @@
name: Firmware build check
# Fires on every push touching firmware source, unlike
# firmware-release-build.yml (which only builds+publishes when
# firmware/version.txt itself is bumped -- the "cut a release" signal).
# This just verifies both board variants still compile; nothing else in
# CI catches a firmware/** push that breaks the build until someone
# happens to bump the version next.
on:
push:
branches: [main]
paths:
- "firmware/**"
- ".gitea/workflows/firmware-build-check.yml"
jobs:
build-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Same docker create/cp/start pattern as firmware-release-build.yml
# (see that file's own comment for why -- the runner's job
# workspace lives in a named Docker volume, not a real host path,
# so a nested `docker run -v "$PWD:..."` bind-mounts nothing
# useful). No release/artifact step here -- this only needs to
# prove `idf.py build` still succeeds for each board.
- name: Build (devkit -- ESP32-C6-DevKitC-1)
run: |
cid=$(docker create -w /workspace/firmware espressif/idf:release-v6.0 bash -c '
git config --global --add safe.directory /workspace &&
. "$IDF_PATH/export.sh" &&
./build_for_board.sh devkit set-target esp32c6 &&
./build_for_board.sh devkit build
')
docker cp "$PWD/." "$cid:/workspace"
docker start -a "$cid"
docker rm "$cid"
- name: Build (xiao -- Seeed XIAO ESP32-C6)
run: |
cid=$(docker create -w /workspace/firmware espressif/idf:release-v6.0 bash -c '
git config --global --add safe.directory /workspace &&
. "$IDF_PATH/export.sh" &&
./build_for_board.sh xiao set-target esp32c6 &&
./build_for_board.sh xiao build
')
docker cp "$PWD/." "$cid:/workspace"
docker start -a "$cid"
docker rm "$cid"
+46 -15
View File
@@ -202,6 +202,31 @@ document.getElementById('tasks_enabled').addEventListener('change', async (e) =>
}
});
// Rewrites #tasks-current-source in place instead of telling the user
// to reload -- the API always assigns a successful "set" to the caller
// (see api_tasks_source), so after either action we already know
// exactly what the new state is without asking the server again.
function renderTasksCurrentSource(label) {
const container = document.getElementById('tasks-current-source');
container.innerHTML = '';
if (!label) return; // matches the template's no-tasks_source branch: nothing rendered
const p = document.createElement('p');
p.className = 'sub';
p.style.marginTop = '10px';
p.append('Currently using your ');
const labelEl = document.createElement('strong');
labelEl.textContent = label;
p.append(labelEl, ' list. ');
const clearBtn = document.createElement('button');
clearBtn.type = 'button';
clearBtn.className = 'btn-inline secondary';
clearBtn.id = 'tasks-source-clear';
clearBtn.textContent = 'Clear';
clearBtn.addEventListener('click', clearTasksSource);
p.append(clearBtn);
container.append(p);
}
// Choosing one of your own CalDAV task lists as this frame's source --
// owner-only (see api_frames.py's api_tasks_source), so these radios
// only ever render for the viewer's own calendars anyway.
@@ -214,7 +239,9 @@ document.querySelectorAll('.tasks-source-choice').forEach((el) => {
body: JSON.stringify({ calendar_key: el.dataset.key }),
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Task list saved. Reload to see the updated source.');
showStatus(true, 'Task list saved.');
const labelEl = el.closest('li').querySelector('label');
renderTasksCurrentSource(labelEl ? labelEl.textContent.trim() : '');
loadCalendarPreview();
} catch (e) {
showStatus(false, e.message);
@@ -222,22 +249,26 @@ document.querySelectorAll('.tasks-source-choice').forEach((el) => {
});
});
async function clearTasksSource() {
try {
const resp = await fetch(`${window.FRAME_API}/tasks-source`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ calendar_key: null }),
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Task list cleared.');
renderTasksCurrentSource(null);
document.querySelectorAll('.tasks-source-choice').forEach((r) => { r.checked = false; });
loadCalendarPreview();
} catch (e) {
showStatus(false, e.message);
}
}
const tasksSourceClear = document.getElementById('tasks-source-clear');
if (tasksSourceClear) {
tasksSourceClear.addEventListener('click', async () => {
try {
const resp = await fetch(`${window.FRAME_API}/tasks-source`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ calendar_key: null }),
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Task list cleared. Reload to see the change.');
loadCalendarPreview();
} catch (e) {
showStatus(false, e.message);
}
});
tasksSourceClear.addEventListener('click', clearTasksSource);
}
function loadCalendarPreview() {
+54 -15
View File
@@ -2,6 +2,39 @@
// api_whiteboard_source), preview, and take control. window.FRAME_API is
// set by the template.
// Rewrites #whiteboard-current-source in place instead of telling the
// user to reload -- the API always assigns a successful "set" to the
// caller (see api_whiteboard_source), so after either action we already
// know exactly what the new state is without asking the server again.
function renderWhiteboardCurrentSource(url) {
const container = document.getElementById('whiteboard-current-source');
container.innerHTML = '';
const p = document.createElement('p');
p.className = 'sub';
p.style.marginTop = '10px';
if (url) {
p.append('Currently showing ');
const urlEl = document.createElement('strong');
urlEl.textContent = url;
p.append(urlEl, ' using your WebDAV account. ');
const clearBtn = document.createElement('button');
clearBtn.type = 'button';
clearBtn.className = 'btn-inline secondary';
clearBtn.id = 'whiteboard-source-clear';
clearBtn.textContent = 'Clear';
clearBtn.addEventListener('click', clearWhiteboardSource);
p.append(clearBtn);
} else {
p.textContent = 'No whiteboard configured yet.';
}
container.append(p);
const label = document.getElementById('whiteboard-source-form-label');
if (label) {
label.textContent = url ? 'Change to one of your own files' : 'Use one of your own files';
}
}
const whiteboardForm = document.getElementById('whiteboard-source-form');
if (whiteboardForm) {
whiteboardForm.addEventListener('submit', async (e) => {
@@ -15,7 +48,8 @@ if (whiteboardForm) {
body: JSON.stringify({ url }),
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Saved. Reload to see the updated source.');
showStatus(true, 'Saved.');
renderWhiteboardCurrentSource(url);
loadWhiteboardPreview();
} catch (e) {
showStatus(false, e.message);
@@ -23,22 +57,27 @@ if (whiteboardForm) {
});
}
async function clearWhiteboardSource() {
try {
const resp = await fetch(`${window.FRAME_API}/whiteboard-source`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: null }),
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Cleared.');
renderWhiteboardCurrentSource(null);
const urlInput = document.getElementById('whiteboard-url-input');
if (urlInput) urlInput.value = '';
loadWhiteboardPreview();
} catch (e) {
showStatus(false, e.message);
}
}
const whiteboardClearBtn = document.getElementById('whiteboard-source-clear');
if (whiteboardClearBtn) {
whiteboardClearBtn.addEventListener('click', async () => {
try {
const resp = await fetch(`${window.FRAME_API}/whiteboard-source`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: null }),
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Cleared. Reload to see the change.');
loadWhiteboardPreview();
} catch (e) {
showStatus(false, e.message);
}
});
whiteboardClearBtn.addEventListener('click', clearWhiteboardSource);
}
function loadWhiteboardPreview(force) {
+9 -7
View File
@@ -163,13 +163,15 @@
<label for="tasks_enabled">Show a task list</label>
</div>
{% if tasks_source %}
<p class="sub" style="margin-top: 10px;">
Currently using <strong>{{ tasks_source.display_name }}</strong>'s
<strong>{{ tasks_source.label }}</strong> list.
<button type="button" class="btn-inline secondary" id="tasks-source-clear">Clear</button>
</p>
{% endif %}
<div id="tasks-current-source">
{% if tasks_source %}
<p class="sub" style="margin-top: 10px;">
Currently using <strong>{{ tasks_source.display_name }}</strong>'s
<strong>{{ tasks_source.label }}</strong> list.
<button type="button" class="btn-inline secondary" id="tasks-source-clear">Clear</button>
</p>
{% endif %}
</div>
{% if viewer_task_calendars %}
<p class="sub" style="margin-top: 10px;">Use one of your own CalDAV task lists:</p>
+13 -11
View File
@@ -27,20 +27,22 @@
scene) fetched over WebDAV -- credentials set up in
<a href="/settings">Settings</a>.</p>
{% if whiteboard_source %}
<p class="sub" style="margin-top: 10px;">
Currently showing <strong>{{ whiteboard_source.url }}</strong>
using <strong>{{ whiteboard_source.display_name }}</strong>'s
WebDAV account.
<button type="button" class="btn-inline secondary" id="whiteboard-source-clear">Clear</button>
</p>
{% else %}
<p class="sub" style="margin-top: 10px;">No whiteboard configured yet.</p>
{% endif %}
<div id="whiteboard-current-source">
{% if whiteboard_source %}
<p class="sub" style="margin-top: 10px;">
Currently showing <strong>{{ whiteboard_source.url }}</strong>
using <strong>{{ whiteboard_source.display_name }}</strong>'s
WebDAV account.
<button type="button" class="btn-inline secondary" id="whiteboard-source-clear">Clear</button>
</p>
{% else %}
<p class="sub" style="margin-top: 10px;">No whiteboard configured yet.</p>
{% endif %}
</div>
{% if viewer_has_webdav_creds %}
<form id="whiteboard-source-form" style="margin-top: 16px;">
<label>{{ "Change to one of your own files" if whiteboard_source else "Use one of your own files" }}
<label><span id="whiteboard-source-form-label">{{ "Change to one of your own files" if whiteboard_source else "Use one of your own files" }}</span>
<input type="text" id="whiteboard-url-input"
placeholder="https://cloud.example.com/remote.php/dav/files/you/Boards/family.whiteboard"
value="{{ whiteboard_source.url if whiteboard_source and whiteboard_source.user_id == user.id else '' }}">