video: Automations page (shared system automations, video view)
Adds an Automations nav entry + page to the video side. The automation engine is app-wide, so this surfaces the SAME system automations the music side runs — filtered to is_system, EXCLUDING Refresh Beatport Cache and any user/playlist-pipeline ones. Reads the shared /api/automations (no music imports), reuses the music .automation-* card look, and supports run-now + enable toggle (system automations aren't editable). Polls every 5s while the page is open. Frontend-only; balance clean. Next: the library-refresh/scan automation wiring (Boulder has questions on how server scanning works first).
This commit is contained in:
parent
937f3094c6
commit
c78b90826a
4 changed files with 186 additions and 0 deletions
|
|
@ -359,6 +359,10 @@
|
|||
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></span>
|
||||
<span class="nav-text">Calendar</span>
|
||||
</a>
|
||||
<a class="nav-button" data-video-page="video-automations" href="/video-automations">
|
||||
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
|
||||
<span class="nav-text">Automations</span>
|
||||
</a>
|
||||
<a class="nav-button" data-video-page="video-tools" href="/video-tools">
|
||||
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18v3h3l6.3-6.3a4 4 0 0 0 5.4-5.4l-2.6 2.6-2.1-.5-.5-2.1 2.7-2.4z"/></svg></span>
|
||||
<span class="nav-text">Tools</span>
|
||||
|
|
@ -1305,6 +1309,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Automations page — the SAME app-wide system automations as music
|
||||
(minus Refresh Beatport Cache + user/playlist ones). Reuses the
|
||||
music .automation-* card look; built by video/video-automations.js. -->
|
||||
<section class="video-subpage" data-video-subpage="video-automations" hidden>
|
||||
<div class="vauto-wrap">
|
||||
<div class="vauto-head">
|
||||
<h1 class="vauto-title">Automations</h1>
|
||||
<p class="vauto-sub" data-vauto-sub>System tasks that keep your library fresh — shared with the music side.</p>
|
||||
</div>
|
||||
<div class="vauto-list" data-vauto-list></div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- TV-show detail (drill-in from a show card; not a nav page).
|
||||
Isolated: built by video/video-detail.js, styled by .vd-* in
|
||||
video-side.css. Inspired by the music artist-detail vibe. -->
|
||||
|
|
@ -10256,6 +10272,8 @@
|
|||
<script src="{{ url_for('static', filename='video/video-download-view.js', v=static_v) }}"></script>
|
||||
<!-- Downloads page (isolated): live status of every grab from the video side -->
|
||||
<script src="{{ url_for('static', filename='video/video-downloads-page.js', v=static_v) }}"></script>
|
||||
<!-- Automations page (shared system automations, video view) -->
|
||||
<script src="{{ url_for('static', filename='video/video-automations.js', v=static_v) }}"></script>
|
||||
<!-- Video library page (isolated; lists movies/shows + scan trigger) -->
|
||||
<script src="{{ url_for('static', filename='video/video-library.js', v=static_v) }}"></script>
|
||||
<!-- Video detail page (isolated; show drill-in: hero + season/episode tree) -->
|
||||
|
|
|
|||
158
webui/static/video/video-automations.js
Normal file
158
webui/static/video/video-automations.js
Normal file
|
|
@ -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, '>').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('<span class="vauto-err">Error: ' + esc(a.last_error) + '</span>');
|
||||
return '<div class="automation-card system' + (a.enabled ? '' : ' disabled') + '" data-auto-id="' + a.id + '">' +
|
||||
'<div class="automation-status ' + (a.enabled ? 'enabled' : 'disabled') + '"></div>' +
|
||||
'<div class="automation-info">' +
|
||||
'<div class="automation-name">' + esc(a.name) + '</div>' +
|
||||
'<div class="automation-flow">' +
|
||||
'<span class="flow-trigger">' + (TICONS[a.trigger_type] || '⚙️') + ' ' + esc(fmtTrigger(a.trigger_type, a.trigger_config)) + '</span>' +
|
||||
'<span class="flow-arrow">→</span>' +
|
||||
'<span class="flow-action">' + esc(fmtAction(a.action_type)) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="automation-meta">' + meta.join(' · ') + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="automation-actions">' +
|
||||
'<button class="automation-run-btn" type="button" data-auto-run="' + a.id + '" title="Run now">▶</button>' +
|
||||
'<label class="automation-toggle"><input type="checkbox" data-auto-toggle="' + a.id + '"' + (a.enabled ? ' checked' : '') + '>' +
|
||||
'<span class="toggle-slider"></span></label>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
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 = '<div class="vauto-empty">No system automations yet.</div>';
|
||||
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();
|
||||
});
|
||||
})();
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
Loading…
Reference in a new issue