summaryrefslogtreecommitdiff
path: root/makima/frontend/src
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-22 14:39:14 +0000
committerGitHub <noreply@github.com>2026-02-22 14:39:14 +0000
commit6a34a6f3c423a7c57616762eb4cea2b7da52eaf3 (patch)
tree7c596eac896918466e7ef3f149b02333fef09212 /makima/frontend/src
parent0523765af84492640928d571f481e17b26008b13 (diff)
downloadsoryu-6a34a6f3c423a7c57616762eb4cea2b7da52eaf3.tar.gz
soryu-6a34a6f3c423a7c57616762eb4cea2b7da52eaf3.zip
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
Diffstat (limited to 'makima/frontend/src')
-rw-r--r--makima/frontend/src/components/NavStrip.tsx1
-rw-r--r--makima/frontend/src/hooks/useMultiTaskSubscription.ts84
-rw-r--r--makima/frontend/src/main.tsx9
-rw-r--r--makima/frontend/src/routes/daemon.tsx746
4 files changed, 756 insertions, 84 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 9556458..1bd0891 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -17,6 +17,7 @@ const NAV_LINKS: NavLink[] = [
{ label: "Mesh", href: "/mesh", requiresAuth: true },
{ label: "Daemons", href: "/daemons", requiresAuth: true },
{ label: "History", href: "/history", requiresAuth: true },
+ { label: "Daemon", href: "/daemon", requiresAuth: true },
];
export function NavStrip() {
diff --git a/makima/frontend/src/hooks/useMultiTaskSubscription.ts b/makima/frontend/src/hooks/useMultiTaskSubscription.ts
index 41489c7..b229e90 100644
--- a/makima/frontend/src/hooks/useMultiTaskSubscription.ts
+++ b/makima/frontend/src/hooks/useMultiTaskSubscription.ts
@@ -31,8 +31,6 @@ export function useMultiTaskSubscription(options: UseMultiTaskSubscriptionOption
const backfilledTasksRef = useRef<Set<string>>(new Set());
const taskMapRef = useRef(taskMap);
const enabledRef = useRef(enabled);
- /** Track which task IDs have already been backfilled to avoid re-fetching */
- const backfilledTasksRef = useRef<Set<string>>(new Set());
// Keep refs in sync
useEffect(() => {
@@ -43,88 +41,6 @@ export function useMultiTaskSubscription(options: UseMultiTaskSubscriptionOption
enabledRef.current = enabled;
}, [enabled]);
- /** Max number of historical events to backfill per task */
- const MAX_BACKFILL_PER_TASK = 200;
-
- /**
- * Convert a TaskEvent (from the REST API) into a MultiTaskOutputEntry.
- * Only converts events with event_type === 'output'.
- */
- const convertTaskEventToEntry = useCallback(
- (event: TaskEvent): MultiTaskOutputEntry | null => {
- if (event.eventType !== "output") return null;
- const data = event.eventData;
- if (!data) return null;
-
- return {
- taskId: event.taskId,
- messageType: (data.messageType as string) || "system",
- content: (data.content as string) || "",
- toolName: data.toolName as string | undefined,
- toolInput: data.toolInput as Record<string, unknown> | undefined,
- isError: data.isError as boolean | undefined,
- costUsd: data.costUsd as number | undefined,
- durationMs: data.durationMs as number | undefined,
- isPartial: false,
- taskLabel:
- taskMapRef.current.get(event.taskId) || event.taskId,
- receivedAt: new Date(event.createdAt).getTime(),
- isBackfill: true,
- };
- },
- []
- );
-
- /**
- * Backfill historical log entries for a task from the REST API.
- * Only fetches once per task ID (tracked in backfilledTasksRef).
- */
- const backfillTask = useCallback(
- async (taskId: string) => {
- if (backfilledTasksRef.current.has(taskId)) return;
- backfilledTasksRef.current.add(taskId);
-
- try {
- const response = await listTaskEvents(taskId);
- const events = response.events;
-
- // The API returns events in DESC order; reverse to get chronological ASC
- const chronologicalEvents = [...events].reverse();
-
- // Filter to output events and convert, limiting to MAX_BACKFILL_PER_TASK
- const backfillEntries: MultiTaskOutputEntry[] = [];
- for (const event of chronologicalEvents) {
- const entry = convertTaskEventToEntry(event);
- if (entry) {
- backfillEntries.push(entry);
- if (backfillEntries.length >= MAX_BACKFILL_PER_TASK) break;
- }
- }
-
- if (backfillEntries.length === 0) return;
-
- // Prepend historical entries before any existing live entries for this task,
- // maintaining overall chronological order across all tasks
- setEntries((prev) => {
- // Merge backfill entries with existing entries, maintaining chronological order
- const merged = [...backfillEntries, ...prev];
- // Sort by receivedAt to ensure proper chronological ordering
- merged.sort((a, b) => a.receivedAt - b.receivedAt);
- // Trim to maxEntries
- if (merged.length > maxEntries) {
- return merged.slice(merged.length - maxEntries);
- }
- return merged;
- });
- } catch (e) {
- console.error(`Failed to backfill task events for ${taskId}:`, e);
- // Remove from backfilled set so it can be retried
- backfilledTasksRef.current.delete(taskId);
- }
- },
- [convertTaskEventToEntry, maxEntries]
- );
-
// Derive task IDs from the map, stabilized to avoid unnecessary effect triggers
const taskIdsKey = useMemo(() => Array.from(taskMap.keys()).sort().join(","), [taskMap]);
const taskIds = useMemo(() => Array.from(taskMap.keys()), [taskIdsKey]); // eslint-disable-line react-hooks/exhaustive-deps
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index 32c05ba..a75d6a0 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -21,6 +21,7 @@ import SettingsPage from "./routes/settings";
import ContractFilePage from "./routes/contract-file";
import SpeakPage from "./routes/speak";
import DirectivesPage from "./routes/directives";
+import DaemonPage from "./routes/daemon";
createRoot(document.getElementById("root")!).render(
<StrictMode>
@@ -162,6 +163,14 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
+ path="/daemon"
+ element={
+ <ProtectedRoute>
+ <DaemonPage />
+ </ProtectedRoute>
+ }
+ />
+ <Route
path="/speak"
element={
<ProtectedRoute>
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 (
+ <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 &rarr;
+ </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">&#x25B8;</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 &rarr;
+ </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"
+ >
+ &#x27F3; 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>
+ );
+}