Adds type definitions + forwarding for status, reconnect_attempt, reconnect_failed, reconnect events. Frontend build now runs tsc --noEmit before vite build so undefined socket events cause failures. Fixes pre-existing type errors exposed by strict mode in the frontend.
330 lines
11 KiB
TypeScript
330 lines
11 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
import type { User, Project, DroneRegistration } from "../lib/api";
|
|
import { projectApi, droneApi } from "../lib/api";
|
|
import Clock from "../components/Clock";
|
|
import GadgetGrid from "../components/GadgetGrid";
|
|
|
|
function SystemReady() {
|
|
return (
|
|
<div className="flex flex-col items-center">
|
|
<div className="mt-8 border-2 border-border-default p-6 rounded bg-bg-secondary">
|
|
<div className="font-mono text-text-secondary text-sm">
|
|
<div className="mb-1 text-text-muted">// SYSTEM READY</div>
|
|
<div className="mb-3">Please sign in to continue.</div>
|
|
<div className="text-text-muted mb-4">
|
|
Accounts are administered. Contact your administrator for access.
|
|
</div>
|
|
<a
|
|
href="/sign-in"
|
|
className="inline-block px-4 py-2 border border-border-highlight text-text-primary hover:bg-bg-tertiary hover:text-text-primary rounded transition-colors"
|
|
>
|
|
Sign In
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface DashboardSidebarProps {
|
|
onNavigate: (view: string, data?: unknown) => void;
|
|
selectedDrone: DroneRegistration | null;
|
|
onSelectDrone: (drone: DroneRegistration | null) => void;
|
|
}
|
|
|
|
function DroneInspector({
|
|
drone,
|
|
onClose,
|
|
}: {
|
|
drone: DroneRegistration;
|
|
onClose: () => void;
|
|
}) {
|
|
return (
|
|
<div className="flex-1 flex items-center justify-center p-8">
|
|
<div className="max-w-lg w-full">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-xl font-semibold">Drone Inspector</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-text-secondary hover:text-text-primary text-sm"
|
|
>
|
|
← Back to Dashboard
|
|
</button>
|
|
</div>
|
|
<div className="bg-bg-secondary border border-border-default rounded p-4 space-y-4">
|
|
<div>
|
|
<div className="text-sm text-text-muted">Hostname</div>
|
|
<div className="font-mono text-text-primary">{drone.hostname}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-text-muted">Workspace</div>
|
|
<div className="font-mono text-text-primary text-sm truncate">
|
|
{drone.workspaceDir}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-text-muted">Status</div>
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`w-2 h-2 rounded-full ${
|
|
drone.status === "available"
|
|
? "bg-green-500"
|
|
: drone.status === "busy"
|
|
? "bg-yellow-500"
|
|
: "bg-gray-600"
|
|
}`}
|
|
/>
|
|
<span className="text-text-primary capitalize">
|
|
{drone.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-text-muted">Registered</div>
|
|
<div className="text-text-primary text-sm">
|
|
{new Date(drone.createdAt).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DashboardSidebar({
|
|
onNavigate: _onNavigate,
|
|
selectedDrone,
|
|
onSelectDrone,
|
|
}: DashboardSidebarProps) {
|
|
const navigate = useNavigate();
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [drones, setDrones] = useState<DroneRegistration[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
const [projectsData, dronesData] = await Promise.all([
|
|
projectApi.getAll(),
|
|
droneApi.getAll(),
|
|
]);
|
|
setProjects(projectsData);
|
|
setDrones(dronesData);
|
|
} catch (err) {
|
|
console.error("Failed to load dashboard data", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<aside className="w-64 border-l border-border-subtle bg-bg-secondary overflow-y-auto">
|
|
<Clock />
|
|
|
|
<div className="p-3 border-t border-border-subtle">
|
|
<div className="flex items-center justify-between text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
|
|
<span>Projects</span>
|
|
<Link
|
|
to="/projects"
|
|
className="text-brand hover:text-red-400 transition-colors"
|
|
>
|
|
[+]
|
|
</Link>
|
|
</div>
|
|
{loading ? (
|
|
<p className="text-sm text-text-muted px-2">Loading...</p>
|
|
) : projects.length === 0 ? (
|
|
<p className="text-sm text-text-muted px-2">No projects yet.</p>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{projects.map((project) => (
|
|
<button
|
|
key={project._id}
|
|
onClick={() => navigate(`/projects/${project.slug}`)}
|
|
className="w-full text-left px-2 py-1.5 text-sm text-text-secondary hover:bg-bg-tertiary hover:text-text-primary rounded truncate transition-colors"
|
|
>
|
|
{project.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-3 border-t border-border-subtle">
|
|
<div className="flex items-center justify-between text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
|
|
<span>Drones</span>
|
|
<Link
|
|
to="/drones"
|
|
className="text-text-secondary hover:text-text-primary transition-colors"
|
|
title="Drone Manager"
|
|
>
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
|
/>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
/>
|
|
</svg>
|
|
</Link>
|
|
</div>
|
|
{loading ? (
|
|
<p className="text-sm text-text-muted px-2">Loading...</p>
|
|
) : drones.length === 0 ? (
|
|
<p className="text-sm text-text-muted px-2">No drones available.</p>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{drones.map((drone) => (
|
|
<button
|
|
key={drone._id}
|
|
onClick={() => onSelectDrone(drone)}
|
|
className={`w-full text-left px-2 py-1 rounded transition-colors ${
|
|
selectedDrone?._id === drone._id
|
|
? "bg-brand text-white"
|
|
: "text-text-secondary hover:bg-bg-tertiary hover:text-text-primary"
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`w-2 h-2 rounded-full shrink-0 ${
|
|
drone.status === "available"
|
|
? "bg-green-500"
|
|
: "bg-yellow-500"
|
|
}`}
|
|
/>
|
|
<div className="overflow-hidden">
|
|
<div className="text-sm truncate">{drone.hostname}</div>
|
|
{drone.workspaceDir && (
|
|
<div
|
|
className={`text-xs truncate ${
|
|
selectedDrone?._id === drone._id
|
|
? "text-white/70"
|
|
: "text-text-muted"
|
|
}`}
|
|
>
|
|
{drone.workspaceDir.length > 24
|
|
? "..." + drone.workspaceDir.slice(-21)
|
|
: drone.workspaceDir}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-3 border-t border-border-subtle">
|
|
<div className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
|
|
Recent Chats
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-text-muted px-2">Loading...</p>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
interface HomeProps {
|
|
user: User | null;
|
|
}
|
|
|
|
export default function Home({ user }: HomeProps) {
|
|
const navigate = useNavigate();
|
|
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(
|
|
null,
|
|
);
|
|
|
|
if (!user) {
|
|
return (
|
|
<div className="relative flex-1 flex bg-bg-primary overflow-hidden">
|
|
<div className="absolute inset-0">
|
|
<GadgetGrid />
|
|
</div>
|
|
<div className="relative z-10 flex items-center justify-center p-8">
|
|
<SystemReady />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const mainContent = selectedDrone ? (
|
|
<div className="relative z-10 flex-1 flex">
|
|
<DroneInspector
|
|
drone={selectedDrone}
|
|
onClose={() => setSelectedDrone(null)}
|
|
/>
|
|
<DashboardSidebar
|
|
onNavigate={(view) => {
|
|
if (view === "project" && typeof navigate === "function") {
|
|
navigate("/projects");
|
|
}
|
|
}}
|
|
selectedDrone={selectedDrone}
|
|
onSelectDrone={setSelectedDrone}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="relative z-10 flex-1 flex flex-col">
|
|
<div className="flex-1 flex">
|
|
<div className="flex-1 flex items-center justify-center p-8">
|
|
<div className="text-center">
|
|
<h1 className="text-2xl font-semibold mb-4">
|
|
Welcome, {user.displayName}!
|
|
</h1>
|
|
<p className="text-text-secondary mb-4">
|
|
Your dashboard is under construction.
|
|
</p>
|
|
<p className="text-text-muted text-sm mb-6">
|
|
Select a project or chat session from the sidebar to get started.
|
|
</p>
|
|
<div className="flex gap-3 justify-center">
|
|
<Link
|
|
to="/projects"
|
|
className="px-4 py-2 border border-border-default text-text-secondary hover:bg-bg-tertiary hover:text-text-primary rounded transition-colors"
|
|
>
|
|
Open Project Manager
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DashboardSidebar
|
|
onNavigate={(view) => {
|
|
if (view === "project" && typeof navigate === "function") {
|
|
navigate("/projects");
|
|
}
|
|
}}
|
|
selectedDrone={selectedDrone}
|
|
onSelectDrone={setSelectedDrone}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="relative flex-1 flex bg-bg-primary overflow-hidden">
|
|
<div className="absolute inset-0">
|
|
<GadgetGrid />
|
|
</div>
|
|
{mainContent}
|
|
</div>
|
|
);
|
|
}
|