From 87044a747b47bd83249d61a45842c7f7b2eae56d Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 11 Jan 2026 05:52:14 +0000 Subject: Contract system --- makima/frontend/src/routes/_index.tsx | 6 +- makima/frontend/src/routes/contracts.tsx | 614 +++++++++++++++++++++++++++++++ makima/frontend/src/routes/files.tsx | 221 +++++++++-- makima/frontend/src/routes/listen.tsx | 45 ++- makima/frontend/src/routes/mesh.tsx | 250 ++++++++++++- makima/frontend/src/routes/settings.tsx | 113 ++++++ makima/frontend/src/routes/workflow.tsx | 205 +++++++++++ 7 files changed, 1418 insertions(+), 36 deletions(-) create mode 100644 makima/frontend/src/routes/contracts.tsx create mode 100644 makima/frontend/src/routes/workflow.tsx (limited to 'makima/frontend/src/routes') diff --git a/makima/frontend/src/routes/_index.tsx b/makima/frontend/src/routes/_index.tsx index 7084c2e..ecdd7f2 100644 --- a/makima/frontend/src/routes/_index.tsx +++ b/makima/frontend/src/routes/_index.tsx @@ -1,5 +1,6 @@ import { Masthead } from "../components/Masthead"; import { Logo } from "../components/Logo"; +import { JapaneseHoverText } from "../components/JapaneseHoverText"; export default function HomePage() { return ( @@ -13,7 +14,10 @@ export default function HomePage() { - Control System +

Mesh Orchestration Platform diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx new file mode 100644 index 0000000..8c90804 --- /dev/null +++ b/makima/frontend/src/routes/contracts.tsx @@ -0,0 +1,614 @@ +import { useState, useCallback, useEffect } from "react"; +import { useParams, useNavigate } from "react-router"; +import { Masthead } from "../components/Masthead"; +import { ContractList } from "../components/contracts/ContractList"; +import { ContractDetail } from "../components/contracts/ContractDetail"; +import { DirectoryInput } from "../components/mesh/DirectoryInput"; +import { useContracts } from "../hooks/useContracts"; +import { useAuth } from "../contexts/AuthContext"; +import { createTask, getDaemonDirectories } from "../lib/api"; +import type { + ContractWithRelations, + ContractPhase, + ContractStatus, + CreateContractRequest, + RepositorySourceType, + DaemonDirectory, +} from "../lib/api"; + +export default function ContractsPage() { + 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 ( +
+ +
+

Loading...

+
+
+ ); + } + + // Don't render if not authenticated (will redirect) + if (isAuthConfigured && !isAuthenticated) { + return null; + } + + return ; +} + +function ContractsPageContent() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { + contracts, + loading, + error, + fetchContract, + saveContract, + editContract, + removeContract, + changePhase, + addRemoteRepo, + addLocalRepo, + createManagedRepo, + removeRepo, + setRepoPrimary, + } = useContracts(); + + const [contractDetail, setContractDetail] = useState(null); + const [detailLoading, setDetailLoading] = useState(false); + const [isCreating, setIsCreating] = useState(false); + const [newContractName, setNewContractName] = useState(""); + const [newContractDescription, setNewContractDescription] = useState(""); + const [initialPhase, setInitialPhase] = useState("research"); + const [repoType, setRepoType] = useState("remote"); + const [repoName, setRepoName] = useState(""); + const [repoUrl, setRepoUrl] = useState(""); + const [repoPath, setRepoPath] = useState(""); + const [createError, setCreateError] = useState(null); + const [suggestedDirectories, setSuggestedDirectories] = useState([]); + + // Fetch daemon directories when "local" repo type is selected + useEffect(() => { + if (repoType === "local" && isCreating) { + getDaemonDirectories() + .then((res) => setSuggestedDirectories(res.directories)) + .catch(() => setSuggestedDirectories([])); + } + }, [repoType, isCreating]); + + // Load contract detail when ID changes + useEffect(() => { + if (id) { + setDetailLoading(true); + fetchContract(id).then((contract) => { + setContractDetail(contract); + setDetailLoading(false); + }); + } else { + setContractDetail(null); + } + }, [id, fetchContract]); + + const handleSelect = useCallback( + (contractId: string) => { + navigate(`/contracts/${contractId}`); + }, + [navigate] + ); + + const handleBack = useCallback(() => { + navigate("/contracts"); + }, [navigate]); + + const handleCreate = useCallback(() => { + setIsCreating(true); + }, []); + + // Validate repository configuration + const isRepoValid = useCallback(() => { + if (!repoName.trim()) return false; + if (repoType === "remote" && !repoUrl.trim()) return false; + if (repoType === "local" && !repoPath.trim()) return false; + return true; + }, [repoType, repoName, repoUrl, repoPath]); + + const handleCreateSubmit = useCallback(async () => { + if (!newContractName.trim()) return; + if (!isRepoValid()) { + setCreateError("Repository configuration is required"); + return; + } + + setCreateError(null); + + const data: CreateContractRequest = { + name: newContractName.trim(), + description: newContractDescription.trim() || undefined, + initialPhase: initialPhase !== "research" ? initialPhase : undefined, + }; + + try { + const contract = await saveContract(data); + if (contract) { + // Add the repository after contract creation + try { + if (repoType === "remote") { + await addRemoteRepo(contract.id, { + name: repoName.trim(), + repositoryUrl: repoUrl.trim(), + isPrimary: true, + }); + } else if (repoType === "local") { + await addLocalRepo(contract.id, { + name: repoName.trim(), + localPath: repoPath.trim(), + isPrimary: true, + }); + } else if (repoType === "managed") { + await createManagedRepo(contract.id, { + name: repoName.trim(), + isPrimary: true, + }); + } + } catch (repoError) { + console.error("Failed to add repository:", repoError); + // Still navigate to the contract - repo can be added later + } + + // Clear form state + setIsCreating(false); + setNewContractName(""); + setNewContractDescription(""); + setInitialPhase("research"); + setRepoType("remote"); + setRepoName(""); + setRepoUrl(""); + setRepoPath(""); + navigate(`/contracts/${contract.id}`); + } + } catch (err) { + setCreateError(err instanceof Error ? err.message : "Failed to create contract"); + } + }, [ + newContractName, + newContractDescription, + repoType, + repoName, + repoUrl, + repoPath, + isRepoValid, + saveContract, + addRemoteRepo, + addLocalRepo, + createManagedRepo, + navigate, + ]); + + const handleCreateCancel = useCallback(() => { + setIsCreating(false); + setNewContractName(""); + setNewContractDescription(""); + setInitialPhase("research"); + setRepoType("remote"); + setRepoName(""); + setRepoUrl(""); + setRepoPath(""); + setCreateError(null); + }, []); + + const handleUpdate = useCallback( + async (name: string, description: string) => { + if (contractDetail) { + const updated = await editContract(contractDetail.id, { + name, + description: description || undefined, + version: contractDetail.version, + }); + if (updated) { + // Refresh detail + const refreshed = await fetchContract(contractDetail.id); + setContractDetail(refreshed); + } + } + }, + [contractDetail, editContract, fetchContract] + ); + + const handleDelete = useCallback(async () => { + if (contractDetail && confirm("Are you sure you want to delete this contract?")) { + const success = await removeContract(contractDetail.id); + if (success) { + navigate("/contracts"); + } + } + }, [contractDetail, removeContract, navigate]); + + const handlePhaseChange = useCallback( + async (phase: ContractPhase) => { + if (contractDetail) { + const updated = await changePhase(contractDetail.id, phase); + if (updated) { + // Refresh detail + const refreshed = await fetchContract(contractDetail.id); + setContractDetail(refreshed); + } + } + }, + [contractDetail, changePhase, fetchContract] + ); + + const handleStatusChange = useCallback( + async (status: ContractStatus) => { + if (contractDetail) { + const updated = await editContract(contractDetail.id, { + status, + version: contractDetail.version, + }); + if (updated) { + // Refresh detail + const refreshed = await fetchContract(contractDetail.id); + setContractDetail(refreshed); + } + } + }, + [contractDetail, editContract, fetchContract] + ); + + // Repository handlers + const handleAddRemoteRepo = useCallback( + async (name: string, url: string, isPrimary: boolean) => { + if (contractDetail) { + await addRemoteRepo(contractDetail.id, { name, repositoryUrl: url, isPrimary }); + const refreshed = await fetchContract(contractDetail.id); + setContractDetail(refreshed); + } + }, + [contractDetail, addRemoteRepo, fetchContract] + ); + + const handleAddLocalRepo = useCallback( + async (name: string, path: string, isPrimary: boolean) => { + if (contractDetail) { + await addLocalRepo(contractDetail.id, { name, localPath: path, isPrimary }); + const refreshed = await fetchContract(contractDetail.id); + setContractDetail(refreshed); + } + }, + [contractDetail, addLocalRepo, fetchContract] + ); + + const handleCreateManagedRepo = useCallback( + async (name: string, isPrimary: boolean) => { + if (contractDetail) { + await createManagedRepo(contractDetail.id, { name, isPrimary }); + const refreshed = await fetchContract(contractDetail.id); + setContractDetail(refreshed); + } + }, + [contractDetail, createManagedRepo, fetchContract] + ); + + const handleDeleteRepo = useCallback( + async (repoId: string) => { + if (contractDetail) { + await removeRepo(contractDetail.id, repoId); + const refreshed = await fetchContract(contractDetail.id); + setContractDetail(refreshed); + } + }, + [contractDetail, removeRepo, fetchContract] + ); + + const handleSetRepoPrimary = useCallback( + async (repoId: string) => { + if (contractDetail) { + await setRepoPrimary(contractDetail.id, repoId); + const refreshed = await fetchContract(contractDetail.id); + setContractDetail(refreshed); + } + }, + [contractDetail, setRepoPrimary, fetchContract] + ); + + // Refresh contract detail (used after file/task operations) + const handleRefresh = useCallback(async () => { + if (contractDetail) { + const refreshed = await fetchContract(contractDetail.id); + setContractDetail(refreshed); + } + }, [contractDetail, fetchContract]); + + // File/task navigation handlers + const handleFileSelect = useCallback( + (fileId: string) => { + navigate(`/files/${fileId}`); + }, + [navigate] + ); + + const handleTaskSelect = useCallback( + (taskId: string) => { + navigate(`/mesh/${taskId}`); + }, + [navigate] + ); + + // Create task within contract context + const handleTaskCreate = useCallback( + async (name: string, plan: string, repositoryUrl?: string) => { + if (!contractDetail) return; + try { + // Create the task with contract_id (task is automatically associated) + const task = await createTask({ + contractId: contractDetail.id, + name, + plan, + repositoryUrl, + }); + // Refresh contract detail to show new task + const refreshed = await fetchContract(contractDetail.id); + setContractDetail(refreshed); + // Navigate to the new task + navigate(`/mesh/${task.id}`); + } catch (e) { + console.error("Failed to create task:", e); + alert(e instanceof Error ? e.message : "Failed to create task"); + } + }, + [contractDetail, fetchContract, navigate] + ); + + return ( +
+ +
+ {error && ( +
+ {error} +
+ )} + + {/* Create contract modal */} + {isCreating && ( +
+
+

+ Create Contract +

+ + {createError && ( +
+ {createError} +
+ )} + +
+ {/* Contract name */} +
+ + setNewContractName(e.target.value)} + placeholder="Contract name" + 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 + /> +
+ + {/* Description */} +
+ +