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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
This commit is contained in:
Claude 2026-06-18 19:56:10 +00:00
parent b49f8ac8f5
commit 3b920fcfb2
No known key found for this signature in database
13 changed files with 430 additions and 336 deletions

View file

@ -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)
}

View file

@ -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<Resource[]> {
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,
}))
},
}

View file

@ -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<string, string>): Promise<TestResult>
listResources?(config: IntegrationConfig, secrets: Record<string, string>): Promise<Resource[]>
}

View file

@ -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 }
})

View file

@ -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()
})
}

View file

@ -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 }
})
}

View file

@ -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 }
})
}

View file

@ -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 }))

View file

@ -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<string, string> = {
connected: '#2ECC71',
error: '#E74C3C',
unknown: '#7A7D85',
}
export default function BottomRow() {
const [integrations, setIntegrations] = useState<Integration[] | null>(null)
const navigate = useNavigate()
useEffect(() => {
api.listIntegrations().then(({ integrations }) => setIntegrations(integrations))
}, [])
return (
<div className="grid w-full grid-cols-[1.8fr_1fr] gap-6">
{/* Network Traffic */}
{/* Connected Integrations */}
<div style={cardBase} className="hover:!border-gold/15">
{/* Background image at very low opacity */}
<div style={{ position: 'absolute', inset: 0, opacity: 0.12, backgroundImage: 'url(/archnest-network-traffic-bg.png)', backgroundSize: 'cover', backgroundPosition: 'center', pointerEvents: 'none' }} />
{/* Gold top edge */}
<div style={{ position: 'absolute', top: 0, left: '5%', right: '5%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.15), transparent)', pointerEvents: 'none' }} />
<div className="relative z-10">
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
Network Traffic
Connected Integrations
</h3>
<div className="flex items-end gap-6">
<div style={{ flex: 1, height: '100px' }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={trafficData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="trafficGold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#C8A434" stopOpacity={0.25} />
<stop offset="100%" stopColor="#C8A434" stopOpacity={0.02} />
</linearGradient>
<linearGradient id="trafficAmber" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#E67E22" stopOpacity={0.15} />
<stop offset="100%" stopColor="#E67E22" stopOpacity={0.02} />
</linearGradient>
</defs>
<Area type="monotone" dataKey="incoming" stroke="#C8A434" strokeWidth={1.5} fill="url(#trafficGold)" dot={false} isAnimationActive={true} animationDuration={1200} />
<Area type="monotone" dataKey="outgoing" stroke="rgba(230,126,34,0.6)" strokeWidth={1} fill="url(#trafficAmber)" dot={false} isAnimationActive={true} animationDuration={1200} />
</AreaChart>
</ResponsiveContainer>
</div>
<div className="flex flex-col gap-3 flex-shrink-0">
<div>
<p style={{ fontSize: '11px', color: '#7A7D85' }}>Incoming</p>
<p style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>1.23 Gbps</p>
<p style={{ fontSize: '11px', color: '#E74C3C' }}> 12.4%</p>
</div>
<div>
<p style={{ fontSize: '11px', color: '#7A7D85' }}>Outgoing</p>
<p style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>1.08 Gbps</p>
<p style={{ fontSize: '11px', color: '#2ECC71' }}> 8.7%</p>
</div>
{integrations === null ? (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>Loading</p>
) : integrations.length === 0 ? (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No integrations added yet add one in Settings.</p>
) : (
<div className="grid grid-cols-3 gap-3">
{integrations.map((i) => (
<div key={i.id} className="flex items-center gap-2.5" style={{ padding: '8px 10px', borderRadius: '8px', backgroundColor: 'rgba(255,255,255,0.02)' }}>
<span style={{ width: '7px', height: '7px', borderRadius: '50%', backgroundColor: statusColor[i.status] ?? '#7A7D85', flexShrink: 0 }} />
<span style={{ fontSize: '13px', color: '#E8E6E0', flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{i.name}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Shortcuts — miniature control panels */}
{/* Shortcuts */}
<div style={cardBase} className="hover:!border-gold/15">
<div style={{ position: 'absolute', inset: 0, opacity: 0.02, backgroundImage: 'radial-gradient(circle, #C8A434 1px, transparent 1px)', backgroundSize: '24px 24px', pointerEvents: 'none' }} />
@ -88,6 +76,7 @@ export default function BottomRow() {
return (
<button
key={item.label}
onClick={() => navigate(item.to)}
className="flex flex-col items-center gap-2 cursor-pointer bg-transparent transition-all duration-200 group/btn"
style={{ padding: '16px 8px', borderRadius: '10px', border: '1px solid rgba(200,164,52,0.08)', boxShadow: '0 0 12px rgba(200,164,52,0.02)' }}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'rgba(200,164,52,0.2)'; e.currentTarget.style.boxShadow = '0 0 16px rgba(200,164,52,0.06)' }}

View file

@ -1,27 +1,6 @@
import { X, CircleCheck, Shield, Play, Settings, User } from 'lucide-react'
const resources = [
{ label: 'Compute', current: 18, max: 24, unit: '' },
{ label: 'Storage', current: 12.4, max: 20, unit: ' TB' },
{ label: 'Database', current: 8, max: 12, unit: '' },
{ label: 'Network', current: 98.7, max: 100, unit: '%' },
{ label: 'Containers', current: 32, max: 40, unit: '' },
]
const activities = [
{ icon: CircleCheck, title: 'Backup completed', source: 'Database Cluster 01', time: '2m ago' },
{ icon: Shield, title: 'Security scan completed', source: 'Web Frontend', time: '8m ago' },
{ icon: Play, title: 'Instance launched', source: 'App Server 03', time: '15m ago' },
{ icon: Settings, title: 'Configuration updated', source: 'Load Balancer', time: '22m ago' },
{ icon: User, title: 'User login detected', source: 'admin@archnest.io', time: '35m ago' },
]
const alerts = [
{ severity: 'high', title: 'High CPU Usage', source: 'App Server 02', time: '2m ago' },
{ severity: 'medium', title: 'Disk Space Low', source: 'Database Cluster 01', time: '15m ago' },
{ severity: 'medium', title: 'Unauthorized Login Attempt', source: 'Web Frontend', time: '32m ago' },
{ severity: 'medium', title: 'SSL Certificate Expiring', source: 'api.archnest.io', time: '1h ago' },
]
import { useEffect, useState } from 'react'
import { CircleCheck, AlertTriangle, Plug, Bookmark as BookmarkIcon, LogIn } from 'lucide-react'
import { api, type Event, type Resource, type Integration } from '../lib/api'
const cardBase: React.CSSProperties = {
backgroundColor: 'rgba(10, 10, 12, 0.92)',
@ -37,109 +16,143 @@ const cardBase: React.CSSProperties = {
flexDirection: 'column',
}
function getBarColor(percentage: number) {
if (percentage >= 90) return '#E74C3C'
if (percentage >= 70) return '#E67E22'
return '#C8A434'
const statusColor: Record<string, string> = {
healthy: '#C8A434',
warning: '#E67E22',
critical: '#E74C3C',
unknown: '#7A7D85',
}
const eventIcons: Record<string, typeof CircleCheck> = {
integration_created: Plug,
integration_tested: CircleCheck,
integration_deleted: Plug,
bookmark_created: BookmarkIcon,
bookmark_deleted: BookmarkIcon,
user_login: LogIn,
account_created: LogIn,
}
function timeAgo(iso: string) {
const diffMs = Date.now() - new Date(iso.replace(' ', 'T') + 'Z').getTime()
const mins = Math.floor(diffMs / 60000)
if (mins < 1) return 'just now'
if (mins < 60) return `${mins}m ago`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours}h ago`
return `${Math.floor(hours / 24)}d ago`
}
export default function MiddleRow() {
const [resources, setResources] = useState<Resource[] | null>(null)
const [events, setEvents] = useState<Event[] | null>(null)
const [integrations, setIntegrations] = useState<Integration[] | null>(null)
useEffect(() => {
api.listResources().then(({ resources }) => setResources(resources))
api.listEvents(5).then(({ events }) => setEvents(events))
api.listIntegrations().then(({ integrations }) => setIntegrations(integrations))
}, [])
const erroredIntegrations = integrations?.filter((i) => i.status === 'error') ?? []
const problemResources = resources?.filter((r) => r.status === 'warning' || r.status === 'critical') ?? []
return (
<div className="grid h-full w-full grid-cols-[1fr_1.4fr_1fr] gap-6">
{/* Resource Overview */}
<div style={cardBase} className="hover:!border-gold/15 group">
{/* Subtle hex pattern overlay */}
<div style={{ position: 'absolute', inset: 0, opacity: 0.03, backgroundImage: 'radial-gradient(circle, #C8A434 1px, transparent 1px)', backgroundSize: '20px 20px', pointerEvents: 'none' }} />
{/* Gold top edge lighting */}
<div style={{ position: 'absolute', top: 0, left: '10%', right: '10%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.15), transparent)', pointerEvents: 'none' }} />
<div className="relative z-10 flex flex-1 flex-col">
<div className="flex items-center justify-between mb-4">
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
Resource Overview
</h3>
<button className="bg-transparent border-none cursor-pointer p-1" style={{ color: '#7A7D85' }}>
<X size={14} />
</button>
{resources === null ? (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>Loading</p>
) : resources.length === 0 ? (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>Connect an integration in Settings to see live resources here.</p>
) : (
<div className="flex flex-1 flex-col justify-around gap-3" style={{ overflowY: 'auto' }}>
{resources.slice(0, 6).map((r, i) => (
<div key={i} className="flex items-center gap-2.5">
<span style={{ width: '7px', height: '7px', borderRadius: '50%', backgroundColor: statusColor[r.status], flexShrink: 0 }} />
<span style={{ fontSize: '13px', color: '#E8E6E0', flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.name}</span>
<span style={{ fontSize: '10px', color: '#7A7D85', flexShrink: 0 }}>{r.integration}</span>
</div>
<div className="flex flex-1 flex-col justify-around gap-4">
{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 (
<div key={res.label} className="flex items-center gap-3">
<span style={{ fontSize: '13px', color: '#E8E6E0', width: '90px' }}>{res.label}</span>
<div style={{ flex: 1, height: '6px', backgroundColor: 'rgba(30,32,37,0.8)', borderRadius: '3px', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${percentage}%`, backgroundColor: getBarColor(percentage), borderRadius: '3px', transition: 'width 0.8s ease' }} />
</div>
<span style={{ fontSize: '12px', color: '#7A7D85', width: '80px', textAlign: 'right' }}>{displayValue}</span>
</div>
)
})}
))}
</div>
)}
</div>
</div>
{/* Recent Activity — visually dominant */}
{/* Recent Activity */}
<div style={cardBase} className="hover:!border-gold/15 group">
{/* City grid texture */}
<div style={{ position: 'absolute', inset: 0, opacity: 0.02, backgroundImage: 'linear-gradient(rgba(200,164,52,0.3) 1px, transparent 1px), linear-gradient(90deg, rgba(200,164,52,0.3) 1px, transparent 1px)', backgroundSize: '40px 40px', pointerEvents: 'none' }} />
{/* Gold top edge */}
<div style={{ position: 'absolute', top: 0, left: '5%', right: '5%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.2), transparent)', pointerEvents: 'none' }} />
<div className="relative z-10 flex flex-1 flex-col">
<div className="flex items-center justify-between mb-4">
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
Recent Activity
</h3>
<button className="bg-transparent border-none cursor-pointer p-1" style={{ color: '#7A7D85' }}>
<X size={14} />
</button>
</div>
{events === null ? (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>Loading</p>
) : events.length === 0 ? (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No activity yet.</p>
) : (
<div className="flex flex-1 flex-col justify-around gap-3">
{activities.map((item, i) => {
const Icon = item.icon
{events.map((item) => {
const Icon = eventIcons[item.type] ?? CircleCheck
return (
<div key={i} className="flex items-start gap-3">
<div key={item.id} className="flex items-start gap-3">
<div style={{ width: '28px', height: '28px', borderRadius: '6px', backgroundColor: 'rgba(200,164,52,0.06)', border: '1px solid rgba(200,164,52,0.08)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, boxShadow: '0 0 8px rgba(200,164,52,0.04)' }}>
<Icon size={13} style={{ color: '#C8A434' }} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<p style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.title}</p>
<p style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.source}</p>
{item.source && <p style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.source}</p>}
</div>
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{item.time}</span>
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{timeAgo(item.created_at)}</span>
</div>
)
})}
</div>
)}
</div>
</div>
{/* Top Alerts */}
<div style={cardBase} className="hover:!border-gold/15 group">
{/* Amber edge lighting */}
<div style={{ position: 'absolute', top: 0, left: '10%', right: '10%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(231,126,34,0.15), transparent)', pointerEvents: 'none' }} />
<div className="relative z-10 flex flex-1 flex-col">
<div className="flex items-center justify-between mb-4">
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
Top Alerts
</h3>
<a href="#" style={{ fontSize: '11px', color: '#C8A434', textDecoration: 'none' }}>View all</a>
</div>
{erroredIntegrations.length === 0 && problemResources.length === 0 ? (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No alerts everything connected is healthy.</p>
) : (
<div className="flex flex-1 flex-col justify-around gap-3">
{alerts.map((alert, i) => (
<div key={i} className="flex items-start gap-3">
<div style={{ width: '8px', height: '8px', borderRadius: '50%', flexShrink: 0, marginTop: '5px', backgroundColor: alert.severity === 'high' ? '#E74C3C' : '#E67E22', boxShadow: alert.severity === 'high' ? '0 0 6px rgba(231,76,60,0.3)' : '0 0 6px rgba(230,126,34,0.2)' }} />
{erroredIntegrations.map((i) => (
<div key={`int-${i.id}`} className="flex items-start gap-3">
<AlertTriangle size={14} style={{ color: '#E74C3C', flexShrink: 0, marginTop: '2px' }} />
<div style={{ flex: 1, minWidth: 0 }}>
<p style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{alert.title}</p>
<p style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{alert.source}</p>
<p style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 500 }}>Connection failing</p>
<p style={{ fontSize: '11px', color: '#7A7D85' }}>{i.name}</p>
</div>
</div>
))}
{problemResources.map((r, idx) => (
<div key={`res-${idx}`} className="flex items-start gap-3">
<AlertTriangle size={14} style={{ color: r.status === 'critical' ? '#E74C3C' : '#E67E22', flexShrink: 0, marginTop: '2px' }} />
<div style={{ flex: 1, minWidth: 0 }}>
<p style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 500 }}>{r.name}</p>
<p style={{ fontSize: '11px', color: '#7A7D85' }}>{r.integration} · {r.detail ?? r.status}</p>
</div>
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{alert.time}</span>
</div>
))}
</div>
)}
</div>
</div>
</div>

View file

@ -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<Integration[] | null>(null)
const [resources, setResources] = useState<Resource[] | null>(null)
const [bookmarks, setBookmarks] = useState<Bookmark[] | null>(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 (
<div className="grid w-full grid-cols-4 gap-5">
{/* System Status */}
<div style={cardStyle} className="hover:!border-gold/20">
<div className="flex items-start justify-between">
<div>
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
System Status
</h3>
<p style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1.3 }}>All Systems</p>
<p style={{ fontSize: '18px', fontWeight: 700, color: '#2ECC71', lineHeight: 1.3 }}>Operational</p>
<h3 style={labelStyle}>System Status</h3>
<p style={{ fontSize: '16px', fontWeight: 700, color: errored > 0 ? '#E74C3C' : '#2ECC71', lineHeight: 1.3 }}>{systemLabel}</p>
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>{connected} of {total} integrations connected</p>
</div>
<ProgressRing percentage={100} size={44} strokeWidth={3} />
</div>
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)' }}>
<SparklineChart data={[100, 100, 99, 100, 100, 100, 98, 100, 100, 100, 100, 100]} color="#C8A434" height={20} />
<p style={{ fontSize: '9px', color: '#7A7D85', marginTop: '4px' }}>Last checked: 2m ago</p>
<ProgressRing percentage={systemPercent} size={44} strokeWidth={3} />
</div>
</div>
{/* Infrastructure */}
<div style={cardStyle} className="hover:!border-gold/20">
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
Infrastructure
</h3>
<h3 style={labelStyle}>Infrastructure</h3>
<div className="flex items-center gap-2">
<Server size={16} style={{ color: '#C8A434' }} />
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>24</span>
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>{resourceTotal}</span>
</div>
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Total Resources</p>
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Resources from connected integrations</p>
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)', fontSize: '9px' }} className="flex items-center gap-2 flex-wrap">
<span className="flex items-center gap-1">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#2ECC71' }} />
<span style={{ color: '#7A7D85' }}>24 Running</span>
<span style={{ color: '#7A7D85' }}>{healthy} Healthy</span>
</span>
<span className="flex items-center gap-1">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E67E22' }} />
<span style={{ color: '#7A7D85' }}>0 Warning</span>
<span style={{ color: '#7A7D85' }}>{warning} Warning</span>
</span>
<span className="flex items-center gap-1">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E74C3C' }} />
<span style={{ color: '#7A7D85' }}>0 Critical</span>
<span style={{ color: '#7A7D85' }}>{critical} Critical</span>
</span>
</div>
</div>
{/* Security */}
{/* Integrations */}
<div style={cardStyle} className="hover:!border-gold/20">
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
Security
</h3>
<h3 style={labelStyle}>Integrations</h3>
<div className="flex items-center gap-2">
<Shield size={16} style={{ color: '#C8A434' }} />
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>2</span>
</div>
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Active Alerts</p>
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)', fontSize: '9px' }} className="flex items-center gap-2 flex-wrap">
<span className="flex items-center gap-1">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#2ECC71' }} />
<span style={{ color: '#7A7D85' }}>2 Low</span>
</span>
<span className="flex items-center gap-1">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E67E22' }} />
<span style={{ color: '#7A7D85' }}>0 Medium</span>
</span>
<span className="flex items-center gap-1">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E74C3C' }} />
<span style={{ color: '#7A7D85' }}>0 High</span>
</span>
<Plug size={16} style={{ color: '#C8A434' }} />
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>{connected}/{total}</span>
</div>
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Connected services</p>
</div>
{/* Network */}
{/* Bookmarks */}
<div style={cardStyle} className="hover:!border-gold/20">
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
Network
</h3>
<h3 style={labelStyle}>Bookmarks</h3>
<div className="flex items-center gap-2">
<Network size={16} style={{ color: '#C8A434' }} />
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>98.7%</span>
</div>
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Network Uptime</p>
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)' }}>
<SparklineChart data={[99, 98, 99, 100, 99, 98, 99, 100, 99, 99, 98, 99, 100, 99, 98, 99, 100, 99, 99, 98, 99, 100, 99, 98]} color="#C8A434" height={24} filled />
<BookMarked size={16} style={{ color: '#C8A434' }} />
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>{bookmarks?.length ?? 0}</span>
</div>
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>{favorites} favorited</p>
</div>
</div>
)

View file

@ -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<void>(`/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
}

View file

@ -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<string, string> = {
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,17 +59,21 @@ 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<string, string> = {
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 (
<div className="flex flex-1 items-center gap-4">
<div className="relative" style={{ width: '120px', height: '120px', flexShrink: 0 }}>
{hasData && (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie data={data} dataKey="value" innerRadius={38} outerRadius={56} paddingAngle={2} isAnimationActive animationDuration={1000}>
@ -128,6 +83,7 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col
</Pie>
</PieChart>
</ResponsiveContainer>
)}
{centerLabel && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<span style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>{centerLabel}</span>
@ -138,8 +94,8 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col
{data.map((entry) => (
<div key={entry.name} className="flex items-center gap-2">
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: entry.color, flexShrink: 0 }} />
<span style={{ fontSize: '12px', color: '#E8E6E0', width: '70px' }}>{entry.name}</span>
<span style={{ fontSize: '12px', color: '#7A7D85' }}>{entry.value}%</span>
<span style={{ fontSize: '12px', color: '#E8E6E0', width: '90px' }}>{entry.name}</span>
<span style={{ fontSize: '12px', color: '#7A7D85' }}>{entry.value}</span>
</div>
))}
</div>
@ -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<Resource[] | null>(null)
const [integrations, setIntegrations] = useState<Integration[] | null>(null)
const [events, setEvents] = useState<Event[] | null>(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<string, number>()
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() {
))}
</div>
<button
onClick={() => navigate('/settings')}
className="flex items-center gap-2 cursor-pointer transition-colors whitespace-nowrap"
style={{
fontSize: '12px',
@ -212,7 +198,7 @@ export default function Infrastructure() {
</div>
{/* Status Cards */}
<div className="grid w-full grid-cols-5 gap-5 shrink-0" style={{ marginTop: '8px', height: '110px' }}>
<div className="grid w-full grid-cols-4 gap-5 shrink-0" style={{ marginTop: '8px', height: '110px' }}>
{statusCards.map((card) => {
const Icon = card.icon
return (
@ -244,7 +230,11 @@ export default function Infrastructure() {
<div className="relative z-10 flex flex-1 flex-col">
<h3 style={sectionTitle}>Resource Distribution</h3>
<div className="flex flex-1 flex-col items-center justify-center">
{distributionData.length > 0 ? (
<Donut data={distributionData} />
) : (
<p style={{ fontSize: '12px', color: '#7A7D85', textAlign: 'center' }}>Connect an integration in Settings to see resource distribution.</p>
)}
</div>
</div>
</div>
@ -255,11 +245,12 @@ export default function Infrastructure() {
<div style={cardVignette} />
<div className="relative z-10 flex flex-1 flex-col">
<h3 style={sectionTitle}>Node Status</h3>
{resources && resources.length > 0 ? (
<div className="grid flex-1 grid-cols-4 gap-3 content-center">
{nodes.map((node) => (
{resources.map((node, i) => (
<div
key={node.name}
title={`${node.name}: ${node.status}`}
key={i}
title={`${node.name}: ${node.detail ?? node.status}`}
style={{
backgroundColor: 'rgba(10, 10, 12, 0.55)',
border: `1px solid ${nodeStatusColor[node.status]}33`,
@ -278,6 +269,11 @@ export default function Infrastructure() {
</div>
))}
</div>
) : (
<div className="flex flex-1 items-center justify-center">
<p style={{ fontSize: '12px', color: '#7A7D85', textAlign: 'center' }}>No resources reported yet. Connect Docker (or another supported integration) in Settings to populate this view.</p>
</div>
)}
</div>
</div>
</div>
@ -285,30 +281,26 @@ export default function Infrastructure() {
{/* Bottom Row */}
<div className="shrink-0">
<div className="grid w-full grid-cols-[1.4fr_1fr_1fr] gap-6">
{/* Resource Trend */}
<div style={{ ...cardBase, height: 'auto', ...framedCard('/archnest-network-traffic-bg.png'), padding: '20px' }} className="hover:!border-gold/15">
<div className="relative z-10">
<h3 style={sectionTitle}>Resource Trend</h3>
<div style={{ height: '100px' }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={trendData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
<XAxis dataKey="day" hide />
<Line type="monotone" dataKey="compute" stroke="#3B82F6" strokeWidth={1.5} dot={false} isAnimationActive animationDuration={1000} />
<Line type="monotone" dataKey="storage" stroke="#E67E22" strokeWidth={1.5} dot={false} isAnimationActive animationDuration={1000} />
<Line type="monotone" dataKey="database" stroke="#2ECC71" strokeWidth={1.5} dot={false} isAnimationActive animationDuration={1000} />
<Line type="monotone" dataKey="network" stroke="#8B5E3C" strokeWidth={1.5} dot={false} isAnimationActive animationDuration={1000} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Cost Breakdown */}
<div className="grid w-full grid-cols-2 gap-6">
{/* Integration Health */}
<div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
<div className="relative z-10 flex flex-col">
<h3 style={sectionTitle}>Cost Breakdown (MTD)</h3>
<Donut data={costData} centerLabel="$24.5K" />
<h3 style={sectionTitle}>Integration Health</h3>
{integrations && integrations.length > 0 ? (
<div className="flex flex-col gap-2">
{integrations.map((i) => (
<div key={i.id} className="flex items-center justify-between">
<span className="flex items-center gap-2" style={{ fontSize: '12px', color: '#E8E6E0' }}>
<CircleCheck size={13} style={{ color: i.status === 'connected' ? '#2ECC71' : i.status === 'error' ? '#E74C3C' : '#7A7D85' }} />
{i.name}
</span>
<span style={{ fontSize: '11px', color: '#7A7D85' }}>{i.status}</span>
</div>
))}
</div>
) : (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No integrations added yet.</p>
)}
</div>
</div>
@ -316,17 +308,19 @@ export default function Infrastructure() {
<div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
<div className="relative z-10">
<h3 style={sectionTitle}>Recent Activity</h3>
{events && events.length > 0 ? (
<div className="flex flex-col gap-3">
{activities.map((item, i) => (
<div key={i} className="flex items-start justify-between gap-3">
{events.map((item) => (
<div key={item.id} className="flex items-start justify-between gap-3">
<div style={{ flex: 1, minWidth: 0 }}>
<p style={{ fontSize: '12px', color: '#E8E6E0', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.title}</p>
<p style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.source}</p>
</div>
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{item.time}</span>
</div>
))}
</div>
) : (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No activity yet.</p>
)}
</div>
</div>
</div>
@ -342,13 +336,14 @@ export default function Infrastructure() {
borderTop: '1px solid rgba(200,164,52,0.08)',
}}
>
<span>AWS</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
<span>6 Regions</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
<span>18 AZs</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
<span>128 Resources</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
<span style={{ color: '#2ECC71' }}>98.7% Health</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
<span>$24,560.75 MTD</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
<span style={{ color: '#E67E22' }}>2 Alerts</span>
<span>{total} Resources</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
<span>{integrations?.filter((i) => i.status === 'connected').length ?? 0} Integrations Connected</span>
{critical > 0 && (
<>
<span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span>
<span style={{ color: '#E74C3C' }}>{critical} Critical</span>
</>
)}
</div>
</>
)