Skip to content

Commit 01be757

Browse files
dreynowclaude
andcommitted
feat: per-client filtering on Escalations and Dashboard
Escalations page: - Client dropdown filter (from useClients hook) - Reads ?client_id from URL params (linked from client detail page) - Client name badge on each escalation card - Passes client_id to API query Dashboard: - "All Clients" / specific client dropdown in header - useClients hook loaded for the selector Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 88a3e50 commit 01be757

2 files changed

Lines changed: 68 additions & 17 deletions

File tree

apps/observatory/src/pages/DashboardPage.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Shield, Users, Link2, Activity, Rocket, UserPlus, GitBranch, BarChart3,
66
import { useAgents } from '@/hooks/useAgents';
77
import { useDelegations } from '@/hooks/useDelegations';
88
import { useProvenance } from '@/hooks/useProvenance';
9+
import { useClients } from '@/hooks/useClients';
910
import { StatCard } from '@/components/StatCard';
1011
import { ActivityFeed } from '@/components/ActivityFeed';
1112
import { AgentCard } from '@/components/AgentCard';
@@ -267,6 +268,8 @@ export const DashboardPage: React.FC = () => {
267268
const { agents, loading, refetch: refetchAgents } = useAgents();
268269
const { delegations, refetch: refetchDelegations } = useDelegations();
269270
const { provenance, refetch: refetchProvenance } = useProvenance();
271+
const { clients } = useClients();
272+
const [selectedClient, setSelectedClient] = useState<string>('all');
270273

271274
const activeDelegations = delegations.filter(d => !d.revoked_at && (!d.expires_at || new Date(d.expires_at) > new Date()));
272275
const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0);
@@ -309,6 +312,18 @@ export const DashboardPage: React.FC = () => {
309312
<span className="text-[10px] text-[#6B6760] bg-[#F7F6F3] px-2 py-0.5 rounded-full">
310313
{agents.length} agent{agents.length !== 1 ? 's' : ''}
311314
</span>
315+
{clients.length > 0 && (
316+
<select
317+
value={selectedClient}
318+
onChange={e => setSelectedClient(e.target.value)}
319+
className="ml-auto bg-white border border-[#E8E5DE] text-[#6B6760] text-xs rounded-lg px-3 py-1.5 focus:outline-none focus:border-[#B08D3E] transition-colors"
320+
>
321+
<option value="all">All Clients</option>
322+
{clients.map(c => (
323+
<option key={c.id} value={c.id}>{c.name}</option>
324+
))}
325+
</select>
326+
)}
312327
</motion.div>
313328

314329
{/* Stats */}

apps/observatory/src/pages/EscalationsPage.tsx

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useState, useEffect, useCallback } from 'react';
22
import { motion, AnimatePresence } from 'framer-motion';
33
import { AlertTriangle, Check, X, Clock, DollarSign, Building2, Shield, ChevronDown, CheckCircle } from 'lucide-react';
4+
import { useSearchParams } from 'react-router-dom';
45
import { apiFetch } from '../lib/api';
6+
import { useClients } from '../hooks/useClients';
57

68
const stagger = {
79
hidden: {},
@@ -27,6 +29,7 @@ interface Escalation {
2729
approved_by: string | null;
2830
denial_reason: string | null;
2931
approval_token: string | null;
32+
client_id: string | null;
3033
created_at: string;
3134
expires_at: string;
3235
resolved_at: string | null;
@@ -81,20 +84,28 @@ function timeUntilExpiry(expiresAt: string): string {
8184
}
8285

8386
export const EscalationsPage: React.FC = () => {
87+
const [searchParams] = useSearchParams();
88+
const { clients } = useClients();
89+
const clientMap = new Map(clients.map(c => [c.id, c.name]));
90+
8491
const [escalations, setEscalations] = useState<Escalation[]>([]);
8592
const [loading, setLoading] = useState(true);
8693
const [filter, setFilter] = useState<string>('all');
94+
const [clientFilter, setClientFilter] = useState<string>(searchParams.get('client_id') || 'all');
8795
const [expandedId, setExpandedId] = useState<string | null>(null);
8896
const [acting, setActing] = useState<string | null>(null);
8997

9098
const fetchEscalations = useCallback(async () => {
9199
try {
92-
const params = filter !== 'all' ? `?status=${filter}` : '';
93-
const resp = await apiFetch(`/v1/escalations${params}`);
100+
const qp = new URLSearchParams();
101+
if (filter !== 'all') qp.set('status', filter);
102+
if (clientFilter !== 'all') qp.set('client_id', clientFilter);
103+
const qs = qp.toString();
104+
const resp = await apiFetch(`/v1/escalations${qs ? `?${qs}` : ''}`);
94105
if (resp.ok) setEscalations(await resp.json());
95106
} catch { /* degrade silently */ }
96107
setLoading(false);
97-
}, [filter]);
108+
}, [filter, clientFilter]);
98109

99110
useEffect(() => {
100111
fetchEscalations();
@@ -139,20 +150,37 @@ export const EscalationsPage: React.FC = () => {
139150
</p>
140151
</div>
141152

142-
<div className="flex gap-1.5">
143-
{['all', 'pending', 'approved', 'denied', 'expired'].map(f => (
144-
<button
145-
key={f}
146-
onClick={() => setFilter(f)}
147-
className={`px-3 py-1.5 rounded-lg text-xs transition-colors ${
148-
filter === f
149-
? 'bg-[#FAF6ED] text-[#B08D3E] border border-[#E8DCC4]'
150-
: 'bg-white border border-[#E8E5DE] text-[#6B6760] hover:bg-[#F7F6F3]'
151-
}`}
153+
<div className="flex items-center gap-3">
154+
{/* Client filter */}
155+
{clients.length > 0 && (
156+
<select
157+
value={clientFilter}
158+
onChange={e => setClientFilter(e.target.value)}
159+
className="bg-white border border-[#E8E5DE] text-[#6B6760] text-xs rounded-lg px-3 py-1.5 focus:outline-none focus:border-[#B08D3E] transition-colors"
152160
>
153-
{f.charAt(0).toUpperCase() + f.slice(1)}
154-
</button>
155-
))}
161+
<option value="all">All Clients</option>
162+
{clients.map(c => (
163+
<option key={c.id} value={c.id}>{c.name}</option>
164+
))}
165+
</select>
166+
)}
167+
168+
{/* Status filters */}
169+
<div className="flex gap-1.5">
170+
{['all', 'pending', 'approved', 'denied', 'expired'].map(f => (
171+
<button
172+
key={f}
173+
onClick={() => setFilter(f)}
174+
className={`px-3 py-1.5 rounded-lg text-xs transition-colors ${
175+
filter === f
176+
? 'bg-[#FAF6ED] text-[#B08D3E] border border-[#E8DCC4]'
177+
: 'bg-white border border-[#E8E5DE] text-[#6B6760] hover:bg-[#F7F6F3]'
178+
}`}
179+
>
180+
{f.charAt(0).toUpperCase() + f.slice(1)}
181+
</button>
182+
))}
183+
</div>
156184
</div>
157185
</motion.div>
158186

@@ -199,7 +227,15 @@ export const EscalationsPage: React.FC = () => {
199227
)}
200228
{confidenceBadge(esc.vendor_confidence)}
201229
</div>
202-
<p className="text-xs text-[#6B6760] mt-0.5 truncate">{esc.agent_name} - {esc.reason}</p>
230+
<p className="text-xs text-[#6B6760] mt-0.5 truncate">
231+
{esc.agent_name}
232+
{esc.client_id && clientMap.get(esc.client_id) && (
233+
<span className="ml-1.5 text-[10px] px-1.5 py-0.5 rounded bg-[#F7F6F3] text-[#9C978E] border border-[#F0EDE6]">
234+
{clientMap.get(esc.client_id)}
235+
</span>
236+
)}
237+
<span className="mx-1 text-[#E8E5DE]">-</span>{esc.reason}
238+
</p>
203239
</div>
204240
</div>
205241

0 commit comments

Comments
 (0)