From 6a34a6f3c423a7c57616762eb4cea2b7da52eaf3 Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 22 Feb 2026 14:39:14 +0000 Subject: feat: Add daemon page with download binary and Cloudflare Agent setup (#77) * feat: soryu-co/soryu - makima: Create DaemonList and DaemonDetail page components * feat: soryu-co/soryu - makima: Add daemon page routes, CSS styles, and navigation * feat: soryu-co/soryu - makima: Create daemon page with download and monitoring * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Integrate Cloudflare Agent setup into daemon page --- frontend/src/components/DaemonList.tsx | 125 +++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 frontend/src/components/DaemonList.tsx (limited to 'frontend/src/components/DaemonList.tsx') diff --git a/frontend/src/components/DaemonList.tsx b/frontend/src/components/DaemonList.tsx new file mode 100644 index 0000000..215c790 --- /dev/null +++ b/frontend/src/components/DaemonList.tsx @@ -0,0 +1,125 @@ +import React, { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' + +interface DaemonSummary { + id: string + ownerId: string + connectionId: string + hostname: string | null + machineId: string | null + maxConcurrentTasks: number + currentTaskCount: number + status: string + lastHeartbeatAt: string + connectedAt: string + disconnectedAt: string | null +} + +function statusDotColor(status: string): string { + switch (status.toLowerCase()) { + case 'connected': + return 'green' + case 'disconnected': + return 'red' + case 'unhealthy': + return 'yellow' + default: + return 'gray' + } +} + +export function DaemonList() { + const [daemons, setDaemons] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + async function fetchDaemons() { + try { + setLoading(true) + const response = await fetch('/api/v1/mesh/daemons') + if (!response.ok) { + throw new Error(`Failed to fetch daemons: ${response.statusText}`) + } + const data = await response.json() + setDaemons(data.daemons || []) + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error') + } finally { + setLoading(false) + } + } + + fetchDaemons() + + const interval = setInterval(async () => { + try { + const response = await fetch('/api/v1/mesh/daemons') + if (response.ok) { + const data = await response.json() + setDaemons(data.daemons || []) + } + } catch { + // Silently ignore refresh errors + } + }, 10000) + + return () => clearInterval(interval) + }, []) + + if (loading) { + return ( +
+
Loading daemons...
+
+ ) + } + + if (error) { + return ( +
+
Error: {error}
+
+ ) + } + + return ( +
+ + Back to Home + +

Daemons

+ {daemons.length === 0 ? ( +

No daemons found

+ ) : ( +
    + {daemons.map((daemon) => ( +
  • + +

    {daemon.hostname || 'Unknown Host'}

    +
    + + {daemon.status} +
    +
    + + Tasks: {daemon.currentTaskCount} / {daemon.maxConcurrentTasks} + + + Connected: {new Date(daemon.connectedAt).toLocaleString()} + + {daemon.machineId && ( + Machine: {daemon.machineId} + )} +
    + +
  • + ))} +
+ )} +
+ ) +} -- cgit v1.2.3