diff options
| author | soryu <soryu@soryu.co> | 2026-02-22 14:39:14 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-22 14:39:14 +0000 |
| commit | 6a34a6f3c423a7c57616762eb4cea2b7da52eaf3 (patch) | |
| tree | 7c596eac896918466e7ef3f149b02333fef09212 /frontend/src/components/DaemonDetail.tsx | |
| parent | 0523765af84492640928d571f481e17b26008b13 (diff) | |
| download | soryu-6a34a6f3c423a7c57616762eb4cea2b7da52eaf3.tar.gz soryu-6a34a6f3c423a7c57616762eb4cea2b7da52eaf3.zip | |
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
Diffstat (limited to 'frontend/src/components/DaemonDetail.tsx')
| -rw-r--r-- | frontend/src/components/DaemonDetail.tsx | 229 |
1 files changed, 229 insertions, 0 deletions
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<Daemon | null>(null) + const [directories, setDirectories] = useState<DaemonDirectory[]>([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState<string | null>(null) + const [activeTab, setActiveTab] = useState<Tab>('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 ( + <div className="daemon-detail-container"> + <div className="loading">Loading daemon...</div> + </div> + ) + } + + if (error) { + return ( + <div className="daemon-detail-container"> + <div className="error">Error: {error}</div> + <Link to="/daemons" className="back-link"> + Back to Daemons + </Link> + </div> + ) + } + + if (!daemon) { + return ( + <div className="daemon-detail-container"> + <div className="not-found">Daemon not found</div> + <Link to="/daemons" className="back-link"> + Back to Daemons + </Link> + </div> + ) + } + + const status = statusIndicator(daemon.status) + const filteredDirectories = directories.filter( + (dir) => dir.hostname === daemon.hostname + ) + + return ( + <div className="daemon-detail-container"> + <div className="daemon-detail-header"> + <Link to="/daemons" className="back-link"> + Back to Daemons + </Link> + <h1 className="daemon-title">{daemon.hostname || 'Unknown Host'}</h1> + <div className="daemon-meta"> + <span> + <span + className="status-dot" + style={{ backgroundColor: status.color }} + /> + {status.label} + </span> + <span> + Tasks: {daemon.currentTaskCount} / {daemon.maxConcurrentTasks} + </span> + </div> + </div> + + <div className="daemon-tabs"> + <button + className={`tab-button ${activeTab === 'overview' ? 'active' : ''}`} + onClick={() => setActiveTab('overview')} + > + Overview + </button> + <button + className={`tab-button ${activeTab === 'directories' ? 'active' : ''}`} + onClick={() => setActiveTab('directories')} + > + Directories ({filteredDirectories.length}) + </button> + </div> + + <div className="daemon-tab-content"> + {activeTab === 'overview' && ( + <div className="tab-panel"> + <h2>Daemon Overview</h2> + <dl className="overview-list"> + <dt>ID</dt> + <dd>{daemon.id}</dd> + <dt>Hostname</dt> + <dd>{daemon.hostname || 'N/A'}</dd> + <dt>Machine ID</dt> + <dd>{daemon.machineId || 'N/A'}</dd> + <dt>Status</dt> + <dd> + <span + className="status-dot" + style={{ backgroundColor: status.color }} + /> + {status.label} + </dd> + <dt>Connection ID</dt> + <dd>{daemon.connectionId}</dd> + <dt>Max Concurrent Tasks</dt> + <dd>{daemon.maxConcurrentTasks}</dd> + <dt>Current Task Count</dt> + <dd>{daemon.currentTaskCount}</dd> + <dt>Connected At</dt> + <dd>{new Date(daemon.connectedAt).toLocaleString()}</dd> + <dt>Last Heartbeat At</dt> + <dd>{new Date(daemon.lastHeartbeatAt).toLocaleString()}</dd> + <dt>Disconnected At</dt> + <dd> + {daemon.disconnectedAt + ? new Date(daemon.disconnectedAt).toLocaleString() + : 'N/A'} + </dd> + </dl> + </div> + )} + + {activeTab === 'directories' && ( + <div className="tab-panel"> + <h2>Directories</h2> + {filteredDirectories.length === 0 ? ( + <p>No directories found for this daemon</p> + ) : ( + <ul className="directory-list"> + {filteredDirectories.map((dir, index) => ( + <li key={`${dir.path}-${index}`} className="directory-item"> + <h3>{dir.label}</h3> + <div className="daemon-meta"> + <span>Path: {dir.path}</span> + <span>Type: {dir.directoryType}</span> + </div> + </li> + ))} + </ul> + )} + </div> + )} + </div> + </div> + ) +} |
