summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/app.js34
-rw-r--r--web/test/tasks-board.test.mjs22
2 files changed, 50 insertions, 6 deletions
diff --git a/web/app.js b/web/app.js
index 8bc6a9f..70184bc 100644
--- a/web/app.js
+++ b/web/app.js
@@ -744,6 +744,24 @@ function updateToggleButton() {
// Shared helper: renders an array of tasks as cards into a container element.
// Now updated to be non-destructive by reusing/updating existing task-card elements.
+// cardContentSignature returns a structural fingerprint of a task card,
+// excluding any .task-log-tail/.running-log subtree — live-streamed log
+// content must never cause renderTasksIntoContainer to think the card
+// "changed" and tear it down (see Task 5 of the tasks-board plan).
+export function cardContentSignature(cardEl) {
+ function walk(el) {
+ if (!el) return '';
+ const isLogTail = el.className && (
+ el.className.split(' ').includes('task-log-tail') ||
+ el.className.split(' ').includes('running-log')
+ );
+ if (isLogTail) return `<logtail:${el.dataset && el.dataset.logTarget || ''}>`;
+ const childSig = (el.children || []).map(walk).join('');
+ return `<${el.tag || ''} class="${el.className || ''}" data-state="${(el.dataset && el.dataset.state) || ''}">${el.textContent || ''}${childSig}`;
+ }
+ return walk(cardEl);
+}
+
function renderTasksIntoContainer(tasks, container, emptyMsg) {
if (!tasks || tasks.length === 0) {
container.innerHTML = `<div class="task-empty">${emptyMsg}</div>`;
@@ -775,12 +793,16 @@ function renderTasksIntoContainer(tasks, container, emptyMsg) {
const newCard = createTaskCard(task);
if (card) {
- // If the content is exactly the same, we could skip replacing,
- // but createTaskCard is fast and ensures we have the latest state.
- // We replace the card in-place to preserve its position if possible.
- if (card.innerHTML !== newCard.innerHTML) {
- // Special case: if user is interacting with THIS card, we might want to skip or merge.
- // For now, createTaskCard ensures we don't disrupt if NOT editing.
+ // Compare everything except live-streamed log content (see
+ // cardContentSignature) so an appending .task-log-tail never triggers
+ // a teardown-and-rebuild of the whole card on its own.
+ if (cardContentSignature(card) !== cardContentSignature(newCard)) {
+ // Preserve the existing (potentially already-streaming) log-tail
+ // element across the swap so its EventSource's target node stays
+ // attached and its accumulated content survives.
+ const oldLogTail = card.querySelector('.task-log-tail');
+ const newLogTail = newCard.querySelector('.task-log-tail');
+ if (oldLogTail && newLogTail) newLogTail.replaceWith(oldLogTail);
container.replaceChild(newCard, card);
}
} else {
diff --git a/web/test/tasks-board.test.mjs b/web/test/tasks-board.test.mjs
index db61880..1e35e18 100644
--- a/web/test/tasks-board.test.mjs
+++ b/web/test/tasks-board.test.mjs
@@ -11,6 +11,7 @@ import {
columnForTaskState,
groupTasksByColumn,
createTaskCard,
+ cardContentSignature,
} from '../app.js';
describe('columnForTaskState', () => {
@@ -227,3 +228,24 @@ describe('createTaskCard elapsed timer', () => {
assert.equal(ready.querySelector('.task-elapsed[data-started-at]'), null);
});
});
+
+// ── renderTasksIntoContainer: log-tail preservation across re-renders ──────
+
+describe('cardContentSignature', () => {
+ it('excludes .task-log-tail content from the signature', () => {
+ const doc = makeMockDoc();
+ const cardA = createTaskCard({ id: 't1', name: 'Task', state: 'READY' }, doc);
+ const cardB = createTaskCard({ id: 't1', name: 'Task', state: 'READY' }, doc);
+ // Simulate cardB having accumulated live log lines that cardA (freshly built) doesn't have.
+ const tailB = cardB.querySelector('.task-log-tail');
+ tailB.children.push({ tag: 'div', className: 'log-line', textContent: 'some streamed output', children: [] });
+ assert.equal(cardContentSignature(cardA), cardContentSignature(cardB));
+ });
+
+ it('still differs when a non-log field changes', () => {
+ const doc = makeMockDoc();
+ const cardA = createTaskCard({ id: 't1', name: 'Task', state: 'READY' }, doc);
+ const cardB = createTaskCard({ id: 't1', name: 'Task', state: 'FAILED' }, doc);
+ assert.notEqual(cardContentSignature(cardA), cardContentSignature(cardB));
+ });
+});