From 1100580c2c6c75ae59bd5b2bad92868aaf6c8f81 Mon Sep 17 00:00:00 2001
From: Thomas Faour
Date: Thu, 23 Jul 2026 19:08:01 -0400
Subject: [PATCH] Live-update whiteboard/tasks source display; add firmware CI
build check
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.
---
.gitea/workflows/firmware-build-check.yml | 51 ++++++++++++++++
server/app/static/frame_calendar.js | 61 ++++++++++++++-----
server/app/static/frame_whiteboard.js | 69 +++++++++++++++++-----
server/app/templates/frame_calendar.html | 16 ++---
server/app/templates/frame_whiteboard.html | 24 ++++----
5 files changed, 173 insertions(+), 48 deletions(-)
create mode 100644 .gitea/workflows/firmware-build-check.yml
diff --git a/.gitea/workflows/firmware-build-check.yml b/.gitea/workflows/firmware-build-check.yml
new file mode 100644
index 0000000..7758912
--- /dev/null
+++ b/.gitea/workflows/firmware-build-check.yml
@@ -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"
diff --git a/server/app/static/frame_calendar.js b/server/app/static/frame_calendar.js
index 6c61704..233adae 100644
--- a/server/app/static/frame_calendar.js
+++ b/server/app/static/frame_calendar.js
@@ -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() {
diff --git a/server/app/static/frame_whiteboard.js b/server/app/static/frame_whiteboard.js
index a4d4de4..13de92d 100644
--- a/server/app/static/frame_whiteboard.js
+++ b/server/app/static/frame_whiteboard.js
@@ -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) {
diff --git a/server/app/templates/frame_calendar.html b/server/app/templates/frame_calendar.html
index 3329ba6..e1d3e8b 100644
--- a/server/app/templates/frame_calendar.html
+++ b/server/app/templates/frame_calendar.html
@@ -163,13 +163,15 @@
- {% if tasks_source %}
-
- Currently using {{ tasks_source.display_name }}'s
- {{ tasks_source.label }} list.
-
-
- {% endif %}
+
+ {% if tasks_source %}
+
+ Currently using {{ tasks_source.display_name }}'s
+ {{ tasks_source.label }} list.
+
+
+ {% endif %}
+
{% if viewer_task_calendars %}
Use one of your own CalDAV task lists:
diff --git a/server/app/templates/frame_whiteboard.html b/server/app/templates/frame_whiteboard.html
index 092d9e3..ed29c3c 100644
--- a/server/app/templates/frame_whiteboard.html
+++ b/server/app/templates/frame_whiteboard.html
@@ -27,20 +27,22 @@
scene) fetched over WebDAV -- credentials set up in
Settings.
- {% if whiteboard_source %}
-
- Currently showing {{ whiteboard_source.url }}
- using {{ whiteboard_source.display_name }}'s
- WebDAV account.
-
-
- {% else %}
-
No whiteboard configured yet.
- {% endif %}
+
+ {% if whiteboard_source %}
+
+ Currently showing {{ whiteboard_source.url }}
+ using {{ whiteboard_source.display_name }}'s
+ WebDAV account.
+
+