diff --git a/webui/index.html b/webui/index.html index d3cbd999..1b37dece 100644 --- a/webui/index.html +++ b/webui/index.html @@ -359,6 +359,10 @@ Calendar + + + Automations + Tools @@ -1305,6 +1309,18 @@ + + @@ -10256,6 +10272,8 @@ + + diff --git a/webui/static/video/video-automations.js b/webui/static/video/video-automations.js new file mode 100644 index 00000000..7507889f --- /dev/null +++ b/webui/static/video/video-automations.js @@ -0,0 +1,158 @@ +/* + * SoulSync — Video Automations page. + * + * The automation engine is app-wide, so these are the SAME system automations the + * music side runs — surfaced on the video side too (minus Refresh Beatport Cache and + * any user/playlist-pipeline automations). Read + toggle + run only; reuses the music + * .automation-* card look. Calls the shared /api/automations endpoint (no music imports). + */ +(function () { + 'use strict'; + + var URL_LIST = '/api/automations'; + var _timer = null, _wired = false; + + function esc(s) { + return String(s == null ? '' : s) + .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + function toast(m, t) { if (typeof showToast === 'function') showToast(m, t); } + function getJSON(u) { + return fetch(u, { headers: { Accept: 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); + } + function post(u) { + return fetch(u, { method: 'POST', headers: { Accept: 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); + } + + // Friendly labels (system action_types only; Beatport/playlist excluded from display). + var ACTIONS = { + process_wishlist: 'Process wishlist', scan_watchlist: 'Scan watchlist', + scan_library: 'Scan library', start_database_update: 'Update database', + deep_scan_library: 'Deep-scan library', clean_search_history: 'Clean search history', + clean_completed_downloads: 'Clean completed downloads', backup_database: 'Back up database', + full_cleanup: 'Full cleanup', clear_quarantine: 'Clear quarantine', + cleanup_wishlist: 'Clean up wishlist', update_discovery_pool: 'Update discovery pool' + }; + var TICONS = { schedule: '⏱️', daily_time: '🕓', weekly_time: '📅', batch_complete: '⬇️', library_scan_completed: '🔄' }; + + function fmtTrigger(type, cfg) { + cfg = cfg || {}; + if (type === 'schedule') return 'Every ' + (cfg.interval || 1) + ' ' + (cfg.unit || 'hours'); + if (type === 'daily_time') return 'Daily at ' + (cfg.time || '00:00'); + if (type === 'weekly_time') return 'Weekly at ' + (cfg.time || '00:00'); + if (type === 'batch_complete') return 'When downloads finish'; + if (type === 'library_scan_completed') return 'After a library scan'; + return String(type || '').replace(/_/g, ' '); + } + function fmtAction(type) { return ACTIONS[type] || String(type || '').replace(/_/g, ' '); } + + function timeAgo(ts) { + var t = Date.parse(String(ts || '').replace(' ', 'T') + (/[zZ]|[+-]\d\d:?\d\d$/.test(ts || '') ? '' : 'Z')); + if (isNaN(t)) return ''; + var s = Math.round((Date.now() - t) / 1000); + if (s < 60) return s + 's ago'; + if (s < 3600) return Math.round(s / 60) + 'm ago'; + if (s < 86400) return Math.round(s / 3600) + 'h ago'; + return Math.round(s / 86400) + 'd ago'; + } + function timeUntil(ts) { + var t = Date.parse(String(ts || '').replace(' ', 'T') + (/[zZ]|[+-]\d\d:?\d\d$/.test(ts || '') ? '' : 'Z')); + if (isNaN(t)) return ''; + var s = Math.round((t - Date.now()) / 1000); + if (s <= 0) return 'due'; + if (s < 60) return 'in ' + s + 's'; + if (s < 3600) return 'in ' + Math.round(s / 60) + 'm'; + if (s < 86400) return 'in ' + Math.round(s / 3600) + 'h'; + return 'in ' + Math.round(s / 86400) + 'd'; + } + + // System automations the video side shows (drop Beatport + user/playlist ones). + function isVideoSystem(a) { + return a && a.is_system && + a.action_type !== 'refresh_beatport_cache' && + a.action_type !== 'playlist_pipeline' && + a.owned_by !== 'playlist_pipeline'; + } + + function cardHTML(a) { + var timers = ['schedule', 'daily_time', 'weekly_time']; + var meta = []; + if (a.last_run) meta.push('Last: ' + esc(timeAgo(a.last_run))); + if (a.next_run && a.enabled && timers.indexOf(a.trigger_type) > -1) meta.push('Next: ' + esc(timeUntil(a.next_run))); + else if (a.enabled && timers.indexOf(a.trigger_type) === -1) meta.push('Listening'); + if (a.run_count) meta.push('Runs: ' + a.run_count); + if (a.last_error) meta.push('Error: ' + esc(a.last_error) + ''); + return '
' + + '
' + + '
' + + '
' + esc(a.name) + '
' + + '
' + + '' + (TICONS[a.trigger_type] || '⚙️') + ' ' + esc(fmtTrigger(a.trigger_type, a.trigger_config)) + '' + + '' + + '' + esc(fmtAction(a.action_type)) + '' + + '
' + + '
' + meta.join(' · ') + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
'; + } + + function render(list) { + var host = document.querySelector('[data-vauto-list]'); if (!host) return; + var sys = (list || []).filter(isVideoSystem); + var sub = document.querySelector('[data-vauto-sub]'); + if (sub) { + var on = sys.filter(function (a) { return a.enabled; }).length; + sub.textContent = sys.length ? (on + ' of ' + sys.length + ' enabled · shared with the music side') + : 'System tasks that keep your library fresh — shared with the music side.'; + } + if (!sys.length) { + host.innerHTML = '
No system automations yet.
'; + return; + } + host.innerHTML = sys.map(cardHTML).join(''); + } + + function load() { getJSON(URL_LIST).then(function (d) { render(Array.isArray(d) ? d : (d && d.automations) || []); }); } + + function start() { + wire(); load(); + if (_timer) clearInterval(_timer); + _timer = setInterval(function () { + if (document.body.getAttribute('data-side') === 'video' && + document.querySelector('[data-video-subpage="video-automations"]:not([hidden])')) load(); + else stop(); + }, 5000); + } + function stop() { if (_timer) { clearInterval(_timer); _timer = null; } } + + function wire() { + if (_wired) return; _wired = true; + var host = document.querySelector('[data-vauto-list]'); if (!host) return; + host.addEventListener('click', function (e) { + var run = e.target.closest('[data-auto-run]'); + if (run) { + run.disabled = true; + post('/api/automations/' + run.getAttribute('data-auto-run') + '/run').then(function (r) { + run.disabled = false; + toast(r && r.success ? 'Automation started' : 'Could not run it', r && r.success ? 'success' : 'error'); + setTimeout(load, 800); + }); + } + }); + host.addEventListener('change', function (e) { + var tg = e.target.closest('[data-auto-toggle]'); + if (tg) post('/api/automations/' + tg.getAttribute('data-auto-toggle') + '/toggle').then(function () { setTimeout(load, 300); }); + }); + } + + document.addEventListener('soulsync:video-page-shown', function (e) { + if (e.detail === 'video-automations') start(); else stop(); + }); +})(); diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index d58c6acd..a81d0b96 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -3349,3 +3349,12 @@ body[data-side="video"] #soulsync-toggle { display: none; } .vdpg-row-retry:hover { transform: translateX(0) scale(1.12) rotate(-40deg); } .vdpg-row-retry:disabled { opacity: 0.5 !important; pointer-events: none; } @media (hover: none) { .vdpg-row-retry { opacity: 1; pointer-events: auto; transform: none; } } + +/* ── Automations page (.vauto-*; reuses the music .automation-* card look) ──── */ +.vauto-wrap { max-width: 1100px; padding: 8px 4px 40px; } +.vauto-head { margin-bottom: 22px; } +.vauto-title { font-size: 34px; font-weight: 900; letter-spacing: -0.03em; margin: 0; color: #fff; } +.vauto-sub { margin: 6px 0 0; font-size: 13.5px; font-weight: 600; color: rgba(255, 255, 255, 0.5); } +.vauto-list { display: flex; flex-direction: column; gap: 9px; } +.vauto-empty { text-align: center; padding: 60px 20px; font-size: 14px; color: rgba(255, 255, 255, 0.4); } +.vauto-err { color: #fca5a5; } diff --git a/webui/static/video/video-side.js b/webui/static/video/video-side.js index 23d8cee2..093951b8 100644 --- a/webui/static/video/video-side.js +++ b/webui/static/video/video-side.js @@ -161,6 +161,7 @@ { id: 'video-wishlist', label: 'Wishlist' }, { id: 'video-downloads', label: 'Downloads' }, { id: 'video-calendar', label: 'Calendar' }, + { id: 'video-automations', label: 'Automations' }, { id: 'video-tools', label: 'Tools' }, { id: 'video-import', label: 'Import', shared: true }, { id: 'video-settings', label: 'Settings' },