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.
This commit is contained in:
@@ -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"
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 '' }}">
|
||||
|
||||
Reference in New Issue
Block a user