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, last_checked_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')) 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 = { export const docker: IntegrationAdapter = {
async testConnection(config) { async testConnection(config) {
@ -12,4 +12,17 @@ export const docker: IntegrationAdapter = {
return { ok: false, message: err instanceof Error ? err.message : 'Connection failed' } 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 message: string
} }
export interface Resource {
name: string
status: 'healthy' | 'warning' | 'critical' | 'unknown'
detail?: string
}
export interface IntegrationAdapter { export interface IntegrationAdapter {
testConnection(config: IntegrationConfig, secrets: Record<string, string>): Promise<TestResult> 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 type { FastifyInstance } from 'fastify'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
import { z } from 'zod' import { z } from 'zod'
import { db } from '../db/index.js' import { db, logEvent } from '../db/index.js'
const credentialsSchema = z.object({ const credentialsSchema = z.object({
username: z.string().min(3).max(64), 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 (?, ?)') .prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)')
.run(username, passwordHash) .run(username, passwordHash)
const token = app.jwt.sign({ sub: Number(result.lastInsertRowid), username }) const token = app.jwt.sign({ sub: Number(result.lastInsertRowid), username })
logEvent('account_created', `Account created for ${username}`)
return { token } return { token }
}) })
@ -45,6 +46,7 @@ export async function authRoutes(app: FastifyInstance) {
return reply.code(401).send({ error: 'Invalid username or password' }) return reply.code(401).send({ error: 'Invalid username or password' })
} }
const token = app.jwt.sign({ sub: user.id, username: user.username }) const token = app.jwt.sign({ sub: user.id, username: user.username })
logEvent('user_login', `${user.username} logged in`)
return { token } return { token }
}) })

View file

@ -1,6 +1,6 @@
import type { FastifyInstance } from 'fastify' import type { FastifyInstance } from 'fastify'
import { z } from 'zod' import { z } from 'zod'
import { db } from '../db/index.js' import { db, logEvent } from '../db/index.js'
const bookmarkSchema = z.object({ const bookmarkSchema = z.object({
categoryId: z.number().int().nullable().optional(), 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 (?, ?, ?, ?, ?)' 'INSERT INTO bookmarks (category_id, title, url, icon, favorite) VALUES (?, ?, ?, ?, ?)'
) )
.run(categoryId ?? null, title, url, icon ?? null, favorite ? 1 : 0) .run(categoryId ?? null, title, url, icon ?? null, favorite ? 1 : 0)
logEvent('bookmark_created', `Bookmark added: ${title}`)
return reply.code(201).send({ id: result.lastInsertRowid }) 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) => { app.delete('/api/bookmarks/:id', async (req, reply) => {
const id = Number((req.params as { id: string }).id) 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) db.prepare('DELETE FROM bookmarks WHERE id = ?').run(id)
if (existing) logEvent('bookmark_deleted', `Bookmark removed: ${existing.title}`)
return reply.code(204).send() 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 type { FastifyInstance } from 'fastify'
import { z } from 'zod' 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 { encryptSecret, decryptSecret } from '../db/crypto.js'
import { adapterRegistry } from '../integrations/registry.js' import { adapterRegistry } from '../integrations/registry.js'
import type { IntegrationType } from '../integrations/types.js' import type { IntegrationType, Resource } from '../integrations/types.js'
const integrationTypes = [ const integrationTypes = [
'proxmox', 'proxmox',
@ -80,6 +80,7 @@ export async function integrationRoutes(app: FastifyInstance) {
insertSecret.run(integrationId, key, encryptSecret(value)) insertSecret.run(integrationId, key, encryptSecret(value))
} }
const row = db.prepare('SELECT * FROM integrations WHERE id = ?').get(integrationId) as IntegrationRow 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) }) 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) => { app.delete('/api/integrations/:id', async (req, reply) => {
const id = Number((req.params as { id: string }).id) 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) 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() return reply.code(204).send()
}) })
@ -136,6 +139,25 @@ export async function integrationRoutes(app: FastifyInstance) {
status, status,
id id
) )
logEvent('integration_tested', `${row.name} test ${result.ok ? 'succeeded' : 'failed'}`, row.type)
return result 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 { authRoutes } from './routes/auth.js'
import { integrationRoutes } from './routes/integrations.js' import { integrationRoutes } from './routes/integrations.js'
import { bookmarkRoutes } from './routes/bookmarks.js' import { bookmarkRoutes } from './routes/bookmarks.js'
import { eventRoutes } from './routes/events.js'
const JWT_SECRET = process.env.ARCHNEST_JWT_SECRET const JWT_SECRET = process.env.ARCHNEST_JWT_SECRET
if (!JWT_SECRET) { if (!JWT_SECRET) {
@ -27,6 +28,7 @@ app.decorate('authenticate', async function (req, reply) {
await app.register(authRoutes) await app.register(authRoutes)
await app.register(integrationRoutes) await app.register(integrationRoutes)
await app.register(bookmarkRoutes) await app.register(bookmarkRoutes)
await app.register(eventRoutes)
app.get('/api/health', async () => ({ ok: true })) app.get('/api/health', async () => ({ ok: true }))

View file

@ -1,17 +1,13 @@
import { AreaChart, Area, ResponsiveContainer } from 'recharts' import { useEffect, useState } from 'react'
import { ServerCog, DatabaseBackup, Rocket, FileText } from 'lucide-react' import { useNavigate } from 'react-router-dom'
import { Plug, ServerCog, BookMarked, Settings as SettingsIcon } from 'lucide-react'
const trafficData = Array.from({ length: 48 }, (_, i) => ({ import { api, type Integration } from '../lib/api'
time: i,
incoming: 800 + Math.sin(i / 6) * 300 + Math.random() * 150,
outgoing: 700 + Math.cos(i / 8) * 250 + Math.random() * 100,
}))
const shortcuts = [ const shortcuts = [
{ icon: ServerCog, label: 'Add Server' }, { icon: ServerCog, label: 'Add Integration', to: '/settings' },
{ icon: DatabaseBackup, label: 'Create Backup' }, { icon: BookMarked, label: 'Add Bookmark', to: '/booknest' },
{ icon: Rocket, label: 'Deploy App' }, { icon: Plug, label: 'Infrastructure', to: '/infrastructure' },
{ icon: FileText, label: 'View Logs' }, { icon: SettingsIcon, label: 'Settings', to: '/settings' },
] ]
const cardBase: React.CSSProperties = { const cardBase: React.CSSProperties = {
@ -25,56 +21,48 @@ const cardBase: React.CSSProperties = {
overflow: 'hidden', overflow: 'hidden',
} }
const statusColor: Record<string, string> = {
connected: '#2ECC71',
error: '#E74C3C',
unknown: '#7A7D85',
}
export default function BottomRow() { export default function BottomRow() {
const [integrations, setIntegrations] = useState<Integration[] | null>(null)
const navigate = useNavigate()
useEffect(() => {
api.listIntegrations().then(({ integrations }) => setIntegrations(integrations))
}, [])
return ( return (
<div className="grid w-full grid-cols-[1.8fr_1fr] gap-6"> <div className="grid w-full grid-cols-[1.8fr_1fr] gap-6">
{/* Network Traffic */} {/* Connected Integrations */}
<div style={cardBase} className="hover:!border-gold/15"> <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 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"> <div className="relative z-10">
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}> <h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
Network Traffic Connected Integrations
</h3> </h3>
<div className="flex items-end gap-6"> {integrations === null ? (
<div style={{ flex: 1, height: '100px' }}> <p style={{ fontSize: '12px', color: '#7A7D85' }}>Loading</p>
<ResponsiveContainer width="100%" height="100%"> ) : integrations.length === 0 ? (
<AreaChart data={trafficData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}> <p style={{ fontSize: '12px', color: '#7A7D85' }}>No integrations added yet add one in Settings.</p>
<defs> ) : (
<linearGradient id="trafficGold" x1="0" y1="0" x2="0" y2="1"> <div className="grid grid-cols-3 gap-3">
<stop offset="0%" stopColor="#C8A434" stopOpacity={0.25} /> {integrations.map((i) => (
<stop offset="100%" stopColor="#C8A434" stopOpacity={0.02} /> <div key={i.id} className="flex items-center gap-2.5" style={{ padding: '8px 10px', borderRadius: '8px', backgroundColor: 'rgba(255,255,255,0.02)' }}>
</linearGradient> <span style={{ width: '7px', height: '7px', borderRadius: '50%', backgroundColor: statusColor[i.status] ?? '#7A7D85', flexShrink: 0 }} />
<linearGradient id="trafficAmber" x1="0" y1="0" x2="0" y2="1"> <span style={{ fontSize: '13px', color: '#E8E6E0', flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{i.name}</span>
<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>
</div> </div>
))}
</div> </div>
)}
</div> </div>
</div> </div>
{/* Shortcuts — miniature control panels */} {/* Shortcuts */}
<div style={cardBase} className="hover:!border-gold/15"> <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' }} /> <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 ( return (
<button <button
key={item.label} 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" 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)' }} 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)' }} 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' import { useEffect, useState } from 'react'
import { CircleCheck, AlertTriangle, Plug, Bookmark as BookmarkIcon, LogIn } from 'lucide-react'
const resources = [ import { api, type Event, type Resource, type Integration } from '../lib/api'
{ 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' },
]
const cardBase: React.CSSProperties = { const cardBase: React.CSSProperties = {
backgroundColor: 'rgba(10, 10, 12, 0.92)', backgroundColor: 'rgba(10, 10, 12, 0.92)',
@ -37,109 +16,143 @@ const cardBase: React.CSSProperties = {
flexDirection: 'column', flexDirection: 'column',
} }
function getBarColor(percentage: number) { const statusColor: Record<string, string> = {
if (percentage >= 90) return '#E74C3C' healthy: '#C8A434',
if (percentage >= 70) return '#E67E22' warning: '#E67E22',
return '#C8A434' 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() { 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 ( return (
<div className="grid h-full w-full grid-cols-[1fr_1.4fr_1fr] gap-6"> <div className="grid h-full w-full grid-cols-[1fr_1.4fr_1fr] gap-6">
{/* Resource Overview */} {/* Resource Overview */}
<div style={cardBase} className="hover:!border-gold/15 group"> <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' }} /> <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 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="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, marginBottom: '16px' }}>
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
Resource Overview Resource Overview
</h3> </h3>
<button className="bg-transparent border-none cursor-pointer p-1" style={{ color: '#7A7D85' }}> {resources === null ? (
<X size={14} /> <p style={{ fontSize: '12px', color: '#7A7D85' }}>Loading</p>
</button> ) : 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>
<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> </div>
</div> </div>
{/* Recent Activity — visually dominant */} {/* Recent Activity */}
<div style={cardBase} className="hover:!border-gold/15 group"> <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' }} /> <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 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="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, marginBottom: '16px' }}>
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
Recent Activity Recent Activity
</h3> </h3>
<button className="bg-transparent border-none cursor-pointer p-1" style={{ color: '#7A7D85' }}> {events === null ? (
<X size={14} /> <p style={{ fontSize: '12px', color: '#7A7D85' }}>Loading</p>
</button> ) : events.length === 0 ? (
</div> <p style={{ fontSize: '12px', color: '#7A7D85' }}>No activity yet.</p>
) : (
<div className="flex flex-1 flex-col justify-around gap-3"> <div className="flex flex-1 flex-col justify-around gap-3">
{activities.map((item, i) => { {events.map((item) => {
const Icon = item.icon const Icon = eventIcons[item.type] ?? CircleCheck
return ( 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)' }}> <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' }} /> <Icon size={13} style={{ color: '#C8A434' }} />
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <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: '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> </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>
)}
</div> </div>
</div> </div>
{/* Top Alerts */} {/* Top Alerts */}
<div style={cardBase} className="hover:!border-gold/15 group"> <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 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="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, marginBottom: '16px' }}>
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
Top Alerts Top Alerts
</h3> </h3>
<a href="#" style={{ fontSize: '11px', color: '#C8A434', textDecoration: 'none' }}>View all</a> {erroredIntegrations.length === 0 && problemResources.length === 0 ? (
</div> <p style={{ fontSize: '12px', color: '#7A7D85' }}>No alerts everything connected is healthy.</p>
) : (
<div className="flex flex-1 flex-col justify-around gap-3"> <div className="flex flex-1 flex-col justify-around gap-3">
{alerts.map((alert, i) => ( {erroredIntegrations.map((i) => (
<div key={i} className="flex items-start gap-3"> <div key={`int-${i.id}`} 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)' }} /> <AlertTriangle size={14} style={{ color: '#E74C3C', flexShrink: 0, marginTop: '2px' }} />
<div style={{ flex: 1, minWidth: 0 }}> <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: '13px', color: '#E8E6E0', fontWeight: 500 }}>Connection failing</p>
<p style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{alert.source}</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> </div>
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{alert.time}</span>
</div> </div>
))} ))}
</div> </div>
)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import { Server, Shield, Network } from 'lucide-react' import { useEffect, useState } from 'react'
import SparklineChart from './SparklineChart' import { Server, Plug, BookMarked } from 'lucide-react'
import ProgressRing from './ProgressRing' import ProgressRing from './ProgressRing'
import { api, type Integration, type Resource, type Bookmark } from '../lib/api'
const cardStyle: React.CSSProperties = { const cardStyle: React.CSSProperties = {
backgroundColor: 'rgba(10, 10, 12, 0.55)', backgroundColor: 'rgba(10, 10, 12, 0.55)',
@ -12,92 +13,96 @@ const cardStyle: React.CSSProperties = {
transition: 'border-color 0.2s ease', 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() { 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 ( return (
<div className="grid w-full grid-cols-4 gap-5"> <div className="grid w-full grid-cols-4 gap-5">
{/* System Status */} {/* System Status */}
<div style={cardStyle} className="hover:!border-gold/20"> <div style={cardStyle} className="hover:!border-gold/20">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div> <div>
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}> <h3 style={labelStyle}>System Status</h3>
System Status <p style={{ fontSize: '16px', fontWeight: 700, color: errored > 0 ? '#E74C3C' : '#2ECC71', lineHeight: 1.3 }}>{systemLabel}</p>
</h3> <p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>{connected} of {total} integrations connected</p>
<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>
</div> </div>
<ProgressRing percentage={100} size={44} strokeWidth={3} /> <ProgressRing percentage={systemPercent} 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>
</div> </div>
</div> </div>
{/* Infrastructure */} {/* Infrastructure */}
<div style={cardStyle} className="hover:!border-gold/20"> <div style={cardStyle} className="hover:!border-gold/20">
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}> <h3 style={labelStyle}>Infrastructure</h3>
Infrastructure
</h3>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Server size={16} style={{ color: '#C8A434' }} /> <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> </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"> <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 className="flex items-center gap-1">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#2ECC71' }} /> <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>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E67E22' }} /> <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>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E74C3C' }} /> <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> </span>
</div> </div>
</div> </div>
{/* Security */} {/* Integrations */}
<div style={cardStyle} className="hover:!border-gold/20"> <div style={cardStyle} className="hover:!border-gold/20">
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}> <h3 style={labelStyle}>Integrations</h3>
Security
</h3>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Shield size={16} style={{ color: '#C8A434' }} /> <Plug size={16} style={{ color: '#C8A434' }} />
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>2</span> <span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>{connected}/{total}</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>
</div> </div>
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Connected services</p>
</div> </div>
{/* Network */} {/* Bookmarks */}
<div style={cardStyle} className="hover:!border-gold/20"> <div style={cardStyle} className="hover:!border-gold/20">
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}> <h3 style={labelStyle}>Bookmarks</h3>
Network
</h3>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Network size={16} style={{ color: '#C8A434' }} /> <BookMarked size={16} style={{ color: '#C8A434' }} />
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>98.7%</span> <span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>{bookmarks?.length ?? 0}</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 />
</div> </div>
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>{favorites} favorited</p>
</div> </div>
</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 }>) => 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) }), apiFetch<{ ok: boolean }>(`/bookmarks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
deleteBookmark: (id: number) => apiFetch<void>(`/bookmarks/${id}`, { method: 'DELETE' }), 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 { export interface Integration {
@ -98,3 +101,18 @@ export interface BookmarkCategory {
icon: string | null icon: string | null
sort_order: number 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 { useEffect, useMemo, useState } from 'react'
import { PieChart, Pie, Cell, ResponsiveContainer, LineChart, Line, XAxis } from 'recharts' import { useNavigate } from 'react-router-dom'
import { Plus, Server, Activity, AlertTriangle, DollarSign } from 'lucide-react' 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 subTabs = ['Overview']
const futureSubTabs = ['Network'] 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 = { const cardBase: React.CSSProperties = {
backgroundColor: 'rgba(10, 10, 12, 0.92)', backgroundColor: 'rgba(10, 10, 12, 0.92)',
border: '1px solid rgba(200, 164, 52, 0.08)', border: '1px solid rgba(200, 164, 52, 0.08)',
@ -108,17 +59,21 @@ const cardDim: React.CSSProperties = {
backgroundColor: 'rgba(8, 8, 10, 0.45)', backgroundColor: 'rgba(8, 8, 10, 0.45)',
} }
const distributionData = [ const nodeStatusColor: Record<string, string> = {
{ name: 'Compute', value: 42, color: '#C8A434' }, healthy: '#2ECC71',
{ name: 'Storage', value: 28, color: '#E67E22' }, warning: '#E67E22',
{ name: 'Database', value: 18, color: '#2ECC71' }, critical: '#E74C3C',
{ name: 'Network', value: 12, color: '#7A7D85' }, unknown: '#7A7D85',
] }
const integrationPalette = ['#C8A434', '#E67E22', '#2ECC71', '#7A7D85', '#3B82F6', '#8B5E3C']
function Donut({ data, centerLabel }: { data: { name: string; value: number; color: string }[]; centerLabel?: string }) { function Donut({ data, centerLabel }: { data: { name: string; value: number; color: string }[]; centerLabel?: string }) {
const hasData = data.some((d) => d.value > 0)
return ( return (
<div className="flex flex-1 items-center gap-4"> <div className="flex flex-1 items-center gap-4">
<div className="relative" style={{ width: '120px', height: '120px', flexShrink: 0 }}> <div className="relative" style={{ width: '120px', height: '120px', flexShrink: 0 }}>
{hasData && (
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Pie data={data} dataKey="value" innerRadius={38} outerRadius={56} paddingAngle={2} isAnimationActive animationDuration={1000}> <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> </Pie>
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
)}
{centerLabel && ( {centerLabel && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none"> <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<span style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>{centerLabel}</span> <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) => ( {data.map((entry) => (
<div key={entry.name} className="flex items-center gap-2"> <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={{ 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: '#E8E6E0', width: '90px' }}>{entry.name}</span>
<span style={{ fontSize: '12px', color: '#7A7D85' }}>{entry.value}%</span> <span style={{ fontSize: '12px', color: '#7A7D85' }}>{entry.value}</span>
</div> </div>
))} ))}
</div> </div>
@ -149,6 +105,35 @@ function Donut({ data, centerLabel }: { data: { name: string; value: number; col
export default function Infrastructure() { export default function Infrastructure() {
const [activeTab, setActiveTab] = useState('Overview') 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 ( return (
<> <>
@ -194,6 +179,7 @@ export default function Infrastructure() {
))} ))}
</div> </div>
<button <button
onClick={() => navigate('/settings')}
className="flex items-center gap-2 cursor-pointer transition-colors whitespace-nowrap" className="flex items-center gap-2 cursor-pointer transition-colors whitespace-nowrap"
style={{ style={{
fontSize: '12px', fontSize: '12px',
@ -212,7 +198,7 @@ export default function Infrastructure() {
</div> </div>
{/* Status Cards */} {/* 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) => { {statusCards.map((card) => {
const Icon = card.icon const Icon = card.icon
return ( return (
@ -244,7 +230,11 @@ export default function Infrastructure() {
<div className="relative z-10 flex flex-1 flex-col"> <div className="relative z-10 flex flex-1 flex-col">
<h3 style={sectionTitle}>Resource Distribution</h3> <h3 style={sectionTitle}>Resource Distribution</h3>
<div className="flex flex-1 flex-col items-center justify-center"> <div className="flex flex-1 flex-col items-center justify-center">
{distributionData.length > 0 ? (
<Donut data={distributionData} /> <Donut data={distributionData} />
) : (
<p style={{ fontSize: '12px', color: '#7A7D85', textAlign: 'center' }}>Connect an integration in Settings to see resource distribution.</p>
)}
</div> </div>
</div> </div>
</div> </div>
@ -255,11 +245,12 @@ export default function Infrastructure() {
<div style={cardVignette} /> <div style={cardVignette} />
<div className="relative z-10 flex flex-1 flex-col"> <div className="relative z-10 flex flex-1 flex-col">
<h3 style={sectionTitle}>Node Status</h3> <h3 style={sectionTitle}>Node Status</h3>
{resources && resources.length > 0 ? (
<div className="grid flex-1 grid-cols-4 gap-3 content-center"> <div className="grid flex-1 grid-cols-4 gap-3 content-center">
{nodes.map((node) => ( {resources.map((node, i) => (
<div <div
key={node.name} key={i}
title={`${node.name}: ${node.status}`} title={`${node.name}: ${node.detail ?? node.status}`}
style={{ style={{
backgroundColor: 'rgba(10, 10, 12, 0.55)', backgroundColor: 'rgba(10, 10, 12, 0.55)',
border: `1px solid ${nodeStatusColor[node.status]}33`, border: `1px solid ${nodeStatusColor[node.status]}33`,
@ -278,6 +269,11 @@ export default function Infrastructure() {
</div> </div>
))} ))}
</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> </div>
</div> </div>
@ -285,30 +281,26 @@ export default function Infrastructure() {
{/* Bottom Row */} {/* Bottom Row */}
<div className="shrink-0"> <div className="shrink-0">
<div className="grid w-full grid-cols-[1.4fr_1fr_1fr] gap-6"> <div className="grid w-full grid-cols-2 gap-6">
{/* Resource Trend */} {/* Integration Health */}
<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 style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15"> <div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
<div className="relative z-10 flex flex-col"> <div className="relative z-10 flex flex-col">
<h3 style={sectionTitle}>Cost Breakdown (MTD)</h3> <h3 style={sectionTitle}>Integration Health</h3>
<Donut data={costData} centerLabel="$24.5K" /> {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>
</div> </div>
@ -316,17 +308,19 @@ export default function Infrastructure() {
<div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15"> <div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
<div className="relative z-10"> <div className="relative z-10">
<h3 style={sectionTitle}>Recent Activity</h3> <h3 style={sectionTitle}>Recent Activity</h3>
{events && events.length > 0 ? (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{activities.map((item, i) => ( {events.map((item) => (
<div key={i} className="flex items-start justify-between gap-3"> <div key={item.id} className="flex items-start justify-between gap-3">
<div style={{ flex: 1, minWidth: 0 }}> <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: '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> </div>
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{item.time}</span>
</div> </div>
))} ))}
</div> </div>
) : (
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No activity yet.</p>
)}
</div> </div>
</div> </div>
</div> </div>
@ -342,13 +336,14 @@ export default function Infrastructure() {
borderTop: '1px solid rgba(200,164,52,0.08)', borderTop: '1px solid rgba(200,164,52,0.08)',
}} }}
> >
<span>AWS</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span> <span>{total} Resources</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>{integrations?.filter((i) => i.status === 'connected').length ?? 0} Integrations Connected</span>
<span>18 AZs</span><span style={{ color: 'rgba(200,164,52,0.3)' }}>|</span> {critical > 0 && (
<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 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: '#E74C3C' }}>{critical} Critical</span>
<span style={{ color: '#E67E22' }}>2 Alerts</span> </>
)}
</div> </div>
</> </>
) )