From 0523765af84492640928d571f481e17b26008b13 Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 21 Feb 2026 23:51:11 +0000 Subject: feat: Add daemon health monitoring page, downloads & K8s support (#76) * feat: soryu-co/soryu - makima: Add server-side daemon binary download endpoint * feat: soryu-co/soryu - makima: Create Kubernetes daemon manifests and Dockerfile * feat: soryu-co/soryu - makima: Create dedicated Daemons page with health monitoring UI * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Integrate daemon platform availability into frontend downloads --- makima/frontend/src/components/NavStrip.tsx | 1 + makima/frontend/src/lib/api.ts | 36 +++ makima/frontend/src/main.tsx | 9 + makima/frontend/src/routes/daemons.tsx | 370 ++++++++++++++++++++++++++++ makima/frontend/src/routes/settings.tsx | 185 +------------- 5 files changed, 422 insertions(+), 179 deletions(-) create mode 100644 makima/frontend/src/routes/daemons.tsx (limited to 'makima/frontend') diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index 4932427..9556458 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -15,6 +15,7 @@ const NAV_LINKS: NavLink[] = [ { label: "Orders", href: "/orders", requiresAuth: true }, { label: "Contracts", href: "/contracts", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, + { label: "Daemons", href: "/daemons", requiresAuth: true }, { label: "History", href: "/history", requiresAuth: true }, ]; diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 43eaa05..458b69d 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -1221,6 +1221,42 @@ export async function restartDaemon(id: string): Promise return res.json(); } +// ============================================================================= +// Daemon Platform Download +// ============================================================================= + +/** A daemon platform with its availability and download URL */ +export interface DaemonPlatform { + platform: string; + available: boolean; + downloadUrl: string; +} + +/** Response from the list daemon platforms endpoint */ +export interface DaemonPlatformsResponse { + platforms: DaemonPlatform[]; +} + +/** + * List available daemon platforms and their download status. + * This is an unauthenticated endpoint. + */ +export async function listDaemonPlatforms(): Promise { + const res = await fetch(`${API_BASE}/api/v1/daemon/download/platforms`); + if (!res.ok) { + throw new Error(`Failed to list daemon platforms: ${res.statusText}`); + } + return res.json(); +} + +/** + * Get the full download URL for a daemon binary. + * Returns the absolute URL including API_BASE for cross-origin usage. + */ +export function getDaemonDownloadUrl(platform: string): string { + return `${API_BASE}/api/v1/daemon/download/${platform}`; +} + // ============================================================================= // Mesh Chat Types for Task Orchestration // ============================================================================= diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx index acc9afc..32c05ba 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -14,6 +14,7 @@ import FilesPage from "./routes/files"; import ContractsPage from "./routes/contracts"; import OrdersPage from "./routes/orders"; import MeshPage from "./routes/mesh"; +import DaemonsPage from "./routes/daemons"; import HistoryPage from "./routes/history"; import LoginPage from "./routes/login"; import SettingsPage from "./routes/settings"; @@ -112,6 +113,14 @@ createRoot(document.getElementById("root")!).render( } /> + + + + } + /> + {children} + + ); +} + +// ============================================================================= +// Alert Component +// ============================================================================= + +function ErrorAlert({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +// ============================================================================= +// Daemons Page +// ============================================================================= + +export default function DaemonsPage() { + const { isAuthenticated, isAuthConfigured } = useAuth(); + const navigate = useNavigate(); + + // Daemon state + const [daemons, setDaemons] = useState([]); + const [daemonsLoading, setDaemonsLoading] = useState(true); + const [daemonsError, setDaemonsError] = useState(null); + const [restartingDaemonId, setRestartingDaemonId] = useState(null); + const [restartConfirmDaemonId, setRestartConfirmDaemonId] = useState(null); + + // Platform availability state + const [platforms, setPlatforms] = useState([]); + const [platformsLoading, setPlatformsLoading] = useState(true); + + // Redirect if not authenticated + useEffect(() => { + if (isAuthConfigured && !isAuthenticated) { + navigate("/login"); + } + }, [isAuthConfigured, isAuthenticated, navigate]); + + const loadDaemons = async () => { + try { + setDaemonsError(null); + const response: DaemonListResponse = await listDaemons(); + setDaemons(response.daemons); + } catch (err) { + setDaemonsError(err instanceof Error ? err.message : "Failed to load daemons"); + } finally { + setDaemonsLoading(false); + } + }; + + const handleRestartDaemon = async (id: string) => { + try { + setRestartingDaemonId(id); + setDaemonsError(null); + const _response: RestartDaemonResponse = await restartDaemon(id); + // Daemon will restart, so refresh the list after a short delay + setTimeout(() => { + loadDaemons(); + }, 2000); + } catch (err) { + setDaemonsError(err instanceof Error ? err.message : "Failed to restart daemon"); + } finally { + setRestartingDaemonId(null); + setRestartConfirmDaemonId(null); + } + }; + + // Friendly labels for platform identifiers + const platformLabels: Record = { + "linux-x86_64": "Linux (Intel/AMD)", + "linux-arm64": "Linux (ARM64)", + "macos-x86_64": "macOS (Intel)", + "macos-arm64": "macOS (Apple Silicon)", + }; + + const loadPlatforms = useCallback(async () => { + try { + setPlatformsLoading(true); + const response = await listDaemonPlatforms(); + setPlatforms(response.platforms); + } catch { + // Fallback: show all platforms as unavailable if API endpoint is missing + setPlatforms([ + { platform: "linux-x86_64", available: false, downloadUrl: "/api/v1/daemon/download/linux-x86_64" }, + { platform: "linux-arm64", available: false, downloadUrl: "/api/v1/daemon/download/linux-arm64" }, + { platform: "macos-x86_64", available: false, downloadUrl: "/api/v1/daemon/download/macos-x86_64" }, + { platform: "macos-arm64", available: false, downloadUrl: "/api/v1/daemon/download/macos-arm64" }, + ]); + } finally { + setPlatformsLoading(false); + } + }, []); + + // Initial load + useEffect(() => { + loadDaemons(); + loadPlatforms(); + }, []); + + // Auto-refresh daemons every 30 seconds + useEffect(() => { + const interval = setInterval(() => { + loadDaemons(); + }, 30000); + return () => clearInterval(interval); + }, []); + + return ( +
+ + +
+ {/* Page Header */} +
+

Daemons

+

+ Daemons are worker processes that connect to Makima and execute tasks on your machines. +

+
+
+ +
+ {/* Left Column */} +
+ {/* Download Section */} +
+ Download Daemon +

+ Download the pre-compiled daemon binary for your platform. The daemon connects to the Makima server and executes tasks. +

+ +
+ {platformsLoading ? ( +

Loading platforms...

+ ) : ( + platforms.map((p) => ( + + + {platformLabels[p.platform] || p.platform} + + + {p.available ? "Available" : "Not bundled"} + + + )) + )} +
+ +
+

+ Quick Install +

+ + curl -fsSL https://raw.githubusercontent.com/soryu-co/soryu/master/install.sh | bash + +
+
+ + {/* Daemon Setup */} +
+ Daemon Setup +

+ Set your API key as an environment variable: +

+ + export MAKIMA_API_KEY="your-key" + +

+ Then run: makima-daemon +

+
+ + {/* Kubernetes Section */} +
+ Run in Kubernetes +

+ Deploy daemons as containers in Kubernetes for scalable task execution. +

+ +

+ Pull Container Image +

+ + docker pull ghcr.io/soryu-co/makima-daemon:latest + + +

+ Environment Variables +

+
+
MAKIMA_API_KEY="your-key"
+
MAKIMA_SERVER_URL="https://your-server"
+
GITHUB_TOKEN="ghp_..." # optional, for repo access
+
+ +

+ Kubernetes manifests available in the repository under k8s/daemon/ +

+
+
+ + {/* Right Column */} +
+ {/* Connected Daemons */} +
+
+
+

+ Connected Daemons +

+ {daemons.length > 0 && ( + + ({daemons.filter(d => d.status === "connected").length} connected / {daemons.length} total) + + )} +
+ +
+ + {daemonsError && {daemonsError}} + + {daemonsLoading && daemons.length === 0 ? ( +

Loading...

+ ) : daemons.length === 0 ? ( +
+

No daemons connected

+

+ Start a daemon to enable task execution +

+
+ ) : ( +
+ {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.substring(0, 16)}... + +
+ )} +
+ {/* Restart Section */} + {daemon.status === "connected" && ( +
+ {restartConfirmDaemonId === daemon.id ? ( +
+ + Restart daemon? Running tasks will be interrupted. + +
+ + +
+
+ ) : ( + + )} +
+ )} +
+ ))} +
+ )} +
+
+
+
+
+ ); +} diff --git a/makima/frontend/src/routes/settings.tsx b/makima/frontend/src/routes/settings.tsx index b93ecbc..73537bd 100644 --- a/makima/frontend/src/routes/settings.tsx +++ b/makima/frontend/src/routes/settings.tsx @@ -10,13 +10,10 @@ import { changePassword, changeEmail, deleteAccount, - listDaemons, - restartDaemon, listRepositoryHistory, deleteRepositoryHistory, type ApiKeyInfo, type CreateApiKeyResponse, - type Daemon, type RepositoryHistoryEntry, } from "../lib/api"; @@ -303,13 +300,6 @@ export default function SettingsPage() { const [deleteLoading, setDeleteLoading] = useState(false); const [deleteError, setDeleteError] = useState(null); - // Daemon state - const [daemons, setDaemons] = useState([]); - const [daemonsLoading, setDaemonsLoading] = useState(true); - const [daemonsError, setDaemonsError] = useState(null); - const [restartingDaemonId, setRestartingDaemonId] = useState(null); - const [restartConfirmDaemonId, setRestartConfirmDaemonId] = useState(null); - // Repository history state const [repoHistory, setRepoHistory] = useState([]); const [repoHistoryLoading, setRepoHistoryLoading] = useState(true); @@ -318,18 +308,9 @@ export default function SettingsPage() { useEffect(() => { loadApiKey(); - loadDaemons(); loadRepoHistory(); }, []); - // Auto-refresh daemons every 30 seconds - useEffect(() => { - const interval = setInterval(() => { - loadDaemons(); - }, 30000); - return () => clearInterval(interval); - }, []); - const loadApiKey = async () => { try { setLoading(true); @@ -343,18 +324,6 @@ export default function SettingsPage() { } }; - const loadDaemons = async () => { - try { - setDaemonsError(null); - const response = await listDaemons(); - setDaemons(response.daemons); - } catch (err) { - setDaemonsError(err instanceof Error ? err.message : "Failed to load daemons"); - } finally { - setDaemonsLoading(false); - } - }; - const loadRepoHistory = async () => { try { setRepoHistoryError(null); @@ -379,23 +348,6 @@ export default function SettingsPage() { } }; - const handleRestartDaemon = async (id: string) => { - try { - setRestartingDaemonId(id); - setDaemonsError(null); - await restartDaemon(id); - // Daemon will restart, so refresh the list after a short delay - setTimeout(() => { - loadDaemons(); - }, 2000); - } catch (err) { - setDaemonsError(err instanceof Error ? err.message : "Failed to restart daemon"); - } finally { - setRestartingDaemonId(null); - setRestartConfirmDaemonId(null); - } - }; - const handleCreate = async () => { try { setActionLoading(true); @@ -648,140 +600,15 @@ export default function SettingsPage() { )} - {/* Daemon Setup */} + {/* Daemons Link */}
- Daemon Setup + Daemons

- Set your API key as an environment variable: -

- - export MAKIMA_API_KEY="your-key" - -

- Then run: makima-daemon + Daemon management has moved to its own page.

-
- - {/* Connected Daemons */} -
-
-
-

- Daemons -

- {daemons.length > 0 && ( - - ({daemons.filter(d => d.status === "connected").length} connected / {daemons.length} total) - - )} -
- -
- - {daemonsError && {daemonsError}} - - {daemonsLoading && daemons.length === 0 ? ( -

Loading...

- ) : daemons.length === 0 ? ( -
-

No daemons connected

-

- Start a daemon to enable task execution -

-
- ) : ( -
- {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.substring(0, 16)}... - -
- )} -
- {/* Restart Section */} - {daemon.status === "connected" && ( -
- {restartConfirmDaemonId === daemon.id ? ( -
- - Restart daemon? Running tasks will be interrupted. - -
- - -
-
- ) : ( - - )} -
- )} -
- ))} -
- )} + + Go to Daemons → +
{/* Repository History */} -- cgit v1.2.3