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
+58
View File
@@ -0,0 +1,58 @@
import { auth } from "@/lib/firebase";
const BASE = process.env.NEXT_PUBLIC_C2_URL ?? "http://localhost:8000";
async function request<T>(path: string, options?: RequestInit): Promise<T> {
const user = auth.currentUser;
const token = user ? await user.getIdToken() : null;
const res = await fetch(`${BASE}${path}`, {
...options,
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options?.headers as Record<string, string> | undefined),
},
});
if (!res.ok) throw new Error(`C2 API error ${res.status}: ${await res.text()}`);
if (res.status === 204) return undefined as T;
return res.json();
}
export const c2api = {
// Nodes
getNodes: () => request<unknown[]>("/nodes"),
getNode: (id: string) => request<unknown>(`/nodes/${id}`),
sendCommand: (nodeId: string, payload: object) =>
request(`/nodes/${nodeId}/command`, { method: "POST", body: JSON.stringify(payload) }),
assignSystem: (nodeId: string, systemId: string) =>
request(`/nodes/${nodeId}/config/${systemId}`, { method: "POST" }),
// Systems
getSystems: () => request<unknown[]>("/systems"),
createSystem: (body: object) =>
request("/systems", { method: "POST", body: JSON.stringify(body) }),
updateSystem: (id: string, body: object) =>
request(`/systems/${id}`, { method: "PUT", body: JSON.stringify(body) }),
deleteSystem: (id: string) =>
request(`/systems/${id}`, { method: "DELETE" }),
// Tokens
getTokens: () => request<unknown[]>("/tokens"),
addToken: (body: { name: string; token: string }) =>
request("/tokens", { method: "POST", body: JSON.stringify(body) }),
deleteToken: (id: string) =>
request(`/tokens/${id}`, { method: "DELETE" }),
// Node approval
approveNode: (id: string) =>
request(`/nodes/${id}/approve`, { method: "POST" }),
rejectNode: (id: string) =>
request(`/nodes/${id}/reject`, { method: "POST" }),
// Calls
getCalls: (params?: Record<string, string>) => {
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
return request<unknown[]>(`/calls${qs}`);
},
};
+18
View File
@@ -0,0 +1,18 @@
import { initializeApp, getApps, getApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};
const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
const databaseId = process.env.NEXT_PUBLIC_FIRESTORE_DATABASE || "(default)";
export const db = getFirestore(app, databaseId);
export const auth = getAuth(app);
export { app };
+51
View File
@@ -0,0 +1,51 @@
export type NodeStatus = "online" | "offline" | "recording" | "unconfigured";
export type ApprovalStatus = "pending" | "approved" | "rejected";
export interface NodeRecord {
node_id: string;
name: string;
lat: number;
lon: number;
status: NodeStatus;
configured: boolean;
last_seen: string | null;
assigned_system_id: string | null;
approval_status: ApprovalStatus | null;
}
export interface SystemRecord {
system_id: string;
name: string;
type: string; // P25 | DMR | NBFM
config: Record<string, unknown>;
}
export interface CallRecord {
call_id: string;
node_id: string;
system_id: string | null;
talkgroup_id: number | null;
talkgroup_name: string | null;
freq: number | null;
started_at: string;
ended_at: string | null;
audio_url: string | null;
transcript: string | null;
incident_id: string | null;
location: { lat: number; lng: number } | null;
tags: string[];
status: "active" | "ended";
}
export interface IncidentRecord {
incident_id: string;
title: string | null;
type: string | null;
status: "active" | "resolved";
location: { lat: number; lng: number } | null;
call_ids: string[];
started_at: string;
updated_at: string;
summary: string | null;
tags: string[];
}
+73
View File
@@ -0,0 +1,73 @@
"use client";
import { useEffect, useState } from "react";
import { collection, onSnapshot, query, orderBy, limit, where, FirestoreError } from "firebase/firestore";
import { onAuthStateChanged } from "firebase/auth";
import { db, auth } from "@/lib/firebase";
import type { CallRecord } from "@/lib/types";
export function useCalls(limitCount = 50) {
const [calls, setCalls] = useState<CallRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let unsubFirestore: (() => void) | undefined;
const unsubAuth = onAuthStateChanged(auth, (user) => {
if (unsubFirestore) { unsubFirestore(); unsubFirestore = undefined; }
if (!user) {
setCalls([]);
setLoading(false);
return;
}
const q = query(
collection(db, "calls"),
orderBy("started_at", "desc"),
limit(limitCount)
);
unsubFirestore = onSnapshot(q, (snap) => {
setCalls(snap.docs.map((d) => d.data() as CallRecord));
setLoading(false);
}, (err: FirestoreError) => { console.error("useCalls:", err); setError(err.message); setLoading(false); });
});
return () => {
unsubAuth();
if (unsubFirestore) unsubFirestore();
};
}, [limitCount]);
return { calls, loading, error };
}
export function useActiveCalls() {
const [calls, setCalls] = useState<CallRecord[]>([]);
useEffect(() => {
let unsubFirestore: (() => void) | undefined;
const unsubAuth = onAuthStateChanged(auth, (user) => {
if (unsubFirestore) { unsubFirestore(); unsubFirestore = undefined; }
if (!user) {
setCalls([]);
return;
}
const q = query(collection(db, "calls"), where("status", "==", "active"));
unsubFirestore = onSnapshot(q, (snap) => {
setCalls(snap.docs.map((d) => d.data() as CallRecord));
}, (err: FirestoreError) => { console.error("useActiveCalls:", err); });
});
return () => {
unsubAuth();
if (unsubFirestore) unsubFirestore();
};
}, []);
return calls;
}
+48
View File
@@ -0,0 +1,48 @@
"use client";
import { useEffect, useState } from "react";
import { collection, onSnapshot, query, FirestoreError } from "firebase/firestore";
import { onAuthStateChanged } from "firebase/auth";
import { db, auth } from "@/lib/firebase";
import type { NodeRecord } from "@/lib/types";
export function useNodes() {
const [nodes, setNodes] = useState<NodeRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let unsubFirestore: (() => void) | undefined;
const unsubAuth = onAuthStateChanged(auth, (user) => {
if (unsubFirestore) { unsubFirestore(); unsubFirestore = undefined; }
if (!user) {
setNodes([]);
setLoading(false);
return;
}
const q = query(collection(db, "nodes"));
unsubFirestore = onSnapshot(q, (snap) => {
setNodes(snap.docs.map((d) => d.data() as NodeRecord));
setLoading(false);
}, (err: FirestoreError) => { console.error("useNodes:", err); setError(err.message); setLoading(false); });
});
return () => {
unsubAuth();
if (unsubFirestore) unsubFirestore();
};
}, []);
return { nodes, loading, error };
}
export function useUnconfiguredNodes() {
const { nodes, loading } = useNodes();
return {
nodes: nodes.filter((n) => !n.configured),
loading,
};
}
+39
View File
@@ -0,0 +1,39 @@
"use client";
import { useEffect, useState } from "react";
import { collection, onSnapshot, FirestoreError } from "firebase/firestore";
import { onAuthStateChanged } from "firebase/auth";
import { db, auth } from "@/lib/firebase";
import type { SystemRecord } from "@/lib/types";
export function useSystems() {
const [systems, setSystems] = useState<SystemRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let unsubFirestore: (() => void) | undefined;
const unsubAuth = onAuthStateChanged(auth, (user) => {
if (unsubFirestore) { unsubFirestore(); unsubFirestore = undefined; }
if (!user) {
setSystems([]);
setLoading(false);
return;
}
unsubFirestore = onSnapshot(collection(db, "systems"), (snap) => {
setSystems(snap.docs.map((d) => d.data() as SystemRecord));
setLoading(false);
}, (err: FirestoreError) => { console.error("useSystems:", err); setError(err.message); setLoading(false); });
});
return () => {
unsubAuth();
if (unsubFirestore) unsubFirestore();
};
}, []);
return { systems, loading, error };
}