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
@@ -101,6 +101,7 @@ def _target_to_response(target) -> PictureTargetResponse:
keepalive_interval=target.keepalive_interval,
state_check_interval=target.state_check_interval,
min_brightness_threshold=target.min_brightness_threshold,
adaptive_fps=target.adaptive_fps,
description=target.description,
auto_start=target.auto_start,
created_at=target.created_at,
@@ -161,6 +162,7 @@ async def create_target(
keepalive_interval=data.keepalive_interval,
state_check_interval=data.state_check_interval,
min_brightness_threshold=data.min_brightness_threshold,
adaptive_fps=data.adaptive_fps,
picture_source_id=data.picture_source_id,
key_colors_settings=kc_settings,
description=data.description,
@@ -279,6 +281,7 @@ async def update_target(
keepalive_interval=data.keepalive_interval,
state_check_interval=data.state_check_interval,
min_brightness_threshold=data.min_brightness_threshold,
adaptive_fps=data.adaptive_fps,
key_colors_settings=kc_settings,
description=data.description,
auto_start=data.auto_start,
@@ -299,6 +302,7 @@ async def update_target(
data.keepalive_interval is not None or
data.state_check_interval is not None or
data.min_brightness_threshold is not None or
data.adaptive_fps is not None or
data.key_colors_settings is not None),
css_changed=data.color_strip_source_id is not None,
device_changed=data.device_id is not None,