summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-21 23:51:11 +0000
committerGitHub <noreply@github.com>2026-02-21 23:51:11 +0000
commit0523765af84492640928d571f481e17b26008b13 (patch)
tree644e0bac90c1945120df27dea36d18c81f4470e9 /makima/frontend
parentd670dcb72984cfa483063d161bb468704038895c (diff)
downloadsoryu-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.tsx1
-rw-r--r--makima/frontend/src/lib/api.ts36
-rw-r--r--makima/frontend/src/main.tsx9
-rw-r--r--makima/frontend/src/routes/daemons.tsx370
-rw-r--r--makima/frontend/src/routes/settings.tsx185
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]"
+ >
+ &#x27F3; 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 &rarr;
+ </a>
</section>
{/* Repository History */}