The video dashboard's Memory Usage / System Uptime cards were stuck at their markup defaults — flatten() only fed library + download figures from /api/video/dashboard, so data-video-stat='memory'/'uptime' never updated. Now it pulls those two from the SAME /api/system/stats the music dashboard uses (one machine → identical figures), with an immediate fill on page-shown and a 10s poll for live parity with music's push loop. Reached over HTTP (not music's socket), so the video isolation contract holds; the poll cheaply no-ops whenever the dashboard isn't the visible page.
202 lines
8.4 KiB
JavaScript
202 lines
8.4 KiB
JavaScript
/*
|
|
* SoulSync — Video dashboard data layer.
|
|
*
|
|
* ISOLATION CONTRACT: like video-side.js this is a self-contained IIFE (no
|
|
* globals, no inline handlers, lives under static/video/ which the script-split
|
|
* integrity scan does not touch). It NEVER references music code, and music
|
|
* never references it.
|
|
*
|
|
* It owns only the *data* of the video dashboard — the markup lives in
|
|
* index.html and reuses music's .dash-card CSS for an identical look. It learns
|
|
* when the dashboard becomes visible by listening for the
|
|
* 'soulsync:video-page-shown' event that video-side.js dispatches (so the two
|
|
* modules stay decoupled — no direct calls between them).
|
|
*
|
|
* TODO(video.db): STUB_STATS below is a placeholder. Once the video database +
|
|
* its /api/video/dashboard endpoint exist, replace loadStats() with a fetch and
|
|
* feed the same shape through applyStats(). Nothing else here needs to change.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
var DASHBOARD_ID = 'video-dashboard';
|
|
|
|
var DASHBOARD_URL = '/api/video/dashboard';
|
|
|
|
// System stats (uptime + memory) come from the SAME endpoint the music
|
|
// dashboard uses — it's one machine, so these figures are identical on both
|
|
// sides. Polled on the dashboard's 10s cadence for parity with music's push
|
|
// loop. (Reached over HTTP, not music's socket, so the isolation contract
|
|
// holds — no music-code reference.)
|
|
var SYSTEM_STATS_URL = '/api/system/stats';
|
|
var systemPollTimer = null;
|
|
|
|
// Fallback only — shown if the /api/video/dashboard call fails. (uptime/memory
|
|
// are NOT here — they come from the shared /api/system/stats via loadSystemStats.)
|
|
var FALLBACK_STATS = {
|
|
'active-downloads': '0',
|
|
'finished-downloads': '0',
|
|
'download-speed': '0 KB/s',
|
|
'disk-usage': '--',
|
|
'movies': '0',
|
|
'shows': '0',
|
|
'episodes': '0',
|
|
'library-size': '--'
|
|
};
|
|
|
|
function formatBytes(n) {
|
|
n = Number(n) || 0;
|
|
if (n <= 0) return '0 B';
|
|
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
var i = Math.floor(Math.log(n) / Math.log(1024));
|
|
if (i >= units.length) i = units.length - 1;
|
|
return (n / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
|
|
}
|
|
|
|
function formatSpeed(bps) {
|
|
return formatBytes(bps) + '/s';
|
|
}
|
|
|
|
// Map the API payload onto the flat data-video-stat keys in the markup.
|
|
function flatten(d) {
|
|
var lib = d.library || {}, dl = d.downloads || {};
|
|
return {
|
|
'active-downloads': String(dl.active != null ? dl.active : 0),
|
|
'finished-downloads': String(dl.finished != null ? dl.finished : 0),
|
|
'download-speed': formatSpeed(dl.speed_bps),
|
|
'disk-usage': formatBytes(lib.size_bytes),
|
|
'movies': String(lib.movies != null ? lib.movies : 0),
|
|
'shows': String(lib.shows != null ? lib.shows : 0),
|
|
'episodes': String(lib.episodes != null ? lib.episodes : 0),
|
|
'library-size': formatBytes(lib.size_bytes)
|
|
};
|
|
}
|
|
|
|
function applyStats(stats) {
|
|
var nodes = document.querySelectorAll('[data-video-stat]');
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
var key = nodes[i].getAttribute('data-video-stat');
|
|
if (Object.prototype.hasOwnProperty.call(stats, key)) {
|
|
nodes[i].textContent = stats[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyBadges(d) {
|
|
var nodes = document.querySelectorAll('[data-video-badge]');
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
var key = nodes[i].getAttribute('data-video-badge');
|
|
if (d[key] != null) nodes[i].textContent = String(d[key]);
|
|
}
|
|
}
|
|
|
|
function loadStats() {
|
|
fetch(DASHBOARD_URL, { headers: { 'Accept': 'application/json' } })
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (d) {
|
|
if (d && !d.error) {
|
|
applyStats(flatten(d));
|
|
applyBadges(d);
|
|
} else {
|
|
applyStats(FALLBACK_STATS);
|
|
}
|
|
})
|
|
.catch(function () { applyStats(FALLBACK_STATS); });
|
|
}
|
|
|
|
// True when the video dashboard subpage is the one currently shown (subpages
|
|
// toggle via the `hidden` attribute in video-side.js).
|
|
function dashboardVisible() {
|
|
var el = document.querySelector('.video-subpage[data-video-subpage="' + DASHBOARD_ID + '"]');
|
|
return !!el && !el.hidden;
|
|
}
|
|
|
|
// Pull the shared system stats and reflect uptime + memory on the cards. The
|
|
// rest of the dashboard's figures (video downloads, library) come from
|
|
// /api/video/dashboard via loadStats(); only the machine-level numbers are
|
|
// shared. applyStats only touches the keys we pass, so nothing else is clobbered.
|
|
function loadSystemStats() {
|
|
fetch(SYSTEM_STATS_URL, { headers: { 'Accept': 'application/json' } })
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (d) {
|
|
if (!d) return;
|
|
applyStats({
|
|
'uptime': d.uptime != null ? String(d.uptime) : '--',
|
|
'memory': d.memory_usage != null ? String(d.memory_usage) : '--'
|
|
});
|
|
})
|
|
.catch(function () { /* keep last-known values on a transient failure */ });
|
|
}
|
|
|
|
// Keep the system figures live while the dashboard is open — one 10s timer
|
|
// that no-ops cheaply whenever the dashboard isn't the visible page.
|
|
function startSystemStatsPolling() {
|
|
if (systemPollTimer) return;
|
|
systemPollTimer = setInterval(function () {
|
|
if (dashboardVisible()) loadSystemStats();
|
|
}, 10000);
|
|
}
|
|
|
|
// Service "Test" buttons are inert until the video services exist. Mark them
|
|
// honestly rather than firing a no-op that looks broken.
|
|
var testWired = false;
|
|
function wireTestButtons() {
|
|
if (testWired) return;
|
|
testWired = true;
|
|
var buttons = document.querySelectorAll('[data-video-test]');
|
|
for (var i = 0; i < buttons.length; i++) {
|
|
(function (btn) {
|
|
btn.disabled = true;
|
|
btn.title = 'Coming soon — video services not configured yet';
|
|
})(buttons[i]);
|
|
}
|
|
}
|
|
|
|
function onPageShown(e) {
|
|
if (!e || e.detail !== DASHBOARD_ID) return;
|
|
wireTestButtons();
|
|
loadStats();
|
|
loadSystemStats(); // immediate fill (memory/uptime)
|
|
startSystemStatsPolling(); // then keep it live
|
|
}
|
|
|
|
// ── Library card: live scan progress (parity with the music dashboard) ──
|
|
// The scan buttons (data-video-scan-mode) are wired by video-scan.js; here
|
|
// we reflect progress on the card and hydrate if a scan is already running
|
|
// (video-scan.js re-emits the progress event on load).
|
|
function dashButtons() {
|
|
return document.querySelectorAll(
|
|
'.video-subpage[data-video-subpage="video-dashboard"] [data-video-scan-mode]');
|
|
}
|
|
|
|
function onDashScanProgress(e) {
|
|
var s = e.detail || {};
|
|
if (s.state !== 'scanning') return;
|
|
var prog = document.querySelector('[data-video-dash-progress]');
|
|
if (prog) prog.classList.remove('hidden');
|
|
var phase = (s.phase || 'scanning');
|
|
var phaseEl = document.querySelector('[data-video-dash-phase]');
|
|
if (phaseEl) phaseEl.textContent = phase.charAt(0).toUpperCase() + phase.slice(1);
|
|
var bar = document.querySelector('[data-video-dash-bar]');
|
|
if (bar) bar.style.width = (s.percent != null ? s.percent : 100) + '%';
|
|
var detail = document.querySelector('[data-video-dash-detail]');
|
|
if (detail) {
|
|
detail.textContent = (s.movies || 0) + ' movies, ' + (s.shows || 0) + ' shows'
|
|
+ (s.percent != null ? ' · ' + s.percent + '%' : '');
|
|
}
|
|
var btns = dashButtons();
|
|
for (var i = 0; i < btns.length; i++) btns[i].disabled = true;
|
|
}
|
|
|
|
function onDashScanDone() {
|
|
var prog = document.querySelector('[data-video-dash-progress]');
|
|
if (prog) prog.classList.add('hidden');
|
|
var btns = dashButtons();
|
|
for (var i = 0; i < btns.length; i++) btns[i].disabled = false;
|
|
loadStats();
|
|
}
|
|
|
|
document.addEventListener('soulsync:video-page-shown', onPageShown);
|
|
document.addEventListener('soulsync:video-scan-progress', onDashScanProgress);
|
|
document.addEventListener('soulsync:video-scan-done', onDashScanDone);
|
|
})();
|