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/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<DaemonSummary[]>([])
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState<string | null>(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 (
+      <div className="daemon-list-container">
+        <div className="loading">Loading daemons...</div>
+      </div>
+    )
+  }
+
+  if (error) {
+    return (
+      <div className="daemon-list-container">
+        <div className="error">Error: {error}</div>
+      </div>
+    )
+  }
+
+  return (
+    <div className="daemon-list-container">
+      <Link to="/" className="back-link">
+        Back to Home
+      </Link>
+      <h1>Daemons</h1>
+      {daemons.length === 0 ? (
+        <p>No daemons found</p>
+      ) : (
+        <ul className="daemon-list">
+          {daemons.map((daemon) => (
+            <li key={daemon.id} className="daemon-item">
+              <Link to={`/daemons/${daemon.id}`}>
+                <h2>{daemon.hostname || 'Unknown Host'}</h2>
+                <div className="daemon-status">
+                  <span
+                    className="status-dot"
+                    style={{ backgroundColor: statusDotColor(daemon.status) }}
+                  />
+                  <span>{daemon.status}</span>
+                </div>
+                <div className="daemon-meta">
+                  <span>
+                    Tasks: {daemon.currentTaskCount} / {daemon.maxConcurrentTasks}
+                  </span>
+                  <span>
+                    Connected: {new Date(daemon.connectedAt).toLocaleString()}
+                  </span>
+                  {daemon.machineId && (
+                    <span>Machine: {daemon.machineId}</span>
+                  )}
+                </div>
+              </Link>
+            </li>
+          ))}
+        </ul>
+      )}
+    </div>
+  )
+}
-- 
cgit v1.2.3

