Add adaptive FPS and honest device reachability during streaming

DDP uses fire-and-forget UDP, so when a WiFi device becomes overwhelmed
by sustained traffic, sends appear successful while the device is
actually unreachable. This adds:

- HTTP liveness probe (GET /json/info, 2s timeout) every 10s during
  streaming, exposed as device_streaming_reachable in target state
- Adaptive FPS (opt-in): exponential backoff when device is unreachable,
  gradual recovery when it stabilizes — finds sustainable send rate
- Honest health checks: removed the lie that forced device_online=true
  during streaming; now runs actual health checks regardless
- Target editor toggle, FPS display shows effective rate when throttled,
  health dot reflects streaming reachability, red highlight when
  unreachable
- Auto-backup scheduling support in settings modal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 20:22:58 +03:00
parent f8656b72a6
commit cadef971e7
23 changed files with 873 additions and 21 deletions

View File

@@ -207,20 +207,32 @@ function _updateRunningMetrics(enrichedRunning) {
// Update text values (use cached refs, fallback to querySelector)
const cached = _metricsElements.get(target.id);
const fpsEl = cached?.fps || document.querySelector(`[data-fps-text="${target.id}"]`);
if (fpsEl) fpsEl.innerHTML = `${fpsCurrent}<span class="dashboard-fps-target">/${fpsTarget}</span>`
+ `<span class="dashboard-fps-avg">avg ${fpsActual}</span>`;
if (fpsEl) {
const effFps = state.fps_effective;
const fpsTargetLabel = (effFps != null && effFps < fpsTarget)
? `${fpsCurrent}<span class="dashboard-fps-target">/${effFps}${fpsTarget}</span>`
: `${fpsCurrent}<span class="dashboard-fps-target">/${fpsTarget}</span>`;
const unreachableClass = state.device_streaming_reachable === false ? ' fps-unreachable' : '';
fpsEl.innerHTML = `<span class="${unreachableClass}">${fpsTargetLabel}</span>`
+ `<span class="dashboard-fps-avg">avg ${fpsActual}</span>`;
}
const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`);
if (errorsEl) errorsEl.textContent = `${errors > 0 ? ICON_WARNING : ICON_OK} ${errors}`;
// Update health dot
// Update health dot — prefer streaming reachability when processing
const isLed = target.target_type === 'led' || target.target_type === 'wled';
if (isLed) {
const row = cached?.row || document.querySelector(`[data-target-id="${target.id}"]`);
if (row) {
const dot = row.querySelector('.health-dot');
if (dot && state.device_last_checked != null) {
dot.className = `health-dot ${state.device_online ? 'health-online' : 'health-offline'}`;
if (dot) {
const streamReachable = state.device_streaming_reachable;
if (state.processing && streamReachable != null) {
dot.className = `health-dot ${streamReachable ? 'health-online' : 'health-offline'}`;
} else if (state.device_last_checked != null) {
dot.className = `health-dot ${state.device_online ? 'health-online' : 'health-offline'}`;
}
}
}
}