|
|
|
@@ -9,9 +9,8 @@ import { showToast, formatUptime, setTabRefreshing } from '../core/ui.js';
|
|
|
|
import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling } from './perf-charts.js';
|
|
|
|
import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling } from './perf-charts.js';
|
|
|
|
import { startAutoRefresh, updateTabBadge } from './tabs.js';
|
|
|
|
import { startAutoRefresh, updateTabBadge } from './tabs.js';
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
getTargetTypeIcon,
|
|
|
|
|
|
|
|
ICON_TARGET, ICON_AUTOMATION, ICON_CLOCK, ICON_WARNING, ICON_OK,
|
|
|
|
ICON_TARGET, ICON_AUTOMATION, ICON_CLOCK, ICON_WARNING, ICON_OK,
|
|
|
|
ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_AUTOSTART, ICON_HELP, ICON_SCENE,
|
|
|
|
ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE, ICON_AUTOSTART, ICON_HELP, ICON_SCENE,
|
|
|
|
} from '../core/icons.js';
|
|
|
|
} from '../core/icons.js';
|
|
|
|
import { loadScenePresets, renderScenePresetsSection } from './scene-presets.js';
|
|
|
|
import { loadScenePresets, renderScenePresetsSection } from './scene-presets.js';
|
|
|
|
|
|
|
|
|
|
|
|
@@ -23,6 +22,7 @@ let _fpsCurrentHistory = {}; // { targetId: number[] } — fps_current
|
|
|
|
let _fpsCharts = {}; // { targetId: Chart }
|
|
|
|
let _fpsCharts = {}; // { targetId: Chart }
|
|
|
|
let _lastRunningIds = []; // sorted target IDs from previous render
|
|
|
|
let _lastRunningIds = []; // sorted target IDs from previous render
|
|
|
|
let _lastAutoStartIds = ''; // comma-joined sorted auto-start IDs
|
|
|
|
let _lastAutoStartIds = ''; // comma-joined sorted auto-start IDs
|
|
|
|
|
|
|
|
let _lastSyncClockIds = ''; // comma-joined sorted sync clock IDs
|
|
|
|
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
|
|
|
|
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
|
|
|
|
let _uptimeTimer = null;
|
|
|
|
let _uptimeTimer = null;
|
|
|
|
let _uptimeElements = {}; // { targetId: HTMLElement } — cached DOM refs
|
|
|
|
let _uptimeElements = {}; // { targetId: HTMLElement } — cached DOM refs
|
|
|
|
@@ -87,14 +87,9 @@ function _destroyFpsCharts() {
|
|
|
|
_fpsCharts = {};
|
|
|
|
_fpsCharts = {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _getAccentColor() {
|
|
|
|
|
|
|
|
return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#4CAF50';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function _createFpsChart(canvasId, actualHistory, currentHistory, fpsTarget) {
|
|
|
|
function _createFpsChart(canvasId, actualHistory, currentHistory, fpsTarget) {
|
|
|
|
const canvas = document.getElementById(canvasId);
|
|
|
|
const canvas = document.getElementById(canvasId);
|
|
|
|
if (!canvas) return null;
|
|
|
|
if (!canvas) return null;
|
|
|
|
const accent = _getAccentColor();
|
|
|
|
|
|
|
|
return new Chart(canvas, {
|
|
|
|
return new Chart(canvas, {
|
|
|
|
type: 'line',
|
|
|
|
type: 'line',
|
|
|
|
data: {
|
|
|
|
data: {
|
|
|
|
@@ -102,8 +97,8 @@ function _createFpsChart(canvasId, actualHistory, currentHistory, fpsTarget) {
|
|
|
|
datasets: [
|
|
|
|
datasets: [
|
|
|
|
{
|
|
|
|
{
|
|
|
|
data: [...actualHistory],
|
|
|
|
data: [...actualHistory],
|
|
|
|
borderColor: accent,
|
|
|
|
borderColor: '#2196F3',
|
|
|
|
backgroundColor: accent + '1f',
|
|
|
|
backgroundColor: 'rgba(33,150,243,0.12)',
|
|
|
|
borderWidth: 1.5,
|
|
|
|
borderWidth: 1.5,
|
|
|
|
tension: 0.3,
|
|
|
|
tension: 0.3,
|
|
|
|
fill: true,
|
|
|
|
fill: true,
|
|
|
|
@@ -111,7 +106,7 @@ function _createFpsChart(canvasId, actualHistory, currentHistory, fpsTarget) {
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
data: [...currentHistory],
|
|
|
|
data: [...currentHistory],
|
|
|
|
borderColor: accent + '80',
|
|
|
|
borderColor: '#4CAF50',
|
|
|
|
borderWidth: 1.5,
|
|
|
|
borderWidth: 1.5,
|
|
|
|
tension: 0.3,
|
|
|
|
tension: 0.3,
|
|
|
|
fill: false,
|
|
|
|
fill: false,
|
|
|
|
@@ -278,6 +273,60 @@ function _updateAutomationsInPlace(automations) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function _updateSyncClocksInPlace(syncClocks) {
|
|
|
|
|
|
|
|
for (const c of syncClocks) {
|
|
|
|
|
|
|
|
const card = document.querySelector(`[data-sync-clock-id="${c.id}"]`);
|
|
|
|
|
|
|
|
if (!card) continue;
|
|
|
|
|
|
|
|
const speedEl = card.querySelector('.dashboard-clock-speed');
|
|
|
|
|
|
|
|
if (speedEl) speedEl.textContent = `${c.speed}x`;
|
|
|
|
|
|
|
|
const badge = card.querySelector('.dashboard-badge-active, .dashboard-badge-stopped');
|
|
|
|
|
|
|
|
if (badge) {
|
|
|
|
|
|
|
|
badge.className = c.is_running ? 'dashboard-badge-active' : 'dashboard-badge-stopped';
|
|
|
|
|
|
|
|
badge.textContent = c.is_running ? t('sync_clock.status.running') : t('sync_clock.status.paused');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const btn = card.querySelector('.dashboard-target-actions .dashboard-action-btn');
|
|
|
|
|
|
|
|
if (btn) {
|
|
|
|
|
|
|
|
btn.className = `dashboard-action-btn ${c.is_running ? 'stop' : 'start'}`;
|
|
|
|
|
|
|
|
btn.setAttribute('onclick', c.is_running ? `dashboardPauseClock('${c.id}')` : `dashboardResumeClock('${c.id}')`);
|
|
|
|
|
|
|
|
btn.innerHTML = c.is_running ? ICON_PAUSE : ICON_START;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderDashboardSyncClock(clock) {
|
|
|
|
|
|
|
|
const statusBadge = clock.is_running
|
|
|
|
|
|
|
|
? `<span class="dashboard-badge-active">${t('sync_clock.status.running')}</span>`
|
|
|
|
|
|
|
|
: `<span class="dashboard-badge-stopped">${t('sync_clock.status.paused')}</span>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const toggleAction = clock.is_running
|
|
|
|
|
|
|
|
? `dashboardPauseClock('${clock.id}')`
|
|
|
|
|
|
|
|
: `dashboardResumeClock('${clock.id}')`;
|
|
|
|
|
|
|
|
const toggleTitle = clock.is_running ? t('sync_clock.action.pause') : t('sync_clock.action.resume');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const subtitle = [
|
|
|
|
|
|
|
|
`<span class="dashboard-clock-speed">${clock.speed}x</span>`,
|
|
|
|
|
|
|
|
clock.description ? escapeHtml(clock.description) : '',
|
|
|
|
|
|
|
|
].filter(Boolean).join(' · ');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-sync-clock-id="${clock.id}" onclick="if(!event.target.closest('button')){navigateToCard('streams','sync','sync-clocks','data-id','${clock.id}')}">
|
|
|
|
|
|
|
|
<div class="dashboard-target-info">
|
|
|
|
|
|
|
|
<span class="dashboard-target-icon">${ICON_CLOCK}</span>
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
|
|
<div class="dashboard-target-name">${escapeHtml(clock.name)} ${statusBadge}</div>
|
|
|
|
|
|
|
|
${subtitle ? `<div class="dashboard-target-subtitle">${subtitle}</div>` : ''}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="dashboard-target-actions">
|
|
|
|
|
|
|
|
<button class="dashboard-action-btn ${clock.is_running ? 'stop' : 'start'}" onclick="${toggleAction}" title="${toggleTitle}">
|
|
|
|
|
|
|
|
${clock.is_running ? ICON_PAUSE : ICON_START}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button class="dashboard-action-btn" onclick="dashboardResetClock('${clock.id}')" title="${t('sync_clock.action.reset')}">
|
|
|
|
|
|
|
|
${ICON_CLOCK}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _renderPollIntervalSelect() {
|
|
|
|
function _renderPollIntervalSelect() {
|
|
|
|
const sec = Math.round(dashboardPollInterval / 1000);
|
|
|
|
const sec = Math.round(dashboardPollInterval / 1000);
|
|
|
|
return `<span class="dashboard-poll-wrap"><input type="range" class="dashboard-poll-slider" min="1" max="10" value="${sec}" oninput="changeDashboardPollInterval(this.value)" title="${t('dashboard.poll_interval')}"><span class="dashboard-poll-value">${sec}s</span></span>`;
|
|
|
|
return `<span class="dashboard-poll-wrap"><input type="range" class="dashboard-poll-slider" min="1" max="10" value="${sec}" oninput="changeDashboardPollInterval(this.value)" title="${t('dashboard.poll_interval')}"><span class="dashboard-poll-value">${sec}s</span></span>`;
|
|
|
|
@@ -368,7 +417,7 @@ export async function loadDashboard(forceFullRender = false) {
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Fire all requests in a single batch to avoid sequential RTTs
|
|
|
|
// Fire all requests in a single batch to avoid sequential RTTs
|
|
|
|
const [targetsResp, automationsResp, devicesResp, cssResp, batchStatesResp, batchMetricsResp, scenePresets] = await Promise.all([
|
|
|
|
const [targetsResp, automationsResp, devicesResp, cssResp, batchStatesResp, batchMetricsResp, scenePresets, syncClocksResp] = await Promise.all([
|
|
|
|
fetchWithAuth('/picture-targets'),
|
|
|
|
fetchWithAuth('/picture-targets'),
|
|
|
|
fetchWithAuth('/automations').catch(() => null),
|
|
|
|
fetchWithAuth('/automations').catch(() => null),
|
|
|
|
fetchWithAuth('/devices').catch(() => null),
|
|
|
|
fetchWithAuth('/devices').catch(() => null),
|
|
|
|
@@ -376,6 +425,7 @@ export async function loadDashboard(forceFullRender = false) {
|
|
|
|
fetchWithAuth('/picture-targets/batch/states').catch(() => null),
|
|
|
|
fetchWithAuth('/picture-targets/batch/states').catch(() => null),
|
|
|
|
fetchWithAuth('/picture-targets/batch/metrics').catch(() => null),
|
|
|
|
fetchWithAuth('/picture-targets/batch/metrics').catch(() => null),
|
|
|
|
loadScenePresets(),
|
|
|
|
loadScenePresets(),
|
|
|
|
|
|
|
|
fetchWithAuth('/sync-clocks').catch(() => null),
|
|
|
|
]);
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const targetsData = await targetsResp.json();
|
|
|
|
const targetsData = await targetsResp.json();
|
|
|
|
@@ -388,6 +438,8 @@ export async function loadDashboard(forceFullRender = false) {
|
|
|
|
const cssData = cssResp && cssResp.ok ? await cssResp.json() : { sources: [] };
|
|
|
|
const cssData = cssResp && cssResp.ok ? await cssResp.json() : { sources: [] };
|
|
|
|
const cssSourceMap = {};
|
|
|
|
const cssSourceMap = {};
|
|
|
|
for (const s of (cssData.sources || [])) { cssSourceMap[s.id] = s; }
|
|
|
|
for (const s of (cssData.sources || [])) { cssSourceMap[s.id] = s; }
|
|
|
|
|
|
|
|
const syncClocksData = syncClocksResp && syncClocksResp.ok ? await syncClocksResp.json() : { clocks: [] };
|
|
|
|
|
|
|
|
const syncClocks = syncClocksData.clocks || [];
|
|
|
|
|
|
|
|
|
|
|
|
const allStates = batchStatesResp && batchStatesResp.ok ? (await batchStatesResp.json()).states : {};
|
|
|
|
const allStates = batchStatesResp && batchStatesResp.ok ? (await batchStatesResp.json()).states : {};
|
|
|
|
const allMetrics = batchMetricsResp && batchMetricsResp.ok ? (await batchMetricsResp.json()).metrics : {};
|
|
|
|
const allMetrics = batchMetricsResp && batchMetricsResp.ok ? (await batchMetricsResp.json()).metrics : {};
|
|
|
|
@@ -397,7 +449,7 @@ export async function loadDashboard(forceFullRender = false) {
|
|
|
|
let runningIds = [];
|
|
|
|
let runningIds = [];
|
|
|
|
let newAutoStartIds = '';
|
|
|
|
let newAutoStartIds = '';
|
|
|
|
|
|
|
|
|
|
|
|
if (targets.length === 0 && automations.length === 0 && scenePresets.length === 0) {
|
|
|
|
if (targets.length === 0 && automations.length === 0 && scenePresets.length === 0 && syncClocks.length === 0) {
|
|
|
|
dynamicHtml = `<div class="dashboard-no-targets">${t('dashboard.no_targets')}</div>`;
|
|
|
|
dynamicHtml = `<div class="dashboard-no-targets">${t('dashboard.no_targets')}</div>`;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
const enriched = targets.map(target => ({
|
|
|
|
const enriched = targets.map(target => ({
|
|
|
|
@@ -414,10 +466,12 @@ export async function loadDashboard(forceFullRender = false) {
|
|
|
|
const newRunningIds = running.map(t => t.id).sort().join(',');
|
|
|
|
const newRunningIds = running.map(t => t.id).sort().join(',');
|
|
|
|
const prevRunningIds = [..._lastRunningIds].sort().join(',');
|
|
|
|
const prevRunningIds = [..._lastRunningIds].sort().join(',');
|
|
|
|
newAutoStartIds = enriched.filter(t => t.auto_start).map(t => t.id).sort().join(',');
|
|
|
|
newAutoStartIds = enriched.filter(t => t.auto_start).map(t => t.id).sort().join(',');
|
|
|
|
|
|
|
|
const newSyncClockIds = syncClocks.map(c => `${c.id}:${c.is_running}`).sort().join(',');
|
|
|
|
const hasExistingDom = !!container.querySelector('.dashboard-perf-persistent');
|
|
|
|
const hasExistingDom = !!container.querySelector('.dashboard-perf-persistent');
|
|
|
|
const structureUnchanged = hasExistingDom && newRunningIds === prevRunningIds && newAutoStartIds === _lastAutoStartIds;
|
|
|
|
const structureUnchanged = hasExistingDom && newRunningIds === prevRunningIds && newAutoStartIds === _lastAutoStartIds && newSyncClockIds === _lastSyncClockIds;
|
|
|
|
if (structureUnchanged && !forceFullRender && running.length > 0) {
|
|
|
|
if (structureUnchanged && !forceFullRender && running.length > 0) {
|
|
|
|
_updateRunningMetrics(running);
|
|
|
|
_updateRunningMetrics(running);
|
|
|
|
|
|
|
|
_updateSyncClocksInPlace(syncClocks);
|
|
|
|
_cacheUptimeElements();
|
|
|
|
_cacheUptimeElements();
|
|
|
|
_startUptimeTimer();
|
|
|
|
_startUptimeTimer();
|
|
|
|
startPerfPolling();
|
|
|
|
startPerfPolling();
|
|
|
|
@@ -427,6 +481,7 @@ export async function loadDashboard(forceFullRender = false) {
|
|
|
|
if (structureUnchanged && forceFullRender) {
|
|
|
|
if (structureUnchanged && forceFullRender) {
|
|
|
|
if (running.length > 0) _updateRunningMetrics(running);
|
|
|
|
if (running.length > 0) _updateRunningMetrics(running);
|
|
|
|
_updateAutomationsInPlace(automations);
|
|
|
|
_updateAutomationsInPlace(automations);
|
|
|
|
|
|
|
|
_updateSyncClocksInPlace(syncClocks);
|
|
|
|
_cacheUptimeElements();
|
|
|
|
_cacheUptimeElements();
|
|
|
|
_startUptimeTimer();
|
|
|
|
_startUptimeTimer();
|
|
|
|
startPerfPolling();
|
|
|
|
startPerfPolling();
|
|
|
|
@@ -504,6 +559,16 @@ export async function loadDashboard(forceFullRender = false) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Sync Clocks section
|
|
|
|
|
|
|
|
if (syncClocks.length > 0) {
|
|
|
|
|
|
|
|
const clockCards = syncClocks.map(c => renderDashboardSyncClock(c)).join('');
|
|
|
|
|
|
|
|
const clockGrid = `<div class="dashboard-autostart-grid">${clockCards}</div>`;
|
|
|
|
|
|
|
|
dynamicHtml += `<div class="dashboard-section">
|
|
|
|
|
|
|
|
${_sectionHeader('sync-clocks', t('dashboard.section.sync_clocks'), syncClocks.length)}
|
|
|
|
|
|
|
|
${_sectionContent('sync-clocks', clockGrid)}
|
|
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (targets.length > 0) {
|
|
|
|
if (targets.length > 0) {
|
|
|
|
let targetsInner = '';
|
|
|
|
let targetsInner = '';
|
|
|
|
|
|
|
|
|
|
|
|
@@ -553,6 +618,7 @@ export async function loadDashboard(forceFullRender = false) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_lastRunningIds = runningIds;
|
|
|
|
_lastRunningIds = runningIds;
|
|
|
|
_lastAutoStartIds = newAutoStartIds;
|
|
|
|
_lastAutoStartIds = newAutoStartIds;
|
|
|
|
|
|
|
|
_lastSyncClockIds = syncClocks.map(c => `${c.id}:${c.is_running}`).sort().join(',');
|
|
|
|
_cacheUptimeElements();
|
|
|
|
_cacheUptimeElements();
|
|
|
|
await _initFpsCharts(runningIds);
|
|
|
|
await _initFpsCharts(runningIds);
|
|
|
|
_startUptimeTimer();
|
|
|
|
_startUptimeTimer();
|
|
|
|
@@ -800,6 +866,42 @@ export async function dashboardStopAll() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function dashboardPauseClock(clockId) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const resp = await fetchWithAuth(`/sync-clocks/${clockId}/pause`, { method: 'POST' });
|
|
|
|
|
|
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
|
|
|
|
|
|
showToast(t('sync_clock.paused'), 'success');
|
|
|
|
|
|
|
|
loadDashboard(true);
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
if (e.isAuth) return;
|
|
|
|
|
|
|
|
showToast(e.message, 'error');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function dashboardResumeClock(clockId) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const resp = await fetchWithAuth(`/sync-clocks/${clockId}/resume`, { method: 'POST' });
|
|
|
|
|
|
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
|
|
|
|
|
|
showToast(t('sync_clock.resumed'), 'success');
|
|
|
|
|
|
|
|
loadDashboard(true);
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
if (e.isAuth) return;
|
|
|
|
|
|
|
|
showToast(e.message, 'error');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function dashboardResetClock(clockId) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const resp = await fetchWithAuth(`/sync-clocks/${clockId}/reset`, { method: 'POST' });
|
|
|
|
|
|
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
|
|
|
|
|
|
showToast(t('sync_clock.reset_done'), 'success');
|
|
|
|
|
|
|
|
loadDashboard(true);
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
if (e.isAuth) return;
|
|
|
|
|
|
|
|
showToast(e.message, 'error');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function stopUptimeTimer() {
|
|
|
|
export function stopUptimeTimer() {
|
|
|
|
_stopUptimeTimer();
|
|
|
|
_stopUptimeTimer();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -828,18 +930,6 @@ document.addEventListener('languageChanged', () => {
|
|
|
|
loadDashboard();
|
|
|
|
loadDashboard();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Update FPS chart colors when accent color changes
|
|
|
|
|
|
|
|
document.addEventListener('accentColorChanged', () => {
|
|
|
|
|
|
|
|
const accent = _getAccentColor();
|
|
|
|
|
|
|
|
for (const chart of Object.values(_fpsCharts)) {
|
|
|
|
|
|
|
|
if (!chart) continue;
|
|
|
|
|
|
|
|
chart.data.datasets[0].borderColor = accent;
|
|
|
|
|
|
|
|
chart.data.datasets[0].backgroundColor = accent + '1f';
|
|
|
|
|
|
|
|
chart.data.datasets[1].borderColor = accent + '80';
|
|
|
|
|
|
|
|
chart.update();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Pause uptime timer when browser tab is hidden, resume when visible
|
|
|
|
// Pause uptime timer when browser tab is hidden, resume when visible
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
|
|
if (document.hidden) {
|
|
|
|
if (document.hidden) {
|
|
|
|
|