diff --git a/public/app.js b/public/app.js index 8c12ad8..e63e5e1 100644 --- a/public/app.js +++ b/public/app.js @@ -5,6 +5,11 @@ let showAll = false; let csrfToken = null; // double-submit CSRF token, sent as X-CSRF-Token on mutating requests const SPLASH_MIN_MS = 1200; // minimum splash display time +// History section state +let historyDays = parseInt(localStorage.getItem('sofarr-history-days'), 10) || 7; +let historyRefreshHandle = null; +const HISTORY_REFRESH_MS = 5 * 60 * 1000; // auto-refresh history every 5 min + // SSE stream state let sseSource = null; let sseReconnectTimer = null; @@ -20,6 +25,7 @@ const SSE_RECONNECT_MS = 3000; // browser already retries, but we add explicit b document.addEventListener('DOMContentLoaded', () => { checkAuthentication(); initThemeSwitcher(); + initHistoryControls(); document.getElementById('login-form').addEventListener('submit', handleLogin); document.getElementById('logout-btn').addEventListener('click', handleLogout); @@ -209,6 +215,7 @@ async function handleLogin(e) { async function handleLogout() { try { stopSSE(); + stopHistoryRefresh(); if (statusRefreshHandle) { clearInterval(statusRefreshHandle); statusRefreshHandle = null; } await fetch('/api/auth/logout', { method: 'POST', @@ -217,6 +224,7 @@ async function handleLogout() { currentUser = null; csrfToken = null; downloads = []; + clearHistory(); showLogin(); } catch (err) { console.error('Logout failed:', err); @@ -238,6 +246,11 @@ function showDashboard() { sp.style.display = 'none'; sp.innerHTML = ''; document.getElementById('admin-controls').style.display = isAdmin ? 'flex' : 'none'; + // Initialise days input from saved value, then load history + const daysInput = document.getElementById('history-days'); + if (daysInput) daysInput.value = historyDays; + loadHistory(); + startHistoryRefresh(); } function showLoginError(message) { @@ -784,3 +797,197 @@ function hideLoading() { const loading = document.getElementById('loading'); loading.style.display = 'none'; } + +// ============================================================================= +// History section +// ============================================================================= + +function initHistoryControls() { + const daysInput = document.getElementById('history-days'); + const refreshBtn = document.getElementById('history-refresh-btn'); + if (daysInput) { + daysInput.addEventListener('change', () => { + const v = parseInt(daysInput.value, 10); + if (v > 0 && v <= 90) { + historyDays = v; + localStorage.setItem('sofarr-history-days', v); + loadHistory(); + } + }); + } + if (refreshBtn) { + refreshBtn.addEventListener('click', () => loadHistory(true)); + } +} + +function startHistoryRefresh() { + stopHistoryRefresh(); + historyRefreshHandle = setInterval(() => loadHistory(), HISTORY_REFRESH_MS); +} + +function stopHistoryRefresh() { + if (historyRefreshHandle) { + clearInterval(historyRefreshHandle); + historyRefreshHandle = null; + } +} + +function clearHistory() { + document.getElementById('history-list').innerHTML = ''; + document.getElementById('no-history').style.display = 'none'; + document.getElementById('history-error').style.display = 'none'; +} + +async function loadHistory(forceRefresh = false) { + const listEl = document.getElementById('history-list'); + const loadingEl = document.getElementById('history-loading'); + const errorEl = document.getElementById('history-error'); + const noHistoryEl = document.getElementById('no-history'); + + loadingEl.style.display = 'block'; + errorEl.style.display = 'none'; + noHistoryEl.style.display = 'none'; + + try { + const params = new URLSearchParams({ days: historyDays }); + if (showAll) params.set('showAll', 'true'); + if (forceRefresh) params.set('_t', Date.now()); + const res = await fetch(`/api/history/recent?${params}`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + loadingEl.style.display = 'none'; + renderHistory(data.history || []); + } catch (err) { + loadingEl.style.display = 'none'; + errorEl.textContent = 'Failed to load history.'; + errorEl.style.display = 'block'; + console.error('[History] Load error:', err); + } +} + +function renderHistory(items) { + const listEl = document.getElementById('history-list'); + const noHistoryEl = document.getElementById('no-history'); + listEl.innerHTML = ''; + if (!items.length) { + noHistoryEl.style.display = 'block'; + return; + } + noHistoryEl.style.display = 'none'; + items.forEach(item => listEl.appendChild(createHistoryCard(item))); +} + +function createHistoryCard(item) { + const card = document.createElement('div'); + card.className = `history-card ${item.type} ${item.outcome}`; + + if (item.coverArt) { + const coverDiv = document.createElement('div'); + coverDiv.className = 'history-cover'; + const img = document.createElement('img'); + img.src = '/api/dashboard/cover-art?url=' + encodeURIComponent(item.coverArt); + img.alt = item.movieName || item.seriesName || item.title; + img.loading = 'lazy'; + coverDiv.appendChild(img); + card.appendChild(coverDiv); + } + + const info = document.createElement('div'); + info.className = 'history-info'; + + // Header row: type badge + outcome badge + const header = document.createElement('div'); + header.className = 'history-card-header'; + + const typeBadge = document.createElement('span'); + typeBadge.className = `history-type-badge ${item.type}`; + typeBadge.textContent = item.type === 'series' ? '📺 Series' : '🎬 Movie'; + header.appendChild(typeBadge); + + const outcomeBadge = document.createElement('span'); + outcomeBadge.className = `history-outcome-badge ${item.outcome}`; + outcomeBadge.textContent = item.outcome === 'imported' ? '✓ Imported' : '✗ Failed'; + header.appendChild(outcomeBadge); + + if (item.instanceName) { + const instBadge = document.createElement('span'); + instBadge.className = 'history-instance-badge'; + instBadge.textContent = item.instanceName; + header.appendChild(instBadge); + } + + if (showAll && item.tagBadges && item.tagBadges.length > 0) { + const unmatched = item.tagBadges.filter(b => !b.matchedUser); + const matched = item.tagBadges.filter(b => b.matchedUser); + for (const b of unmatched) { + const badge = document.createElement('span'); + badge.className = 'download-user-badge unmatched'; + badge.textContent = b.label; + header.appendChild(badge); + } + for (const b of matched) { + const badge = document.createElement('span'); + badge.className = 'download-user-badge'; + badge.textContent = b.matchedUser; + header.appendChild(badge); + } + } else if (item.matchedUserTag) { + const badge = document.createElement('span'); + badge.className = 'download-user-badge'; + badge.textContent = item.matchedUserTag; + header.appendChild(badge); + } + + info.appendChild(header); + + // Title + const title = document.createElement('h3'); + title.className = 'history-title'; + title.textContent = item.title; + info.appendChild(title); + + // Series/movie name with optional arr link + if (item.seriesName) { + const p = document.createElement('p'); + p.className = 'history-media-name'; + if (isAdmin && item.arrLink) { + p.innerHTML = 'Series: ' + escapeHtml(item.seriesName) + ''; + } else { + p.textContent = 'Series: ' + item.seriesName; + } + info.appendChild(p); + } + if (item.movieName) { + const p = document.createElement('p'); + p.className = 'history-media-name'; + if (isAdmin && item.arrLink) { + p.innerHTML = 'Movie: ' + escapeHtml(item.movieName) + ''; + } else { + p.textContent = 'Movie: ' + item.movieName; + } + info.appendChild(p); + } + + // Detail pills + const details = document.createElement('div'); + details.className = 'history-details'; + + if (item.completedAt) { + details.appendChild(createDetailItem('Completed', formatDate(item.completedAt))); + } + if (item.quality) { + details.appendChild(createDetailItem('Quality', item.quality)); + } + + // Failed imports: show failure message + if (item.outcome === 'failed' && item.failureMessage) { + const failItem = document.createElement('div'); + failItem.className = 'history-failure-message'; + failItem.textContent = item.failureMessage; + details.appendChild(failItem); + } + + info.appendChild(details); + card.appendChild(info); + return card; +} diff --git a/public/index.html b/public/index.html index ae54cd0..ef5a0ab 100644 --- a/public/index.html +++ b/public/index.html @@ -83,6 +83,24 @@
+