feat: add complete frontend with React, Tailwind 4K, and Docker setup

- Vite + React 18 + TypeScript scaffolding
- Tailwind CSS configured for 4K dark theme (24px base)
- Three full-screen rotating views: Network Topology (D3.js),
  Kanban Board (Odoo tasks), Calendar (Odoo events)
- Hooks for data fetching, WebSocket, and view rotation
- Header with live clock and connection status
- Framer Motion fade transitions between views
- Docker Compose with backend (host network for nmap) and
  frontend (nginx proxy to backend API/WS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 09:34:01 +00:00
parent e6f6dbbab6
commit a7967ecb4a
27 changed files with 3913 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
import { useState } from "react";
import type { NetworkNode } from "../../types";
const ICON_MAP: Record<string, string> = {
router: "\uD83C\uDF10",
server: "\uD83D\uDDA5\uFE0F",
switch: "\uD83D\uDD00",
ap: "\uD83D\uDCE1",
pc: "\uD83D\uDCBB",
nas: "\uD83D\uDCBE",
device: "\uD83D\uDCF1",
};
interface NodeCardProps {
node: NetworkNode;
x: number;
y: number;
}
export function NodeCard({ node, x, y }: NodeCardProps) {
const [showPassword, setShowPassword] = useState(false);
const statusColor =
node.status === "up"
? "bg-success"
: node.status === "down"
? "bg-danger"
: "bg-warning";
return (
<foreignObject x={x - 120} y={y - 80} width={240} height={180}>
<div
className="bg-bg-card border border-border rounded-xl p-4 flex flex-col items-center gap-1 shadow-lg cursor-pointer select-none"
onClick={() => setShowPassword((prev) => !prev)}
>
<span className="text-3xl">{ICON_MAP[node.icon] || "\uD83D\uDCE6"}</span>
<span className="text-base font-bold text-text-primary truncate w-full text-center">
{node.name}
</span>
<span className="text-sm text-text-secondary font-mono">{node.ip}</span>
{node.username && (
<span className="text-sm text-text-secondary">
{node.username} / {showPassword ? node.password : "\u2022\u2022\u2022\u2022"}
</span>
)}
{node.public_url && (
<span className="text-xs text-accent truncate w-full text-center">
{node.public_url}
</span>
)}
<div className="flex items-center gap-2 mt-1">
<span className={`w-3 h-3 rounded-full ${statusColor}`} />
<span className="text-xs text-text-secondary">
{node.status === "up" ? "Online" : node.status === "down" ? "Offline" : "Unknown"}
</span>
</div>
</div>
</foreignObject>
);
}