From cf0a25af1d2834bfe6c5ea892ce5769936e5a673 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Tue, 3 Feb 2026 22:01:29 +0000
Subject: Add makima chain mechanism

---
 makima/frontend/src/routes/chains.tsx | 283 ++++++++++++++++++++++++++++++++++
 1 file changed, 283 insertions(+)
 create mode 100644 makima/frontend/src/routes/chains.tsx

(limited to 'makima/frontend/src/routes/chains.tsx')

diff --git a/makima/frontend/src/routes/chains.tsx b/makima/frontend/src/routes/chains.tsx
new file mode 100644
index 0000000..f01d5a6
--- /dev/null
+++ b/makima/frontend/src/routes/chains.tsx
@@ -0,0 +1,283 @@
+import { useState, useCallback, useEffect } from "react";
+import { useParams, useNavigate } from "react-router";
+import { Masthead } from "../components/Masthead";
+import { ChainList } from "../components/chains/ChainList";
+import { ChainEditor } from "../components/chains/ChainEditor";
+import { useChains } from "../hooks/useChains";
+import { useAuth } from "../contexts/AuthContext";
+import type {
+  ChainSummary,
+  ChainWithContracts,
+  ChainGraphResponse,
+  CreateChainRequest,
+} from "../lib/api";
+
+export default function ChainsPage() {
+  const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
+  const navigate = useNavigate();
+
+  // Redirect to login if not authenticated (when auth is configured)
+  useEffect(() => {
+    if (!authLoading && isAuthConfigured && !isAuthenticated) {
+      navigate("/login");
+    }
+  }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
+
+  // Show loading while checking auth
+  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>
+    );
+  }
+
+  // Don't render if not authenticated (will redirect)
+  if (isAuthConfigured && !isAuthenticated) {
+    return null;
+  }
+
+  return <ChainsPageContent />;
+}
+
+function ChainsPageContent() {
+  const { id } = useParams<{ id: string }>();
+  const navigate = useNavigate();
+  const {
+    chains,
+    loading,
+    error,
+    createNewChain,
+    archiveExistingChain,
+    getChainById,
+    getGraph,
+  } = useChains();
+
+  const [chainDetail, setChainDetail] = useState<ChainWithContracts | null>(null);
+  const [chainGraph, setChainGraph] = useState<ChainGraphResponse | null>(null);
+  const [detailLoading, setDetailLoading] = useState(false);
+  const [isCreating, setIsCreating] = useState(false);
+
+  // Load chain detail when ID changes
+  useEffect(() => {
+    if (id) {
+      setDetailLoading(true);
+      Promise.all([getChainById(id), getGraph(id)]).then(([chain, graph]) => {
+        setChainDetail(chain);
+        setChainGraph(graph);
+        setDetailLoading(false);
+      });
+    } else {
+      setChainDetail(null);
+      setChainGraph(null);
+    }
+  }, [id, getChainById, getGraph]);
+
+  const handleSelect = useCallback(
+    (chainId: string) => {
+      navigate(`/chains/${chainId}`);
+    },
+    [navigate]
+  );
+
+  const handleBack = useCallback(() => {
+    navigate("/chains");
+  }, [navigate]);
+
+  const handleCreate = useCallback(() => {
+    setIsCreating(true);
+  }, []);
+
+  const handleCreateSubmit = useCallback(
+    async (name: string, description: string) => {
+      const data: CreateChainRequest = {
+        name: name.trim(),
+        description: description.trim() || undefined,
+      };
+
+      try {
+        const result = await createNewChain(data);
+        if (result) {
+          setIsCreating(false);
+          navigate(`/chains/${result.chain.id}`);
+        }
+      } catch (err) {
+        console.error("Failed to create chain:", err);
+      }
+    },
+    [createNewChain, navigate]
+  );
+
+  const handleCreateCancel = useCallback(() => {
+    setIsCreating(false);
+  }, []);
+
+  const handleArchive = useCallback(
+    async (chain: ChainSummary) => {
+      if (confirm(`Are you sure you want to archive "${chain.name}"?`)) {
+        const success = await archiveExistingChain(chain.id);
+        if (success && chain.id === id) {
+          navigate("/chains");
+        }
+      }
+    },
+    [archiveExistingChain, id, navigate]
+  );
+
+  const handleRefresh = useCallback(async () => {
+    if (id) {
+      const [chain, graph] = await Promise.all([getChainById(id), getGraph(id)]);
+      setChainDetail(chain);
+      setChainGraph(graph);
+    }
+  }, [id, getChainById, getGraph]);
+
+  const handleContractClick = useCallback(
+    (contractId: string) => {
+      navigate(`/contracts/${contractId}`);
+    },
+    [navigate]
+  );
+
+  return (
+    <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
+      <Masthead showNav />
+      <main className="flex-1 flex flex-col p-4 pt-2 gap-4 overflow-hidden">
+        {error && (
+          <div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm">
+            {error}
+          </div>
+        )}
+
+        {/* Create chain modal */}
+        {isCreating && (
+          <CreateChainModal
+            onSubmit={handleCreateSubmit}
+            onCancel={handleCreateCancel}
+          />
+        )}
+
+        <div className="flex-1 grid grid-cols-[350px_1fr] gap-4 min-h-0">
+          {/* Chain list */}
+          <ChainList
+            chains={chains}
+            loading={loading}
+            onSelect={handleSelect}
+            onCreate={handleCreate}
+            selectedId={id}
+            onArchive={handleArchive}
+          />
+
+          {/* Chain detail/editor or empty state */}
+          {chainDetail ? (
+            <ChainEditor
+              chain={chainDetail}
+              graph={chainGraph}
+              loading={detailLoading}
+              onBack={handleBack}
+              onRefresh={handleRefresh}
+              onContractClick={handleContractClick}
+            />
+          ) : (
+            <div className="panel h-full flex items-center justify-center">
+              <div className="text-center">
+                <p className="font-mono text-sm text-[#555] mb-4">
+                  Select a chain or create a new one
+                </p>
+                <button
+                  onClick={handleCreate}
+                  className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
+                >
+                  + New Chain
+                </button>
+              </div>
+            </div>
+          )}
+        </div>
+      </main>
+    </div>
+  );
+}
+
+interface CreateChainModalProps {
+  onSubmit: (name: string, description: string) => void;
+  onCancel: () => void;
+}
+
+function CreateChainModal({ onSubmit, onCancel }: CreateChainModalProps) {
+  const [name, setName] = useState("");
+  const [description, setDescription] = useState("");
+
+  const handleSubmit = () => {
+    if (name.trim()) {
+      onSubmit(name.trim(), description.trim());
+    }
+  };
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
+      <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]">
+        <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4">
+          Create Chain
+        </h3>
+
+        <div className="space-y-4">
+          {/* Chain name */}
+          <div>
+            <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
+              Chain Name
+            </label>
+            <input
+              type="text"
+              value={name}
+              onChange={(e) => setName(e.target.value)}
+              placeholder="e.g., Feature Implementation"
+              className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
+              autoFocus
+            />
+          </div>
+
+          {/* Description */}
+          <div>
+            <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
+              Description (optional)
+            </label>
+            <textarea
+              value={description}
+              onChange={(e) => setDescription(e.target.value)}
+              placeholder="Describe what this chain accomplishes..."
+              rows={3}
+              className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none"
+            />
+          </div>
+
+          <p className="font-mono text-xs text-[#8b949e]">
+            A chain links multiple contracts together in a directed acyclic graph (DAG).
+            Contracts can depend on each other, and dependent contracts start automatically
+            when their dependencies complete.
+          </p>
+
+          {/* Actions */}
+          <div className="flex gap-2 justify-end pt-2">
+            <button
+              onClick={onCancel}
+              className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
+            >
+              Cancel
+            </button>
+            <button
+              onClick={handleSubmit}
+              disabled={!name.trim()}
+              className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+            >
+              Create
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
-- 
cgit v1.2.3


From dcbf8c834626870a43b633b099f409d69d4f9b87 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Tue, 3 Feb 2026 22:35:19 +0000
Subject: Fix: FE type error

---
 makima/frontend/src/components/chains/ChainEditor.tsx | 10 +++++-----
 makima/frontend/src/lib/api.ts                        |  5 ++---
 makima/frontend/src/routes/chains.tsx                 |  2 +-
 3 files changed, 8 insertions(+), 9 deletions(-)

(limited to 'makima/frontend/src/routes/chains.tsx')

diff --git a/makima/frontend/src/components/chains/ChainEditor.tsx b/makima/frontend/src/components/chains/ChainEditor.tsx
index 92bd496..0dcabe1 100644
--- a/makima/frontend/src/components/chains/ChainEditor.tsx
+++ b/makima/frontend/src/components/chains/ChainEditor.tsx
@@ -114,19 +114,19 @@ export function ChainEditor({
               Back
             </button>
             <div>
-              <h2 className="font-mono text-sm text-[#dbe7ff]">{chain.chain.name}</h2>
-              {chain.chain.description && (
-                <p className="font-mono text-xs text-[#8b949e]">{chain.chain.description}</p>
+              <h2 className="font-mono text-sm text-[#dbe7ff]">{chain.name}</h2>
+              {chain.description && (
+                <p className="font-mono text-xs text-[#8b949e]">{chain.description}</p>
               )}
             </div>
           </div>
           <div className="flex items-center gap-2">
             <span
               className={`font-mono text-[10px] uppercase ${
-                statusColors[chain.chain.status] || "text-[#555]"
+                statusColors[chain.status] || "text-[#555]"
               }`}
             >
-              {chain.chain.status}
+              {chain.status}
             </span>
             <button
               onClick={onRefresh}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 445537c..56491fd 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -3004,9 +3004,8 @@ export interface ChainContractDetail {
   createdAt: string;
 }
 
-/** Chain with contracts */
-export interface ChainWithContracts {
-  chain: Chain;
+/** Chain with contracts (chain fields are flattened via serde(flatten)) */
+export interface ChainWithContracts extends Chain {
   contracts: ChainContractDetail[];
 }
 
diff --git a/makima/frontend/src/routes/chains.tsx b/makima/frontend/src/routes/chains.tsx
index f01d5a6..23484b4 100644
--- a/makima/frontend/src/routes/chains.tsx
+++ b/makima/frontend/src/routes/chains.tsx
@@ -102,7 +102,7 @@ function ChainsPageContent() {
         const result = await createNewChain(data);
         if (result) {
           setIsCreating(false);
-          navigate(`/chains/${result.chain.id}`);
+          navigate(`/chains/${result.id}`);
         }
       } catch (err) {
         console.error("Failed to create chain:", err);
-- 
cgit v1.2.3

