@@ -496,19 +489,11 @@ function autoSyncHistoryEntryHtml(entry, index = 0) {
${started ? `${_esc(started)}` : ''}
${duration ? `${_esc(duration)}` : ''}
${_esc(entry.trigger_source || 'pipeline')}
-
+ Expand
-
- ${autoSyncHistoryStatHtml('Tracks', before.track_count, after.track_count, trackDelta)}
- ${autoSyncHistoryStatHtml('Discovered', before.discovered_count, after.discovered_count, discoveredDelta)}
- ${autoSyncHistoryStatHtml('Wishlisted', before.wishlisted_count, after.wishlisted_count, wishlistDelta)}
- ${autoSyncHistoryStatHtml('In library', before.in_library_count, after.in_library_count, libraryDelta)}
-
-
- ${resultHtml || 'No detailed result payload recorded for this run.'}
-
+ ${autoSyncHistoryDetailHtml(entry, before, after, result, { trackDelta, discoveredDelta, wishlistDelta, libraryDelta })}
`;
@@ -556,7 +541,18 @@ function autoSyncHistoryFallbackSummary(before, after, status) {
function autoSyncToggleHistoryEntry(entryId) {
const el = document.getElementById(entryId);
- if (el) el.classList.toggle('expanded');
+ const card = document.getElementById(`${entryId}-card`);
+ const row = card?.querySelector('.auto-sync-history-row');
+ if (!el) return;
+ const expanded = el.classList.toggle('expanded');
+ if (card) card.classList.toggle('expanded', expanded);
+ if (row) row.setAttribute('aria-expanded', expanded ? 'true' : 'false');
+}
+
+function autoSyncHistoryEntryKeydown(event, entryId) {
+ if (event.key !== 'Enter' && event.key !== ' ') return;
+ event.preventDefault();
+ autoSyncToggleHistoryEntry(entryId);
}
function autoSyncHistoryStatusLabel(status) {
@@ -610,6 +606,141 @@ function autoSyncHistoryResultPill(label, value) {
return `
${_esc(label)}: ${_esc(String(value))}`;
}
+function autoSyncHistoryDetailHtml(entry, before, after, result, deltas) {
+ const resultPills = [
+ autoSyncHistoryResultPill('Refreshed', result.playlists_refreshed),
+ autoSyncHistoryResultPill('Discovered', result.tracks_discovered),
+ autoSyncHistoryResultPill('Synced', result.tracks_synced),
+ autoSyncHistoryResultPill('Skipped', result.sync_skipped),
+ autoSyncHistoryResultPill('Wishlisted', result.wishlist_queued),
+ autoSyncHistoryResultPill('Duration', result.duration_seconds ? autoSyncDurationLabel(result.duration_seconds) : ''),
+ result.error ? `
${_esc(result.error)}` : '',
+ ].filter(Boolean).join('');
+ const timeline = [
+ ['Started', autoSyncFormatDateTime(entry.started_at)],
+ ['Finished', autoSyncFormatDateTime(entry.finished_at)],
+ ['Duration', entry.duration_seconds ? autoSyncDurationLabel(entry.duration_seconds) : 'Not recorded'],
+ ['Trigger', entry.trigger_source || 'pipeline'],
+ ['Source', entry.source || after.source || before.source || 'Unknown'],
+ ['Playlist ID', entry.playlist_id || after.playlist_id || before.playlist_id || 'Unknown'],
+ ];
+ return `
+
+
+ Run Summary
+
+ ${autoSyncHistoryStatHtml('Tracks', before.track_count, after.track_count, deltas.trackDelta)}
+ ${autoSyncHistoryStatHtml('Discovered', before.discovered_count, after.discovered_count, deltas.discoveredDelta)}
+ ${autoSyncHistoryStatHtml('Wishlisted', before.wishlisted_count, after.wishlisted_count, deltas.wishlistDelta)}
+ ${autoSyncHistoryStatHtml('In library', before.in_library_count, after.in_library_count, deltas.libraryDelta)}
+
+
+ ${resultPills || 'No detailed result payload recorded for this run.'}
+
+
+
+ Timeline
+
+ ${timeline.map(([label, value]) => autoSyncHistoryFactHtml(label, value)).join('')}
+
+
+
+
+ ${autoSyncHistorySnapshotHtml('Before refresh', before)}
+ ${autoSyncHistorySnapshotHtml('After pipeline', after)}
+
+ ${autoSyncHistoryObjectHtml('Result payload', result, { skipPrivate: true })}
+ ${autoSyncHistoryLogsHtml(entry.log_lines)}
+ `;
+}
+
+function autoSyncHistoryFactHtml(label, value) {
+ return `
+
+ ${_esc(label)}
+ ${_esc(autoSyncValueLabel(value))}
+
+ `;
+}
+
+function autoSyncHistorySnapshotHtml(title, snapshot) {
+ const fields = [
+ ['Name', snapshot.name],
+ ['Source', snapshot.source],
+ ['Tracks', snapshot.track_count],
+ ['Discovered', snapshot.discovered_count],
+ ['Wishlisted', snapshot.wishlisted_count],
+ ['In library', snapshot.in_library_count],
+ ];
+ return `
+
+ ${_esc(title)}
+
+ ${fields.map(([label, value]) => autoSyncHistoryFactHtml(label, value)).join('')}
+
+
+ `;
+}
+
+function autoSyncHistoryObjectHtml(title, obj, options = {}) {
+ if (!obj || typeof obj !== 'object') return '';
+ const entries = Object.entries(obj)
+ .filter(([key, value]) => !(options.skipPrivate && key.startsWith('_')) && value !== undefined && value !== null && value !== '')
+ .slice(0, 24);
+ if (!entries.length) return '';
+ return `
+
+ ${_esc(title)}
+
+ ${entries.map(([key, value]) => `
+
+ ${_esc(autoSyncHumanizeKey(key))}
+ ${_esc(autoSyncValueLabel(value))}
+
+ `).join('')}
+
+
+ `;
+}
+
+function autoSyncHistoryLogsHtml(logLines) {
+ if (!Array.isArray(logLines) || !logLines.length) return '';
+ return `
+
+ Run Log
+
+ ${logLines.slice(-12).map(line => {
+ const text = typeof line === 'string' ? line : (line.message || line.log_line || JSON.stringify(line));
+ const type = typeof line === 'object' ? (line.type || line.log_type || 'info') : 'info';
+ return `
${_esc(text)}
`;
+ }).join('')}
+
+
+ `;
+}
+
+function autoSyncFormatDateTime(value) {
+ if (!value) return '';
+ const ts = _autoParseUTC(value);
+ if (!Number.isFinite(ts)) return value;
+ return new Date(ts).toLocaleString();
+}
+
+function autoSyncHumanizeKey(key) {
+ return String(key || '')
+ .replace(/^_+/, '')
+ .replace(/_/g, ' ')
+ .replace(/\b\w/g, ch => ch.toUpperCase());
+}
+
+function autoSyncValueLabel(value) {
+ if (value === undefined || value === null || value === '') return 'Not recorded';
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No';
+ if (Array.isArray(value)) return value.length ? value.map(autoSyncValueLabel).join(', ') : 'None';
+ if (typeof value === 'object') return JSON.stringify(value);
+ return String(value);
+}
+
function autoSyncAutomationCardHtml(auto, playlists) {
const cfg = auto.action_config || {};
const playlistId = autoSyncPlaylistIdFromAutomation(auto);
diff --git a/webui/static/style.css b/webui/static/style.css
index ef29229d..9c85642a 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -12088,6 +12088,11 @@ body.helper-mode-active #dashboard-activity-feed:hover {
box-shadow: none;
}
+.auto-sync-history-entry.expanded {
+ border-color: rgba(var(--accent-rgb), 0.26);
+ background: rgba(26, 26, 26, 0.98);
+}
+
.auto-sync-history-entry:empty {
display: flex;
align-items: center;
@@ -12116,6 +12121,11 @@ body.helper-mode-active #dashboard-activity-feed:hover {
cursor: pointer;
}
+.auto-sync-history-row:focus-visible {
+ outline: 2px solid rgba(var(--accent-rgb), 0.45);
+ outline-offset: -3px;
+}
+
.auto-sync-history-status {
padding: 4px 8px;
border-radius: 999px;
@@ -12196,11 +12206,11 @@ body.helper-mode-active #dashboard-activity-feed:hover {
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
+ max-width: 190px;
}
.auto-sync-history-meta span,
-.auto-sync-history-result span,
-.auto-sync-history-meta button {
+.auto-sync-history-result span {
padding: 4px 7px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.055);
@@ -12209,32 +12219,74 @@ body.helper-mode-active #dashboard-activity-feed:hover {
font-weight: 700;
}
-.auto-sync-history-meta button {
+.auto-sync-history-meta .auto-sync-history-expand-label {
border: 1px solid rgba(var(--accent-rgb), 0.18);
color: rgb(var(--accent-light-rgb));
- cursor: pointer;
+ padding-right: 20px;
+ position: relative;
}
-.auto-sync-history-meta button:hover {
+.auto-sync-history-meta .auto-sync-history-expand-label::after {
+ content: '';
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ width: 6px;
+ height: 6px;
+ border-right: 1.5px solid currentColor;
+ border-bottom: 1.5px solid currentColor;
+ transform: translateY(-65%) rotate(45deg);
+ transition: transform 160ms ease;
+}
+
+.auto-sync-history-entry.expanded .auto-sync-history-expand-label::after {
+ transform: translateY(-30%) rotate(225deg);
+}
+
+.auto-sync-history-entry:hover .auto-sync-history-expand-label {
background: rgba(var(--accent-rgb), 0.14);
border-color: rgba(var(--accent-rgb), 0.35);
}
.auto-sync-history-detail {
display: none;
- padding: 0 16px 16px;
+ padding: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.07);
}
.auto-sync-history-detail.expanded {
- display: block;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.auto-sync-history-detail-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.45fr) minmax(220px, 0.8fr);
+ gap: 12px;
+}
+
+.auto-sync-history-section {
+ min-width: 0;
+ padding: 12px;
+ border: 1px solid rgba(255, 255, 255, 0.075);
+ border-radius: 9px;
+ background: rgba(255, 255, 255, 0.028);
+}
+
+.auto-sync-history-section-title {
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 10px;
+ font-weight: 850;
+ letter-spacing: 0;
+ text-transform: uppercase;
}
.auto-sync-history-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
- padding-top: 14px;
+ padding-top: 10px;
}
.auto-sync-history-stats div {
@@ -12265,7 +12317,7 @@ body.helper-mode-active #dashboard-activity-feed:hover {
display: flex;
flex-wrap: wrap;
gap: 7px;
- margin-top: 9px;
+ margin-top: 10px;
}
.auto-sync-history-result span {
@@ -12286,6 +12338,81 @@ body.helper-mode-active #dashboard-activity-feed:hover {
border-color: rgba(255, 255, 255, 0.08);
}
+.auto-sync-history-snapshots {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.auto-sync-history-facts,
+.auto-sync-history-payload {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
+ margin-top: 10px;
+}
+
+.auto-sync-history-facts.compact {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.auto-sync-history-facts div,
+.auto-sync-history-payload div {
+ min-width: 0;
+ padding: 9px;
+ border-radius: 7px;
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.auto-sync-history-facts span,
+.auto-sync-history-payload span,
+.auto-sync-history-facts strong,
+.auto-sync-history-payload strong {
+ display: block;
+ min-width: 0;
+}
+
+.auto-sync-history-facts span,
+.auto-sync-history-payload span {
+ color: rgba(255, 255, 255, 0.36);
+ font-size: 10px;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.auto-sync-history-facts strong,
+.auto-sync-history-payload strong {
+ margin-top: 4px;
+ color: rgba(255, 255, 255, 0.78);
+ font-size: 11px;
+ line-height: 1.35;
+ overflow-wrap: anywhere;
+}
+
+.auto-sync-history-logs {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ max-height: 190px;
+ overflow: auto;
+ margin-top: 10px;
+ padding: 10px;
+ border-radius: 8px;
+ background: rgba(0, 0, 0, 0.18);
+}
+
+.auto-sync-history-logs div {
+ color: rgba(255, 255, 255, 0.54);
+ font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace;
+ font-size: 10px;
+ line-height: 1.45;
+ overflow-wrap: anywhere;
+}
+
+.auto-sync-history-logs .success { color: #4ade80; }
+.auto-sync-history-logs .error { color: #ef4444; }
+.auto-sync-history-logs .warning { color: #facc15; }
+
.auto-sync-history-empty {
margin: 24px;
padding: 44px;
@@ -12358,6 +12485,15 @@ body.helper-mode-active #dashboard-activity-feed:hover {
grid-template-columns: repeat(10, minmax(230px, 250px));
padding: 18px;
}
+
+ .auto-sync-history-detail-grid,
+ .auto-sync-history-snapshots {
+ grid-template-columns: 1fr;
+ }
+
+ .auto-sync-history-facts.compact {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
}
@media (max-height: 760px) {
@@ -12428,12 +12564,19 @@ body.helper-mode-active #dashboard-activity-feed:hover {
.auto-sync-history-meta {
justify-content: flex-start;
+ max-width: none;
}
.auto-sync-history-stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+ .auto-sync-history-facts,
+ .auto-sync-history-facts.compact,
+ .auto-sync-history-payload {
+ grid-template-columns: 1fr;
+ }
+
.auto-sync-scheduled-card {
grid-template-columns: 1fr;
}