feat: proactive alerts, Jellyfin control, agent sparklines, CCR roster fix

- Proactive alerts (#1): polls every 60s; baselines on load so old alerts are silent; speaks new critical/warning alerts aloud if voice active; adds chat bubble for all new alerts
- Jellyfin control (#2): pause/stop/next/previous via voice — auto-detects active session; 12 KB intents; jellyfin.php control action uses Jellyfin General Commands API
- Agent sparklines (#6): metrics.php returns 2h CPU+MEM history per agent; SVG polyline sparklines rendered in each agent card (cyan=CPU, green=MEM); non-blocking fetch so existing view shows instantly
- Agent health CCR (#7): updated hourly cloud routine to current 13-agent roster, removed ollama-ai and alien-pc, added all active agents with correct IPs/IDs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 02:30:13 +00:00
parent e381858299
commit c29d1bf4c7
5 changed files with 187 additions and 0 deletions
+40
View File
@@ -1722,6 +1722,46 @@ if (!$reply) {
$source = 'intent:network_scan';
break;
case 'jellyfin_pause':
case 'jellyfin_stop':
case 'jellyfin_next':
case 'jellyfin_previous': {
$intentCmd = [
'jellyfin_pause' => 'TogglePause',
'jellyfin_stop' => 'Stop',
'jellyfin_next' => 'NextTrack',
'jellyfin_previous' => 'PreviousTrack',
][$matched['intent']];
// Get first active session
$jfSessions = @json_decode(@file_get_contents(
JELLYFIN_URL . '/Sessions?api_key=' . JELLYFIN_API_KEY, false,
stream_context_create(['http' => ['timeout' => 4]])
), true) ?? [];
$activeSession = null;
foreach ($jfSessions as $s) {
if (isset($s['NowPlayingItem'])) { $activeSession = $s; break; }
}
if (!$activeSession) {
$reply = "Nothing is currently playing on Jellyfin, {$userAddr}.";
} else {
$sid = $activeSession['Id'];
$url = JELLYFIN_URL . '/Sessions/' . rawurlencode($sid) . '/Command/' . $intentCmd
. '?api_key=' . JELLYFIN_API_KEY;
$ctx = stream_context_create(['http' => ['method' => 'POST', 'timeout' => 5,
'header' => 'Content-Type: application/json', 'content' => '{}', 'ignore_errors' => true]]);
@file_get_contents($url, false, $ctx);
$titles = [
'TogglePause' => 'Playback toggled, {$userAddr}.',
'Stop' => 'Playback stopped, {$userAddr}.',
'NextTrack' => 'Skipping to next track, {$userAddr}.',
'PreviousTrack' => 'Going back to previous, {$userAddr}.',
];
$reply = strtr($titles[$intentCmd], ['{$userAddr}' => $userAddr]);
}
$source = 'intent:jellyfin_control';
break;
}
case 'jellyfin_now_playing':
$jSessions = @json_decode(@file_get_contents(
JELLYFIN_URL . '/Sessions?api_key=' . JELLYFIN_API_KEY, false,