From fa194e0f0adcffe4fc0348bad817fa9cd8ea3930 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Wed, 23 Sep 2026 23:53:46 -0400 Subject: [PATCH] frontend: search, filters and load-more on Incidents; open Archive to viewers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Incidents page: text search (title, location, summary, units, vehicles, tags, location mentions), status and type filters, and a Load more button that pages the Firestore query 100 at a time. Filtering runs over the loaded window, and the page says so when older incidents exist. Archive (/calls): readable by every org member, not just admins. GET /calls/search now takes any Firebase token scoped to the caller's org — the Firestore rules already let members read every call in their org, so this widens nothing. Attach/detach stays admin-only (UI and routes). Co-Authored-By: Claude Opus 5.5 --- drb-c2-core/app/routers/calls.py | 8 ++- drb-frontend/app/calls/page.tsx | 33 +++++++----- drb-frontend/app/incidents/page.tsx | 84 ++++++++++++++++++++++++++--- drb-frontend/lib/useIncidents.ts | 6 ++- 4 files changed, 108 insertions(+), 23 deletions(-) diff --git a/drb-c2-core/app/routers/calls.py b/drb-c2-core/app/routers/calls.py index 03caab4..f1ad4c4 100644 --- a/drb-c2-core/app/routers/calls.py +++ b/drb-c2-core/app/routers/calls.py @@ -5,6 +5,7 @@ from typing import Optional from app.internal import firestore as fstore from app.internal.auth import ( require_admin_token, + require_firebase_token, require_service_or_firebase_token, resolve_caller_org_id, reprocess_limiter, @@ -54,7 +55,7 @@ async def search_calls( link: str = Query("any", pattern="^(any|orphan|linked)$"), transcript: str = Query("any", pattern="^(any|yes|no)$"), q: Optional[str] = Query(None, description="case-insensitive substring of the transcript"), - decoded: dict = Depends(require_admin_token), + decoded: dict = Depends(require_firebase_token), ): """ Paged, filterable call archive — the backend for the /calls page. @@ -72,6 +73,11 @@ async def search_calls( `window_exhausted` says the scan hit its cap before filling the page, so an empty result means "not in this window", not "none exist". + + Open to every org member (viewer included), not just admins: the Firestore + rules already let any member read every call doc in their org + (firestore.rules `calls` → docInMyOrg), so this route exposes nothing a + viewer's browser couldn't already read directly. """ org_id = await resolve_caller_org_id(decoded) if org_id is None: diff --git a/drb-frontend/app/calls/page.tsx b/drb-frontend/app/calls/page.tsx index 29c528e..c4996e3 100644 --- a/drb-frontend/app/calls/page.tsx +++ b/drb-frontend/app/calls/page.tsx @@ -6,8 +6,9 @@ // never correlated was invisible. That is the wrong way round when correlation // quality is the thing under development — the orphans are the evidence. // -// Admin-only, because it exposes every call in the org regardless of node -// ownership and carries the manual attribution controls. +// Readable by every org member — the Firestore rules already let any member +// read every call in their org. The manual attribution controls stay +// admin-only, matching the admin gate on the link/unlink routes. import { useCallback, useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; @@ -68,11 +69,13 @@ function ArchiveRow({ call, systemName, incidents, + canEdit, onChanged, }: { call: CallRecord; systemName?: string; incidents: IncidentRecord[]; + canEdit: boolean; onChanged: () => void; }) { const [open, setOpen] = useState(false); @@ -178,18 +181,18 @@ function ArchiveRow({
attached to {inc?.title ?? id.slice(0, 8)} - + }
); })} -
+ {canEdit &&
setSearch(e.target.value)} + placeholder="Search title, location, units…" + className="bg-surface border border-line rounded-lg text-sm text-ink px-3 py-2 w-full sm:w-64 focus:outline-none focus:border-accent" + /> +
+ +
+ + +