summaryrefslogblamecommitdiff
path: root/makima/frontend/src/routes/daemons.tsx
blob: f551543a21b57b2c09fa735798b30500c86cd65f (plain) (tree)

















































































































































































































































































































































































                                                                                                                                                                                               
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>
  );
}