File size: 15,932 Bytes
23b413b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
/**
 * SystemStatusDialog β€” live health dashboard for HomePilot services.
 *
 * Shows a donut chart (pure CSS conic-gradient), service health cards,
 * and architecture overview.  No chart library needed.
 */
import React, { useEffect, useMemo, useState } from 'react'
import { X, Activity, Database, Cpu, Server, Bot, PlugZap } from 'lucide-react'
import { fetchSystemOverview, type SystemOverviewResponse } from './systemApi'
import SystemResourcesCard from './SystemResourcesCard'

/* ── helpers ───────────────────────────────────────────── */

function formatUptime(sec: number) {
  const h = Math.floor(sec / 3600)
  const m = Math.floor((sec % 3600) / 60)
  return `${h}h ${m}m`
}

function statusTone(ok?: boolean) {
  if (ok) return 'text-emerald-300/90 border-emerald-500/15 bg-emerald-500/8'
  return 'text-red-300/80 border-red-400/15 bg-red-500/6'
}

const SERVICE_LABELS: Record<string, string> = {
  backend: 'Backend',
  ollama: 'Ollama',
  avatar_svc: 'Avatar Svc',
  comfyui: 'ComfyUI',
  forge: 'ContextForge',
  sqlite: 'SQLite',
}

/* ── main component ────────────────────────────────────── */

export default function SystemStatusDialog({
  backendUrl,
  apiKey,
  onClose,
}: {
  backendUrl: string
  apiKey?: string
  onClose: () => void
}) {
  const [data, setData] = useState<SystemOverviewResponse | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  /* Escape to close */
  useEffect(() => {
    const onEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
    document.addEventListener('keydown', onEsc)
    return () => document.removeEventListener('keydown', onEsc)
  }, [onClose])

  /* Fetch data */
  useEffect(() => {
    let mounted = true
    ;(async () => {
      try {
        setLoading(true)
        const result = await fetchSystemOverview(backendUrl, apiKey)
        if (mounted) setData(result)
      } catch (e: any) {
        if (mounted) setError(e?.message || 'Failed to load system status')
      } finally {
        if (mounted) setLoading(false)
      }
    })()
    return () => { mounted = false }
  }, [backendUrl, apiKey])

  /* Donut gradient β€” glow only on the green arc */
  const healthyPct = data
    ? Math.round((data.overview.healthy_services / Math.max(data.overview.total_services, 1)) * 100)
    : 0
  const donut = useMemo(() => {
    if (!data) return 'conic-gradient(#374151 0% 100%)'
    return `conic-gradient(#34d399 0% ${healthyPct}%, rgba(255,255,255,0.06) ${healthyPct}% 100%)`
  }, [data, healthyPct])

  const handleBackdrop = (e: React.MouseEvent) => { if (e.target === e.currentTarget) onClose() }

  return (
    <div
      className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm"
      onClick={handleBackdrop}
      style={{ animation: 'statusFadeIn 200ms ease-out' }}
    >
      <div
        className="relative w-[960px] max-w-[96vw] max-h-[92vh] overflow-hidden rounded-3xl border border-white/[0.07] bg-[#0c0c18] shadow-2xl"
        style={{ animation: 'statusSlideUp 250ms ease-out' }}
      >
        {/* Close β€” smaller, subtler, brighter on hover */}
        <button
          type="button"
          onClick={onClose}
          className="absolute top-4 right-4 z-10 h-7 w-7 rounded-full grid place-items-center text-white/25 hover:text-white/80 hover:bg-white/8 transition-colors"
        >
          <X size={14} />
        </button>

        {/* Accent bar */}
        <div className="h-1 w-full bg-gradient-to-r from-emerald-500 via-cyan-500 to-blue-500" />

        <div className="px-8 pt-7 pb-8 overflow-y-auto max-h-[calc(92vh-4px)]">
          {/* Header */}
          <div className="flex items-start justify-between gap-6 mb-7">
            <div>
              <div className="text-[10px] uppercase tracking-[0.25em] text-white/25 mb-2">System Overview</div>
              <h2 className="text-3xl font-bold text-white tracking-tight">HomePilot Runtime</h2>
              <p className="text-sm text-white/40 mt-1.5">Live health dashboard for your backend, models, and services.</p>
            </div>
            {data && (
              <div className="px-3 py-1 rounded-full text-xs font-semibold border text-emerald-300/80 border-emerald-500/15 bg-emerald-500/8">
                v{data.overview.version}
              </div>
            )}
          </div>

          {loading ? (
            <div className="text-white/50 text-sm py-12 text-center">Loading system status...</div>
          ) : error ? (
            <div className="text-red-300/80 text-sm py-12 text-center">{error}</div>
          ) : data ? (
            <>
              {/* Top metrics β€” dimmer labels, more spacing */}
              <div className="grid grid-cols-4 gap-4 mb-7">
                <MetricCard label="Uptime" value={formatUptime(data.overview.uptime_seconds)} delay={0} />
                <MetricCard label="Healthy Services" value={`${data.overview.healthy_services}/${data.overview.total_services}`} delay={60} />
                <MetricCard label="Avg Latency" value={`${data.overview.avg_latency_ms}ms`} delay={120} />
                <MetricCard label="Active Entities" value={`${data.overview.active_entities}`} delay={180} />
              </div>

              {/* Machine Capacity β€” additive, fails gracefully */}
              <SystemResourcesCard backendUrl={backendUrl} apiKey={apiKey} />

              {/* Donut + Architecture */}
              <div className="grid grid-cols-[280px_1fr] gap-6 mb-7">
                {/* Donut chart β€” flat card, no nested box */}
                <div className="rounded-3xl border border-white/[0.07] bg-white/[0.02] px-6 pt-5 pb-6 flex flex-col items-center">
                  <div className="text-sm font-semibold text-white/90 self-start mb-5">Service Stability</div>

                  {/* Donut: thinner stroke, more center room */}
                  <div className="relative h-40 w-40 mb-1">
                    {/* Green arc glow (behind the donut) */}
                    <div
                      className="absolute inset-[-4px] rounded-full blur-md opacity-30 donut-ring"
                      style={{ background: `conic-gradient(rgba(52,211,153,0.5) 0% ${healthyPct}%, transparent ${healthyPct}% 100%)` }}
                    />
                    {/* Donut track */}
                    <div className="absolute inset-0 rounded-full donut-ring" style={{ background: donut }} />
                    {/* Thick cutout β€” thinner ring (inset-[14px] β†’ thinner stroke) */}
                    <div className="absolute inset-[14px] rounded-full bg-[#0c0c18]" />
                    {/* Center text */}
                    <div className="absolute inset-0 grid place-items-center">
                      <div className="text-center">
                        <div className="text-3xl font-bold text-white tracking-tight">{healthyPct}%</div>
                        <div className="text-[11px] text-white/30 mt-0.5">healthy</div>
                        <div className="text-[10px] text-white/20 mt-0.5">
                          {data.overview.healthy_services} of {data.overview.total_services} online
                        </div>
                      </div>
                    </div>
                  </div>

                  {/* Legend β€” smaller, dimmer, more spacing from donut */}
                  <div className="flex items-center gap-5 mt-5 text-[11px]">
                    <Legend color="bg-emerald-400/80" label="Running" />
                    <Legend color="bg-white/15" label="Down" />
                  </div>
                </div>

                {/* Architecture flow β€” more gap, padding, dimmer descriptions */}
                <div className="rounded-3xl border border-white/[0.07] bg-white/[0.02] p-6">
                  <div className="text-sm font-semibold text-white/90 mb-5">Architecture Flow</div>
                  <div className="grid grid-cols-4 gap-5 text-sm">
                    <FlowCard
                      title="Inputs"
                      icon={<PlugZap size={14} />}
                      items={[`${data.architecture.inputs.virtual_servers_active}/${data.architecture.inputs.virtual_servers_total} virtual servers`]}
                    />
                    <FlowCard
                      title="Gateway"
                      icon={<Activity size={14} />}
                      items={[`ContextForge ${data.architecture.gateway.contextforge_ok ? 'online' : 'offline'}`]}
                    />
                    <FlowCard
                      title="Infrastructure"
                      icon={<Database size={14} />}
                      items={[data.architecture.infrastructure.database, data.architecture.infrastructure.memory_mode]}
                    />
                    <FlowCard
                      title="Outputs"
                      icon={<Bot size={14} />}
                      items={[
                        `${data.architecture.outputs.mcp_servers_active}/${data.architecture.outputs.mcp_servers_total} MCP`,
                        `${data.architecture.outputs.a2a_agents_active}/${data.architecture.outputs.a2a_agents_total} A2A`,
                        `${data.architecture.outputs.tools_active}/${data.architecture.outputs.tools_total} Tools`,
                      ]}
                    />
                  </div>
                </div>
              </div>

              {/* Services + Inventory */}
              <div className="grid grid-cols-2 gap-6">
                <div className="rounded-3xl border border-white/[0.07] bg-white/[0.02] p-6">
                  <div className="text-sm font-semibold text-white/90 mb-4">Services</div>
                  <div className="space-y-2.5">
                    {Object.entries(data.services).map(([key, svc]) => (
                      <div key={key} className="status-card flex items-center justify-between rounded-2xl border border-white/[0.04] bg-black/20 px-4 py-3">
                        <div className="flex items-center gap-3">
                          <div className="h-9 w-9 rounded-xl bg-white/[0.04] border border-white/[0.04] grid place-items-center text-white/50">
                            {key === 'sqlite' ? <Database size={15} /> :
                             key === 'backend' ? <Server size={15} /> :
                             key === 'avatar_svc' ? <Cpu size={15} /> :
                             <Activity size={15} />}
                          </div>
                          <div>
                            <div className="text-[13px] font-medium text-white/90">{SERVICE_LABELS[key] || key}</div>
                            <div className="text-[11px] text-white/25 truncate max-w-[160px]">
                              {svc.url || svc.base_url || svc.service || 'internal'}
                            </div>
                          </div>
                        </div>
                        <div className="flex items-center gap-3">
                          <div className="text-[11px] text-white/35">
                            {svc.latency_ms != null ? `${svc.latency_ms}ms` : '\u2014'}
                          </div>
                          <div className={`px-2.5 py-1 rounded-full border text-[11px] font-semibold ${statusTone(svc.ok)}`}>
                            {svc.ok ? 'Healthy' : 'Offline'}
                          </div>
                        </div>
                      </div>
                    ))}
                  </div>
                </div>

                <div className="rounded-3xl border border-white/[0.07] bg-white/[0.02] p-6">
                  <div className="text-sm font-semibold text-white/90 mb-4">Inventory</div>
                  <div className="grid grid-cols-2 gap-3.5">
                    <SmallCount label="Virtual Servers" value={data.architecture.inputs.virtual_servers_active} sub={`${data.architecture.inputs.virtual_servers_total} total`} />
                    <SmallCount label="MCP Servers" value={data.architecture.outputs.mcp_servers_active} sub={`${data.architecture.outputs.mcp_servers_total} total`} />
                    <SmallCount label="A2A Agents" value={data.architecture.outputs.a2a_agents_active} sub={`${data.architecture.outputs.a2a_agents_total} total`} />
                    <SmallCount label="Tools" value={data.architecture.outputs.tools_active} sub={`${data.architecture.outputs.tools_total} total`} />
                    <SmallCount label="Prompts" value={data.architecture.outputs.prompts_active} sub={`${data.architecture.outputs.prompts_total} total`} />
                    <SmallCount label="Resources" value={data.architecture.outputs.resources_active} sub={`${data.architecture.outputs.resources_total} total`} />
                  </div>
                </div>
              </div>
            </>
          ) : null}
        </div>
      </div>

      <style>{`
        @keyframes statusFadeIn { from { opacity: 0 } to { opacity: 1 } }
        @keyframes statusSlideUp { from { opacity: 0; transform: translateY(16px) scale(0.97) } to { opacity: 1; transform: translateY(0) scale(1) } }
        @keyframes statusCardIn { from { opacity: 0; transform: translateY(12px) } to { opacity: 1; transform: translateY(0) } }
        @keyframes donutSpin { from { transform: rotate(-90deg) } to { transform: rotate(0deg) } }
        .status-card { animation: statusCardIn 350ms ease-out both }
        .status-card:nth-child(1) { animation-delay: 60ms }
        .status-card:nth-child(2) { animation-delay: 120ms }
        .status-card:nth-child(3) { animation-delay: 180ms }
        .status-card:nth-child(4) { animation-delay: 240ms }
        .status-card:nth-child(5) { animation-delay: 300ms }
        .status-card:nth-child(6) { animation-delay: 360ms }
        .donut-ring { animation: donutSpin 800ms cubic-bezier(0.34,1.56,0.64,1) both; animation-delay: 200ms }
      `}</style>
    </div>
  )
}

/* ── sub-components ────────────────────────────────────── */

function MetricCard({ label, value, delay = 0 }: { label: string; value: string; delay?: number }) {
  return (
    <div
      className="rounded-2xl border border-white/[0.07] bg-white/[0.02] px-4 py-4"
      style={{ animation: `statusCardIn 350ms ease-out ${delay}ms both` }}
    >
      <div className="text-[10px] uppercase tracking-wider text-white/25 mb-2">{label}</div>
      <div className="text-2xl font-bold text-white">{value}</div>
    </div>
  )
}

function SmallCount({ label, value, sub }: { label: string; value: number; sub: string }) {
  return (
    <div className="rounded-2xl border border-white/[0.04] bg-black/20 px-4 py-4">
      <div className="text-[10px] uppercase tracking-wider text-white/25 mb-2">{label}</div>
      <div className="text-2xl font-bold text-white">{value}</div>
      <div className="text-[11px] text-white/25 mt-1">{sub}</div>
    </div>
  )
}

function Legend({ color, label }: { color: string; label: string }) {
  return (
    <div className="flex items-center gap-1.5 text-white/35">
      <div className={`h-2 w-2 rounded-full ${color}`} />
      <span>{label}</span>
    </div>
  )
}

function FlowCard({ title, icon, items }: { title: string; icon: React.ReactNode; items: string[] }) {
  return (
    <div className="rounded-2xl border border-white/[0.04] bg-black/20 p-4">
      <div className="flex items-center gap-2 text-white/70 font-medium text-[13px] mb-3">
        {icon}
        {title}
      </div>
      <div className="space-y-2">
        {items.map((item) => (
          <div key={item} className="text-[11px] text-white/35">{item}</div>
        ))}
      </div>
    </div>
  )
}