collect(); return response()->json([ 'system' => [ 'cpu' => $base['cpu'], 'memory' => $base['memory'], 'disk' => $base['disk'], 'load' => $base['load'], 'uptime' => $this->uptime(), ], 'services' => [ 'ollama' => $base['ollama'], 'openai' => $base['openai'], 'reverb' => $this->reverbStatus(), 'google_directions' => $this->googleApiStatus('directions'), 'google_places' => $this->googleApiStatus('places'), 'google_geocoding' => $this->googleApiStatus('geocoding'), 'searxng' => $this->searxStatus(), 'queue' => ['len' => $base['queue']], ], 'fox' => [ 'version' => '0.9.0', 'mode' => config('services.tts.engine', 'piper'), 'voice' => config('services.openai.tts_voice', 'onyx'), 'last_action' => $this->lastActionMeta(), ], 'timestamp' => now()->toIso8601String(), ]); } protected function uptime(): array { $raw = (string) @file_get_contents('/proc/uptime'); $secs = (float) explode(' ', trim($raw))[0]; return [ 'seconds' => (int) $secs, 'human' => $this->humanDuration((int) $secs), ]; } protected function humanDuration(int $secs): string { $d = intdiv($secs, 86400); $h = intdiv($secs % 86400, 3600); $m = intdiv($secs % 3600, 60); return ($d ? $d.'d ' : '').($h ? $h.'h ' : '').$m.'m'; } protected function reverbStatus(): array { try { $r = Http::timeout(2)->get('http://reverb:8080/'); return ['online' => $r->status() < 500]; } catch (\Throwable) { return ['online' => false]; } } protected function googleApiStatus(string $api): array { $key = config('services.google.maps_key'); if (empty($key)) { return ['online' => false, 'reason' => 'no_key']; } // Cache 60s damit jeder Status-Call nicht alle APIs pingt. return Cache::remember('fox.google.'.$api, 60, function () use ($api, $key) { try { if ($api === 'directions') { $r = Http::timeout(3)->get('https://maps.googleapis.com/maps/api/directions/json', [ 'origin' => '48.2082,16.3738', 'destination' => '48.21,16.37', 'key' => $key, ]); $status = $r->json('status'); return ['online' => $r->successful() && in_array($status, ['OK', 'ZERO_RESULTS'], true)]; } if ($api === 'geocoding') { $r = Http::timeout(3)->get('https://maps.googleapis.com/maps/api/geocode/json', [ 'latlng' => '48.2082,16.3738', 'key' => $key, ]); return ['online' => $r->successful() && $r->json('status') === 'OK']; } if ($api === 'places') { $r = Http::timeout(3) ->withHeaders(['X-Goog-Api-Key' => $key, 'X-Goog-FieldMask' => 'places.id']) ->post('https://places.googleapis.com/v1/places:searchText', [ 'textQuery' => 'wien', 'maxResultCount' => 1, ]); return ['online' => $r->successful()]; } return ['online' => false]; } catch (\Throwable) { return ['online' => false]; } }); } protected function searxStatus(): array { try { $r = Http::timeout(2)->get(rtrim((string) config('services.searxng.url'), '/').'/search', [ 'q' => 'test', 'format' => 'json', ]); return ['online' => $r->status() < 500]; } catch (\Throwable) { return ['online' => false]; } } protected function lastActionMeta(): ?array { $cached = Cache::get('fox.last_pois'); if (! is_array($cached)) { return null; } return [ 'type' => 'poi_search', 'label' => $cached['label'] ?? null, 'count' => count($cached['pois'] ?? []), ]; } }