import { useState, useEffect, type FormEvent } from "react";
import { useAuth } from "../contexts/AuthContext";
import { useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import {
getApiKey,
createApiKey,
refreshApiKey,
revokeApiKey,
changePassword,
changeEmail,
deleteAccount,
listDaemons,
listRepositoryHistory,
deleteRepositoryHistory,
type ApiKeyInfo,
type CreateApiKeyResponse,
type Daemon,
type RepositoryHistoryEntry,
} from "../lib/api";
// =============================================================================
// Password Strength Indicator
// =============================================================================
interface PasswordStrength {
score: number;
label: string;
color: string;
requirements: { met: boolean; text: string }[];
}
function getPasswordStrength(password: string): PasswordStrength {
const requirements = [
{ met: password.length >= 6, text: "At least 6 characters" },
];
const score = requirements.filter((r) => r.met).length;
const label = score === 1 ? "Valid" : "Too short";
const color = score === 1 ? "bg-green-500" : "bg-red-500";
return { score, label, color, requirements };
}
// =============================================================================
// Confirmation Dialog Component
// =============================================================================
interface ConfirmDialogProps {
isOpen: boolean;
title: string;
message: string;
confirmText: string;
confirmButtonClass?: string;
requireInput?: string;
inputPlaceholder?: string;
onConfirm: () => void;
onCancel: () => void;
}
function ConfirmDialog({
isOpen,
title,
message,
confirmText,
confirmButtonClass = "bg-red-900/50 border-red-700 hover:bg-red-800/50",
requireInput,
inputPlaceholder,
onConfirm,
onCancel,
}: ConfirmDialogProps) {
const [inputValue, setInputValue] = useState("");
useEffect(() => {
if (!isOpen) {
setInputValue("");
}
}, [isOpen]);
if (!isOpen) return null;
const canConfirm = !requireInput || inputValue === requireInput;
return (
{title}
{message}
{requireInput && (
setInputValue(e.target.value)}
placeholder={inputPlaceholder}
className="w-full px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
/>
)}
);
}
// =============================================================================
// Section Header Component
// =============================================================================
function SectionHeader({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
// =============================================================================
// Form Input Component
// =============================================================================
function FormInput({
label,
type = "text",
value,
onChange,
placeholder,
required,
disabled,
}: {
label: string;
type?: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
required?: boolean;
disabled?: boolean;
}) {
return (
onChange(e.target.value)}
placeholder={placeholder}
required={required}
disabled={disabled}
className="w-full px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3] disabled:opacity-50"
/>
);
}
// =============================================================================
// Alert Components
// =============================================================================
function ErrorAlert({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
function SuccessAlert({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
// =============================================================================
// Button Components
// =============================================================================
function PrimaryButton({
children,
onClick,
disabled,
type = "button",
}: {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
type?: "button" | "submit";
}) {
return (
);
}
function SecondaryButton({
children,
onClick,
disabled,
}: {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}) {
return (
);
}
function DangerButton({
children,
onClick,
disabled,
}: {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}) {
return (
);
}
// =============================================================================
// Main Settings Page
// =============================================================================
export default function SettingsPage() {
const { user, isAuthConfigured, signOut } = useAuth();
const navigate = useNavigate();
// API Key state
const [apiKeyInfo, setApiKeyInfo] = useState(null);
const [newKey, setNewKey] = useState(null);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState(false);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
// Password change state
const [passwordForm, setPasswordForm] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
const [passwordLoading, setPasswordLoading] = useState(false);
const [passwordError, setPasswordError] = useState(null);
const [passwordSuccess, setPasswordSuccess] = useState(null);
// Email change state
const [emailForm, setEmailForm] = useState({
password: "",
newEmail: "",
});
const [emailLoading, setEmailLoading] = useState(false);
const [emailError, setEmailError] = useState(null);
const [emailSuccess, setEmailSuccess] = useState(null);
// Account deletion state
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deletePassword, setDeletePassword] = useState("");
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteError, setDeleteError] = useState(null);
// Daemon state
const [daemons, setDaemons] = useState([]);
const [daemonsLoading, setDaemonsLoading] = useState(true);
const [daemonsError, setDaemonsError] = useState(null);
// Repository history state
const [repoHistory, setRepoHistory] = useState([]);
const [repoHistoryLoading, setRepoHistoryLoading] = useState(true);
const [repoHistoryError, setRepoHistoryError] = useState(null);
const [deletingRepoId, setDeletingRepoId] = useState(null);
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);
setError(null);
const key = await getApiKey();
setApiKeyInfo(key);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load API key");
} finally {
setLoading(false);
}
};
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);
const response = await listRepositoryHistory();
setRepoHistory(response.entries);
} catch (err) {
setRepoHistoryError(err instanceof Error ? err.message : "Failed to load repository history");
} finally {
setRepoHistoryLoading(false);
}
};
const handleDeleteRepoHistory = async (id: string) => {
try {
setDeletingRepoId(id);
await deleteRepositoryHistory(id);
setRepoHistory((prev) => prev.filter((entry) => entry.id !== id));
} catch (err) {
setRepoHistoryError(err instanceof Error ? err.message : "Failed to delete entry");
} finally {
setDeletingRepoId(null);
}
};
const handleCreate = async () => {
try {
setActionLoading(true);
setError(null);
setNewKey(null);
const response: CreateApiKeyResponse = await createApiKey("Web UI");
setNewKey(response.key);
setApiKeyInfo({
id: response.id,
prefix: response.prefix,
name: response.name,
lastUsedAt: null,
createdAt: response.createdAt,
});
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create API key");
} finally {
setActionLoading(false);
}
};
const handleRefresh = async () => {
try {
setActionLoading(true);
setError(null);
setNewKey(null);
const response = await refreshApiKey("Web UI (Refreshed)");
setNewKey(response.key);
setApiKeyInfo({
id: response.id,
prefix: response.prefix,
name: response.name,
lastUsedAt: null,
createdAt: response.createdAt,
});
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to refresh API key");
} finally {
setActionLoading(false);
}
};
const handleRevoke = async () => {
if (!confirm("Are you sure you want to revoke this API key? Any applications using it will stop working.")) {
return;
}
try {
setActionLoading(true);
setError(null);
setNewKey(null);
await revokeApiKey();
setApiKeyInfo(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to revoke API key");
} finally {
setActionLoading(false);
}
};
const copyToClipboard = async () => {
if (!newKey) return;
try {
await navigator.clipboard.writeText(newKey);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy:", err);
}
};
// Password change handlers
const handlePasswordChange = async (e: FormEvent) => {
e.preventDefault();
setPasswordError(null);
setPasswordSuccess(null);
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
setPasswordError("New passwords do not match");
return;
}
const strength = getPasswordStrength(passwordForm.newPassword);
if (strength.score < 1) {
setPasswordError("Password must be at least 6 characters");
return;
}
try {
setPasswordLoading(true);
await changePassword(passwordForm.currentPassword, passwordForm.newPassword);
setPasswordSuccess("Password changed successfully. Please sign in with your new password.");
setPasswordForm({ currentPassword: "", newPassword: "", confirmPassword: "" });
setTimeout(async () => {
await signOut();
navigate("/login");
}, 1500);
} catch (err) {
setPasswordError(err instanceof Error ? err.message : "Failed to change password");
} finally {
setPasswordLoading(false);
}
};
// Email change handlers
const handleEmailChange = async (e: FormEvent) => {
e.preventDefault();
setEmailError(null);
setEmailSuccess(null);
if (!emailForm.newEmail.includes("@")) {
setEmailError("Please enter a valid email address");
return;
}
try {
setEmailLoading(true);
await changeEmail(emailForm.password, emailForm.newEmail);
setEmailSuccess("Email changed successfully");
setEmailForm({ password: "", newEmail: "" });
} catch (err) {
setEmailError(err instanceof Error ? err.message : "Failed to change email");
} finally {
setEmailLoading(false);
}
};
// Account deletion handlers
const DELETE_CONFIRMATION = "DELETE MY ACCOUNT";
const handleDeleteAccount = async () => {
try {
setDeleteLoading(true);
setDeleteError(null);
await deleteAccount(deletePassword, DELETE_CONFIRMATION);
await signOut();
navigate("/login");
} catch (err) {
setDeleteError(err instanceof Error ? err.message : "Failed to delete account");
setDeleteLoading(false);
}
};
const passwordStrength = getPasswordStrength(passwordForm.newPassword);
return (
{/* Page Header */}
{/* Left Column */}
{/* Account Info */}
Account
{isAuthConfigured && user ? (
Email
{user.email}
User ID
{user.id}
) : (
{isAuthConfigured
? "Not signed in"
: "Authentication not configured (API key mode)"}
)}
{/* API Key Section */}
API Key
Authenticate daemon and CLI tools. One active key at a time.
{error && {error}}
{newKey && (
Key created. Copy now - won't be shown again.
{newKey}
)}
{loading ? (
Loading...
) : apiKeyInfo ? (
Prefix
{apiKeyInfo.prefix}...
Created
{new Date(apiKeyInfo.createdAt).toLocaleDateString()}
{apiKeyInfo.lastUsedAt && (
Last used
{new Date(apiKeyInfo.lastUsedAt).toLocaleDateString()}
)}
{actionLoading ? "..." : "Rotate"}
{actionLoading ? "..." : "Revoke"}
) : (
No API key configured.
{actionLoading ? "Creating..." : "Create API Key"}
)}
{/* Daemon Setup */}
Daemon Setup
Set your API key as an environment variable:
export MAKIMA_API_KEY="your-key"
Then run: makima-daemon
{/* Connected Daemons */}
Daemons
{daemons.length > 0 && (
({daemons.filter(d => d.status === "connected").length} connected / {daemons.length} total)
)}
{daemonsError && {daemonsError}}
{daemonsLoading && daemons.length === 0 ? (
Loading...
) : daemons.length === 0 ? (
No daemons connected
Start a daemon to enable task execution
) : (
{daemons.map((daemon) => (
{daemon.hostname || "Unknown Host"}
{daemon.status}
Tasks
{daemon.currentTaskCount} / {daemon.maxConcurrentTasks}
Connected
{new Date(daemon.connectedAt).toLocaleString()}
{daemon.machineId && (
Machine
{daemon.machineId.substring(0, 16)}...
)}
))}
)}
{/* Repository History */}
Repository History
{repoHistory.length > 0 && (
({repoHistory.length} saved)
)}
Previously used repositories will appear as suggestions when adding repos to contracts.
{repoHistoryError && {repoHistoryError}}
{repoHistoryLoading && repoHistory.length === 0 ? (
Loading...
) : repoHistory.length === 0 ? (
No repository history
Add repositories to contracts to build history
) : (
{repoHistory.map((entry) => (
{entry.name}
{entry.sourceType}
{entry.repositoryUrl || entry.localPath}
Used {entry.useCount}×
Last: {new Date(entry.lastUsedAt).toLocaleDateString()}
))}
)}
{/* Right Column */}
{/* Password Change */}
{isAuthConfigured && user && (
Change Password
{passwordError && {passwordError}}
{passwordSuccess && {passwordSuccess}}
)}
{/* Email Change */}
{isAuthConfigured && user && (
Change Email
{emailError && {emailError}}
{emailSuccess && {emailSuccess}}
)}
{/* Danger Zone */}
{isAuthConfigured && user && (
Danger Zone
Permanently delete your account and all data. This cannot be undone.
{deleteError && {deleteError}}
setDeleteDialogOpen(true)}
disabled={!deletePassword || deleteLoading}
>
{deleteLoading ? "Deleting..." : "Delete Account"}
)}
{/* Delete Confirmation Dialog */}
{
setDeleteDialogOpen(false);
handleDeleteAccount();
}}
onCancel={() => setDeleteDialogOpen(false)}
/>
);
}