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 (
<h2 className="text-[11px] font-mono uppercase tracking-wide text-[#8899aa] mb-3 pb-2 border-b border-[rgba(117,170,252,0.15)]">
{children}
</h2>
);
}
function ErrorAlert({ children }: { children: React.ReactNode }) {
return (
<div className="border border-red-700/50 bg-red-900/20 text-red-400 px-3 py-2 mb-4 font-mono text-xs">
{children}
</div>
);
}
function CodeBlock({ children }: { children: React.ReactNode }) {
return (
<code className="block bg-black/50 px-3 py-2 text-[11px] font-mono text-green-400 mb-2 overflow-x-auto">
{children}
</code>
);
}
function StepNumber({ n }: { n: number }) {
return (
<span className="inline-flex items-center justify-center w-5 h-5 border border-[rgba(117,170,252,0.35)] text-[10px] font-mono text-[#75aafc] mr-2 shrink-0">
{n}
</span>
);
}
// =============================================================================
// Download Section
// =============================================================================
function DownloadSection() {
const [release, setRelease] = useState<GitHubRelease | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
<SectionHeader>Download Daemon</SectionHeader>
{loading && (
<p className="text-[#7788aa] font-mono text-xs">
Fetching latest release...
</p>
)}
{error && <ErrorAlert>Failed to load release: {error}</ErrorAlert>}
{release && (
<>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<span className="text-[#9bc3ff] font-mono text-sm">
{release.tag_name}
</span>
<span className="text-[#556677] font-mono text-[10px]">
{formatDate(release.published_at)}
</span>
</div>
<a
href="https://github.com/soryu-co/makima/releases"
target="_blank"
rel="noopener noreferrer"
className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
>
All Releases →
</a>
</div>
<div className="space-y-2">
{sortedPlatforms.map((p) => (
<div
key={p.arch}
className={`border p-3 flex items-center justify-between ${
p.recommended
? "border-[rgba(117,170,252,0.5)] bg-[#0a1628]"
: "border-[rgba(117,170,252,0.15)] bg-[#0a1525]"
}`}
>
<div className="flex items-center gap-3">
<span className="font-mono text-xs text-[#9bc3ff]">
{p.label}
</span>
{p.recommended && (
<span className="text-[9px] font-mono uppercase px-1.5 py-0.5 border border-[rgba(117,170,252,0.4)] text-[#75aafc] bg-[rgba(117,170,252,0.08)]">
Detected
</span>
)}
{p.asset && (
<span className="text-[10px] font-mono text-[#556677]">
{formatBytes(p.asset.size)}
</span>
)}
</div>
{p.asset ? (
<a
href={p.asset.browser_download_url}
className={`text-[10px] font-mono uppercase px-3 py-1.5 border transition-colors ${
p.recommended
? "text-[#9bc3ff] border-[rgba(117,170,252,0.5)] bg-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.2)]"
: "text-[#75aafc] border-[rgba(117,170,252,0.25)] hover:bg-[rgba(117,170,252,0.1)]"
}`}
download
>
Download
</a>
) : (
<span className="text-[10px] font-mono text-[#556677]">
Not available
</span>
)}
</div>
))}
</div>
</>
)}
</section>
);
}
// =============================================================================
// Setup Instructions Section
// =============================================================================
function SetupSection() {
const [showConfig, setShowConfig] = useState(false);
return (
<section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
<SectionHeader>Setup Instructions</SectionHeader>
<div className="space-y-3">
<div className="flex items-start">
<StepNumber n={1} />
<div className="flex-1">
<p className="text-[#7788aa] font-mono text-[10px] mb-1">
Download the binary for your platform above
</p>
</div>
</div>
<div className="flex items-start">
<StepNumber n={2} />
<div className="flex-1">
<p className="text-[#7788aa] font-mono text-[10px] mb-1">
Extract the archive
</p>
<CodeBlock>tar xzf makima-*.tar.gz</CodeBlock>
</div>
</div>
<div className="flex items-start">
<StepNumber n={3} />
<div className="flex-1">
<p className="text-[#7788aa] font-mono text-[10px] mb-1">
Move to PATH
</p>
<CodeBlock>sudo mv makima /usr/local/bin/</CodeBlock>
</div>
</div>
<div className="flex items-start">
<StepNumber n={4} />
<div className="flex-1">
<p className="text-[#7788aa] font-mono text-[10px] mb-1">
Set your API key (
<a
href="/settings"
className="text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
>
generate one in Settings
</a>
)
</p>
<CodeBlock>export MAKIMA_API_KEY="your-key"</CodeBlock>
</div>
</div>
<div className="flex items-start">
<StepNumber n={5} />
<div className="flex-1">
<p className="text-[#7788aa] font-mono text-[10px] mb-1">
Set server URL
</p>
<CodeBlock>
export MAKIMA_DAEMON_SERVER_URL="ws://your-server:8080"
</CodeBlock>
</div>
</div>
<div className="flex items-start">
<StepNumber n={6} />
<div className="flex-1">
<p className="text-[#7788aa] font-mono text-[10px] mb-1">
Run the daemon
</p>
<CodeBlock>makima daemon</CodeBlock>
</div>
</div>
</div>
{/* Config file alternative */}
<div className="mt-4 pt-3 border-t border-[rgba(117,170,252,0.1)]">
<button
onClick={() => setShowConfig(!showConfig)}
className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] transition-colors bg-transparent border-none cursor-pointer p-0"
>
{showConfig ? "- Hide" : "+"} Config file alternative
</button>
{showConfig && (
<div className="mt-3">
<p className="text-[#7788aa] font-mono text-[10px] mb-2">
Create <code className="text-green-400">makima-daemon.toml</code>{" "}
in the working directory:
</p>
<code className="block bg-black/50 px-3 py-2 text-[11px] font-mono text-green-400 whitespace-pre overflow-x-auto">
{`[daemon]
api_key = "your-key"
server_url = "ws://your-server:8080"
max_concurrent_tasks = 4`}
</code>
</div>
)}
</div>
</section>
);
}
// =============================================================================
// 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 (
<section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
<SectionHeader>Edge Deployment</SectionHeader>
<p className="text-[#7788aa] font-mono text-[10px] mb-4 leading-relaxed">
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.
</p>
{/* Benefits */}
<div className="space-y-2 mb-4">
{benefits.map((b) => (
<div
key={b.label}
className="flex items-start gap-2 text-[10px] font-mono"
>
<span className="text-[#75aafc] mt-px shrink-0">▸</span>
<div>
<span className="text-[#9bc3ff]">{b.label}</span>
<span className="text-[#556677] ml-1">— {b.desc}</span>
</div>
</div>
))}
</div>
{/* Quick Setup */}
<div className="border-t border-[rgba(117,170,252,0.1)] pt-3">
<button
onClick={() => setShowSetup(!showSetup)}
className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] transition-colors bg-transparent border-none cursor-pointer p-0"
>
{showSetup ? "- Hide" : "+"} Quick setup
</button>
{showSetup && (
<div className="mt-3 space-y-3">
<div className="flex items-start">
<StepNumber n={1} />
<div className="flex-1">
<p className="text-[#7788aa] font-mono text-[10px] mb-1">
Navigate to the Cloudflare agent directory
</p>
<CodeBlock>cd makima/cloudflare-agent</CodeBlock>
</div>
</div>
<div className="flex items-start">
<StepNumber n={2} />
<div className="flex-1">
<p className="text-[#7788aa] font-mono text-[10px] mb-1">
Run the setup script
</p>
<CodeBlock>./setup.sh</CodeBlock>
</div>
</div>
<div className="flex items-start">
<StepNumber n={3} />
<div className="flex-1">
<p className="text-[#7788aa] font-mono text-[10px] mb-1">
Deploy to Cloudflare
</p>
<CodeBlock>npx wrangler deploy</CodeBlock>
</div>
</div>
</div>
)}
</div>
{/* Link to repo */}
<div className="mt-4 pt-3 border-t border-[rgba(117,170,252,0.1)] flex items-center justify-between">
<span className="text-[10px] font-mono text-[#556677]">
Full documentation & source
</span>
<a
href="https://github.com/soryu-co/soryu/tree/master/makima/cloudflare-agent"
target="_blank"
rel="noopener noreferrer"
className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] transition-colors px-3 py-1.5 border border-[rgba(117,170,252,0.25)] hover:bg-[rgba(117,170,252,0.1)]"
>
View on GitHub →
</a>
</div>
</section>
);
}
// =============================================================================
// Connected Daemons Section
// =============================================================================
function ConnectedDaemonsSection() {
const [daemons, setDaemons] = useState<Daemon[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [restartingDaemonId, setRestartingDaemonId] = useState<string | null>(
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 (
<section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
<div className="flex items-center justify-between mb-3 pb-2 border-b border-[rgba(117,170,252,0.15)]">
<div className="flex items-center gap-2">
<h2 className="text-[11px] font-mono uppercase tracking-wide text-[#8899aa]">
Connected Daemons
</h2>
{daemons.length > 0 && (
<span className="text-[10px] font-mono text-[#556677]">
({daemons.filter((d) => d.status === "connected").length}{" "}
connected / {daemons.length} total)
</span>
)}
</div>
<button
onClick={loadDaemons}
disabled={loading}
className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] disabled:opacity-50 bg-transparent border-none cursor-pointer p-0"
title="Refresh"
>
{loading ? "..." : "\u21BB"}
</button>
</div>
{error && <ErrorAlert>{error}</ErrorAlert>}
{loading && daemons.length === 0 ? (
<p className="text-[#7788aa] font-mono text-xs">Loading...</p>
) : daemons.length === 0 ? (
<div className="text-center py-6">
<p className="text-[#7788aa] font-mono text-xs mb-2">
No daemons connected
</p>
<p className="text-[#556677] font-mono text-[10px]">
Follow the setup instructions above to connect a daemon
</p>
</div>
) : (
<div className="space-y-2">
{daemons.map((daemon) => (
<div
key={daemon.id}
className="border border-[rgba(117,170,252,0.15)] bg-[#0a1525] p-3"
>
<div className="flex items-center justify-between mb-2">
<span className="font-mono text-xs text-[#9bc3ff]">
{daemon.hostname || "Unknown Host"}
</span>
<div className="flex items-center gap-2">
<span
className={`text-[10px] font-mono uppercase px-2 py-0.5 border ${
daemon.status === "connected"
? "text-green-400 border-green-700/50 bg-green-900/20"
: daemon.status === "unhealthy"
? "text-yellow-400 border-yellow-700/50 bg-yellow-900/20"
: "text-[#8899aa] border-[rgba(117,170,252,0.25)]"
}`}
>
{daemon.status}
</span>
</div>
</div>
<div className="font-mono text-[10px] text-[#7788aa] space-y-1">
<div className="flex justify-between">
<span>Tasks</span>
<span className="text-[#9bc3ff]">
{daemon.currentTaskCount} / {daemon.maxConcurrentTasks}
</span>
</div>
<div className="flex justify-between">
<span>Connected</span>
<span className="text-[#75aafc]">
{new Date(daemon.connectedAt).toLocaleString()}
</span>
</div>
{daemon.machineId && (
<div className="flex justify-between">
<span>Machine</span>
<span
className="text-[#556677] truncate ml-2"
title={daemon.machineId}
>
{daemon.machineId.substring(0, 16)}...
</span>
</div>
)}
</div>
{/* Restart Section */}
{daemon.status === "connected" && (
<div className="mt-3 pt-2 border-t border-[rgba(117,170,252,0.1)]">
{restartConfirmDaemonId === daemon.id ? (
<div className="flex items-center justify-between gap-2">
<span className="text-[10px] font-mono text-yellow-400">
Restart daemon? Running tasks will be interrupted.
</span>
<div className="flex gap-2">
<button
onClick={() => setRestartConfirmDaemonId(null)}
className="text-[10px] font-mono text-[#7788aa] hover:text-[#9bc3ff] px-2 py-1 bg-transparent border-none cursor-pointer"
disabled={restartingDaemonId === daemon.id}
>
Cancel
</button>
<button
onClick={() => handleRestartDaemon(daemon.id)}
disabled={restartingDaemonId === daemon.id}
className="text-[10px] font-mono text-red-400 hover:text-red-300 px-2 py-1 border border-red-700/50 bg-red-900/20 disabled:opacity-50 cursor-pointer"
>
{restartingDaemonId === daemon.id
? "Restarting..."
: "Confirm"}
</button>
</div>
</div>
) : (
<button
onClick={() => setRestartConfirmDaemonId(daemon.id)}
className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] bg-transparent border-none cursor-pointer p-0"
>
⟳ Restart Daemon
</button>
)}
</div>
)}
</div>
))}
</div>
)}
</section>
);
}
// =============================================================================
// 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 (
<div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
<Masthead showNav />
<main className="flex-1 flex items-center justify-center">
<p className="text-[#7788aa] font-mono text-sm">Loading...</p>
</main>
</div>
);
}
return (
<div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
<Masthead showNav />
<main className="flex-1 px-4 py-6 max-w-4xl mx-auto w-full">
{/* Page header */}
<div className="mb-6">
<h1 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff] mb-1">
Daemon Management
</h1>
<p className="text-[#556677] font-mono text-[10px]">
Download, configure, and monitor Makima daemons
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column: Downloads & Setup */}
<div className="space-y-6">
<DownloadSection />
<SetupSection />
</div>
{/* Right Column: Edge Deployment & Connected Daemons */}
<div className="space-y-6">
<ConnectedDaemonsSection />
<CloudflareAgentSection />
</div>
</div>
</main>
</div>
);
}