diff --git a/apps/dashboard/backend/app/services/aggregator.py b/apps/dashboard/backend/app/services/aggregator.py index 5e7f2ee..3902b41 100644 --- a/apps/dashboard/backend/app/services/aggregator.py +++ b/apps/dashboard/backend/app/services/aggregator.py @@ -120,7 +120,7 @@ class AggregatorService: async def get_immich(self) -> ImmichSnapshot: return await self.cache.get_or_load( "immich", - self.settings.cache_ttl_storage_seconds, + self.settings.cache_ttl_services_seconds, self.immich_client.fetch_stats, ) @@ -191,19 +191,42 @@ class AggregatorService: async def _build_storage(self) -> StorageResponse: snapshot = await self.beszel_client.fetch_system_snapshot() now = datetime.now(timezone.utc) + disks = [self._map_disk(d, snapshot.source_status) for d in snapshot.disks] - total_used = sum(d.used_gb for d in snapshot.disks) - total_size = sum(d.total_gb for d in snapshot.disks) - total_free = sum(d.free_gb for d in snapshot.disks) - overall_pct = round(total_used / total_size * 100, 1) if total_size > 0 else 0.0 + storage_source_status = snapshot.source_status + if snapshot.source_status == "online" and not disks: + storage_source_status = "unsupported" + + if disks: + root = next((d for d in disks if d.mount == "/"), disks[0]) + else: + root = StorageDisk( + name="rootfs", + mount="/", + used_gb=0.0, + total_gb=0.0, + free_gb=0.0, + usage_percent=0.0, + status="offline" if snapshot.source_status == "offline" else "online", + ) + + critical_count = sum(1 for d in disks if d.status == "critical") + warning_count = sum(1 for d in disks if d.status == "warning") + overall_status: OverallStatus = ( + "offline" if snapshot.source_status == "offline" + else ("degraded" if critical_count or warning_count else "online") + ) + return StorageResponse( generated_at=now, summary=StorageSummary( - total_used_gb=round(total_used, 2), - total_size_gb=round(total_size, 2), - total_free_gb=round(total_free, 2), - overall_usage_percent=overall_pct, + overall_status=overall_status, + source_status=storage_source_status, + critical_disks=critical_count, + warning_disks=warning_count, + total_disks=len(disks), ), + root=root, disks=disks, ) @@ -214,46 +237,57 @@ class AggregatorService: ) now = datetime.now(timezone.utc) - monitor_by_name = {m.name.lower(): m for m in uk_snap.monitors} + monitor_by_name = { + self._normalize_identifier(m.name): m for m in uk_snap.monitors + } + docker_by_name = { + self._normalize_identifier(c.name): c for c in docker_snap.containers + } items: list[ServiceItem] = [] - for container in docker_snap.containers: - name_lower = container.name.lower() - monitor = monitor_by_name.get(name_lower) - overall = self._resolve_overall_status(container.state, monitor) + merged_names = sorted(set(docker_by_name) | set(monitor_by_name)) + for norm in merged_names: + container = docker_by_name.get(norm) + monitor = monitor_by_name.get(norm) + status = self._resolve_overall_status( + container.state if container else "unknown", monitor + ) items.append(ServiceItem( - name=container.name, - docker_state=container.state, - uptime_kuma_status=monitor.status if monitor else None, - overall_status=overall, - health=self._status_to_health(overall), + id=norm, + name=monitor.name if monitor else container.name, + kind="service", + status=status, + health=self._status_to_health(status), + latency_ms=monitor.latency_ms if monitor else None, + docker_state=container.state if container else "unknown", + url=None, + source="uptime_kuma" if monitor else "docker", + last_checked=now.isoformat(), )) - statuses: list[OverallStatus] = [i.overall_status for i in items] - summary_status = self._aggregate_statuses(statuses) + statuses = [i.status for i in items] + overall = self._aggregate_statuses(statuses) return ServicesResponse( generated_at=now, summary=ServicesSummary( - overall_status=summary_status, - total=len(items), - online=sum(1 for s in statuses if s == "online"), - degraded=sum(1 for s in statuses if s == "degraded"), - offline=sum(1 for s in statuses if s == "offline"), + overall_status=overall, + docker=ServicesDockerSummary( + running=docker_snap.running, + stopped=docker_snap.stopped, + unhealthy=docker_snap.unhealthy, + total=docker_snap.total, + source_status=docker_snap.source_status, + ), + uptime_kuma=ServicesUptimeKumaSummary( + monitors_up=uk_snap.monitors_up, + monitors_down=uk_snap.monitors_down, + monitors_paused=uk_snap.monitors_paused, + total=uk_snap.total, + source_status=uk_snap.source_status, + ), ), - docker=ServicesDockerSummary( - running=docker_snap.running, - stopped=docker_snap.stopped, - unhealthy=docker_snap.unhealthy, - total=docker_snap.total, - source_status=docker_snap.source_status, - ), - uptime_kuma=ServicesUptimeKumaSummary( - monitors_up=uk_snap.monitors_up, - monitors_down=uk_snap.monitors_down, - source_status=uk_snap.source_status, - ), - items=items, + services=items, ) async def _build_overview(self) -> OverviewResponse: @@ -267,8 +301,11 @@ class AggregatorService: statuses: list[OverallStatus] = [] for container in docker_snap.containers: - name_lower = container.name.lower() - monitor = next((m for m in uk_snap.monitors if m.name.lower() == name_lower), None) + name_lower = self._normalize_identifier(container.name) + monitor = next( + (m for m in uk_snap.monitors if self._normalize_identifier(m.name) == name_lower), + None, + ) statuses.append(self._resolve_overall_status(container.state, monitor)) overall = self._aggregate_statuses(statuses) @@ -293,7 +330,7 @@ class AggregatorService: system=OverviewSystemSummary( cpu_percent=system_snap.cpu_usage_percent, ram_percent=system_snap.memory_usage_percent, - root_storage_percent=system_snap.disks[0].usage_percent if system_snap.disks else 0, + root_storage_percent=system_snap.disks[0].usage_percent if system_snap.disks else 0.0, network_rx_mbps=system_snap.network_rx_mbps, network_tx_mbps=system_snap.network_tx_mbps, uptime_seconds=system_snap.uptime_seconds, @@ -303,10 +340,14 @@ class AggregatorService: label=ha_snap.label, version=ha_snap.version, response_time_ms=ha_snap.response_time_ms, - last_checked=ha_snap.last_checked, + last_checked=ha_snap.last_checked.isoformat() if ha_snap.last_checked else None, ), ) + @staticmethod + def _normalize_identifier(value: str) -> str: + return "".join(ch.lower() for ch in value if ch.isalnum()) + @staticmethod def _resolve_overall_status( docker_state: str, @@ -317,7 +358,6 @@ class AggregatorService: return "offline" if monitor.status == "degraded": return "degraded" - if docker_state == "unhealthy": return "degraded" if docker_state == "stopped": @@ -342,7 +382,8 @@ class AggregatorService: elif disk.usage_percent >= 75: status = "warning" else: - status = "healthy" + status = "online" + return StorageDisk( name=disk.name, mount=disk.mount, @@ -358,9 +399,9 @@ class AggregatorService: normalized = list(statuses) if not normalized: return "offline" - if any(status == "offline" for status in normalized): - return "degraded" if any(status == "online" for status in normalized) else "offline" - if any(status in {"degraded", "warning", "critical"} for status in normalized): + if any(s == "offline" for s in normalized): + return "degraded" if any(s == "online" for s in normalized) else "offline" + if any(s in {"degraded", "warning", "critical"} for s in normalized): return "degraded" return "online"