v0.11.4-devices
This commit is contained in:
+140
-1
@@ -35,7 +35,146 @@ function showHistoryTab(tab, { push = true, load = true } = {}) {
|
||||
if (load) loadHistory();
|
||||
}
|
||||
|
||||
function confirmManualCommandForDisabledZone(id) {
|
||||
const zone = disabledZoneForDevice(id);
|
||||
if (!zone) return true;
|
||||
return confirm(tr('devices.manualDisabledZoneConfirm', { zone: zone.name }));
|
||||
}
|
||||
|
||||
function pingSamples(id) {
|
||||
if (!app.pingMonitor.samples[id]) app.pingMonitor.samples[id] = [];
|
||||
return app.pingMonitor.samples[id];
|
||||
}
|
||||
|
||||
function addPingSample(id, value, error = '') {
|
||||
const samples = pingSamples(id);
|
||||
samples.push({ at: Date.now(), value: Number.isFinite(Number(value)) ? Math.max(0, Math.round(Number(value))) : null, error: String(error || '') });
|
||||
if (samples.length > 36) samples.splice(0, samples.length - 36);
|
||||
}
|
||||
|
||||
function pingSparkline(samples) {
|
||||
const width = 360, height = 92, padX = 8, padY = 9;
|
||||
const valid = samples.map((sample, index) => ({ index, value: sample.value == null ? NaN : Number(sample.value) })).filter(item => Number.isFinite(item.value));
|
||||
if (!valid.length) return `<div class="ping-empty">${esc(tr('devices.pingNoSamples'))}</div>`;
|
||||
const values = valid.map(item => item.value);
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const ceiling = Math.max(max, 10);
|
||||
const floor = Math.min(min, 0);
|
||||
const range = Math.max(1, ceiling - floor);
|
||||
const lastIndex = Math.max(1, samples.length - 1);
|
||||
const points = valid.map(item => {
|
||||
const x = padX + (item.index / lastIndex) * (width - padX * 2);
|
||||
const y = height - padY - ((item.value - floor) / range) * (height - padY * 2);
|
||||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
}).join(' ');
|
||||
const guide = [0.25, 0.5, 0.75].map(ratio => `<line x1="${padX}" y1="${(height * ratio).toFixed(1)}" x2="${width - padX}" y2="${(height * ratio).toFixed(1)}"></line>`).join('');
|
||||
return `<svg class="ping-sparkline" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" role="img" aria-label="${esc(tr('devices.pingLive'))}"><g class="ping-grid-lines">${guide}</g><polyline points="${points}"></polyline></svg>`;
|
||||
}
|
||||
|
||||
function pingStats(samples) {
|
||||
const values = samples.map(sample => sample.value == null ? NaN : Number(sample.value)).filter(Number.isFinite);
|
||||
if (!values.length) return { current: null, average: null, min: null, max: null };
|
||||
const current = [...samples].reverse().find(sample => sample.value != null && Number.isFinite(Number(sample.value)))?.value ?? null;
|
||||
return {
|
||||
current,
|
||||
average: Math.round(values.reduce((sum, value) => sum + value, 0) / values.length),
|
||||
min: Math.min(...values),
|
||||
max: Math.max(...values),
|
||||
};
|
||||
}
|
||||
|
||||
function pingValue(value) {
|
||||
return Number.isFinite(Number(value)) ? `${Math.round(Number(value))} ms` : '—';
|
||||
}
|
||||
|
||||
function renderPingDialog() {
|
||||
const dialog = $('#pingDialog');
|
||||
const select = $('#pingDeviceSelect');
|
||||
const all = $('#pingAllDevices');
|
||||
const toggle = $('#pingToggleButton');
|
||||
const grid = $('#pingLiveGrid');
|
||||
if (!dialog || !select || !all || !toggle || !grid) return;
|
||||
if (!app.pingMonitor.targetId || !app.devices.some(device => device.id === app.pingMonitor.targetId)) app.pingMonitor.targetId = app.devices[0]?.id || '';
|
||||
select.innerHTML = app.devices.map(device => `<option value="${esc(device.id)}" ${device.id === app.pingMonitor.targetId ? 'selected' : ''}>${esc(device.name)}</option>`).join('');
|
||||
select.disabled = app.pingMonitor.all;
|
||||
all.checked = app.pingMonitor.all;
|
||||
toggle.textContent = tr(app.pingMonitor.running ? 'devices.pingStop' : 'devices.pingStart');
|
||||
toggle.classList.toggle('primary', app.pingMonitor.running);
|
||||
toggle.classList.toggle('secondary', !app.pingMonitor.running);
|
||||
const devices = app.pingMonitor.all ? app.devices : app.devices.filter(device => device.id === app.pingMonitor.targetId);
|
||||
grid.innerHTML = devices.length ? devices.map(device => {
|
||||
const samples = pingSamples(device.id);
|
||||
const stats = pingStats(samples);
|
||||
const last = samples.length ? samples[samples.length - 1] : null;
|
||||
const state = last?.error ? `<span class="ping-state error">${esc(tr('devices.pingFailed'))}</span>` : `<span class="ping-state ${device.online ? 'online' : ''}">${esc(tr(device.online ? 'status.online' : 'status.offline'))}</span>`;
|
||||
return `<article class="ping-live-card">
|
||||
<div class="ping-live-head"><div><strong>${esc(device.name)}</strong><small>${esc(device.ip)}</small></div>${state}</div>
|
||||
${pingSparkline(samples)}
|
||||
<div class="ping-stat-grid">
|
||||
<div><span>${esc(tr('devices.pingCurrent'))}</span><strong>${esc(pingValue(stats.current))}</strong></div>
|
||||
<div><span>${esc(tr('devices.pingAverage'))}</span><strong>${esc(pingValue(stats.average))}</strong></div>
|
||||
<div><span>${esc(tr('devices.pingMin'))}</span><strong>${esc(pingValue(stats.min))}</strong></div>
|
||||
<div><span>${esc(tr('devices.pingMax'))}</span><strong>${esc(pingValue(stats.max))}</strong></div>
|
||||
</div>
|
||||
<small class="ping-sample-count">${esc(tr('devices.pingSamples'))}: ${samples.length}${last?.error ? ` · ${esc(last.error)}` : ''}</small>
|
||||
</article>`;
|
||||
}).join('') : `<div class="empty"><strong>${esc(tr('devices.emptyTitle'))}</strong>${esc(tr('devices.emptyText'))}</div>`;
|
||||
}
|
||||
|
||||
function schedulePingCycle(delay = 1800) {
|
||||
clearTimeout(app.pingMonitor.timer);
|
||||
app.pingMonitor.timer = null;
|
||||
if (!app.pingMonitor.running || !$('#pingDialog')?.open) return;
|
||||
app.pingMonitor.timer = setTimeout(runPingCycle, delay);
|
||||
}
|
||||
|
||||
async function runPingCycle() {
|
||||
if (!app.pingMonitor.running || !$('#pingDialog')?.open || app.pingMonitor.inFlight) return;
|
||||
const targets = app.pingMonitor.all ? [...app.devices] : app.devices.filter(device => device.id === app.pingMonitor.targetId);
|
||||
if (!targets.length) { renderPingDialog(); schedulePingCycle(); return; }
|
||||
app.pingMonitor.inFlight = true;
|
||||
await Promise.allSettled(targets.map(async device => {
|
||||
try {
|
||||
const updated = await api(`/api/devices/${encodeURIComponent(device.id)}/poll`, { method: 'POST' });
|
||||
updateDevice(updated);
|
||||
addPingSample(device.id, updated.response_time_ms);
|
||||
} catch (error) {
|
||||
addPingSample(device.id, null, error.message || String(error));
|
||||
}
|
||||
}));
|
||||
app.pingMonitor.inFlight = false;
|
||||
renderDevices();
|
||||
renderPingDialog();
|
||||
schedulePingCycle();
|
||||
}
|
||||
|
||||
function startPingMonitor() {
|
||||
if (!app.devices.length) return;
|
||||
app.pingMonitor.running = true;
|
||||
renderPingDialog();
|
||||
clearTimeout(app.pingMonitor.timer);
|
||||
app.pingMonitor.timer = null;
|
||||
runPingCycle();
|
||||
}
|
||||
|
||||
function stopPingMonitor() {
|
||||
app.pingMonitor.running = false;
|
||||
clearTimeout(app.pingMonitor.timer);
|
||||
app.pingMonitor.timer = null;
|
||||
renderPingDialog();
|
||||
}
|
||||
|
||||
function openDevicePing(id) {
|
||||
app.pingMonitor.targetId = id || app.devices[0]?.id || '';
|
||||
app.pingMonitor.all = false;
|
||||
openDialog('pingDialog');
|
||||
renderPingDialog();
|
||||
startPingMonitor();
|
||||
}
|
||||
|
||||
async function sendDeviceCommand(id, commandOrFactory) {
|
||||
if (!confirmManualCommandForDisabledZone(id)) return;
|
||||
const previous = app.deviceControlQueue[id] || Promise.resolve();
|
||||
const request = previous.catch(() => { }).then(() => {
|
||||
const current = app.devices.find(device => device.id === id);
|
||||
@@ -204,7 +343,7 @@ function showDiscoveryNames(ids) {
|
||||
function populateDeviceRename(id) {
|
||||
const device = app.devices.find(item => item.id === id); if (!device) return;
|
||||
const form = $('#renameDeviceForm'); form.reset();
|
||||
form.id.value = device.id; form.name.value = device.name; form.protocol_version.value = String(device.protocol_version ?? 0);
|
||||
form.id.value = device.id; form.name.value = device.name; form.ip.value = device.ip; form.port.value = String(device.port ?? 7000); form.protocol_version.value = String(device.protocol_version ?? 0);
|
||||
openDialog('renameDeviceDialog');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user