Initial commit — DRB server stack

Includes c2-core (FastAPI/MQTT/Firestore), discord-bot (slash commands),
frontend (Next.js admin UI), and mosquitto config.
This commit is contained in:
Logan
2026-04-05 19:01:39 -04:00
commit 2f0597c81b
77 changed files with 4126 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
"use client";
import { useEffect } from "react";
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet";
import L from "leaflet";
import type { NodeRecord, CallRecord } from "@/lib/types";
import "leaflet/dist/leaflet.css";
// Fix Leaflet default icon paths broken by webpack
delete (L.Icon.Default.prototype as unknown as Record<string, unknown>)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
});
const nodeIcon = (status: string) =>
L.divIcon({
className: "",
html: `<div style="
width:14px;height:14px;border-radius:50%;
background:${status === "online" || status === "recording" ? "#4ade80" : status === "unconfigured" ? "#818cf8" : "#6b7280"};
border:2px solid #111827;
box-shadow:0 0 6px ${status === "recording" ? "#fb923c" : "transparent"};
"></div>`,
iconSize: [14, 14],
iconAnchor: [7, 7],
});
interface Props {
nodes: NodeRecord[];
activeCalls: CallRecord[];
}
export default function MapView({ nodes, activeCalls }: Props) {
const activeByNode = Object.fromEntries(
activeCalls.map((c) => [c.node_id, c])
);
const center: [number, number] =
nodes.length > 0 ? [nodes[0].lat, nodes[0].lon] : [39.5, -98.35];
return (
<MapContainer
center={center}
zoom={nodes.length > 0 ? 10 : 4}
className="w-full h-full rounded-lg"
style={{ background: "#111827" }}
>
<TileLayer
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
attribution='&copy; <a href="https://carto.com/">CARTO</a>'
/>
{nodes.map((node) => (
<Marker
key={node.node_id}
position={[node.lat, node.lon]}
icon={nodeIcon(node.status)}
>
<Popup className="font-mono">
<div className="text-gray-900">
<p className="font-bold">{node.name}</p>
<p className="text-xs text-gray-500">{node.node_id}</p>
<p className="text-xs mt-1 capitalize">{node.status}</p>
{activeByNode[node.node_id] && (
<p className="text-xs text-orange-600 mt-1">
TG {activeByNode[node.node_id].talkgroup_id ?? "—"}{" "}
{activeByNode[node.node_id].talkgroup_name}
</p>
)}
</div>
</Popup>
</Marker>
))}
</MapContainer>
);
}