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 &&
+
}
{error && }
@@ -219,8 +222,9 @@ function ArchiveRow({
}
export default function ArchivePage() {
- const { isAdmin, loading: authLoading } = useAuth();
+ const { user, orgId, isAdmin, loading: authLoading } = useAuth();
const router = useRouter();
+ const canView = Boolean(user && (orgId || isAdmin));
const { systems } = useSystems();
const { incidents } = useIncidents(200);
@@ -237,8 +241,8 @@ export default function ArchivePage() {
const [submittedQ, setSubmittedQ] = useState("");
useEffect(() => {
- if (!authLoading && !isAdmin) router.replace("/");
- }, [authLoading, isAdmin, router]);
+ if (!authLoading && !canView) router.replace("/");
+ }, [authLoading, canView, router]);
const load = useCallback(
async (nextCursor: string | null, append: boolean) => {
@@ -267,9 +271,9 @@ export default function ArchivePage() {
// Reload from the top whenever a filter changes.
useEffect(() => {
- if (authLoading || !isAdmin) return;
+ if (authLoading || !canView) return;
load(null, false);
- }, [authLoading, isAdmin, load]);
+ }, [authLoading, canView, load]);
const systemName = useMemo(() => {
const m = new Map(systems.map((s) => [s.system_id, s.name]));
@@ -277,7 +281,7 @@ export default function ArchivePage() {
}, [systems]);
// Every hook runs before this guard — see the note in app/nodes/page.tsx.
- if (authLoading || !isAdmin) return null;
+ if (authLoading || !canView) return null;
const orphanCount = calls.filter((c) => callIncidentIds(c).length === 0).length;
const noTranscript = calls.filter((c) => !(c.transcript_corrected || c.transcript)).length;
@@ -286,7 +290,9 @@ export default function ArchivePage() {