From 3b920fcfb275c7233a6fa3d50e15a555217a5960 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 19:56:10 +0000 Subject: [PATCH] Replace mock data on Glance and Infrastructure with real backend data Adds an events table + logEvent helper for a genuine activity log, and a /api/integrations/resources aggregate endpoint backed by a new optional listResources adapter method (implemented for Docker via its containers API). StatusCards, MiddleRow, BottomRow, and Infrastructure now render real integration/resource/event data instead of hardcoded numbers, with empty states where no data source exists yet (AWS cost, historical trends). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF --- backend/src/db/index.ts | 12 ++ backend/src/integrations/docker.ts | 15 +- backend/src/integrations/types.ts | 7 + backend/src/routes/auth.ts | 4 +- backend/src/routes/bookmarks.ts | 5 +- backend/src/routes/events.ts | 13 ++ backend/src/routes/integrations.ts | 26 ++- backend/src/server.ts | 2 + src/components/BottomRow.tsx | 87 +++++----- src/components/MiddleRow.tsx | 207 ++++++++++++----------- src/components/StatusCards.tsx | 107 ++++++------ src/lib/api.ts | 18 ++ src/pages/Infrastructure.tsx | 263 ++++++++++++++--------------- 13 files changed, 430 insertions(+), 336 deletions(-) create mode 100644 backend/src/routes/events.ts diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts index f984da1..da75948 100644 --- a/backend/src/db/index.ts +++ b/backend/src/db/index.ts @@ -57,4 +57,16 @@ db.exec(` last_checked_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); + + CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + title TEXT NOT NULL, + source TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); `) + +export function logEvent(type: string, title: string, source?: string | null) { + db.prepare('INSERT INTO events (type, title, source) VALUES (?, ?, ?)').run(type, title, source ?? null) +} diff --git a/backend/src/integrations/docker.ts b/backend/src/integrations/docker.ts index 9f261d2..b2e3a3e 100644 --- a/backend/src/integrations/docker.ts +++ b/backend/src/integrations/docker.ts @@ -1,4 +1,4 @@ -import type { IntegrationAdapter } from './types.js' +import type { IntegrationAdapter, Resource } from './types.js' export const docker: IntegrationAdapter = { async testConnection(config) { @@ -12,4 +12,17 @@ export const docker: IntegrationAdapter = { return { ok: false, message: err instanceof Error ? err.message : 'Connection failed' } } }, + + async listResources(config): Promise { + const baseUrl = config.baseUrl?.replace(/\/$/, '') + if (!baseUrl) return [] + const res = await fetch(`${baseUrl}/containers/json?all=true`) + if (!res.ok) return [] + const containers = (await res.json()) as { Names: string[]; State: string }[] + return containers.map((c) => ({ + name: c.Names[0]?.replace(/^\//, '') ?? 'unknown', + status: c.State === 'running' ? 'healthy' : c.State === 'restarting' ? 'warning' : 'critical', + detail: c.State, + })) + }, } diff --git a/backend/src/integrations/types.ts b/backend/src/integrations/types.ts index 186401e..c25ab42 100644 --- a/backend/src/integrations/types.ts +++ b/backend/src/integrations/types.ts @@ -16,6 +16,13 @@ export interface TestResult { message: string } +export interface Resource { + name: string + status: 'healthy' | 'warning' | 'critical' | 'unknown' + detail?: string +} + export interface IntegrationAdapter { testConnection(config: IntegrationConfig, secrets: Record): Promise + listResources?(config: IntegrationConfig, secrets: Record): Promise } diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 021b787..74abc23 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,7 +1,7 @@ import type { FastifyInstance } from 'fastify' import bcrypt from 'bcryptjs' import { z } from 'zod' -import { db } from '../db/index.js' +import { db, logEvent } from '../db/index.js' const credentialsSchema = z.object({ username: z.string().min(3).max(64), @@ -29,6 +29,7 @@ export async function authRoutes(app: FastifyInstance) { .prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)') .run(username, passwordHash) const token = app.jwt.sign({ sub: Number(result.lastInsertRowid), username }) + logEvent('account_created', `Account created for ${username}`) return { token } }) @@ -45,6 +46,7 @@ export async function authRoutes(app: FastifyInstance) { return reply.code(401).send({ error: 'Invalid username or password' }) } const token = app.jwt.sign({ sub: user.id, username: user.username }) + logEvent('user_login', `${user.username} logged in`) return { token } }) diff --git a/backend/src/routes/bookmarks.ts b/backend/src/routes/bookmarks.ts index b7ba787..b7f17cc 100644 --- a/backend/src/routes/bookmarks.ts +++ b/backend/src/routes/bookmarks.ts @@ -1,6 +1,6 @@ import type { FastifyInstance } from 'fastify' import { z } from 'zod' -import { db } from '../db/index.js' +import { db, logEvent } from '../db/index.js' const bookmarkSchema = z.object({ categoryId: z.number().int().nullable().optional(), @@ -48,6 +48,7 @@ export async function bookmarkRoutes(app: FastifyInstance) { 'INSERT INTO bookmarks (category_id, title, url, icon, favorite) VALUES (?, ?, ?, ?, ?)' ) .run(categoryId ?? null, title, url, icon ?? null, favorite ? 1 : 0) + logEvent('bookmark_created', `Bookmark added: ${title}`) return reply.code(201).send({ id: result.lastInsertRowid }) }) @@ -72,7 +73,9 @@ export async function bookmarkRoutes(app: FastifyInstance) { app.delete('/api/bookmarks/:id', async (req, reply) => { const id = Number((req.params as { id: string }).id) + const existing = db.prepare('SELECT title FROM bookmarks WHERE id = ?').get(id) as { title: string } | undefined db.prepare('DELETE FROM bookmarks WHERE id = ?').run(id) + if (existing) logEvent('bookmark_deleted', `Bookmark removed: ${existing.title}`) return reply.code(204).send() }) } diff --git a/backend/src/routes/events.ts b/backend/src/routes/events.ts new file mode 100644 index 0000000..47e5004 --- /dev/null +++ b/backend/src/routes/events.ts @@ -0,0 +1,13 @@ +import type { FastifyInstance } from 'fastify' +import { db } from '../db/index.js' + +export async function eventRoutes(app: FastifyInstance) { + app.addHook('onRequest', app.authenticate) + + app.get('/api/events', async (req) => { + const query = req.query as { limit?: string } + const limit = Math.min(Number(query.limit) || 20, 100) + const events = db.prepare('SELECT * FROM events ORDER BY created_at DESC, id DESC LIMIT ?').all(limit) + return { events } + }) +} diff --git a/backend/src/routes/integrations.ts b/backend/src/routes/integrations.ts index 1d197a3..6380405 100644 --- a/backend/src/routes/integrations.ts +++ b/backend/src/routes/integrations.ts @@ -1,9 +1,9 @@ import type { FastifyInstance } from 'fastify' import { z } from 'zod' -import { db } from '../db/index.js' +import { db, logEvent } from '../db/index.js' import { encryptSecret, decryptSecret } from '../db/crypto.js' import { adapterRegistry } from '../integrations/registry.js' -import type { IntegrationType } from '../integrations/types.js' +import type { IntegrationType, Resource } from '../integrations/types.js' const integrationTypes = [ 'proxmox', @@ -80,6 +80,7 @@ export async function integrationRoutes(app: FastifyInstance) { insertSecret.run(integrationId, key, encryptSecret(value)) } const row = db.prepare('SELECT * FROM integrations WHERE id = ?').get(integrationId) as IntegrationRow + logEvent('integration_created', `${name} integration added`, type) return reply.code(201).send({ integration: serialize(row) }) }) @@ -116,7 +117,9 @@ export async function integrationRoutes(app: FastifyInstance) { app.delete('/api/integrations/:id', async (req, reply) => { const id = Number((req.params as { id: string }).id) + const existing = db.prepare('SELECT * FROM integrations WHERE id = ?').get(id) as IntegrationRow | undefined db.prepare('DELETE FROM integrations WHERE id = ?').run(id) + if (existing) logEvent('integration_deleted', `${existing.name} integration removed`, existing.type) return reply.code(204).send() }) @@ -136,6 +139,25 @@ export async function integrationRoutes(app: FastifyInstance) { status, id ) + logEvent('integration_tested', `${row.name} test ${result.ok ? 'succeeded' : 'failed'}`, row.type) return result }) + + app.get('/api/integrations/resources', async () => { + const rows = db.prepare("SELECT * FROM integrations WHERE enabled = 1 AND status = 'connected'").all() as IntegrationRow[] + const resources: (Resource & { integration: string })[] = [] + for (const row of rows) { + const adapter = adapterRegistry[row.type as IntegrationType] + if (!adapter.listResources) continue + const config = JSON.parse(row.config_json) + const secrets = loadSecrets(row.id) + try { + const found = await adapter.listResources(config, secrets) + for (const r of found) resources.push({ ...r, integration: row.name }) + } catch { + // adapter unreachable — skip, connection test already surfaces this + } + } + return { resources } + }) } diff --git a/backend/src/server.ts b/backend/src/server.ts index 6d2e361..3ccb11d 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -5,6 +5,7 @@ import jwt from '@fastify/jwt' import { authRoutes } from './routes/auth.js' import { integrationRoutes } from './routes/integrations.js' import { bookmarkRoutes } from './routes/bookmarks.js' +import { eventRoutes } from './routes/events.js' const JWT_SECRET = process.env.ARCHNEST_JWT_SECRET if (!JWT_SECRET) { @@ -27,6 +28,7 @@ app.decorate('authenticate', async function (req, reply) { await app.register(authRoutes) await app.register(integrationRoutes) await app.register(bookmarkRoutes) +await app.register(eventRoutes) app.get('/api/health', async () => ({ ok: true })) diff --git a/src/components/BottomRow.tsx b/src/components/BottomRow.tsx index cb93897..7e0a427 100644 --- a/src/components/BottomRow.tsx +++ b/src/components/BottomRow.tsx @@ -1,17 +1,13 @@ -import { AreaChart, Area, ResponsiveContainer } from 'recharts' -import { ServerCog, DatabaseBackup, Rocket, FileText } from 'lucide-react' - -const trafficData = Array.from({ length: 48 }, (_, i) => ({ - time: i, - incoming: 800 + Math.sin(i / 6) * 300 + Math.random() * 150, - outgoing: 700 + Math.cos(i / 8) * 250 + Math.random() * 100, -})) +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { Plug, ServerCog, BookMarked, Settings as SettingsIcon } from 'lucide-react' +import { api, type Integration } from '../lib/api' const shortcuts = [ - { icon: ServerCog, label: 'Add Server' }, - { icon: DatabaseBackup, label: 'Create Backup' }, - { icon: Rocket, label: 'Deploy App' }, - { icon: FileText, label: 'View Logs' }, + { icon: ServerCog, label: 'Add Integration', to: '/settings' }, + { icon: BookMarked, label: 'Add Bookmark', to: '/booknest' }, + { icon: Plug, label: 'Infrastructure', to: '/infrastructure' }, + { icon: SettingsIcon, label: 'Settings', to: '/settings' }, ] const cardBase: React.CSSProperties = { @@ -25,56 +21,48 @@ const cardBase: React.CSSProperties = { overflow: 'hidden', } +const statusColor: Record = { + connected: '#2ECC71', + error: '#E74C3C', + unknown: '#7A7D85', +} + export default function BottomRow() { + const [integrations, setIntegrations] = useState(null) + const navigate = useNavigate() + + useEffect(() => { + api.listIntegrations().then(({ integrations }) => setIntegrations(integrations)) + }, []) + return (
- {/* Network Traffic */} + {/* Connected Integrations */}
- {/* Background image at very low opacity */} -
- {/* Gold top edge */}

- Network Traffic + Connected Integrations

-
-
- - - - - - - - - - - - - - - - + {integrations === null ? ( +

Loading…

+ ) : integrations.length === 0 ? ( +

No integrations added yet — add one in Settings.

+ ) : ( +
+ {integrations.map((i) => ( +
+ + {i.name} +
+ ))}
-
-
-

Incoming

-

1.23 Gbps

-

↓ 12.4%

-
-
-

Outgoing

-

1.08 Gbps

-

↑ 8.7%

-
-
-
+ )}
- {/* Shortcuts — miniature control panels */} + {/* Shortcuts */}
@@ -88,6 +76,7 @@ export default function BottomRow() { return ( -
-
- {resources.map((res) => { - const percentage = res.unit === '%' ? res.current : (res.current / res.max) * 100 - const displayValue = res.unit === '%' ? `${res.current}%` : `${res.current} / ${res.max}${res.unit}` - return ( -
- {res.label} -
-
-
- {displayValue} +

+ Resource Overview +

+ {resources === null ? ( +

Loading…

+ ) : resources.length === 0 ? ( +

Connect an integration in Settings to see live resources here.

+ ) : ( +
+ {resources.slice(0, 6).map((r, i) => ( +
+ + {r.name} + {r.integration}
- ) - })} -
+ ))} +
+ )}
- {/* Recent Activity — visually dominant */} + {/* Recent Activity */}
- {/* City grid texture */}
- {/* Gold top edge */}
-
-

- Recent Activity -

- -
-
- {activities.map((item, i) => { - const Icon = item.icon - return ( -
-
- +

+ Recent Activity +

+ {events === null ? ( +

Loading…

+ ) : events.length === 0 ? ( +

No activity yet.

+ ) : ( +
+ {events.map((item) => { + const Icon = eventIcons[item.type] ?? CircleCheck + return ( +
+
+ +
+
+

{item.title}

+ {item.source &&

{item.source}

} +
+ {timeAgo(item.created_at)}
-
-

{item.title}

-

{item.source}

-
- {item.time} -
- ) - })} -
+ ) + })} +
+ )}
{/* Top Alerts */}
- {/* Amber edge lighting */}
-
-

- Top Alerts -

- View all -
-
- {alerts.map((alert, i) => ( -
-
-
-

{alert.title}

-

{alert.source}

+

+ Top Alerts +

+ {erroredIntegrations.length === 0 && problemResources.length === 0 ? ( +

No alerts — everything connected is healthy.

+ ) : ( +
+ {erroredIntegrations.map((i) => ( +
+ +
+

Connection failing

+

{i.name}

+
- {alert.time} -
- ))} -
+ ))} + {problemResources.map((r, idx) => ( +
+ +
+

{r.name}

+

{r.integration} · {r.detail ?? r.status}

+
+
+ ))} +
+ )}
diff --git a/src/components/StatusCards.tsx b/src/components/StatusCards.tsx index a185cad..302288c 100644 --- a/src/components/StatusCards.tsx +++ b/src/components/StatusCards.tsx @@ -1,6 +1,7 @@ -import { Server, Shield, Network } from 'lucide-react' -import SparklineChart from './SparklineChart' +import { useEffect, useState } from 'react' +import { Server, Plug, BookMarked } from 'lucide-react' import ProgressRing from './ProgressRing' +import { api, type Integration, type Resource, type Bookmark } from '../lib/api' const cardStyle: React.CSSProperties = { backgroundColor: 'rgba(10, 10, 12, 0.55)', @@ -12,92 +13,96 @@ const cardStyle: React.CSSProperties = { transition: 'border-color 0.2s ease', } +const labelStyle: React.CSSProperties = { + fontSize: '10px', + textTransform: 'uppercase', + letterSpacing: '1.5px', + color: '#7A7D85', + fontWeight: 500, + marginBottom: '8px', +} + export default function StatusCards() { + const [integrations, setIntegrations] = useState(null) + const [resources, setResources] = useState(null) + const [bookmarks, setBookmarks] = useState(null) + + useEffect(() => { + api.listIntegrations().then(({ integrations }) => setIntegrations(integrations)) + api.listResources().then(({ resources }) => setResources(resources)) + api.listBookmarks().then(({ bookmarks }) => setBookmarks(bookmarks)) + }, []) + + const connected = integrations?.filter((i) => i.status === 'connected').length ?? 0 + const errored = integrations?.filter((i) => i.status === 'error').length ?? 0 + const total = integrations?.length ?? 0 + + const healthy = resources?.filter((r) => r.status === 'healthy').length ?? 0 + const warning = resources?.filter((r) => r.status === 'warning').length ?? 0 + const critical = resources?.filter((r) => r.status === 'critical').length ?? 0 + const resourceTotal = resources?.length ?? 0 + + const favorites = bookmarks?.filter((b) => b.favorite).length ?? 0 + + const systemLabel = errored > 0 ? 'Issues Detected' : total === 0 ? 'Not Configured' : 'All Systems Operational' + const systemPercent = total === 0 ? 0 : Math.round((connected / total) * 100) + return (
{/* System Status */}
-

- System Status -

-

All Systems

-

Operational

+

System Status

+

0 ? '#E74C3C' : '#2ECC71', lineHeight: 1.3 }}>{systemLabel}

+

{connected} of {total} integrations connected

- -
-
- -

Last checked: 2m ago

+
{/* Infrastructure */}
-

- Infrastructure -

+

Infrastructure

- 24 + {resourceTotal}
-

Total Resources

+

Resources from connected integrations

- 24 Running + {healthy} Healthy - 0 Warning + {warning} Warning - 0 Critical + {critical} Critical
- {/* Security */} + {/* Integrations */}
-

- Security -

+

Integrations

- - 2 -
-

Active Alerts

-
- - - 2 Low - - - - 0 Medium - - - - 0 High - + + {connected}/{total}
+

Connected services

- {/* Network */} + {/* Bookmarks */}
-

- Network -

+

Bookmarks

- - 98.7% -
-

Network Uptime

-
- + + {bookmarks?.length ?? 0}
+

{favorites} favorited

) diff --git a/src/lib/api.ts b/src/lib/api.ts index 5657a34..8ea4791 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -67,6 +67,9 @@ export const api = { updateBookmark: (id: number, data: Partial<{ categoryId: number | null; title: string; url: string; icon: string; favorite: boolean }>) => apiFetch<{ ok: boolean }>(`/bookmarks/${id}`, { method: 'PUT', body: JSON.stringify(data) }), deleteBookmark: (id: number) => apiFetch(`/bookmarks/${id}`, { method: 'DELETE' }), + + listEvents: (limit = 20) => apiFetch<{ events: Event[] }>(`/events?limit=${limit}`), + listResources: () => apiFetch<{ resources: Resource[] }>('/integrations/resources'), } export interface Integration { @@ -98,3 +101,18 @@ export interface BookmarkCategory { icon: string | null sort_order: number } + +export interface Event { + id: number + type: string + title: string + source: string | null + created_at: string +} + +export interface Resource { + name: string + status: 'healthy' | 'warning' | 'critical' | 'unknown' + detail?: string + integration: string +} diff --git a/src/pages/Infrastructure.tsx b/src/pages/Infrastructure.tsx index db38b46..c6f942d 100644 --- a/src/pages/Infrastructure.tsx +++ b/src/pages/Infrastructure.tsx @@ -1,61 +1,12 @@ -import { useState } from 'react' -import { PieChart, Pie, Cell, ResponsiveContainer, LineChart, Line, XAxis } from 'recharts' -import { Plus, Server, Activity, AlertTriangle, DollarSign } from 'lucide-react' +import { useEffect, useMemo, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts' +import { Plus, Server, Activity, AlertTriangle, CircleCheck } from 'lucide-react' +import { api, type Resource, type Integration, type Event } from '../lib/api' const subTabs = ['Overview'] const futureSubTabs = ['Network'] -const statusCards = [ - { label: 'Total Resources', value: '128', icon: Server, sub: '+4 this week' }, - { label: 'Healthy', value: '124', icon: Activity, sub: '96.9%', color: '#2ECC71' }, - { label: 'Warnings', value: '3', icon: AlertTriangle, sub: 'Needs attention', color: '#E67E22' }, - { label: 'Critical', value: '1', icon: AlertTriangle, sub: 'Action required', color: '#E74C3C' }, - { label: 'Monthly Cost', value: '$24,560.75', icon: DollarSign, sub: '↑ 3.2% MTD' }, -] - -const costData = [ - { name: 'Compute', value: 38, color: '#C8A434' }, - { name: 'Storage', value: 22, color: '#E67E22' }, - { name: 'Database', value: 25, color: '#2ECC71' }, - { name: 'Network', value: 15, color: '#7A7D85' }, -] - -const nodes = [ - { name: 'web-frontend-lb', status: 'healthy' }, - { name: 'app-server-01', status: 'healthy' }, - { name: 'app-server-02', status: 'healthy' }, - { name: 'app-server-03', status: 'warning' }, - { name: 'db-primary-01', status: 'healthy' }, - { name: 'db-replica-02', status: 'healthy' }, - { name: 'cache-cluster-02', status: 'critical' }, - { name: 'batch-worker-04', status: 'healthy' }, - { name: 'storage-node-01', status: 'healthy' }, - { name: 'storage-node-02', status: 'healthy' }, - { name: 'monitoring-01', status: 'healthy' }, - { name: 'vpn-gateway', status: 'warning' }, -] - -const nodeStatusColor: Record = { - healthy: '#2ECC71', - warning: '#E67E22', - critical: '#E74C3C', -} - -const trendData = Array.from({ length: 14 }, (_, i) => ({ - day: i, - compute: 60 + i * 0.6 + Math.sin(i / 2) * 3, - storage: 35 + i * 0.4 + Math.cos(i / 3) * 2, - database: 20 + i * 0.2 + Math.sin(i / 4) * 1.5, - network: 45 + i * 0.3 + Math.cos(i / 2.5) * 2.5, -})) - -const activities = [ - { title: 'Auto-scaling triggered', source: 'App Server Group', time: '4m ago' }, - { title: 'Resource provisioned', source: 'db-replica-02', time: '19m ago' }, - { title: 'Health check failed', source: 'cache-cluster-02', time: '27m ago' }, - { title: 'Tag updated', source: '12 resources', time: '1h ago' }, -] - const cardBase: React.CSSProperties = { backgroundColor: 'rgba(10, 10, 12, 0.92)', border: '1px solid rgba(200, 164, 52, 0.08)', @@ -108,26 +59,31 @@ const cardDim: React.CSSProperties = { backgroundColor: 'rgba(8, 8, 10, 0.45)', } -const distributionData = [ - { name: 'Compute', value: 42, color: '#C8A434' }, - { name: 'Storage', value: 28, color: '#E67E22' }, - { name: 'Database', value: 18, color: '#2ECC71' }, - { name: 'Network', value: 12, color: '#7A7D85' }, -] +const nodeStatusColor: Record = { + healthy: '#2ECC71', + warning: '#E67E22', + critical: '#E74C3C', + unknown: '#7A7D85', +} + +const integrationPalette = ['#C8A434', '#E67E22', '#2ECC71', '#7A7D85', '#3B82F6', '#8B5E3C'] function Donut({ data, centerLabel }: { data: { name: string; value: number; color: string }[]; centerLabel?: string }) { + const hasData = data.some((d) => d.value > 0) return (
- - - - {data.map((entry) => ( - - ))} - - - + {hasData && ( + + + + {data.map((entry) => ( + + ))} + + + + )} {centerLabel && (
{centerLabel} @@ -138,8 +94,8 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col {data.map((entry) => (
- {entry.name} - {entry.value}% + {entry.name} + {entry.value}
))}
@@ -149,6 +105,35 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col export default function Infrastructure() { const [activeTab, setActiveTab] = useState('Overview') + const [resources, setResources] = useState(null) + const [integrations, setIntegrations] = useState(null) + const [events, setEvents] = useState(null) + const navigate = useNavigate() + + useEffect(() => { + api.listResources().then(({ resources }) => setResources(resources)) + api.listIntegrations().then(({ integrations }) => setIntegrations(integrations)) + api.listEvents(4).then(({ events }) => setEvents(events)) + }, []) + + const healthy = resources?.filter((r) => r.status === 'healthy').length ?? 0 + const warning = resources?.filter((r) => r.status === 'warning').length ?? 0 + const critical = resources?.filter((r) => r.status === 'critical').length ?? 0 + const total = resources?.length ?? 0 + + const statusCards = [ + { label: 'Total Resources', value: String(total), icon: Server, sub: `${integrations?.filter((i) => i.status === 'connected').length ?? 0} integrations connected` }, + { label: 'Healthy', value: String(healthy), icon: Activity, sub: total ? `${Math.round((healthy / total) * 100)}%` : '—', color: '#2ECC71' }, + { label: 'Warnings', value: String(warning), icon: AlertTriangle, sub: warning ? 'Needs attention' : 'None', color: '#E67E22' }, + { label: 'Critical', value: String(critical), icon: AlertTriangle, sub: critical ? 'Action required' : 'None', color: '#E74C3C' }, + ] + + const distributionData = useMemo(() => { + if (!resources) return [] + const byIntegration = new Map() + for (const r of resources) byIntegration.set(r.integration, (byIntegration.get(r.integration) ?? 0) + 1) + return Array.from(byIntegration.entries()).map(([name, value], i) => ({ name, value, color: integrationPalette[i % integrationPalette.length] })) + }, [resources]) return ( <> @@ -194,6 +179,7 @@ export default function Infrastructure() { ))}
{/* Status Cards */} -
+
{statusCards.map((card) => { const Icon = card.icon return ( @@ -244,7 +230,11 @@ export default function Infrastructure() {

Resource Distribution

- + {distributionData.length > 0 ? ( + + ) : ( +

Connect an integration in Settings to see resource distribution.

+ )}
@@ -255,29 +245,35 @@ export default function Infrastructure() {

Node Status

-
- {nodes.map((node) => ( -
-
- - + {resources && resources.length > 0 ? ( +
+ {resources.map((node, i) => ( +
+
+ + +
+ {node.name}
- {node.name} -
- ))} -
+ ))} +
+ ) : ( +
+

No resources reported yet. Connect Docker (or another supported integration) in Settings to populate this view.

+
+ )}
@@ -285,30 +281,26 @@ export default function Infrastructure() { {/* Bottom Row */}
-
- {/* Resource Trend */} -
-
-

Resource Trend

-
- - - - - - - - - -
-
-
- - {/* Cost Breakdown */} +
+ {/* Integration Health */}
-

Cost Breakdown (MTD)

- +

Integration Health

+ {integrations && integrations.length > 0 ? ( +
+ {integrations.map((i) => ( +
+ + + {i.name} + + {i.status} +
+ ))} +
+ ) : ( +

No integrations added yet.

+ )}
@@ -316,17 +308,19 @@ export default function Infrastructure() {

Recent Activity

-
- {activities.map((item, i) => ( -
-
-

{item.title}

-

{item.source}

+ {events && events.length > 0 ? ( +
+ {events.map((item) => ( +
+
+

{item.title}

+
- {item.time} -
- ))} -
+ ))} +
+ ) : ( +

No activity yet.

+ )}
@@ -342,13 +336,14 @@ export default function Infrastructure() { borderTop: '1px solid rgba(200,164,52,0.08)', }} > - AWS| - 6 Regions| - 18 AZs| - 128 Resources| - 98.7% Health| - $24,560.75 MTD| - 2 Alerts + {total} Resources| + {integrations?.filter((i) => i.status === 'connected').length ?? 0} Integrations Connected + {critical > 0 && ( + <> + | + {critical} Critical + + )}
)