diff --git a/webui/static/auto-sync.js b/webui/static/auto-sync.js index 83fc38e6..1feb491a 100644 --- a/webui/static/auto-sync.js +++ b/webui/static/auto-sync.js @@ -459,16 +459,9 @@ function autoSyncHistoryEntryHtml(entry, index = 0) { const entryId = `auto-sync-history-${entry.id}`; const playlistName = entry.playlist_name || after.name || before.name || `Playlist #${entry.playlist_id || 'unknown'}`; const summary = entry.summary || autoSyncHistoryFallbackSummary(before, after, status); - const resultHtml = [ - autoSyncHistoryResultPill('Refreshed', result.playlists_refreshed), - autoSyncHistoryResultPill('Synced', result.tracks_synced), - autoSyncHistoryResultPill('Skipped', result.sync_skipped), - autoSyncHistoryResultPill('Queued', result.wishlist_queued), - result.error ? `${_esc(result.error)}` : '', - ].filter(Boolean).join(''); return ` -
-
+
+
`; @@ -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; }