Harden auto-sync history row rendering
Render playlist pipeline history rows with DOM construction instead of per-card HTML strings so malformed payload values cannot collapse the row into an empty bordered shell. Add a visible per-row render fallback for bad history records while preserving the expandable detail panel.
This commit is contained in:
parent
4aec5584e1
commit
8f72cc3113
1 changed files with 149 additions and 44 deletions
|
|
@ -245,6 +245,7 @@ function renderAutoSyncScheduleModal() {
|
||||||
<div class="auto-sync-tab-panel ${historyActive ? 'active' : ''}" id="auto-sync-history-panel">${historyPanel}</div>
|
<div class="auto-sync-tab-panel ${historyActive ? 'active' : ''}" id="auto-sync-history-panel">${historyPanel}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
populateAutoSyncHistoryList(overlay);
|
||||||
bindAutoSyncHistoryCardInteractions(overlay);
|
bindAutoSyncHistoryCardInteractions(overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,13 +440,61 @@ function renderAutoSyncHistoryPanel(history, total) {
|
||||||
<button onclick="refreshAutoSyncScheduleModal()">Refresh</button>
|
<button onclick="refreshAutoSyncScheduleModal()">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="auto-sync-history-list">
|
<div class="auto-sync-history-list">
|
||||||
${history.map((entry, index) => autoSyncHistoryEntryHtml(entry, index)).join('')}
|
<div class="auto-sync-history-loading">Loading run history...</div>
|
||||||
${total > history.length ? `<div class="auto-sync-history-total">Showing ${history.length} of ${total} runs</div>` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoSyncHistoryEntryHtml(entry, index = 0) {
|
function populateAutoSyncHistoryList(root = document) {
|
||||||
|
const list = root.querySelector('.auto-sync-history-list');
|
||||||
|
if (!list) return;
|
||||||
|
const history = _autoSyncScheduleState.runHistory || [];
|
||||||
|
const total = _autoSyncScheduleState.runHistoryTotal || 0;
|
||||||
|
list.innerHTML = '';
|
||||||
|
history.forEach((entry, index) => {
|
||||||
|
try {
|
||||||
|
list.appendChild(createAutoSyncHistoryEntryElement(entry, index));
|
||||||
|
} catch (err) {
|
||||||
|
list.appendChild(createAutoSyncHistoryErrorElement(entry, index, err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (total > history.length) {
|
||||||
|
const totalEl = document.createElement('div');
|
||||||
|
totalEl.className = 'auto-sync-history-total';
|
||||||
|
totalEl.textContent = `Showing ${history.length} of ${total} runs`;
|
||||||
|
list.appendChild(totalEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAutoSyncHistoryErrorElement(entry, index, err) {
|
||||||
|
const card = document.createElement('article');
|
||||||
|
card.className = 'auto-sync-history-entry auto-sync-history-entry-error';
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'auto-sync-history-row';
|
||||||
|
const head = document.createElement('div');
|
||||||
|
head.className = 'auto-sync-history-card-head';
|
||||||
|
const titleBlock = document.createElement('div');
|
||||||
|
titleBlock.className = 'auto-sync-history-title-block';
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.className = 'auto-sync-history-title-row';
|
||||||
|
const dot = document.createElement('span');
|
||||||
|
dot.className = 'auto-sync-card-status-dot disabled';
|
||||||
|
const name = document.createElement('strong');
|
||||||
|
name.textContent = entry?.playlist_name || `Run #${entry?.id || index + 1}`;
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'auto-sync-history-status error';
|
||||||
|
badge.textContent = 'Render error';
|
||||||
|
title.append(dot, name, badge);
|
||||||
|
const summary = document.createElement('small');
|
||||||
|
summary.textContent = err?.message || 'This run history row could not be rendered.';
|
||||||
|
titleBlock.append(title, summary);
|
||||||
|
head.appendChild(titleBlock);
|
||||||
|
row.appendChild(head);
|
||||||
|
card.appendChild(row);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAutoSyncHistoryEntryElement(entry, index = 0) {
|
||||||
entry = autoSyncNormalizeHistoryEntry(entry, index);
|
entry = autoSyncNormalizeHistoryEntry(entry, index);
|
||||||
const status = entry.status || 'completed';
|
const status = entry.status || 'completed';
|
||||||
const before = entry.before_json || {};
|
const before = entry.before_json || {};
|
||||||
|
|
@ -460,46 +509,98 @@ function autoSyncHistoryEntryHtml(entry, index = 0) {
|
||||||
const entryId = `auto-sync-history-${entry.id}`;
|
const entryId = `auto-sync-history-${entry.id}`;
|
||||||
const playlistName = entry.playlist_name || after.name || before.name || `Playlist #${entry.playlist_id || 'unknown'}`;
|
const playlistName = entry.playlist_name || after.name || before.name || `Playlist #${entry.playlist_id || 'unknown'}`;
|
||||||
const summary = entry.summary || autoSyncHistoryFallbackSummary(before, after, status);
|
const summary = entry.summary || autoSyncHistoryFallbackSummary(before, after, status);
|
||||||
return `
|
|
||||||
<article class="auto-sync-history-entry" id="${entryId}-card" data-history-entry="${entryId}">
|
const card = document.createElement('article');
|
||||||
<div class="auto-sync-history-row" role="button" tabindex="0" aria-expanded="false" aria-controls="${entryId}" data-history-toggle="${entryId}">
|
card.className = 'auto-sync-history-entry';
|
||||||
<div class="auto-sync-history-card-head">
|
card.id = `${entryId}-card`;
|
||||||
<div class="auto-sync-history-title-block">
|
card.dataset.historyEntry = entryId;
|
||||||
<div class="auto-sync-history-title-row">
|
|
||||||
<span class="auto-sync-card-status-dot ${autoSyncHistoryStatusClass(status)}"></span>
|
const row = document.createElement('div');
|
||||||
<strong>${_esc(playlistName)}</strong>
|
row.className = 'auto-sync-history-row';
|
||||||
<span class="auto-sync-history-status ${_escAttr(status)}">${_esc(autoSyncHistoryStatusLabel(status))}</span>
|
row.setAttribute('role', 'button');
|
||||||
</div>
|
row.tabIndex = 0;
|
||||||
<small>${_esc(summary)}</small>
|
row.setAttribute('aria-expanded', 'false');
|
||||||
</div>
|
row.setAttribute('aria-controls', entryId);
|
||||||
<div class="auto-sync-history-meta">
|
row.dataset.historyToggle = entryId;
|
||||||
${started ? `<span>${_esc(started)}</span>` : ''}
|
|
||||||
${duration ? `<span>${_esc(duration)}</span>` : ''}
|
const head = document.createElement('div');
|
||||||
<span>${_esc(entry.trigger_source || 'pipeline')}</span>
|
head.className = 'auto-sync-history-card-head';
|
||||||
<button type="button" class="auto-sync-history-expand-label" data-history-toggle-button="${entryId}">View details</button>
|
const titleBlock = document.createElement('div');
|
||||||
</div>
|
titleBlock.className = 'auto-sync-history-title-block';
|
||||||
</div>
|
const titleRow = document.createElement('div');
|
||||||
<div class="auto-sync-card-flow">
|
titleRow.className = 'auto-sync-history-title-row';
|
||||||
<span class="flow-trigger">${_esc(entry.trigger_source || 'pipeline')}</span>
|
const dot = document.createElement('span');
|
||||||
<span class="flow-arrow">→</span>
|
dot.className = `auto-sync-card-status-dot ${autoSyncHistoryStatusClass(status)}`;
|
||||||
<span class="flow-action">Refresh</span>
|
const title = document.createElement('strong');
|
||||||
<span class="flow-arrow">→</span>
|
title.textContent = playlistName;
|
||||||
<span class="flow-action">Discover</span>
|
const statusBadge = document.createElement('span');
|
||||||
<span class="flow-arrow">→</span>
|
statusBadge.className = `auto-sync-history-status ${status}`;
|
||||||
<span class="flow-notify">Sync + wishlist</span>
|
statusBadge.textContent = autoSyncHistoryStatusLabel(status);
|
||||||
</div>
|
titleRow.append(dot, title, statusBadge);
|
||||||
<div class="auto-sync-history-preview">
|
const summaryEl = document.createElement('small');
|
||||||
${autoSyncHistoryPreviewPill('Tracks', before.track_count, after.track_count, trackDelta)}
|
summaryEl.textContent = summary;
|
||||||
${autoSyncHistoryPreviewPill('Discovered', before.discovered_count, after.discovered_count, discoveredDelta)}
|
titleBlock.append(titleRow, summaryEl);
|
||||||
${autoSyncHistoryPreviewPill('Wishlisted', before.wishlisted_count, after.wishlisted_count, wishlistDelta)}
|
|
||||||
${autoSyncHistoryPreviewPill('Library', before.in_library_count, after.in_library_count, libraryDelta)}
|
const meta = document.createElement('div');
|
||||||
</div>
|
meta.className = 'auto-sync-history-meta';
|
||||||
</div>
|
[started, duration, entry.trigger_source || 'pipeline'].filter(Boolean).forEach(value => {
|
||||||
<div id="${entryId}" class="auto-sync-history-detail">
|
const span = document.createElement('span');
|
||||||
${autoSyncHistoryDetailHtml(entry, before, after, result, { trackDelta, discoveredDelta, wishlistDelta, libraryDelta })}
|
span.textContent = value;
|
||||||
</div>
|
meta.appendChild(span);
|
||||||
</article>
|
});
|
||||||
`;
|
const expand = document.createElement('button');
|
||||||
|
expand.type = 'button';
|
||||||
|
expand.className = 'auto-sync-history-expand-label';
|
||||||
|
expand.dataset.historyToggleButton = entryId;
|
||||||
|
expand.textContent = 'View details';
|
||||||
|
meta.appendChild(expand);
|
||||||
|
head.append(titleBlock, meta);
|
||||||
|
|
||||||
|
const flow = document.createElement('div');
|
||||||
|
flow.className = 'auto-sync-card-flow';
|
||||||
|
autoSyncAppendFlowChip(flow, entry.trigger_source || 'pipeline', 'flow-trigger');
|
||||||
|
autoSyncAppendFlowArrow(flow);
|
||||||
|
autoSyncAppendFlowChip(flow, 'Refresh', 'flow-action');
|
||||||
|
autoSyncAppendFlowArrow(flow);
|
||||||
|
autoSyncAppendFlowChip(flow, 'Discover', 'flow-action');
|
||||||
|
autoSyncAppendFlowArrow(flow);
|
||||||
|
autoSyncAppendFlowChip(flow, 'Sync + wishlist', 'flow-notify');
|
||||||
|
|
||||||
|
const preview = document.createElement('div');
|
||||||
|
preview.className = 'auto-sync-history-preview';
|
||||||
|
[
|
||||||
|
['Tracks', before.track_count, after.track_count, trackDelta],
|
||||||
|
['Discovered', before.discovered_count, after.discovered_count, discoveredDelta],
|
||||||
|
['Wishlisted', before.wishlisted_count, after.wishlisted_count, wishlistDelta],
|
||||||
|
['Library', before.in_library_count, after.in_library_count, libraryDelta],
|
||||||
|
].forEach(([label, beforeValue, afterValue, delta]) => {
|
||||||
|
const pill = document.createElement('span');
|
||||||
|
pill.textContent = autoSyncHistoryPreviewText(label, beforeValue, afterValue, delta);
|
||||||
|
preview.appendChild(pill);
|
||||||
|
});
|
||||||
|
|
||||||
|
const detail = document.createElement('div');
|
||||||
|
detail.id = entryId;
|
||||||
|
detail.className = 'auto-sync-history-detail';
|
||||||
|
detail.innerHTML = autoSyncHistoryDetailHtml(entry, before, after, result, { trackDelta, discoveredDelta, wishlistDelta, libraryDelta });
|
||||||
|
|
||||||
|
row.append(head, flow, preview);
|
||||||
|
card.append(row, detail);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoSyncAppendFlowChip(parent, text, className) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = className;
|
||||||
|
span.textContent = text;
|
||||||
|
parent.appendChild(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoSyncAppendFlowArrow(parent) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = 'flow-arrow';
|
||||||
|
span.textContent = '->';
|
||||||
|
parent.appendChild(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoSyncNormalizeHistoryEntry(entry, index) {
|
function autoSyncNormalizeHistoryEntry(entry, index) {
|
||||||
|
|
@ -613,10 +714,14 @@ function autoSyncHistoryStatHtml(label, before, after, delta) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoSyncHistoryPreviewPill(label, before, after, delta) {
|
function autoSyncHistoryPreviewPill(label, before, after, delta) {
|
||||||
|
return `<span>${_esc(autoSyncHistoryPreviewText(label, before, after, delta))}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoSyncHistoryPreviewText(label, before, after, delta) {
|
||||||
const beforeValue = parseInt(before, 10) || 0;
|
const beforeValue = parseInt(before, 10) || 0;
|
||||||
const afterValue = parseInt(after, 10) || 0;
|
const afterValue = parseInt(after, 10) || 0;
|
||||||
const deltaText = delta ? ` ${delta > 0 ? '+' : ''}${delta}` : '';
|
const deltaText = delta ? ` ${delta > 0 ? '+' : ''}${delta}` : '';
|
||||||
return `<span>${_esc(label)} ${beforeValue}->${afterValue}${_esc(deltaText)}</span>`;
|
return `${label} ${beforeValue}->${afterValue}${deltaText}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoSyncHistoryResultPill(label, value) {
|
function autoSyncHistoryResultPill(label, value) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue