diff options
| author | soryu <soryu@soryu.co> | 2026-02-21 23:51:11 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-21 23:51:11 +0000 |
| commit | 0523765af84492640928d571f481e17b26008b13 (patch) | |
| tree | 644e0bac90c1945120df27dea36d18c81f4470e9 /makima/frontend | |
| parent | d670dcb72984cfa483063d161bb468704038895c (diff) | |
| download | soryu-0523765af84492640928d571f481e17b26008b13.tar.gz soryu-0523765af84492640928d571f481e17b26008b13.zip | |
feat: Add daemon health monitoring page, downloads & K8s support (#76)
* feat: soryu-co/soryu - makima: Add server-side daemon binary download endpoint
* feat: soryu-co/soryu - makima: Create Kubernetes daemon manifests and Dockerfile
* feat: soryu-co/soryu - makima: Create dedicated Daemons page with health monitoring UI
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Integrate daemon platform availability into frontend downloads
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 1 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 36 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 9 | ||||
| -rw-r--r-- | makima/frontend/src/routes/daemons.tsx | 370 | ||||
| -rw-r--r-- | makima/frontend/src/routes/settings.tsx | 185 |
5 files changed, 422 insertions, 179 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index 4932427..9556458 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -15,6 +15,7 @@ const NAV_LINKS: NavLink[] = [ { label: "Orders", href: "/orders", requiresAuth: true }, { label: "Contracts", href: "/contracts", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, + { label: "Daemons", href: "/daemons", requiresAuth: true }, { label: "History", href: "/history", requiresAuth: true }, ]; diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 43eaa05..458b69d 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -1222,6 +1222,42 @@ export async function restartDaemon(id: string): Promise<RestartDaemonResponse> } // ============================================================================= +// Daemon Platform Download +// ============================================================================= + +/** A daemon platform with its availability and download URL */ +export interface DaemonPlatform { + platform: string; + available: boolean; + downloadUrl: string; +} + +/** Response from the list daemon platforms endpoint */ +export interface DaemonPlatformsResponse { + platforms: DaemonPlatform[]; +} + +/** + * List available daemon platforms and their download status. + * This is an unauthenticated endpoint. + */ +export async function listDaemonPlatforms(): Promise<DaemonPlatformsResponse> { + const res = await fetch(`${API_BASE}/api/v1/daemon/download/platforms`); + if (!res.ok) { + throw new Error(`Failed to list daemon platforms: ${res.statusText}`); + } + return res.json(); +} + +/** + * Get the full download URL for a daemon binary. + * Returns the absolute URL including API_BASE for cross-origin usage. + */ +export function getDaemonDownloadUrl(platform: string): string { + return `${API_BASE}/api/v1/daemon/download/${platform}`; +} + +// ============================================================================= // Mesh Chat Types for Task Orchestration // ============================================================================= diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx index acc9afc..32c05ba 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -14,6 +14,7 @@ import FilesPage from "./routes/files"; import ContractsPage from "./routes/contracts"; import OrdersPage from "./routes/orders"; import MeshPage from "./routes/mesh"; +import DaemonsPage from "./routes/daemons"; import HistoryPage from "./routes/history"; import LoginPage from "./routes/login"; import SettingsPage from "./routes/settings"; @@ -113,6 +114,14 @@ createRoot(document.getElementById("root")!).render( } /> <Route + path="/daemons" + element={ + <ProtectedRoute> + <DaemonsPage /> + </ProtectedRoute> + } + /> + <Route path="/history" element={ <ProtectedRoute> diff --git a/makima/frontend/src/routes/daemons.tsx b/makima/frontend/src/routes/daemons.tsx new file mode 100644 index 0000000..f551543 --- /dev/null +++ b/makima/frontend/src/routes/daemons.tsx @@ -0,0 +1,370 @@ +import { useState, useEffect, useCallback } from "react"; +import { useAuth } from "../contexts/AuthContext"; +import { useNavigate } from "react-router"; +import { Masthead } from "../components/Masthead"; +import { + listDaemons, + restartDaemon, + listDaemonPlatforms, + API_BASE, + type Daemon, + type DaemonListResponse, + type DaemonPlatform, + type RestartDaemonResponse, +} 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> + ); +} + +// ============================================================================= +// 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); + + // Platform availability state + const [platforms, setPlatforms] = useState<DaemonPlatform[]>([]); + const [platformsLoading, setPlatformsLoading] = useState(true); + + // 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); + const _response: RestartDaemonResponse = 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); + } + }; + + // Friendly labels for platform identifiers + const platformLabels: Record<string, string> = { + "linux-x86_64": "Linux (Intel/AMD)", + "linux-arm64": "Linux (ARM64)", + "macos-x86_64": "macOS (Intel)", + "macos-arm64": "macOS (Apple Silicon)", + }; + + const loadPlatforms = useCallback(async () => { + try { + setPlatformsLoading(true); + const response = await listDaemonPlatforms(); + setPlatforms(response.platforms); + } catch { + // Fallback: show all platforms as unavailable if API endpoint is missing + setPlatforms([ + { platform: "linux-x86_64", available: false, downloadUrl: "/api/v1/daemon/download/linux-x86_64" }, + { platform: "linux-arm64", available: false, downloadUrl: "/api/v1/daemon/download/linux-arm64" }, + { platform: "macos-x86_64", available: false, downloadUrl: "/api/v1/daemon/download/macos-x86_64" }, + { platform: "macos-arm64", available: false, downloadUrl: "/api/v1/daemon/download/macos-arm64" }, + ]); + } finally { + setPlatformsLoading(false); + } + }, []); + + // Initial load + useEffect(() => { + loadDaemons(); + loadPlatforms(); + }, []); + + // 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"> + {platformsLoading ? ( + <p className="col-span-2 text-[#7788aa] font-mono text-[10px]">Loading platforms...</p> + ) : ( + platforms.map((p) => ( + <a + key={p.platform} + href={p.available ? `${API_BASE}${p.downloadUrl}` : undefined} + download={p.available ? true : undefined} + 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 ${ + p.available + ? "hover:bg-[#0d1f3a] cursor-pointer" + : "opacity-50 pointer-events-none" + }`} + > + <span className="text-[10px] uppercase tracking-wide"> + {platformLabels[p.platform] || p.platform} + </span> + <span + className={`text-[10px] ${ + p.available ? "text-green-400" : "text-[#556677]" + }`} + > + {p.available ? "Available" : "Not bundled"} + </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/soryu/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 in the repository under <code className="text-[#75aafc]">k8s/daemon/</code> + </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> + {/* 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" + 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> + ) : ( + <button + onClick={() => setRestartConfirmDaemonId(daemon.id)} + className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff]" + > + ⟳ Restart Daemon + </button> + )} + </div> + )} + </div> + ))} + </div> + )} + </section> + </div> + </div> + </main> + </div> + ); +} diff --git a/makima/frontend/src/routes/settings.tsx b/makima/frontend/src/routes/settings.tsx index b93ecbc..73537bd 100644 --- a/makima/frontend/src/routes/settings.tsx +++ b/makima/frontend/src/routes/settings.tsx @@ -10,13 +10,10 @@ import { changePassword, changeEmail, deleteAccount, - listDaemons, - restartDaemon, listRepositoryHistory, deleteRepositoryHistory, type ApiKeyInfo, type CreateApiKeyResponse, - type Daemon, type RepositoryHistoryEntry, } from "../lib/api"; @@ -303,13 +300,6 @@ export default function SettingsPage() { const [deleteLoading, setDeleteLoading] = useState(false); const [deleteError, setDeleteError] = useState<string | null>(null); - // 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); - // Repository history state const [repoHistory, setRepoHistory] = useState<RepositoryHistoryEntry[]>([]); const [repoHistoryLoading, setRepoHistoryLoading] = useState(true); @@ -318,18 +308,9 @@ export default function SettingsPage() { useEffect(() => { loadApiKey(); - loadDaemons(); loadRepoHistory(); }, []); - // Auto-refresh daemons every 30 seconds - useEffect(() => { - const interval = setInterval(() => { - loadDaemons(); - }, 30000); - return () => clearInterval(interval); - }, []); - const loadApiKey = async () => { try { setLoading(true); @@ -343,18 +324,6 @@ export default function SettingsPage() { } }; - const loadDaemons = async () => { - try { - setDaemonsError(null); - const response = await listDaemons(); - setDaemons(response.daemons); - } catch (err) { - setDaemonsError(err instanceof Error ? err.message : "Failed to load daemons"); - } finally { - setDaemonsLoading(false); - } - }; - const loadRepoHistory = async () => { try { setRepoHistoryError(null); @@ -379,23 +348,6 @@ export default function SettingsPage() { } }; - 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); - } - }; - const handleCreate = async () => { try { setActionLoading(true); @@ -648,140 +600,15 @@ export default function SettingsPage() { )} </section> - {/* Daemon Setup */} + {/* Daemons Link */} <section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4"> - <SectionHeader>Daemon Setup</SectionHeader> + <SectionHeader>Daemons</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> + Daemon management has moved to its own page. </p> - </section> - - {/* 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]"> - 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 ? "..." : "↻"} - </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> - {/* 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" - 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> - ) : ( - <button - onClick={() => setRestartConfirmDaemonId(daemon.id)} - className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff]" - > - ⟳ Restart Daemon - </button> - )} - </div> - )} - </div> - ))} - </div> - )} + <a href="/daemons" className="text-[#75aafc] hover:text-[#9bc3ff] text-xs font-mono"> + Go to Daemons → + </a> </section> {/* Repository History */} |
