summaryrefslogblamecommitdiff
path: root/makima/frontend/src/routes/daemons.tsx
blob: 0f5519088498aaf24091d69d1909df86ebc6284e (plain) (tree)
1
2
3
4
5
6
7
8
9
                                            





                                                  

                          








































                                                                                                                                    






















                                                                                     
                              











                                                                                       






                                                                                                         



                   


































                                                                                                                                       















                                                                                                                                                                                                                            






                                                                                                             
                                                                                                       









































                                                                                                                                                             








                                                            




































































































































                                                                                                                                                                        
import { useState, useEffect } from "react";
import { useAuth } from "../contexts/AuthContext";
import { useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import {
  listDaemons,
  restartDaemon,
  type Daemon,
  type DaemonListResponse,
} 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);

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

  // Static platform data for download links
  const downloadPlatforms = [
    { key: "linux-x86_64", label: "Linux (Intel/AMD)", filename: "makima-vX.X.X-linux-x86_64.tar.gz" },
    { key: "linux-arm64", label: "Linux (ARM64)", filename: "makima-vX.X.X-linux-arm64.tar.gz" },
    { key: "macos-x86_64", label: "macOS (Intel)", filename: "makima-vX.X.X-macos-x86_64.tar.gz" },
    { key: "macos-arm64", label: "macOS (Apple Silicon)", filename: "makima-vX.X.X-macos-arm64.tar.gz" },
  ];

  // Initial load
  useEffect(() => {
    loadDaemons();
  }, []);

  // 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">
                {downloadPlatforms.map((p) => (
                  <a
                    key={p.key}
                    href="https://github.com/soryu-co/makima/releases/latest"
                    target="_blank"
                    rel="noopener noreferrer"
                    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 hover:bg-[#0d1f3a] cursor-pointer"
                  >
                    <span className="text-[10px] uppercase tracking-wide">
                      {p.label}
                    </span>
                    <span className="text-[10px] text-[#556677]">
                      {p.filename}
                    </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/makima/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 at{" "}
                <a
                  href="https://github.com/soryu-co/makima"
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-[#75aafc] hover:underline"
                >
                  github.com/soryu-co/makima
                </a>
              </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>
  );
}