import { useState, useEffect, useCallback, useRef } from "react";
import { useAuth } from "../contexts/AuthContext";
import { useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import {
listDaemons,
restartDaemon,
triggerDaemonReauth,
submitDaemonAuthCode,
getDaemonReauthStatus,
type Daemon,
type DaemonListResponse,
} from "../lib/api";
// =============================================================================
// Section Header Component
// =============================================================================
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>
);
}
// =============================================================================
// Alert Component
// =============================================================================
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>
);
}
// =============================================================================
// Reauth Modal Component
// =============================================================================
type ReauthState =
| { phase: "initiating" }
| { phase: "url_ready"; loginUrl: string; requestId: string }
| { phase: "submitting"; requestId: string }
| { phase: "success" }
| { phase: "error"; message: string };
function ReauthModal({
daemon,
onClose,
}: {
daemon: Daemon;
onClose: () => void;
}) {
const [state, setState] = useState<ReauthState>({ phase: "initiating" });
const [authCode, setAuthCode] = useState("");
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Cleanup polling on unmount
useEffect(() => {
return () => {
if (pollingRef.current) {
clearInterval(pollingRef.current);
}
};
}, []);
// Start polling for status updates (URL ready, then completion)
const startPolling = useCallback(
(requestId: string) => {
if (pollingRef.current) {
clearInterval(pollingRef.current);
}
pollingRef.current = setInterval(async () => {
try {
const status = await getDaemonReauthStatus(daemon.id, requestId);
if (status.status === "completed") {
setState({ phase: "success" });
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
} else if (status.status === "url_ready" && status.loginUrl) {
setState((prev) => {
// Only update if we haven't already shown the URL
if (prev.phase === "initiating") {
return {
phase: "url_ready",
loginUrl: status.loginUrl!,
requestId,
};
}
return prev;
});
// Keep polling for completion - don't stop here
} else if (status.status === "failed") {
setState({
phase: "error",
message: status.error || "Reauth failed",
});
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
}
} catch {
// Polling errors are non-fatal, keep trying
}
}, 2000);
},
[daemon.id],
);
// Trigger reauth on mount
useEffect(() => {
let cancelled = false;
const trigger = async () => {
try {
const res = await triggerDaemonReauth(daemon.id);
if (cancelled) return;
startPolling(res.requestId);
} catch (err) {
if (cancelled) return;
setState({
phase: "error",
message:
err instanceof Error ? err.message : "Failed to trigger reauth",
});
}
};
trigger();
return () => {
cancelled = true;
};
}, [daemon.id, startPolling]);
const handleSubmitCode = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
if (!authCode.trim() || state.phase !== "url_ready") return;
const requestId = state.requestId;
setState({ phase: "submitting", requestId });
try {
await submitDaemonAuthCode(daemon.id, authCode.trim(), requestId);
// Poll for completion
pollingRef.current = setInterval(async () => {
try {
const status = await getDaemonReauthStatus(
daemon.id,
requestId,
);
if (status.status === "completed") {
setState({ phase: "success" });
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
} else if (status.status === "failed") {
setState({
phase: "error",
message: status.error || "Auth code submission failed",
});
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
}
} catch {
// Keep polling
}
}, 2000);
// Also set a timeout so we don't poll forever
setTimeout(() => {
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
// If still submitting after 30s, assume success (setup-token completed)
setState((prev) =>
prev.phase === "submitting" ? { phase: "success" } : prev,
);
}, 30000);
} catch (err) {
setState({
phase: "error",
message:
err instanceof Error
? err.message
: "Failed to submit auth code",
});
}
},
[authCode, daemon.id, state],
);
const handleRetry = useCallback(() => {
setAuthCode("");
setState({ phase: "initiating" });
const trigger = async () => {
try {
const res = await triggerDaemonReauth(daemon.id);
startPolling(res.requestId);
} catch (err) {
setState({
phase: "error",
message:
err instanceof Error
? err.message
: "Failed to trigger reauth",
});
}
};
trigger();
}, [daemon.id, startPolling]);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
<div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-6 max-w-md w-full mx-4 shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-xs font-mono uppercase tracking-wide text-[#9bc3ff]">
Reauthorize Daemon
</h3>
<button
onClick={onClose}
className="text-[#7788aa] hover:text-[#9bc3ff] text-sm font-mono"
>
✕
</button>
</div>
<p className="text-[10px] font-mono text-[#7788aa] mb-4">
{daemon.hostname || "Unknown Host"}
</p>
{/* Initiating */}
{state.phase === "initiating" && (
<div className="flex items-center gap-2 py-4">
<div className="w-3 h-3 border border-[#75aafc] border-t-transparent rounded-full animate-spin" />
<span className="text-[10px] font-mono text-[#7788aa]">
Initiating reauthorization...
</span>
</div>
)}
{/* URL Ready */}
{state.phase === "url_ready" && (
<div className="space-y-3">
<p className="text-[10px] font-mono text-[#7788aa]">
Click the button below to open the OAuth login page, then paste the code:
</p>
<a
href={state.loginUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-center bg-amber-500 hover:bg-amber-400 text-black font-mono text-xs font-medium px-4 py-2 transition-colors"
>
1. Login to Claude
</a>
<form onSubmit={handleSubmitCode} className="flex gap-2">
<input
type="text"
value={authCode}
onChange={(e) => setAuthCode(e.target.value)}
placeholder="2. Paste authentication code"
className="flex-1 bg-[#0a1525] border border-amber-500/30 px-3 py-2 text-xs font-mono text-amber-100 placeholder-amber-500/50 focus:outline-none focus:border-amber-400"
/>
<button
type="submit"
disabled={!authCode.trim()}
className="bg-amber-500 hover:bg-amber-400 disabled:bg-amber-700 disabled:cursor-not-allowed text-black font-mono text-xs font-medium px-4 py-2 transition-colors"
>
Submit
</button>
</form>
</div>
)}
{/* Submitting */}
{state.phase === "submitting" && (
<div className="flex items-center gap-2 py-4">
<div className="w-3 h-3 border border-amber-400 border-t-transparent rounded-full animate-spin" />
<span className="text-[10px] font-mono text-[#7788aa]">
Submitting auth code...
</span>
</div>
)}
{/* Success */}
{state.phase === "success" && (
<div className="py-4">
<div className="flex items-center gap-2 text-green-400 mb-2">
<span className="text-sm">✓</span>
<span className="text-xs font-mono font-medium">
Authentication successful
</span>
</div>
<p className="text-[10px] font-mono text-[#7788aa] mb-3">
The daemon's OAuth token has been refreshed. Tasks can now run normally.
</p>
<button
onClick={onClose}
className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff]"
>
Close
</button>
</div>
)}
{/* Error */}
{state.phase === "error" && (
<div className="py-4">
<div className="border border-red-700/50 bg-red-900/20 text-red-400 px-3 py-2 mb-3 font-mono text-xs">
{state.message}
</div>
<div className="flex gap-2">
<button
onClick={handleRetry}
className="text-[10px] font-mono text-amber-400 hover:text-amber-300 px-2 py-1 border border-amber-700/50 bg-amber-900/20"
>
Retry
</button>
<button
onClick={onClose}
className="text-[10px] font-mono text-[#7788aa] hover:text-[#9bc3ff] px-2 py-1"
>
Close
</button>
</div>
</div>
)}
</div>
</div>
);
}
// =============================================================================
// Daemons Page
// =============================================================================
export default function DaemonsPage() {
const { isAuthenticated, isAuthConfigured } = useAuth();
const navigate = useNavigate();
// Daemon state
const [daemons, setDaemons] = useState<Daemon[]>([]);
const [daemonsLoading, setDaemonsLoading] = useState(true);
const [daemonsError, setDaemonsError] = useState<string | null>(null);
const [restartingDaemonId, setRestartingDaemonId] = useState<string | null>(null);
const [restartConfirmDaemonId, setRestartConfirmDaemonId] = useState<string | null>(null);
const [reauthDaemon, setReauthDaemon] = useState<Daemon | null>(null);
// 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);
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);
}
};
// Static platform data for download links
const downloadPlatforms = [
{ key: "linux-x86_64", label: "Linux (Intel/AMD)", filename: "makima-vX.X.X-linux-x86_64.tar.gz" },
{ key: "linux-arm64", label: "Linux (ARM64)", filename: "makima-vX.X.X-linux-arm64.tar.gz" },
{ key: "macos-x86_64", label: "macOS (Intel)", filename: "makima-vX.X.X-macos-x86_64.tar.gz" },
{ key: "macos-arm64", label: "macOS (Apple Silicon)", filename: "makima-vX.X.X-macos-arm64.tar.gz" },
];
// Initial load
useEffect(() => {
loadDaemons();
}, []);
// Auto-refresh daemons every 30 seconds
useEffect(() => {
const interval = setInterval(() => {
loadDaemons();
}, 30000);
return () => clearInterval(interval);
}, []);
return (
<div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
<Masthead showNav />
<main className="flex-1 max-w-4xl mx-auto p-6 w-full">
{/* Page Header */}
<div className="mb-8">
<h1 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff]">Daemons</h1>
<p className="text-[#7788aa] font-mono text-[10px] mt-2">
Daemons are worker processes that connect to Makima and execute tasks on your machines.
</p>
<div className="h-px bg-[rgba(117,170,252,0.35)] mt-2" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column */}
<div className="space-y-6">
{/* Download Section */}
<section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
<SectionHeader>Download Daemon</SectionHeader>
<p className="text-[#7788aa] font-mono text-[10px] mb-4">
Download the pre-compiled daemon binary for your platform. The daemon connects to the Makima server and executes tasks.
</p>
<div className="grid grid-cols-2 gap-2 mb-4">
{downloadPlatforms.map((p) => (
<a
key={p.key}
href="https://github.com/soryu-co/makima/releases/latest"
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center justify-center gap-1 p-3 border border-[rgba(117,170,252,0.25)] bg-[#0a1525] font-mono text-xs text-[#9bc3ff] transition-colors hover:bg-[#0d1f3a] cursor-pointer"
>
<span className="text-[10px] uppercase tracking-wide">
{p.label}
</span>
<span className="text-[10px] text-[#556677]">
{p.filename}
</span>
</a>
))}
</div>
<div className="border-t border-[rgba(117,170,252,0.15)] pt-3">
<p className="text-[10px] font-mono uppercase tracking-wide text-[#8899aa] mb-2">
Quick Install
</p>
<code className="block bg-black/50 px-3 py-2 text-[11px] font-mono text-green-400 break-all">
curl -fsSL https://raw.githubusercontent.com/soryu-co/makima/master/install.sh | bash
</code>
</div>
</section>
{/* Daemon Setup */}
<section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
<SectionHeader>Daemon Setup</SectionHeader>
<p className="text-[#7788aa] font-mono text-[10px] mb-3">
Set your API key as an environment variable:
</p>
<code className="block bg-black/50 px-3 py-2 text-[11px] font-mono text-green-400 mb-3">
export MAKIMA_API_KEY="your-key"
</code>
<p className="text-[#7788aa] font-mono text-[10px]">
Then run: <code className="text-green-400">makima-daemon</code>
</p>
</section>
{/* Kubernetes Section */}
<section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
<SectionHeader>Run in Kubernetes</SectionHeader>
<p className="text-[#7788aa] font-mono text-[10px] mb-3">
Deploy daemons as containers in Kubernetes for scalable task execution.
</p>
<p className="text-[10px] font-mono uppercase tracking-wide text-[#8899aa] mb-2">
Pull Container Image
</p>
<code className="block bg-black/50 px-3 py-2 text-[11px] font-mono text-green-400 mb-4 break-all">
docker pull ghcr.io/soryu-co/makima-daemon:latest
</code>
<p className="text-[10px] font-mono uppercase tracking-wide text-[#8899aa] mb-2">
Environment Variables
</p>
<div className="bg-black/50 px-3 py-2 text-[11px] font-mono text-green-400 space-y-1 mb-3">
<div>MAKIMA_API_KEY=<span className="text-[#7788aa]">"your-key"</span></div>
<div>MAKIMA_SERVER_URL=<span className="text-[#7788aa]">"https://your-server"</span></div>
<div>GITHUB_TOKEN=<span className="text-[#7788aa]">"ghp_..." </span><span className="text-[#556677]"># optional, for repo access</span></div>
</div>
<p className="text-[#556677] font-mono text-[10px]">
Kubernetes manifests available at{" "}
<a
href="https://github.com/soryu-co/makima"
target="_blank"
rel="noopener noreferrer"
className="text-[#75aafc] hover:underline"
>
github.com/soryu-co/makima
</a>
</p>
</section>
</div>
{/* Right Column */}
<div className="space-y-6">
{/* Connected Daemons */}
<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={daemonsLoading}
className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] disabled:opacity-50"
title="Refresh"
>
{daemonsLoading ? "..." : "\u21BB"}
</button>
</div>
{daemonsError && <ErrorAlert>{daemonsError}</ErrorAlert>}
{daemonsLoading && daemons.length === 0 ? (
<p className="text-[#7788aa] font-mono text-xs">Loading...</p>
) : daemons.length === 0 ? (
<div className="text-center py-4">
<p className="text-[#7788aa] font-mono text-xs mb-2">No daemons connected</p>
<p className="text-[#556677] font-mono text-[10px]">
Start a daemon to enable task execution
</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>
{/* Actions 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"
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"
>
{restartingDaemonId === daemon.id ? "Restarting..." : "Confirm"}
</button>
</div>
</div>
) : (
<div className="flex items-center gap-3">
<button
onClick={() => setReauthDaemon(daemon)}
className="text-[10px] font-mono text-amber-400 hover:text-amber-300"
>
🔑 Reauthorize
</button>
<button
onClick={() => setRestartConfirmDaemonId(daemon.id)}
className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff]"
>
⟳ Restart Daemon
</button>
</div>
)}
</div>
)}
</div>
))}
</div>
)}
</section>
</div>
</div>
</main>
{/* Reauth Modal */}
{reauthDaemon && (
<ReauthModal
daemon={reauthDaemon}
onClose={() => setReauthDaemon(null)}
/>
)}
</div>
);
}