Big updates
This commit is contained in:
@@ -88,16 +88,18 @@ export default function IncidentDetailPage() {
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<TypeBadge type={incident.type} />
|
||||
<h1 className="text-xl font-bold text-white font-mono">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<TypeBadge type={incident.type} />
|
||||
<StatusBadge status={incident.status} />
|
||||
</div>
|
||||
<h1 className="text-lg sm:text-xl font-bold text-white font-mono leading-snug">
|
||||
{incident.title ?? "Incident"}
|
||||
</h1>
|
||||
<StatusBadge status={incident.status} />
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<div className="flex gap-2 shrink-0 flex-wrap">
|
||||
<button
|
||||
onClick={handleSummarize}
|
||||
disabled={summarizing}
|
||||
@@ -261,8 +263,8 @@ export default function IncidentDetailPage() {
|
||||
<tr className="text-xs text-gray-500 uppercase tracking-wider border-b border-gray-800">
|
||||
<th className="px-4 py-2 text-left">Time</th>
|
||||
<th className="px-4 py-2 text-left">Talkgroup</th>
|
||||
<th className="px-4 py-2 text-left">System</th>
|
||||
<th className="px-4 py-2 text-left">Node</th>
|
||||
<th className="px-4 py-2 text-left hidden sm:table-cell">System</th>
|
||||
<th className="px-4 py-2 text-left hidden sm:table-cell">Node</th>
|
||||
<th className="px-4 py-2 text-left">Duration</th>
|
||||
<th className="px-4 py-2 text-left">Audio</th>
|
||||
<th className="px-4 py-2"></th>
|
||||
|
||||
@@ -136,37 +136,85 @@ function CreateModal({ onClose, onCreate }: {
|
||||
);
|
||||
}
|
||||
|
||||
function IncidentCards({ incidents, isAdmin, onResolve }: {
|
||||
incidents: IncidentRecord[];
|
||||
isAdmin: boolean;
|
||||
onResolve: (id: string) => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{incidents.map((inc) => (
|
||||
<div
|
||||
key={inc.incident_id}
|
||||
className="bg-gray-900 border border-gray-800 rounded-xl p-4 cursor-pointer active:bg-gray-800"
|
||||
onClick={() => router.push(`/incidents/${inc.incident_id}`)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 mb-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
{typeBadge(inc.type)}
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
||||
inc.status === "active" ? "bg-green-900 text-green-300" : "bg-gray-800 text-gray-400"
|
||||
}`}>{inc.status}</span>
|
||||
</div>
|
||||
{isAdmin && inc.status === "active" && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onResolve(inc.incident_id); }}
|
||||
className="text-xs bg-gray-800 hover:bg-gray-700 text-gray-300 px-2 py-1 rounded transition-colors"
|
||||
>
|
||||
Resolve
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-white text-sm font-semibold leading-snug">{inc.title ?? "—"}</p>
|
||||
<p className="text-gray-500 text-xs mt-1 font-mono">
|
||||
{fmtTime(inc.started_at)} · {inc.call_ids.length} call{inc.call_ids.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IncidentTable({ incidents, isAdmin, onResolve }: {
|
||||
incidents: IncidentRecord[];
|
||||
isAdmin: boolean;
|
||||
onResolve: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800 text-xs text-gray-500 uppercase">
|
||||
<th className="px-4 py-3">Type</th>
|
||||
<th className="px-4 py-3">Title</th>
|
||||
<th className="px-4 py-3">Status</th>
|
||||
<th className="px-4 py-3">Calls</th>
|
||||
<th className="px-4 py-3">Started</th>
|
||||
<th className="px-4 py-3">Updated</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{incidents.map((inc) => (
|
||||
<IncidentRow
|
||||
key={inc.incident_id}
|
||||
incident={inc}
|
||||
isAdmin={isAdmin}
|
||||
onResolve={onResolve}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<>
|
||||
{/* Mobile card view */}
|
||||
<div className="sm:hidden">
|
||||
<IncidentCards incidents={incidents} isAdmin={isAdmin} onResolve={onResolve} />
|
||||
</div>
|
||||
|
||||
{/* Desktop table view */}
|
||||
<div className="hidden sm:block bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800 text-xs text-gray-500 uppercase">
|
||||
<th className="px-4 py-3">Type</th>
|
||||
<th className="px-4 py-3">Title</th>
|
||||
<th className="px-4 py-3">Status</th>
|
||||
<th className="px-4 py-3">Calls</th>
|
||||
<th className="px-4 py-3">Started</th>
|
||||
<th className="px-4 py-3">Updated</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{incidents.map((inc) => (
|
||||
<IncidentRow
|
||||
key={inc.incident_id}
|
||||
incident={inc}
|
||||
isAdmin={isAdmin}
|
||||
onResolve={onResolve}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -233,6 +233,17 @@ export default function NodeDetailPage() {
|
||||
>
|
||||
Restart OP25
|
||||
</button>
|
||||
<button
|
||||
disabled={sending}
|
||||
onClick={() => {
|
||||
if (confirm("Restart the node container to apply any pulled image updates?")) {
|
||||
sendCommand("node_update");
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg text-sm font-mono transition-colors disabled:opacity-50"
|
||||
>
|
||||
Update Node
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowDiscordJoin(true)}
|
||||
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg text-sm font-mono transition-colors"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run before pushing — catches TypeScript errors without a full build.
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
echo "→ TypeScript check…"
|
||||
npm run typecheck
|
||||
echo "✓ No type errors."
|
||||
@@ -30,6 +30,7 @@ export function CallRow({ call, systemName, isAdmin }: Props) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editText, setEditText] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const isActive = call.status === "active";
|
||||
const hasDetails = call.transcript || call.transcript_corrected || (call.tags && call.tags.length > 0) || call.incident_id;
|
||||
const displayTranscript = (!showOriginal && call.transcript_corrected) ? call.transcript_corrected : call.transcript;
|
||||
@@ -44,11 +45,12 @@ export function CallRow({ call, systemName, isAdmin }: Props) {
|
||||
async function saveEdit(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
await c2api.patchTranscript(call.call_id, editText);
|
||||
setEditing(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setSaveError(err instanceof Error ? err.message : "Save failed");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -71,8 +73,8 @@ export function CallRow({ call, systemName, isAdmin }: Props) {
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-400">{systemName ?? call.system_id ?? "—"}</td>
|
||||
<td className="px-4 py-2 text-gray-400">{call.node_id}</td>
|
||||
<td className="px-4 py-2 text-gray-400 hidden sm:table-cell">{systemName ?? call.system_id ?? "—"}</td>
|
||||
<td className="px-4 py-2 text-gray-400 hidden sm:table-cell">{call.node_id}</td>
|
||||
<td className="px-4 py-2">
|
||||
{isActive ? (
|
||||
<span className="text-orange-400 animate-pulse">● live</span>
|
||||
@@ -136,7 +138,7 @@ export function CallRow({ call, systemName, isAdmin }: Props) {
|
||||
rows={4}
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded-lg px-3 py-2 text-xs text-gray-200 font-mono focus:outline-none focus:border-indigo-500 resize-none"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={saveEdit}
|
||||
disabled={saving}
|
||||
@@ -150,6 +152,9 @@ export function CallRow({ call, systemName, isAdmin }: Props) {
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{saveError && (
|
||||
<span className="text-xs text-red-400 font-mono">{saveError}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : hasSegments ? (
|
||||
|
||||
Reference in New Issue
Block a user