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. + +

+ {% else %} +

No whiteboard configured yet.

+ {% endif %} +
{% if viewer_has_webdav_creds %}
-