From 6a34a6f3c423a7c57616762eb4cea2b7da52eaf3 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
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<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>
+  )
+}
-- 
cgit v1.2.3

