2026-06-18 19:56:10 +00:00
|
|
|
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'
|
2026-06-18 16:15:34 +00:00
|
|
|
|
2026-06-18 16:41:45 +00:00
|
|
|
const subTabs = ['Overview']
|
2026-06-18 18:02:09 +00:00
|
|
|
const futureSubTabs = ['Network']
|
2026-06-18 16:15:34 +00:00
|
|
|
|
|
|
|
|
const cardBase: React.CSSProperties = {
|
|
|
|
|
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
|
|
|
|
border: '1px solid rgba(200, 164, 52, 0.08)',
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
padding: '20px',
|
|
|
|
|
boxShadow: '0 0 20px rgba(200, 164, 52, 0.03)',
|
|
|
|
|
transition: 'border-color 0.2s ease',
|
|
|
|
|
position: 'relative',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
height: '100%',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sectionTitle: React.CSSProperties = {
|
|
|
|
|
fontSize: '11px',
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
letterSpacing: '1.5px',
|
|
|
|
|
color: '#7A7D85',
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 17:06:53 +00:00
|
|
|
function framedCard(bgUrl: string): React.CSSProperties {
|
|
|
|
|
return {
|
|
|
|
|
backgroundImage: `url(${bgUrl})`,
|
|
|
|
|
backgroundSize: '100% 100%',
|
|
|
|
|
backgroundPosition: 'center',
|
|
|
|
|
backgroundRepeat: 'no-repeat',
|
|
|
|
|
position: 'relative',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
height: '100%',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
padding: '20px 20px 64px 20px',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 17:47:09 +00:00
|
|
|
const cardVignette: React.CSSProperties = {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
inset: 0,
|
|
|
|
|
pointerEvents: 'none',
|
|
|
|
|
background: 'radial-gradient(ellipse closest-side at center, transparent 70%, var(--color-page) 100%)',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cardDim: React.CSSProperties = {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
inset: 0,
|
|
|
|
|
pointerEvents: 'none',
|
|
|
|
|
backgroundColor: 'rgba(8, 8, 10, 0.45)',
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 19:56:10 +00:00
|
|
|
const nodeStatusColor: Record<string, string> = {
|
|
|
|
|
healthy: '#2ECC71',
|
|
|
|
|
warning: '#E67E22',
|
|
|
|
|
critical: '#E74C3C',
|
|
|
|
|
unknown: '#7A7D85',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const integrationPalette = ['#C8A434', '#E67E22', '#2ECC71', '#7A7D85', '#3B82F6', '#8B5E3C']
|
2026-06-18 17:26:39 +00:00
|
|
|
|
2026-06-18 16:15:34 +00:00
|
|
|
function Donut({ data, centerLabel }: { data: { name: string; value: number; color: string }[]; centerLabel?: string }) {
|
2026-06-18 19:56:10 +00:00
|
|
|
const hasData = data.some((d) => d.value > 0)
|
2026-06-18 16:15:34 +00:00
|
|
|
return (
|
|
|
|
|
<div className="flex flex-1 items-center gap-4">
|
|
|
|
|
<div className="relative" style={{ width: '120px', height: '120px', flexShrink: 0 }}>
|
2026-06-18 19:56:10 +00:00
|
|
|
{hasData && (
|
|
|
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
|
|
|
<PieChart>
|
|
|
|
|
<Pie data={data} dataKey="value" innerRadius={38} outerRadius={56} paddingAngle={2} isAnimationActive animationDuration={1000}>
|
|
|
|
|
{data.map((entry) => (
|
|
|
|
|
<Cell key={entry.name} fill={entry.color} stroke="none" />
|
|
|
|
|
))}
|
|
|
|
|
</Pie>
|
|
|
|
|
</PieChart>
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
)}
|
2026-06-18 16:15:34 +00:00
|
|
|
{centerLabel && (
|
|
|
|
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
|
|
|
<span style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>{centerLabel}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
{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 }} />
|
2026-06-18 19:56:10 +00:00
|
|
|
<span style={{ fontSize: '12px', color: '#E8E6E0', width: '90px' }}>{entry.name}</span>
|
|
|
|
|
<span style={{ fontSize: '12px', color: '#7A7D85' }}>{entry.value}</span>
|
2026-06-18 16:15:34 +00:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function Infrastructure() {
|
|
|
|
|
const [activeTab, setActiveTab] = useState('Overview')
|
2026-06-18 19:56:10 +00:00
|
|
|
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])
|
2026-06-18 16:15:34 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2026-06-18 16:50:06 +00:00
|
|
|
{/* Sub-tabs + Add Resource — hero banner is rendered at the layout level behind this */}
|
|
|
|
|
<div className="flex items-center justify-between shrink-0">
|
|
|
|
|
<div className="flex items-center gap-1 overflow-x-auto" style={{ scrollbarWidth: 'none' }}>
|
|
|
|
|
{subTabs.map((tab) => {
|
|
|
|
|
const active = tab === activeTab
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={tab}
|
|
|
|
|
onClick={() => setActiveTab(tab)}
|
|
|
|
|
className="cursor-pointer bg-transparent border-none transition-colors whitespace-nowrap"
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
padding: '8px 14px',
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
color: active ? '#C8A434' : '#7A7D85',
|
|
|
|
|
backgroundColor: active ? 'rgba(200,164,52,0.1)' : 'transparent',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{tab}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
})}
|
2026-06-18 18:02:09 +00:00
|
|
|
{futureSubTabs.map((tab) => (
|
|
|
|
|
<button
|
|
|
|
|
key={tab}
|
|
|
|
|
disabled
|
|
|
|
|
title="Coming soon"
|
|
|
|
|
className="cursor-not-allowed bg-transparent border-none whitespace-nowrap"
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
padding: '8px 14px',
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
color: '#4A4D55',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{tab}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
2026-06-18 16:50:06 +00:00
|
|
|
</div>
|
|
|
|
|
<button
|
2026-06-18 19:56:10 +00:00
|
|
|
onClick={() => navigate('/settings')}
|
2026-06-18 16:50:06 +00:00
|
|
|
className="flex items-center gap-2 cursor-pointer transition-colors whitespace-nowrap"
|
2026-06-18 16:15:34 +00:00
|
|
|
style={{
|
2026-06-18 16:50:06 +00:00
|
|
|
fontSize: '12px',
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
color: '#0A0B0D',
|
|
|
|
|
backgroundColor: '#C8A434',
|
|
|
|
|
border: 'none',
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
padding: '9px 16px',
|
|
|
|
|
boxShadow: '0 0 14px rgba(200,164,52,0.2)',
|
2026-06-18 16:15:34 +00:00
|
|
|
}}
|
2026-06-18 16:50:06 +00:00
|
|
|
>
|
|
|
|
|
<Plus size={14} />
|
|
|
|
|
Add Resource
|
|
|
|
|
</button>
|
2026-06-18 16:15:34 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Status Cards */}
|
2026-06-18 19:56:10 +00:00
|
|
|
<div className="grid w-full grid-cols-4 gap-5 shrink-0" style={{ marginTop: '8px', height: '110px' }}>
|
2026-06-18 16:15:34 +00:00
|
|
|
{statusCards.map((card) => {
|
|
|
|
|
const Icon = card.icon
|
|
|
|
|
return (
|
2026-06-18 17:39:47 +00:00
|
|
|
<div
|
|
|
|
|
key={card.label}
|
|
|
|
|
style={{ ...cardBase, backgroundColor: 'rgba(10, 10, 12, 0.5)', padding: '18px', justifyContent: 'center', alignItems: 'center', gap: '8px' }}
|
|
|
|
|
className="hover:!border-gold/20"
|
|
|
|
|
>
|
|
|
|
|
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, textAlign: 'center' }}>
|
2026-06-18 17:28:52 +00:00
|
|
|
{card.label}
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="flex items-center gap-2">
|
2026-06-18 17:39:47 +00:00
|
|
|
<Icon size={18} style={{ color: card.color ?? '#C8A434' }} />
|
|
|
|
|
<span style={{ fontSize: '26px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>{card.value}</span>
|
2026-06-18 16:15:34 +00:00
|
|
|
</div>
|
2026-06-18 17:39:47 +00:00
|
|
|
<p style={{ fontSize: '10px', color: card.color ?? '#7A7D85', textAlign: 'center' }}>{card.sub}</p>
|
2026-06-18 16:15:34 +00:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Middle Row */}
|
|
|
|
|
<div className="min-h-0 flex-1">
|
2026-06-18 17:26:39 +00:00
|
|
|
<div className="grid h-full w-full grid-cols-[1fr_1.6fr] gap-6">
|
|
|
|
|
{/* Resource Distribution */}
|
2026-06-18 17:32:11 +00:00
|
|
|
<div style={framedCard('/blank-kpi-bg.png')}>
|
2026-06-18 17:47:09 +00:00
|
|
|
<div style={cardDim} />
|
|
|
|
|
<div style={cardVignette} />
|
2026-06-18 17:32:11 +00:00
|
|
|
<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">
|
2026-06-18 19:56:10 +00:00
|
|
|
{distributionData.length > 0 ? (
|
|
|
|
|
<Donut data={distributionData} />
|
|
|
|
|
) : (
|
|
|
|
|
<p style={{ fontSize: '12px', color: '#7A7D85', textAlign: 'center' }}>Connect an integration in Settings to see resource distribution.</p>
|
|
|
|
|
)}
|
2026-06-18 17:32:11 +00:00
|
|
|
</div>
|
2026-06-18 17:26:39 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-18 17:56:11 +00:00
|
|
|
{/* Node Status — expanded */}
|
2026-06-18 17:32:11 +00:00
|
|
|
<div style={framedCard('/blank-kpi-bg.png')}>
|
2026-06-18 17:47:09 +00:00
|
|
|
<div style={cardDim} />
|
|
|
|
|
<div style={cardVignette} />
|
2026-06-18 16:15:34 +00:00
|
|
|
<div className="relative z-10 flex flex-1 flex-col">
|
2026-06-18 17:56:11 +00:00
|
|
|
<h3 style={sectionTitle}>Node Status</h3>
|
2026-06-18 19:56:10 +00:00
|
|
|
{resources && resources.length > 0 ? (
|
Containers/Infrastructure styling fixes + Terminal Nerd Font fallback (#36)
* Add mesh prerequisite gate (NetBird verification before app config)
Implements the design in docs/mesh-prerequisite-gate.md per the user's
DECIDE A-D answers: a permanent admin override, B1 (reachable) verification
with host mesh IP shown informationally, members allowed in with a notice
instead of being blocked, and mesh.required defaulting off so the live
production instance is unaffected.
- system_config kv table + getConfig/setConfig helpers
- /api/system/mesh-status, /mesh/verify, /mesh/override, /mesh/required
- AuthContext gains a 'needs-mesh' status (admins only) and exposes
meshStatus for a member-facing banner
- MeshGate page reuses the integration create+test flow to connect NetBird
* Make mesh verification universal (CIDR check, not NetBird-specific)
Replace the NetBird-adapter-based "reachable" check with a vendor-agnostic
one: the admin supplies the mesh's IP range (CIDR), and verification just
confirms this host has an address inside it. Works identically for
NetBird, WireGuard, ZeroTier, Tailscale, or any other mesh tech, with no
integration record or vendor API call required.
* Add reachability fallback for routed meshes (VPC peering, etc.)
A host can be on the mesh's "side" of a routed network (e.g. a VPC peered
into a NetBird/WireGuard mesh) without holding a local IP in the mesh's
own CIDR. Local-IP-in-CIDR stays the primary check; if it fails, the admin
can supply a known peer/gateway IP on the mesh and we verify by pinging
it instead. Adds iputils to the backend image for the ping binary.
* Add Mesh section to Settings for configuring/testing the mesh gate
Admins can now toggle mesh.required, run verify/override, and see
current mesh status entirely from the app, without hitting the API
directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hu9pZvJY4BgmcQeAw2ugk
* Show a host-specific Docker remote-API setup script in Settings
When adding/editing a Docker integration with a tcp:// or http:// remote
URL, display a copyable systemd override + curl verification script
scoped to the entered host:port, so enabling the daemon's API doesn't
require looking up the steps separately.
* Expand Help page with quick-start guide and real-world examples
Adds a quick-start ordering card and per-feature example callouts (with icons) so first-time users see concrete use cases, not just descriptions.
* Update HANDOFF/README for handoff: mesh gate shipped, Docker UX work, no feature queued
Corrects the stale 'mesh gate not built' framing (it shipped across 4 commits, all merged) and documents the Docker setup-script hint + Help page expansion done this session. Leaves a clear next-task list for the picking-up agent: decide on merging claude/youthful-cerf-ibvxfb, then check with the user for the next priority.
* Improve Containers table/tab readability: bold centered headers, taller rows, filing-cabinet tabs
* Make Node Status card scrollable with a 5-column layout and invisible-by-default scrollbar
* Add Nerd Font icon fallback to the Terminal so Starship-style prompts render correctly
Bundles Symbols Nerd Font Mono (MIT, ryanoasis/nerd-fonts) as a glyph-only @font-face and appends it to every Terminal font-family option, so distro icons / git branch glyphs / etc. from prompts like Starship show up instead of broken-glyph boxes. It carries no letterforms, so it never changes how normal text renders.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-21 05:01:39 -04:00
|
|
|
<div className="scrollbar-ghost grid min-h-0 flex-1 grid-cols-5 content-start gap-2" style={{ overflowY: 'auto' }}>
|
2026-06-18 19:56:10 +00:00
|
|
|
{resources.map((node, i) => (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
title={`${node.name}: ${node.detail ?? node.status}`}
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: 'rgba(10, 10, 12, 0.55)',
|
|
|
|
|
border: `1px solid ${nodeStatusColor[node.status]}33`,
|
|
|
|
|
borderRadius: '8px',
|
Containers/Infrastructure styling fixes + Terminal Nerd Font fallback (#36)
* Add mesh prerequisite gate (NetBird verification before app config)
Implements the design in docs/mesh-prerequisite-gate.md per the user's
DECIDE A-D answers: a permanent admin override, B1 (reachable) verification
with host mesh IP shown informationally, members allowed in with a notice
instead of being blocked, and mesh.required defaulting off so the live
production instance is unaffected.
- system_config kv table + getConfig/setConfig helpers
- /api/system/mesh-status, /mesh/verify, /mesh/override, /mesh/required
- AuthContext gains a 'needs-mesh' status (admins only) and exposes
meshStatus for a member-facing banner
- MeshGate page reuses the integration create+test flow to connect NetBird
* Make mesh verification universal (CIDR check, not NetBird-specific)
Replace the NetBird-adapter-based "reachable" check with a vendor-agnostic
one: the admin supplies the mesh's IP range (CIDR), and verification just
confirms this host has an address inside it. Works identically for
NetBird, WireGuard, ZeroTier, Tailscale, or any other mesh tech, with no
integration record or vendor API call required.
* Add reachability fallback for routed meshes (VPC peering, etc.)
A host can be on the mesh's "side" of a routed network (e.g. a VPC peered
into a NetBird/WireGuard mesh) without holding a local IP in the mesh's
own CIDR. Local-IP-in-CIDR stays the primary check; if it fails, the admin
can supply a known peer/gateway IP on the mesh and we verify by pinging
it instead. Adds iputils to the backend image for the ping binary.
* Add Mesh section to Settings for configuring/testing the mesh gate
Admins can now toggle mesh.required, run verify/override, and see
current mesh status entirely from the app, without hitting the API
directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hu9pZvJY4BgmcQeAw2ugk
* Show a host-specific Docker remote-API setup script in Settings
When adding/editing a Docker integration with a tcp:// or http:// remote
URL, display a copyable systemd override + curl verification script
scoped to the entered host:port, so enabling the daemon's API doesn't
require looking up the steps separately.
* Expand Help page with quick-start guide and real-world examples
Adds a quick-start ordering card and per-feature example callouts (with icons) so first-time users see concrete use cases, not just descriptions.
* Update HANDOFF/README for handoff: mesh gate shipped, Docker UX work, no feature queued
Corrects the stale 'mesh gate not built' framing (it shipped across 4 commits, all merged) and documents the Docker setup-script hint + Help page expansion done this session. Leaves a clear next-task list for the picking-up agent: decide on merging claude/youthful-cerf-ibvxfb, then check with the user for the next priority.
* Improve Containers table/tab readability: bold centered headers, taller rows, filing-cabinet tabs
* Make Node Status card scrollable with a 5-column layout and invisible-by-default scrollbar
* Add Nerd Font icon fallback to the Terminal so Starship-style prompts render correctly
Bundles Symbols Nerd Font Mono (MIT, ryanoasis/nerd-fonts) as a glyph-only @font-face and appends it to every Terminal font-family option, so distro icons / git branch glyphs / etc. from prompts like Starship show up instead of broken-glyph boxes. It carries no letterforms, so it never changes how normal text renders.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-21 05:01:39 -04:00
|
|
|
padding: '8px 10px',
|
2026-06-18 19:56:10 +00:00
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
gap: '6px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<span style={{ width: '7px', height: '7px', borderRadius: '50%', backgroundColor: nodeStatusColor[node.status], boxShadow: `0 0 6px ${nodeStatusColor[node.status]}` }} />
|
|
|
|
|
<Server size={12} style={{ color: '#7A7D85' }} />
|
|
|
|
|
</div>
|
|
|
|
|
<span style={{ fontSize: '11px', color: '#E8E6E0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{node.name}</span>
|
2026-06-18 17:56:11 +00:00
|
|
|
</div>
|
2026-06-18 19:56:10 +00:00
|
|
|
))}
|
|
|
|
|
</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>
|
|
|
|
|
)}
|
2026-06-18 16:15:34 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Bottom Row */}
|
|
|
|
|
<div className="shrink-0">
|
2026-06-18 19:56:10 +00:00
|
|
|
<div className="grid w-full grid-cols-2 gap-6">
|
|
|
|
|
{/* Integration Health */}
|
2026-06-20 07:15:32 -04:00
|
|
|
<div style={{ ...framedCard('/archnest-network-traffic-bg.png'), height: 'auto', padding: '20px' }} className="hover:!border-gold/15">
|
2026-06-20 06:54:21 -04:00
|
|
|
<div style={cardDim} />
|
|
|
|
|
<div style={cardVignette} />
|
2026-06-18 16:15:34 +00:00
|
|
|
<div className="relative z-10 flex flex-col">
|
2026-06-18 19:56:10 +00:00
|
|
|
<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>
|
|
|
|
|
)}
|
2026-06-18 16:15:34 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Recent Activity */}
|
|
|
|
|
<div style={{ ...cardBase, height: 'auto' }} className="hover:!border-gold/15">
|
|
|
|
|
<div className="relative z-10">
|
|
|
|
|
<h3 style={sectionTitle}>Recent Activity</h3>
|
2026-06-18 19:56:10 +00:00
|
|
|
{events && events.length > 0 ? (
|
|
|
|
|
<div className="flex flex-col 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>
|
|
|
|
|
</div>
|
2026-06-18 16:15:34 +00:00
|
|
|
</div>
|
2026-06-18 19:56:10 +00:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<p style={{ fontSize: '12px', color: '#7A7D85' }}>No activity yet.</p>
|
|
|
|
|
)}
|
2026-06-18 16:15:34 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Footer stats bar */}
|
|
|
|
|
<div
|
|
|
|
|
className="shrink-0 flex items-center justify-center gap-3"
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: '11px',
|
|
|
|
|
color: '#7A7D85',
|
|
|
|
|
padding: '8px 0',
|
|
|
|
|
borderTop: '1px solid rgba(200,164,52,0.08)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-06-18 19:56:10 +00:00
|
|
|
<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>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-06-18 16:15:34 +00:00
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|