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/DaemonDetail.tsx | 229 +++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 frontend/src/components/DaemonDetail.tsx (limited to 'frontend/src/components/DaemonDetail.tsx') diff --git a/frontend/src/components/DaemonDetail.tsx b/frontend/src/components/DaemonDetail.tsx new file mode 100644 index 0000000..2de0997 --- /dev/null +++ b/frontend/src/components/DaemonDetail.tsx @@ -0,0 +1,229 @@ +import React, { useEffect, useState } from 'react' +import { useParams, Link } from 'react-router-dom' + +interface Daemon { + 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 +} + +interface DaemonDirectory { + path: string + label: string + directoryType: string + hostname: string | null + exists?: boolean +} + +type Tab = 'overview' | 'directories' + +function statusIndicator(status: string): { color: string; label: string } { + switch (status.toLowerCase()) { + case 'connected': + return { color: 'green', label: 'Connected' } + case 'disconnected': + return { color: 'red', label: 'Disconnected' } + case 'unhealthy': + return { color: 'yellow', label: 'Unhealthy' } + default: + return { color: 'gray', label: status } + } +} + +export function DaemonDetail() { + const { id } = useParams<{ id: string }>() + const [daemon, setDaemon] = useState(null) + const [directories, setDirectories] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [activeTab, setActiveTab] = useState('overview') + + useEffect(() => { + async function fetchDaemon() { + if (!id) return + + try { + setLoading(true) + const [daemonRes, dirRes] = await Promise.all([ + fetch(`/api/v1/mesh/daemons/${id}`), + fetch('/api/v1/mesh/daemons/directories'), + ]) + + if (!daemonRes.ok) { + throw new Error(`Failed to fetch daemon: ${daemonRes.statusText}`) + } + + const daemonData = await daemonRes.json() + setDaemon(daemonData) + + if (dirRes.ok) { + const dirData = await dirRes.json() + setDirectories(dirData.directories || []) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error') + } finally { + setLoading(false) + } + } + + fetchDaemon() + + const interval = setInterval(async () => { + if (!id) return + try { + const response = await fetch(`/api/v1/mesh/daemons/${id}`) + if (response.ok) { + const daemonData = await response.json() + setDaemon(daemonData) + } + } catch { + // Silently ignore refresh errors + } + }, 10000) + + return () => clearInterval(interval) + }, [id]) + + if (loading) { + return ( +
+
Loading daemon...
+
+ ) + } + + if (error) { + return ( +
+
Error: {error}
+ + Back to Daemons + +
+ ) + } + + if (!daemon) { + return ( +
+
Daemon not found
+ + Back to Daemons + +
+ ) + } + + const status = statusIndicator(daemon.status) + const filteredDirectories = directories.filter( + (dir) => dir.hostname === daemon.hostname + ) + + return ( +
+
+ + Back to Daemons + +

{daemon.hostname || 'Unknown Host'}

+
+ + + {status.label} + + + Tasks: {daemon.currentTaskCount} / {daemon.maxConcurrentTasks} + +
+
+ +
+ + +
+ +
+ {activeTab === 'overview' && ( +
+

Daemon Overview

+
+
ID
+
{daemon.id}
+
Hostname
+
{daemon.hostname || 'N/A'}
+
Machine ID
+
{daemon.machineId || 'N/A'}
+
Status
+
+ + {status.label} +
+
Connection ID
+
{daemon.connectionId}
+
Max Concurrent Tasks
+
{daemon.maxConcurrentTasks}
+
Current Task Count
+
{daemon.currentTaskCount}
+
Connected At
+
{new Date(daemon.connectedAt).toLocaleString()}
+
Last Heartbeat At
+
{new Date(daemon.lastHeartbeatAt).toLocaleString()}
+
Disconnected At
+
+ {daemon.disconnectedAt + ? new Date(daemon.disconnectedAt).toLocaleString() + : 'N/A'} +
+
+
+ )} + + {activeTab === 'directories' && ( +
+

Directories

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

No directories found for this daemon

+ ) : ( +
    + {filteredDirectories.map((dir, index) => ( +
  • +

    {dir.label}

    +
    + Path: {dir.path} + Type: {dir.directoryType} +
    +
  • + ))} +
+ )} +
+ )} +
+
+ ) +} -- cgit v1.2.3