diff --git a/app/backend/auth/src/auth.js b/app/backend/auth/src/auth.js index 1e63520..ae12029 100644 --- a/app/backend/auth/src/auth.js +++ b/app/backend/auth/src/auth.js @@ -78,8 +78,10 @@ const trustedOrigins = [ 'http://localhost:3001', 'http://localhost:5173', 'http://localhost:5174', + 'http://localhost:5175', 'http://127.0.0.1:5173', 'http://127.0.0.1:5174', + 'http://127.0.0.1:5175', ] .map((origin) => (origin || '').trim()) .filter(Boolean); diff --git a/app/frontend/src/features/secondary/pages.tsx b/app/frontend/src/features/secondary/pages.tsx index ccb097e..d5dcbed 100644 --- a/app/frontend/src/features/secondary/pages.tsx +++ b/app/frontend/src/features/secondary/pages.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { @@ -19,6 +19,7 @@ import { } from '@/lib/api-client'; import { getAuthBaseUrl, signOutAuthSession } from '@/lib/auth-client'; import { useBuildUpdates } from '@/lib/use-build-updates'; +import { EnhancedMetricCard, LineAreaChart, DonutChart, SegmentedBar, BarChart } from '@/shared/components'; import { Clock, Activity, @@ -47,6 +48,8 @@ import { GitBranch, Link2, Unlink, + Cpu, + MemoryStick, } from 'lucide-react'; function SecondaryPageHeader({ title, description }: { title: string; description: string }) { @@ -207,6 +210,52 @@ export function UsagePage() { const uniqueServices = new Set(builds.map((build) => build.serviceId).filter(Boolean)).size; const host = hostQuery.data; const agents = agentsQuery.data ?? []; + + // Accumulate host samples for the load sparkline (polled every 15s). + const [loadHistory, setLoadHistory] = useState([]); + useEffect(() => { + const sample = hostQuery.data; + if (!sample || sample.cpu.cores <= 0) { + return; + } + const loadPercent = Math.min(100, (sample.load.load1m / sample.cpu.cores) * 100); + // eslint-disable-next-line react-hooks/set-state-in-effect -- accumulating polled samples is a legitimate sync-to-external pattern + setLoadHistory((prev) => [...prev, loadPercent].slice(-24)); + }, [hostQuery.data]); + + const hostLoadPercent = host && host.cpu.cores > 0 + ? Math.min(100, (host.load.load1m / host.cpu.cores) * 100) + : 0; + + const hostStatus = (percent: number): 'good' | 'average' | 'warning' => + percent < 50 ? 'good' : percent < 80 ? 'average' : 'warning'; + + // Builds per day over the last 14 days, normalized to bar heights. + // Anchored on the newest build timestamp so the chart is render-pure. + const buildsPerDay = useMemo(() => { + const days = 14; + const counts = new Array(days).fill(0); + const timestamps = (buildsQuery.data?.builds ?? []) + .map((build) => new Date((build.startedAt ?? build.completedAt) as string).getTime()) + .filter((t) => Number.isFinite(t)); + if (timestamps.length === 0) { + return counts; + } + const anchor = Math.max(...timestamps); + (buildsQuery.data?.builds ?? []).forEach((build) => { + const timestamp = build.startedAt ?? build.completedAt; + if (!timestamp) { + return; + } + const created = new Date(timestamp).getTime(); + const dayIndex = days - 1 - Math.floor((anchor - created) / 86_400_000); + if (dayIndex >= 0 && dayIndex < days) { + counts[dayIndex] += 1; + } + }); + const peak = Math.max(...counts, 1); + return counts.map((count) => (count / peak) * 100); + }, [buildsQuery.data]); const onlineAgents = agents.filter((agent) => agent.status === 'online' || agent.status === 'connecting').length; const totalAgentMemory = agents.reduce((sum, agent) => sum + agent.resources.memory.total, 0); const availableAgentMemory = agents.reduce((sum, agent) => sum + agent.resources.memory.available, 0); @@ -276,26 +325,30 @@ containr-agent`; ) : (
- 0 ? `${totalBuildHours.toFixed(1)}h` : '—'} - description={runtimeBody} - icon={Clock} - color={completedDurationsMs.length > 0 ? 'success' : 'default'} + statusText={completedDurationsMs.length > 0 ? 'Active' : ''} + status="good" + subtitle={runtimeBody} + icon={} /> - 0 ? 'warning' : 'success'} + status={failedBuilds > 0 ? 'average' : 'good'} + statusText={failedBuilds > 0 ? `${failedBuilds} failed` : 'Healthy'} + subtitle={buildActivityBody} + icon={} + chart={} /> - 0 ? 'warning' : 'default'} + status={activeBuildPressure > 0 ? 'average' : 'good'} + statusText={activeBuildPressure > 0 ? 'Busy' : 'Idle'} + subtitle={capacityBody} + icon={} />
@@ -307,7 +360,11 @@ containr-agent`;

Host Monitoring

-

Autoscaling base signal from this Containr host.

+

+ {host + ? `${host.hostname} · ${host.os}/${host.architecture} · uptime ${formatUptime(host.uptimeSeconds)}${host.dockerAvailable ? '' : ' · Docker unavailable'}` + : 'Autoscaling base signal from this Containr host.'} +