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 --- makima/frontend/src/routes/daemon.tsx | 746 ++++++++++++++++++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100644 makima/frontend/src/routes/daemon.tsx (limited to 'makima/frontend/src/routes') diff --git a/makima/frontend/src/routes/daemon.tsx b/makima/frontend/src/routes/daemon.tsx new file mode 100644 index 0000000..66154ad --- /dev/null +++ b/makima/frontend/src/routes/daemon.tsx @@ -0,0 +1,746 @@ +import { useState, useEffect, useCallback } from "react"; +import { useNavigate } from "react-router"; +import { Masthead } from "../components/Masthead"; +import { useAuth } from "../contexts/AuthContext"; +import { + listDaemons, + restartDaemon, + type Daemon, +} from "../lib/api"; + +// ============================================================================= +// Types +// ============================================================================= + +interface GitHubAsset { + name: string; + browser_download_url: string; + size: number; +} + +interface GitHubRelease { + tag_name: string; + name: string; + published_at: string; + html_url: string; + assets: GitHubAsset[]; +} + +interface PlatformDownload { + label: string; + arch: string; + pattern: string; + asset: GitHubAsset | null; + recommended: boolean; +} + +// ============================================================================= +// Helpers +// ============================================================================= + +function detectPlatform(): string { + const ua = navigator.userAgent.toLowerCase(); + const platform = navigator.platform?.toLowerCase() || ""; + + if (platform.includes("mac") || ua.includes("macintosh")) { + // Check for Apple Silicon + // navigator.platform is "MacIntel" even on ARM for some browsers, + // but we can check userAgent for hints or default to arm64 for modern Macs + if ( + ua.includes("arm") || + ua.includes("aarch64") || + // Chrome 93+ on ARM Macs reports this + (typeof navigator !== "undefined" && + "userAgentData" in navigator && + // @ts-expect-error -- userAgentData may not be typed + navigator.userAgentData?.platform === "macOS") + ) { + return "macos-arm64"; + } + return "macos-arm64"; // Default to ARM64 for modern Macs + } + + if (platform.includes("linux") || ua.includes("linux")) { + return "linux-x86_64"; + } + + return "linux-x86_64"; // fallback +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; +} + +function formatDate(dateStr: string): string { + return new Date(dateStr).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +// ============================================================================= +// Sub-components +// ============================================================================= + +function SectionHeader({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ); +} + +function ErrorAlert({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function CodeBlock({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +function StepNumber({ n }: { n: number }) { + return ( + + {n} + + ); +} + +// ============================================================================= +// Download Section +// ============================================================================= + +function DownloadSection() { + const [release, setRelease] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [userPlatform] = useState(detectPlatform); + + useEffect(() => { + const fetchRelease = async () => { + try { + setLoading(true); + setError(null); + const res = await fetch( + "https://api.github.com/repos/soryu-co/makima/releases/latest" + ); + if (!res.ok) { + throw new Error(`GitHub API returned ${res.status}`); + } + const data: GitHubRelease = await res.json(); + setRelease(data); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to fetch release info" + ); + } finally { + setLoading(false); + } + }; + fetchRelease(); + }, []); + + const platforms: PlatformDownload[] = [ + { + label: "Linux x86_64", + arch: "linux-x86_64", + pattern: "linux-x86_64.tar.gz", + asset: null, + recommended: userPlatform === "linux-x86_64", + }, + { + label: "macOS Intel (x86_64)", + arch: "macos-x86_64", + pattern: "macos-x86_64.tar.gz", + asset: null, + recommended: userPlatform === "macos-x86_64", + }, + { + label: "macOS Apple Silicon (ARM64)", + arch: "macos-arm64", + pattern: "macos-arm64.tar.gz", + asset: null, + recommended: userPlatform === "macos-arm64", + }, + ]; + + // Match assets to platforms + if (release) { + for (const p of platforms) { + p.asset = + release.assets.find((a) => a.name.includes(p.pattern)) || null; + } + } + + // Sort recommended first + const sortedPlatforms = [...platforms].sort( + (a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0) + ); + + return ( +
+ Download Daemon + + {loading && ( +

+ Fetching latest release... +

+ )} + + {error && Failed to load release: {error}} + + {release && ( + <> +
+
+ + {release.tag_name} + + + {formatDate(release.published_at)} + +
+ + All Releases → + +
+ +
+ {sortedPlatforms.map((p) => ( +
+
+ + {p.label} + + {p.recommended && ( + + Detected + + )} + {p.asset && ( + + {formatBytes(p.asset.size)} + + )} +
+ {p.asset ? ( + + Download + + ) : ( + + Not available + + )} +
+ ))} +
+ + )} +
+ ); +} + +// ============================================================================= +// Setup Instructions Section +// ============================================================================= + +function SetupSection() { + const [showConfig, setShowConfig] = useState(false); + + return ( +
+ Setup Instructions +
+
+ +
+

+ Download the binary for your platform above +

+
+
+ +
+ +
+

+ Extract the archive +

+ tar xzf makima-*.tar.gz +
+
+ +
+ +
+

+ Move to PATH +

+ sudo mv makima /usr/local/bin/ +
+
+ +
+ +
+

+ Set your API key ( + + generate one in Settings + + ) +

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

+ Set server URL +

+ + export MAKIMA_DAEMON_SERVER_URL="ws://your-server:8080" + +
+
+ +
+ +
+

+ Run the daemon +

+ makima daemon +
+
+
+ + {/* Config file alternative */} +
+ + {showConfig && ( +
+

+ Create makima-daemon.toml{" "} + in the working directory: +

+ + {`[daemon] +api_key = "your-key" +server_url = "ws://your-server:8080" +max_concurrent_tasks = 4`} + +
+ )} +
+
+ ); +} + +// ============================================================================= +// Cloudflare Edge Deployment Section +// ============================================================================= + +function CloudflareAgentSection() { + const [showSetup, setShowSetup] = useState(false); + + const benefits = [ + { + label: "Global edge presence", + desc: "Lower latency from 300+ Cloudflare locations worldwide", + }, + { + label: "Auto-scaling & hibernation", + desc: "Cost-efficient — only runs when needed", + }, + { + label: "WebSocket relay", + desc: "Coordinate remote daemon instances through persistent connections", + }, + { + label: "Durable Objects", + desc: "Built on Cloudflare's stateful edge compute primitives", + }, + ]; + + return ( +
+ Edge Deployment + +

+ Deploy a lightweight Makima relay agent on Cloudflare's edge network for + global, low-latency daemon coordination. Ideal for distributed teams or + production deployments requiring high availability. +

+ + {/* Benefits */} +
+ {benefits.map((b) => ( +
+ +
+ {b.label} + — {b.desc} +
+
+ ))} +
+ + {/* Quick Setup */} +
+ + {showSetup && ( +
+
+ +
+

+ Navigate to the Cloudflare agent directory +

+ cd makima/cloudflare-agent +
+
+
+ +
+

+ Run the setup script +

+ ./setup.sh +
+
+
+ +
+

+ Deploy to Cloudflare +

+ npx wrangler deploy +
+
+
+ )} +
+ + {/* Link to repo */} +
+ + Full documentation & source + + + View on GitHub → + +
+
+ ); +} + +// ============================================================================= +// Connected Daemons Section +// ============================================================================= + +function ConnectedDaemonsSection() { + const [daemons, setDaemons] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [restartingDaemonId, setRestartingDaemonId] = useState( + null + ); + const [restartConfirmDaemonId, setRestartConfirmDaemonId] = useState< + string | null + >(null); + + const loadDaemons = useCallback(async () => { + try { + setError(null); + const response = await listDaemons(); + setDaemons(response.daemons); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to load daemons" + ); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadDaemons(); + }, [loadDaemons]); + + // Auto-refresh every 30 seconds + useEffect(() => { + const interval = setInterval(() => { + loadDaemons(); + }, 30000); + return () => clearInterval(interval); + }, [loadDaemons]); + + const handleRestartDaemon = async (id: string) => { + try { + setRestartingDaemonId(id); + setError(null); + await restartDaemon(id); + setRestartConfirmDaemonId(null); + // Daemon will restart, so refresh the list after a short delay + setTimeout(() => { + loadDaemons(); + }, 2000); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to restart daemon" + ); + } finally { + setRestartingDaemonId(null); + } + }; + + return ( +
+
+
+

+ Connected Daemons +

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

Loading...

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

+ No daemons connected +

+

+ Follow the setup instructions above to connect a daemon +

+
+ ) : ( +
+ {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. + +
+ + +
+
+ ) : ( + + )} +
+ )} +
+ ))} +
+ )} +
+ ); +} + +// ============================================================================= +// Main Page +// ============================================================================= + +export default function DaemonPage() { + const { + isAuthenticated, + isAuthConfigured, + isLoading: authLoading, + } = useAuth(); + const navigate = useNavigate(); + + useEffect(() => { + if (!authLoading && isAuthConfigured && !isAuthenticated) { + navigate("/login"); + } + }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); + + if (authLoading) { + return ( +
+ +
+

Loading...

+
+
+ ); + } + + return ( +
+ +
+ {/* Page header */} +
+

+ Daemon Management +

+

+ Download, configure, and monitor Makima daemons +

+
+ +
+ {/* Left Column: Downloads & Setup */} +
+ + +
+ + {/* Right Column: Edge Deployment & Connected Daemons */} +
+ + +
+
+
+
+ ); +} -- cgit v1.2.3