import { useState, useEffect, useRef } from "react";
import { Link, useNavigate } from "react-router-dom";
import type { User, Project, DroneRegistration, ChatSession, SubProcessStat } from "../lib/api";
import { projectApi, droneApi, chatSessionApi } from "../lib/api";
import { socketClient } from "../lib/socket";
import Clock from "../components/Clock";
import GadgetGrid from "../components/GadgetGrid";
import LogRenderer from "../components/LogRenderer";
import type { LogRendererEntry } from "../components/LogRenderer";
function SystemReady() {
return (
// SYSTEM READY
Please sign in to continue.
Accounts are administered. Contact your administrator for access.
Sign In
);
}
interface DashboardSidebarProps {
onNavigate: (view: string, data?: unknown) => void;
selectedDrone: DroneRegistration | null;
onSelectDrone: (drone: DroneRegistration | null) => void;
}
function DroneInspector({
drone,
onClose,
}: {
drone: DroneRegistration;
onClose: () => void;
}) {
const navigate = useNavigate();
const [processStats, setProcessStats] = useState([]);
const [logEntries, setLogEntries] = useState([]);
const mountedRef = useRef(true);
const pollRef = useRef | null>(null);
useEffect(() => {
mountedRef.current = true;
const regId = drone._id;
if (drone.status === 'offline') return;
socketClient.subscribeDrone(regId);
const handleLog = (data: { timestamp: string; level: string; component: string; message: string }) => {
if (!mountedRef.current) return;
setLogEntries((prev) => {
const next = [...prev, {
id: `insp-log-${prev.length}`,
timestamp: new Date(data.timestamp),
level: data.level,
component: data.component,
message: data.message,
}];
return next.length > 100 ? next.slice(next.length - 100) : next;
});
};
socketClient.on('drone:log', handleLog);
const poll = async () => {
if (!mountedRef.current) return;
const result = await socketClient.requestProcessStats(regId);
if (result.success && result.processes && mountedRef.current) {
setProcessStats(result.processes as SubProcessStat[]);
}
};
poll();
pollRef.current = setInterval(poll, 1000);
return () => {
mountedRef.current = false;
if (pollRef.current) clearInterval(pollRef.current);
socketClient.off('drone:log', handleLog);
socketClient.unsubscribeDrone(regId);
};
}, [drone._id, drone.status]);
const runningCount = processStats.filter((p) => p.status === 'running').length;
return (
Drone Inspector
Hostname
{drone.hostname}
Workspace
{drone.workspaceDir}
SubProcesses
{runningCount > 0
? `${runningCount} running`
: processStats.length > 0
? 'All stopped'
: 'No managed processes'}
Registered
{new Date(drone.createdAt).toLocaleString()}
{drone.status !== 'offline' && (
)}
{drone.status !== 'offline' && logEntries.length > 0 && (
)}
);
}
function DashboardSidebar({
onNavigate: _onNavigate,
selectedDrone,
onSelectDrone,
}: DashboardSidebarProps) {
const navigate = useNavigate();
const [projects, setProjects] = useState([]);
const [drones, setDrones] = useState([]);
const [recentChats, setRecentChats] = useState([]);
const [loading, setLoading] = useState(true);
const [noDroneMessage, setNoDroneMessage] = useState(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
const [projectsData, dronesData, recentChatsData] = await Promise.all([
projectApi.getAll(),
droneApi.getAll(),
chatSessionApi.getAll(undefined, 10),
]);
setProjects(projectsData);
setDrones(dronesData);
setRecentChats(recentChatsData);
} catch (err) {
console.error("Failed to load dashboard data", err);
} finally {
setLoading(false);
}
};
const getProjectName = (session: ChatSession): string => {
const projectRef = session.project;
if (typeof projectRef === 'object' && projectRef !== null && 'name' in projectRef) {
return projectRef.name;
}
return 'Unknown Project';
};
const getProjectId = (session: ChatSession): string => {
const projectRef = session.project;
if (typeof projectRef === 'object' && projectRef !== null && '_id' in projectRef) {
return projectRef._id;
}
return typeof projectRef === 'string' ? projectRef : '';
};
const handleOpenChat = (session: ChatSession) => {
if (!selectedDrone) {
setNoDroneMessage('Select a drone first to resume a chat');
setTimeout(() => setNoDroneMessage(null), 3000);
return;
}
const projectId = getProjectId(session);
navigate(`/projects/${projectId}/chat-session/${session._id}`, {
state: {
drone: selectedDrone,
project: typeof session.project === 'object' ? session.project : undefined,
},
});
};
const modeBadgeColors: Record = {
plan: 'bg-blue-500/20 text-blue-400',
build: 'bg-brand/20 text-brand',
test: 'bg-green-500/20 text-green-400',
ship: 'bg-purple-500/20 text-purple-400',
dev: 'bg-orange-500/20 text-orange-400',
};
const formatRelativeTime = (dateStr: string): string => {
const now = Date.now();
const then = new Date(dateStr).getTime();
const diffMs = now - then;
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHr = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHr / 24);
if (diffSec < 60) return 'just now';
if (diffMin < 60) return `${diffMin}m ago`;
if (diffHr < 24) return `${diffHr}h ago`;
if (diffDay < 7) return `${diffDay}d ago`;
return new Date(dateStr).toLocaleDateString();
};
return (
);
}
interface HomeProps {
user: User | null;
}
export default function Home({ user }: HomeProps) {
const navigate = useNavigate();
const [selectedDrone, setSelectedDrone] = useState(
null,
);
if (!user) {
return (
);
}
const mainContent = selectedDrone ? (
setSelectedDrone(null)}
/>
{
if (view === "project" && typeof navigate === "function") {
navigate("/projects");
}
}}
selectedDrone={selectedDrone}
onSelectDrone={setSelectedDrone}
/>
) : (
Welcome, {user.displayName}!
Your dashboard is under construction.
Select a project or chat session from the sidebar to get started.
Open Project Manager
{
if (view === "project" && typeof navigate === "function") {
navigate("/projects");
}
}}
selectedDrone={selectedDrone}
onSelectDrone={setSelectedDrone}
/>
);
return (
);
}