diff options
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 1 | ||||
| -rw-r--r-- | makima/frontend/src/hooks/useMultiTaskSubscription.ts | 84 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 9 | ||||
| -rw-r--r-- | makima/frontend/src/routes/daemon.tsx | 746 | ||||
| -rw-r--r-- | makima/frontend/tsconfig.tsbuildinfo | 2 |
5 files changed, 757 insertions, 85 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 → + </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> + ); +} diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index 68f2eac..410931d 100644 --- a/makima/frontend/tsconfig.tsbuildinfo +++ b/makima/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/directives/directivedag.tsx","./src/components/directives/directivedetail.tsx","./src/components/directives/directivelist.tsx","./src/components/directives/directivelogstream.tsx","./src/components/directives/stepnode.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/orders/orderdetail.tsx","./src/components/orders/orderlist.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usedirectives.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usemultitasksubscription.ts","./src/hooks/useorders.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/directives.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/orders.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/directives/directivedag.tsx","./src/components/directives/directivedetail.tsx","./src/components/directives/directivelist.tsx","./src/components/directives/directivelogstream.tsx","./src/components/directives/orchestratorstepnode.tsx","./src/components/directives/stepnode.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/orders/orderdetail.tsx","./src/components/orders/orderlist.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usedirectives.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usemultitasksubscription.ts","./src/hooks/useorders.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/daemon.tsx","./src/routes/directives.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/orders.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/types/messages.ts"],"errors":true,"version":"5.9.3"}
\ No newline at end of file |
