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
+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) {