mohsin-devs Cursor commited on
Commit
526fdb9
·
1 Parent(s): c004697

fix: remove duplicate SecurityPage component breaking HF Docker build

Browse files
Files changed (1) hide show
  1. frontend/src/app/security/page.tsx +37 -519
frontend/src/app/security/page.tsx CHANGED
@@ -56,20 +56,13 @@ function RiskBar({ score }: { score: number }) {
56
  return (
57
  <div className="flex items-center gap-2">
58
  <div className="h-1.5 w-24 rounded-full bg-white/10 overflow-hidden">
59
- <motion.div
60
- initial={{ width: 0 }}
61
- animate={{ width: `${score}%` }}
62
- transition={{ duration: 0.8, ease: "easeOut" }}
63
- className="h-full rounded-full"
64
- style={{ background: color }}
65
- />
66
  </div>
67
  <span className="text-xs font-mono" style={{ color }}>{score}</span>
68
  </div>
69
  );
70
  }
71
 
72
- // ─── Alert Row with expandable AI explanation ─────────────────────────────────
73
  function AlertRow({ alert, index }: { alert: FraudAlert; index: number }) {
74
  const [expanded, setExpanded] = useState(false);
75
  const [explanation, setExplanation] = useState<FraudExplanation | null>(null);
@@ -96,72 +89,33 @@ function AlertRow({ alert, index }: { alert: FraudAlert; index: number }) {
96
  };
97
 
98
  return (
99
- <motion.div
100
- initial={{ opacity: 0, x: -8 }}
101
- animate={{ opacity: 1, x: 0 }}
102
- transition={{ delay: index * 0.04 }}
103
- className={`border-b border-white/5 last:border-0 ${alert.risk_score >= 70 ? "bg-red-500/2" : ""}`}
104
- >
105
- {/* Main row */}
106
- <div
107
- className="flex items-center gap-4 px-6 py-4 hover:bg-white/[0.02] transition-colors cursor-pointer"
108
- onClick={loadExplanation}
109
- >
110
- <div className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl border ${
111
- alert.risk_score >= 70 ? "bg-red-500/10 border-red-500/20"
112
- : alert.risk_score >= 40 ? "bg-amber-500/10 border-amber-500/20"
113
- : "bg-emerald-500/10 border-emerald-500/20"
114
- }`}>
115
- {alert.risk_score >= 70 ? <XCircle className="h-5 w-5 text-red-400" />
116
- : alert.risk_score >= 40 ? <AlertTriangle className="h-5 w-5 text-amber-400" />
117
- : <CheckCircle className="h-5 w-5 text-emerald-400" />}
118
  </div>
119
-
120
  <div className="flex-1 min-w-0">
121
  <div className="flex items-center gap-2 flex-wrap">
122
- <p className="text-sm font-medium text-white truncate">
123
- {alert.merchant || `Transaction ${(alert.transaction_id || alert.id).slice(0, 8)}...`}
124
- </p>
125
  <RiskBadge score={alert.risk_score} />
126
- {alert.status && (
127
- <span className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-zinc-400 capitalize">
128
- {alert.status}
129
- </span>
130
- )}
131
  </div>
132
  <p className="text-xs text-zinc-500 mt-0.5 truncate">
133
- {typeof alert.details === "string" ? alert.details
134
- : Array.isArray(alert.details) ? (alert.details as string[]).join(" · ")
135
- : "Suspicious activity detected"}
136
  </p>
137
  </div>
138
-
139
- <div className="flex-shrink-0 hidden sm:block">
140
- <RiskBar score={alert.risk_score} />
141
- </div>
142
-
143
- {alert.amount != null && (
144
- <div className="flex-shrink-0 text-right">
145
- <p className="text-sm font-semibold text-white">${Math.abs(alert.amount).toFixed(2)}</p>
146
- </div>
147
- )}
148
-
149
  <div className="flex-shrink-0 flex items-center gap-1 text-zinc-500">
150
  <Sparkles className="h-3.5 w-3.5 text-cyan-400" />
151
  {expanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
152
  </div>
153
  </div>
154
 
155
- {/* AI Explanation panel */}
156
  <AnimatePresence>
157
  {expanded && (
158
- <motion.div
159
- initial={{ height: 0, opacity: 0 }}
160
- animate={{ height: "auto", opacity: 1 }}
161
- exit={{ height: 0, opacity: 0 }}
162
- transition={{ duration: 0.25 }}
163
- className="overflow-hidden"
164
- >
165
  <div className="mx-6 mb-4 rounded-xl border border-cyan-500/20 bg-cyan-500/5 p-4">
166
  <div className="flex items-start gap-2">
167
  <Sparkles className="h-4 w-4 text-cyan-400 flex-shrink-0 mt-0.5" />
@@ -188,20 +142,10 @@ function AlertRow({ alert, index }: { alert: FraudAlert; index: number }) {
188
  )}
189
  {explanation.amount_vs_avg > 0 && (
190
  <div className="flex items-center gap-4 pt-1 border-t border-white/8">
191
- <div>
192
- <p className="text-[10px] text-zinc-500">This transaction</p>
193
- <p className="text-sm font-bold text-white">${alert.amount?.toFixed(2)}</p>
194
- </div>
195
  <div className="text-zinc-600">vs</div>
196
- <div>
197
- <p className="text-[10px] text-zinc-500">Your average</p>
198
- <p className="text-sm font-bold text-zinc-300">${explanation.user_avg_amount.toFixed(2)}</p>
199
- </div>
200
- <div className="ml-auto">
201
- <span className="rounded-full bg-red-500/15 border border-red-500/30 px-2 py-0.5 text-xs font-bold text-red-400">
202
- {explanation.amount_vs_avg.toFixed(1)}× average
203
- </span>
204
- </div>
205
  </div>
206
  )}
207
  </div>
@@ -216,14 +160,8 @@ function AlertRow({ alert, index }: { alert: FraudAlert; index: number }) {
216
  );
217
  }
218
 
219
- const containerVariants = {
220
- hidden: { opacity: 0 },
221
- visible: { opacity: 1, transition: { staggerChildren: 0.07 } },
222
- };
223
- const itemVariants = {
224
- hidden: { opacity: 0, y: 16 },
225
- visible: { opacity: 1, y: 0, transition: { duration: 0.4 } },
226
- };
227
 
228
  export default function SecurityPage() {
229
  const { user } = useAuthStore();
@@ -233,51 +171,34 @@ export default function SecurityPage() {
233
  const [filter, setFilter] = useState<"all" | "high" | "pending">("all");
234
 
235
  const load = useCallback(async () => {
236
- setLoading(true);
237
- setError(null);
238
  try {
239
  const res = await aiApi.fraudAnalysis(user?.user_id);
240
  const raw = res as unknown as Record<string, unknown>;
241
  const alerts: FraudAlert[] = (raw.alerts as FraudAlert[]) || [];
242
- setData({
243
- total_alerts: (raw.total_alerts as number) ?? alerts.length,
244
- pending_reviews: (raw.pending_reviews as number) ?? alerts.filter((a) => a.status === "pending").length,
245
- alerts,
246
- });
247
- } catch (err) {
248
- setError((err as Error).message);
249
- } finally {
250
- setLoading(false);
251
- }
252
  }, [user?.user_id]);
253
 
254
  useEffect(() => { load(); }, [load]);
255
 
256
  const alerts = data?.alerts || [];
257
- const filtered = alerts.filter((a) => {
258
- if (filter === "high") return a.risk_score >= 70;
259
- if (filter === "pending") return a.status === "pending" || !a.status;
260
- return true;
261
- });
262
-
263
  const highRiskCount = alerts.filter((a) => a.risk_score >= 70).length;
264
  const suspiciousCount = alerts.filter((a) => a.risk_score >= 40 && a.risk_score < 70).length;
265
  const safeCount = alerts.filter((a) => a.risk_score < 40).length;
266
- const avgScore = alerts.length
267
- ? Math.round(alerts.reduce((s, a) => s + a.risk_score, 0) / alerts.length)
268
- : 0;
269
 
270
  return (
271
  <motion.div variants={containerVariants} initial="hidden" animate="visible" className="flex flex-col gap-6">
272
- {/* Header */}
273
  <motion.div variants={itemVariants} className="flex items-center justify-between">
274
  <div>
275
  <h1 className="text-3xl font-bold tracking-tight text-white">Security Center</h1>
276
  <p className="text-zinc-400 mt-1 text-sm">AI-powered fraud detection · Click any alert for a full AI explanation</p>
277
  </div>
278
  <button onClick={load} className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-400 hover:text-white transition-colors">
279
- <RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} />
280
- Refresh
281
  </button>
282
  </motion.div>
283
 
@@ -288,24 +209,15 @@ export default function SecurityPage() {
288
  </motion.div>
289
  )}
290
 
291
- {/* AI Shield Banner */}
292
  <motion.div variants={itemVariants} className="flex items-center gap-4 rounded-2xl border border-emerald-500/20 bg-gradient-to-r from-emerald-500/10 via-cyan-500/5 to-blue-500/10 px-5 py-4">
293
- <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex-shrink-0">
294
- <Shield className="h-5 w-5 text-emerald-400" />
295
- </div>
296
  <div className="flex-1">
297
  <p className="text-sm font-semibold text-white">AI Fraud Shield Active</p>
298
- <p className="text-xs text-zinc-400 mt-0.5">
299
- Click any flagged transaction to get a full AI explanation of exactly why it was flagged.
300
- </p>
301
- </div>
302
- <div className="flex items-center gap-1.5">
303
- <div className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
304
- <span className="text-xs text-emerald-400 font-medium">Live</span>
305
  </div>
 
306
  </motion.div>
307
 
308
- {/* Stats */}
309
  <div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
310
  {[
311
  { label: "Total Alerts", value: loading ? "—" : String(data?.total_alerts ?? 0), icon: Activity, color: "text-blue-400", border: "border-blue-500/20", bg: "from-blue-500/10 to-blue-500/5" },
@@ -315,17 +227,13 @@ export default function SecurityPage() {
315
  ].map((stat) => (
316
  <motion.div key={stat.label} variants={itemVariants} className={`rounded-2xl border ${stat.border} bg-gradient-to-br ${stat.bg} p-5`}>
317
  <div className="flex items-start justify-between">
318
- <div>
319
- <p className="text-xs text-zinc-400 uppercase tracking-wider">{stat.label}</p>
320
- <p className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</p>
321
- </div>
322
  <stat.icon className={`h-5 w-5 ${stat.color} opacity-60`} />
323
  </div>
324
  </motion.div>
325
  ))}
326
  </div>
327
 
328
- {/* Detection Rules */}
329
  <motion.div variants={itemVariants} className="glass-card p-6">
330
  <h2 className="text-base font-semibold text-white mb-4">Active Detection Rules</h2>
331
  <div className="grid gap-3 sm:grid-cols-2">
@@ -336,16 +244,11 @@ export default function SecurityPage() {
336
  { icon: Eye, label: "Duplicate Payment Check", desc: "Catches same merchant + amount within 10 minutes" },
337
  ].map((rule) => (
338
  <div key={rule.label} className="flex items-start gap-3 rounded-xl border border-white/8 bg-white/[0.02] p-4">
339
- <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex-shrink-0">
340
- <rule.icon className="h-4 w-4 text-emerald-400" />
341
- </div>
342
  <div className="flex-1 min-w-0">
343
  <div className="flex items-center justify-between gap-2">
344
  <p className="text-sm font-medium text-white">{rule.label}</p>
345
- <div className="flex items-center gap-1 flex-shrink-0">
346
- <div className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
347
- <span className="text-[10px] text-emerald-400">ON</span>
348
- </div>
349
  </div>
350
  <p className="text-xs text-zinc-500 mt-0.5">{rule.desc}</p>
351
  </div>
@@ -354,19 +257,15 @@ export default function SecurityPage() {
354
  </div>
355
  </motion.div>
356
 
357
- {/* Alerts Table */}
358
  <motion.div variants={itemVariants} className="glass-card overflow-hidden">
359
  <div className="flex items-center justify-between px-6 py-4 border-b border-white/8">
360
  <div>
361
  <h2 className="text-base font-semibold text-white">Fraud Alerts</h2>
362
- <p className="text-xs text-zinc-400 mt-0.5">
363
- {loading ? "Loading..." : `${filtered.length} alert${filtered.length !== 1 ? "s" : ""} · Click any row for AI explanation`}
364
- </p>
365
  </div>
366
  <div className="flex items-center gap-1 rounded-xl border border-white/10 bg-white/5 p-1">
367
  {(["all", "high", "pending"] as const).map((f) => (
368
- <button key={f} onClick={() => setFilter(f)}
369
- className={`rounded-lg px-3 py-1.5 text-xs font-medium capitalize transition-all ${filter === f ? "bg-white/10 text-white" : "text-zinc-500 hover:text-zinc-300"}`}>
370
  {f === "all" ? "All" : f === "high" ? "High Risk" : "Pending"}
371
  </button>
372
  ))}
@@ -374,25 +273,15 @@ export default function SecurityPage() {
374
  </div>
375
 
376
  {loading ? (
377
- <div className="space-y-px p-4">
378
- {[1, 2, 3].map((i) => <Skeleton key={i} className="h-16 w-full rounded-xl" />)}
379
- </div>
380
  ) : filtered.length === 0 ? (
381
  <div className="flex flex-col items-center gap-3 py-16 text-center">
382
  <CheckCircle className="h-12 w-12 text-emerald-500/40" />
383
- <p className="text-sm font-medium text-zinc-400">
384
- {alerts.length === 0 ? "No fraud alerts detected" : "No alerts match this filter"}
385
- </p>
386
- <p className="text-xs text-zinc-600">
387
- {alerts.length === 0 ? "All transactions look clean. AI Shield is monitoring in real-time." : "Try a different filter."}
388
- </p>
389
  </div>
390
  ) : (
391
- <div>
392
- {filtered.map((alert, i) => (
393
- <AlertRow key={alert.id} alert={alert} index={i} />
394
- ))}
395
- </div>
396
  )}
397
  </motion.div>
398
 
@@ -400,378 +289,7 @@ export default function SecurityPage() {
400
  <motion.div variants={itemVariants} className="flex items-center gap-3 rounded-2xl border border-emerald-500/20 bg-emerald-500/5 px-5 py-3">
401
  <CheckCircle className="h-4 w-4 text-emerald-400 flex-shrink-0" />
402
  <p className="text-xs text-zinc-400">
403
- <span className="text-emerald-400 font-medium">{safeCount} transaction{safeCount !== 1 ? "s" : ""} verified safe</span>
404
- {" "}— risk score below threshold. No action required.
405
- </p>
406
- </motion.div>
407
- )}
408
- </motion.div>
409
- );
410
- }
411
- import {
412
- Shield, AlertTriangle, CheckCircle, RefreshCw,
413
- TrendingUp, Clock, Zap, Eye, XCircle, Activity
414
- } from "lucide-react";
415
- import { aiApi } from "@/lib/api";
416
- import { useAuthStore } from "@/lib/stores/authStore";
417
-
418
- interface FraudAlert {
419
- id: string;
420
- risk_score: number;
421
- details: string;
422
- status?: string;
423
- transaction_id?: string;
424
- merchant?: string;
425
- amount?: number;
426
- timestamp?: string;
427
- }
428
-
429
- interface FraudData {
430
- total_alerts: number;
431
- pending_reviews: number;
432
- alerts: FraudAlert[];
433
- }
434
-
435
- function Skeleton({ className }: { className?: string }) {
436
- return <div className={`shimmer rounded-lg ${className}`} />;
437
- }
438
-
439
- function RiskBadge({ score }: { score: number }) {
440
- const level =
441
- score >= 70 ? { label: "High Risk", color: "text-red-400", bg: "bg-red-500/10 border-red-500/20" }
442
- : score >= 40 ? { label: "Suspicious", color: "text-amber-400", bg: "bg-amber-500/10 border-amber-500/20" }
443
- : { label: "Low Risk", color: "text-emerald-400", bg: "bg-emerald-500/10 border-emerald-500/20" };
444
-
445
- return (
446
- <span className={`rounded-full border px-2.5 py-1 text-xs font-semibold ${level.color} ${level.bg}`}>
447
- {level.label}
448
- </span>
449
- );
450
- }
451
-
452
- function RiskBar({ score }: { score: number }) {
453
- const color = score >= 70 ? "#ef4444" : score >= 40 ? "#f59e0b" : "#10b981";
454
- return (
455
- <div className="flex items-center gap-2">
456
- <div className="h-1.5 w-24 rounded-full bg-white/10 overflow-hidden">
457
- <motion.div
458
- initial={{ width: 0 }}
459
- animate={{ width: `${score}%` }}
460
- transition={{ duration: 0.8, ease: "easeOut" }}
461
- className="h-full rounded-full"
462
- style={{ background: color }}
463
- />
464
- </div>
465
- <span className="text-xs font-mono" style={{ color }}>{score}</span>
466
- </div>
467
- );
468
- }
469
-
470
- const containerVariants = {
471
- hidden: { opacity: 0 },
472
- visible: { opacity: 1, transition: { staggerChildren: 0.07 } },
473
- };
474
- const itemVariants = {
475
- hidden: { opacity: 0, y: 16 },
476
- visible: { opacity: 1, y: 0, transition: { duration: 0.4 } },
477
- };
478
-
479
- export default function SecurityPage() {
480
- const { user } = useAuthStore();
481
- const [data, setData] = useState<FraudData | null>(null);
482
- const [loading, setLoading] = useState(true);
483
- const [error, setError] = useState<string | null>(null);
484
- const [filter, setFilter] = useState<"all" | "high" | "pending">("all");
485
-
486
- const load = useCallback(async () => {
487
- setLoading(true);
488
- setError(null);
489
- try {
490
- const res = await aiApi.fraudAnalysis(user?.user_id);
491
- // Normalize response shape
492
- const raw = res as unknown as Record<string, unknown>;
493
- const alerts: FraudAlert[] = (raw.alerts as FraudAlert[]) || [];
494
- setData({
495
- total_alerts: (raw.total_alerts as number) ?? alerts.length,
496
- pending_reviews: (raw.pending_reviews as number) ?? alerts.filter((a) => a.status === "pending").length,
497
- alerts,
498
- });
499
- } catch (err) {
500
- setError((err as Error).message);
501
- } finally {
502
- setLoading(false);
503
- }
504
- }, [user?.user_id]);
505
-
506
- useEffect(() => { load(); }, [load]);
507
-
508
- const alerts = data?.alerts || [];
509
- const filtered = alerts.filter((a) => {
510
- if (filter === "high") return a.risk_score >= 70;
511
- if (filter === "pending") return a.status === "pending" || !a.status;
512
- return true;
513
- });
514
-
515
- const highRiskCount = alerts.filter((a) => a.risk_score >= 70).length;
516
- const suspiciousCount = alerts.filter((a) => a.risk_score >= 40 && a.risk_score < 70).length;
517
- const safeCount = alerts.filter((a) => a.risk_score < 40).length;
518
- const avgScore = alerts.length
519
- ? Math.round(alerts.reduce((s, a) => s + a.risk_score, 0) / alerts.length)
520
- : 0;
521
-
522
- return (
523
- <motion.div
524
- variants={containerVariants}
525
- initial="hidden"
526
- animate="visible"
527
- className="flex flex-col gap-6"
528
- >
529
- {/* Header */}
530
- <motion.div variants={itemVariants} className="flex items-center justify-between">
531
- <div>
532
- <h1 className="text-3xl font-bold tracking-tight text-white">Security Center</h1>
533
- <p className="text-zinc-400 mt-1 text-sm">
534
- AI-powered fraud detection and transaction monitoring
535
- </p>
536
- </div>
537
- <button
538
- onClick={load}
539
- className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-400 hover:text-white transition-colors"
540
- >
541
- <RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} />
542
- Refresh
543
- </button>
544
- </motion.div>
545
-
546
- {/* Error */}
547
- {error && (
548
- <motion.div variants={itemVariants} className="flex items-center gap-3 rounded-2xl border border-amber-500/20 bg-amber-500/5 px-5 py-3">
549
- <AlertTriangle className="h-4 w-4 text-amber-400 flex-shrink-0" />
550
- <p className="text-xs text-zinc-400">
551
- <span className="text-amber-400 font-medium">Could not load fraud data</span> — {error}
552
- </p>
553
- </motion.div>
554
- )}
555
-
556
- {/* AI Shield Banner */}
557
- <motion.div
558
- variants={itemVariants}
559
- className="flex items-center gap-4 rounded-2xl border border-emerald-500/20 bg-gradient-to-r from-emerald-500/10 via-cyan-500/5 to-blue-500/10 px-5 py-4"
560
- >
561
- <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex-shrink-0">
562
- <Shield className="h-5 w-5 text-emerald-400" />
563
- </div>
564
- <div className="flex-1">
565
- <p className="text-sm font-semibold text-white">AI Fraud Shield Active</p>
566
- <p className="text-xs text-zinc-400 mt-0.5">
567
- Monitoring amount spikes, late-night activity, rapid transactions, and duplicate payments in real-time.
568
- </p>
569
- </div>
570
- <div className="flex items-center gap-1.5">
571
- <div className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
572
- <span className="text-xs text-emerald-400 font-medium">Live</span>
573
- </div>
574
- </motion.div>
575
-
576
- {/* Stats */}
577
- <div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
578
- {[
579
- {
580
- label: "Total Alerts",
581
- value: loading ? "—" : String(data?.total_alerts ?? 0),
582
- icon: Activity,
583
- color: "text-blue-400",
584
- border: "border-blue-500/20",
585
- bg: "from-blue-500/10 to-blue-500/5",
586
- },
587
- {
588
- label: "High Risk",
589
- value: loading ? "—" : String(highRiskCount),
590
- icon: XCircle,
591
- color: "text-red-400",
592
- border: "border-red-500/20",
593
- bg: "from-red-500/10 to-red-500/5",
594
- },
595
- {
596
- label: "Suspicious",
597
- value: loading ? "—" : String(suspiciousCount),
598
- icon: AlertTriangle,
599
- color: "text-amber-400",
600
- border: "border-amber-500/20",
601
- bg: "from-amber-500/10 to-amber-500/5",
602
- },
603
- {
604
- label: "Avg Risk Score",
605
- value: loading ? "—" : `${avgScore}/100`,
606
- icon: TrendingUp,
607
- color: "text-purple-400",
608
- border: "border-purple-500/20",
609
- bg: "from-purple-500/10 to-purple-500/5",
610
- },
611
- ].map((stat) => (
612
- <motion.div
613
- key={stat.label}
614
- variants={itemVariants}
615
- className={`rounded-2xl border ${stat.border} bg-gradient-to-br ${stat.bg} p-5`}
616
- >
617
- <div className="flex items-start justify-between">
618
- <div>
619
- <p className="text-xs text-zinc-400 uppercase tracking-wider">{stat.label}</p>
620
- <p className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</p>
621
- </div>
622
- <stat.icon className={`h-5 w-5 ${stat.color} opacity-60`} />
623
- </div>
624
- </motion.div>
625
- ))}
626
- </div>
627
-
628
- {/* Detection Rules */}
629
- <motion.div variants={itemVariants} className="glass-card p-6">
630
- <h2 className="text-base font-semibold text-white mb-4">Active Detection Rules</h2>
631
- <div className="grid gap-3 sm:grid-cols-2">
632
- {[
633
- { icon: TrendingUp, label: "Amount Spike Detection", desc: "Flags transactions 3.5× above your average", active: true },
634
- { icon: Clock, label: "Late-Night Monitoring", desc: "Alerts on activity between 11PM – 4AM", active: true },
635
- { icon: Zap, label: "Rapid Transaction Guard", desc: "Detects multiple transactions within 3 minutes", active: true },
636
- { icon: Eye, label: "Duplicate Payment Check", desc: "Catches same merchant + amount within 10 minutes", active: true },
637
- ].map((rule) => (
638
- <div key={rule.label} className="flex items-start gap-3 rounded-xl border border-white/8 bg-white/[0.02] p-4">
639
- <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex-shrink-0">
640
- <rule.icon className="h-4 w-4 text-emerald-400" />
641
- </div>
642
- <div className="flex-1 min-w-0">
643
- <div className="flex items-center justify-between gap-2">
644
- <p className="text-sm font-medium text-white">{rule.label}</p>
645
- <div className="flex items-center gap-1 flex-shrink-0">
646
- <div className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
647
- <span className="text-[10px] text-emerald-400">ON</span>
648
- </div>
649
- </div>
650
- <p className="text-xs text-zinc-500 mt-0.5">{rule.desc}</p>
651
- </div>
652
- </div>
653
- ))}
654
- </div>
655
- </motion.div>
656
-
657
- {/* Alerts Table */}
658
- <motion.div variants={itemVariants} className="glass-card overflow-hidden">
659
- <div className="flex items-center justify-between px-6 py-4 border-b border-white/8">
660
- <div>
661
- <h2 className="text-base font-semibold text-white">Fraud Alerts</h2>
662
- <p className="text-xs text-zinc-400 mt-0.5">
663
- {loading ? "Loading..." : `${filtered.length} alert${filtered.length !== 1 ? "s" : ""}`}
664
- </p>
665
- </div>
666
- <div className="flex items-center gap-1 rounded-xl border border-white/10 bg-white/5 p-1">
667
- {(["all", "high", "pending"] as const).map((f) => (
668
- <button
669
- key={f}
670
- onClick={() => setFilter(f)}
671
- className={`rounded-lg px-3 py-1.5 text-xs font-medium capitalize transition-all ${
672
- filter === f ? "bg-white/10 text-white" : "text-zinc-500 hover:text-zinc-300"
673
- }`}
674
- >
675
- {f === "all" ? "All" : f === "high" ? "High Risk" : "Pending"}
676
- </button>
677
- ))}
678
- </div>
679
- </div>
680
-
681
- {loading ? (
682
- <div className="space-y-px p-4">
683
- {[1, 2, 3].map((i) => <Skeleton key={i} className="h-16 w-full rounded-xl" />)}
684
- </div>
685
- ) : filtered.length === 0 ? (
686
- <div className="flex flex-col items-center gap-3 py-16 text-center">
687
- <CheckCircle className="h-12 w-12 text-emerald-500/40" />
688
- <p className="text-sm font-medium text-zinc-400">
689
- {alerts.length === 0 ? "No fraud alerts detected" : "No alerts match this filter"}
690
- </p>
691
- <p className="text-xs text-zinc-600">
692
- {alerts.length === 0
693
- ? "All your transactions look clean. AI Shield is monitoring in real-time."
694
- : "Try a different filter to see more alerts."}
695
- </p>
696
- </div>
697
- ) : (
698
- <div className="divide-y divide-white/5">
699
- {filtered.map((alert, i) => (
700
- <motion.div
701
- key={alert.id}
702
- initial={{ opacity: 0, x: -8 }}
703
- animate={{ opacity: 1, x: 0 }}
704
- transition={{ delay: i * 0.04 }}
705
- className="flex items-center gap-4 px-6 py-4 hover:bg-white/[0.02] transition-colors"
706
- >
707
- {/* Risk indicator */}
708
- <div
709
- className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl border ${
710
- alert.risk_score >= 70
711
- ? "bg-red-500/10 border-red-500/20"
712
- : alert.risk_score >= 40
713
- ? "bg-amber-500/10 border-amber-500/20"
714
- : "bg-emerald-500/10 border-emerald-500/20"
715
- }`}
716
- >
717
- {alert.risk_score >= 70 ? (
718
- <XCircle className="h-5 w-5 text-red-400" />
719
- ) : alert.risk_score >= 40 ? (
720
- <AlertTriangle className="h-5 w-5 text-amber-400" />
721
- ) : (
722
- <CheckCircle className="h-5 w-5 text-emerald-400" />
723
- )}
724
- </div>
725
-
726
- {/* Details */}
727
- <div className="flex-1 min-w-0">
728
- <div className="flex items-center gap-2 flex-wrap">
729
- <p className="text-sm font-medium text-white truncate">
730
- {alert.merchant || `Transaction ${alert.transaction_id?.slice(0, 8) || alert.id.slice(0, 8)}...`}
731
- </p>
732
- <RiskBadge score={alert.risk_score} />
733
- {alert.status && (
734
- <span className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-zinc-400 capitalize">
735
- {alert.status}
736
- </span>
737
- )}
738
- </div>
739
- <p className="text-xs text-zinc-500 mt-0.5 truncate">
740
- {typeof alert.details === "string"
741
- ? alert.details
742
- : Array.isArray(alert.details)
743
- ? (alert.details as string[]).join(" · ")
744
- : "Suspicious activity detected"}
745
- </p>
746
- </div>
747
-
748
- {/* Score bar */}
749
- <div className="flex-shrink-0 hidden sm:block">
750
- <RiskBar score={alert.risk_score} />
751
- </div>
752
-
753
- {/* Amount */}
754
- {alert.amount != null && (
755
- <div className="flex-shrink-0 text-right">
756
- <p className="text-sm font-semibold text-white">${Math.abs(alert.amount).toFixed(2)}</p>
757
- </div>
758
- )}
759
- </motion.div>
760
- ))}
761
- </div>
762
- )}
763
- </motion.div>
764
-
765
- {/* Safe status */}
766
- {!loading && safeCount > 0 && (
767
- <motion.div
768
- variants={itemVariants}
769
- className="flex items-center gap-3 rounded-2xl border border-emerald-500/20 bg-emerald-500/5 px-5 py-3"
770
- >
771
- <CheckCircle className="h-4 w-4 text-emerald-400 flex-shrink-0" />
772
- <p className="text-xs text-zinc-400">
773
- <span className="text-emerald-400 font-medium">{safeCount} transaction{safeCount !== 1 ? "s" : ""} verified safe</span>
774
- {" "}— risk score below threshold. No action required.
775
  </p>
776
  </motion.div>
777
  )}
 
56
  return (
57
  <div className="flex items-center gap-2">
58
  <div className="h-1.5 w-24 rounded-full bg-white/10 overflow-hidden">
59
+ <motion.div initial={{ width: 0 }} animate={{ width: `${score}%` }} transition={{ duration: 0.8, ease: "easeOut" }} className="h-full rounded-full" style={{ background: color }} />
 
 
 
 
 
 
60
  </div>
61
  <span className="text-xs font-mono" style={{ color }}>{score}</span>
62
  </div>
63
  );
64
  }
65
 
 
66
  function AlertRow({ alert, index }: { alert: FraudAlert; index: number }) {
67
  const [expanded, setExpanded] = useState(false);
68
  const [explanation, setExplanation] = useState<FraudExplanation | null>(null);
 
89
  };
90
 
91
  return (
92
+ <motion.div initial={{ opacity: 0, x: -8 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: index * 0.04 }}
93
+ className={`border-b border-white/5 last:border-0 ${alert.risk_score >= 70 ? "bg-red-500/2" : ""}`}>
94
+ <div className="flex items-center gap-4 px-6 py-4 hover:bg-white/[0.02] transition-colors cursor-pointer" onClick={loadExplanation}>
95
+ <div className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl border ${alert.risk_score >= 70 ? "bg-red-500/10 border-red-500/20" : alert.risk_score >= 40 ? "bg-amber-500/10 border-amber-500/20" : "bg-emerald-500/10 border-emerald-500/20"}`}>
96
+ {alert.risk_score >= 70 ? <XCircle className="h-5 w-5 text-red-400" /> : alert.risk_score >= 40 ? <AlertTriangle className="h-5 w-5 text-amber-400" /> : <CheckCircle className="h-5 w-5 text-emerald-400" />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  </div>
 
98
  <div className="flex-1 min-w-0">
99
  <div className="flex items-center gap-2 flex-wrap">
100
+ <p className="text-sm font-medium text-white truncate">{alert.merchant || `Transaction ${(alert.transaction_id || alert.id).slice(0, 8)}...`}</p>
 
 
101
  <RiskBadge score={alert.risk_score} />
102
+ {alert.status && <span className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-zinc-400 capitalize">{alert.status}</span>}
 
 
 
 
103
  </div>
104
  <p className="text-xs text-zinc-500 mt-0.5 truncate">
105
+ {typeof alert.details === "string" ? alert.details : Array.isArray(alert.details) ? (alert.details as string[]).join(" · ") : "Suspicious activity detected"}
 
 
106
  </p>
107
  </div>
108
+ <div className="flex-shrink-0 hidden sm:block"><RiskBar score={alert.risk_score} /></div>
109
+ {alert.amount != null && <div className="flex-shrink-0 text-right"><p className="text-sm font-semibold text-white">${Math.abs(alert.amount).toFixed(2)}</p></div>}
 
 
 
 
 
 
 
 
 
110
  <div className="flex-shrink-0 flex items-center gap-1 text-zinc-500">
111
  <Sparkles className="h-3.5 w-3.5 text-cyan-400" />
112
  {expanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
113
  </div>
114
  </div>
115
 
 
116
  <AnimatePresence>
117
  {expanded && (
118
+ <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25 }} className="overflow-hidden">
 
 
 
 
 
 
119
  <div className="mx-6 mb-4 rounded-xl border border-cyan-500/20 bg-cyan-500/5 p-4">
120
  <div className="flex items-start gap-2">
121
  <Sparkles className="h-4 w-4 text-cyan-400 flex-shrink-0 mt-0.5" />
 
142
  )}
143
  {explanation.amount_vs_avg > 0 && (
144
  <div className="flex items-center gap-4 pt-1 border-t border-white/8">
145
+ <div><p className="text-[10px] text-zinc-500">This transaction</p><p className="text-sm font-bold text-white">${alert.amount?.toFixed(2)}</p></div>
 
 
 
146
  <div className="text-zinc-600">vs</div>
147
+ <div><p className="text-[10px] text-zinc-500">Your average</p><p className="text-sm font-bold text-zinc-300">${explanation.user_avg_amount.toFixed(2)}</p></div>
148
+ <div className="ml-auto"><span className="rounded-full bg-red-500/15 border border-red-500/30 px-2 py-0.5 text-xs font-bold text-red-400">{explanation.amount_vs_avg.toFixed(1)}× average</span></div>
 
 
 
 
 
 
 
149
  </div>
150
  )}
151
  </div>
 
160
  );
161
  }
162
 
163
+ const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.07 } } };
164
+ const itemVariants = { hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0, transition: { duration: 0.4 } } };
 
 
 
 
 
 
165
 
166
  export default function SecurityPage() {
167
  const { user } = useAuthStore();
 
171
  const [filter, setFilter] = useState<"all" | "high" | "pending">("all");
172
 
173
  const load = useCallback(async () => {
174
+ setLoading(true); setError(null);
 
175
  try {
176
  const res = await aiApi.fraudAnalysis(user?.user_id);
177
  const raw = res as unknown as Record<string, unknown>;
178
  const alerts: FraudAlert[] = (raw.alerts as FraudAlert[]) || [];
179
+ setData({ total_alerts: (raw.total_alerts as number) ?? alerts.length, pending_reviews: (raw.pending_reviews as number) ?? alerts.filter((a) => a.status === "pending").length, alerts });
180
+ } catch (err) { setError((err as Error).message); }
181
+ finally { setLoading(false); }
 
 
 
 
 
 
 
182
  }, [user?.user_id]);
183
 
184
  useEffect(() => { load(); }, [load]);
185
 
186
  const alerts = data?.alerts || [];
187
+ const filtered = alerts.filter((a) => filter === "high" ? a.risk_score >= 70 : filter === "pending" ? a.status === "pending" || !a.status : true);
 
 
 
 
 
188
  const highRiskCount = alerts.filter((a) => a.risk_score >= 70).length;
189
  const suspiciousCount = alerts.filter((a) => a.risk_score >= 40 && a.risk_score < 70).length;
190
  const safeCount = alerts.filter((a) => a.risk_score < 40).length;
191
+ const avgScore = alerts.length ? Math.round(alerts.reduce((s, a) => s + a.risk_score, 0) / alerts.length) : 0;
 
 
192
 
193
  return (
194
  <motion.div variants={containerVariants} initial="hidden" animate="visible" className="flex flex-col gap-6">
 
195
  <motion.div variants={itemVariants} className="flex items-center justify-between">
196
  <div>
197
  <h1 className="text-3xl font-bold tracking-tight text-white">Security Center</h1>
198
  <p className="text-zinc-400 mt-1 text-sm">AI-powered fraud detection · Click any alert for a full AI explanation</p>
199
  </div>
200
  <button onClick={load} className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-400 hover:text-white transition-colors">
201
+ <RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} />Refresh
 
202
  </button>
203
  </motion.div>
204
 
 
209
  </motion.div>
210
  )}
211
 
 
212
  <motion.div variants={itemVariants} className="flex items-center gap-4 rounded-2xl border border-emerald-500/20 bg-gradient-to-r from-emerald-500/10 via-cyan-500/5 to-blue-500/10 px-5 py-4">
213
+ <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex-shrink-0"><Shield className="h-5 w-5 text-emerald-400" /></div>
 
 
214
  <div className="flex-1">
215
  <p className="text-sm font-semibold text-white">AI Fraud Shield Active</p>
216
+ <p className="text-xs text-zinc-400 mt-0.5">Click any flagged transaction to get a full AI explanation of exactly why it was flagged.</p>
 
 
 
 
 
 
217
  </div>
218
+ <div className="flex items-center gap-1.5"><div className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" /><span className="text-xs text-emerald-400 font-medium">Live</span></div>
219
  </motion.div>
220
 
 
221
  <div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
222
  {[
223
  { label: "Total Alerts", value: loading ? "—" : String(data?.total_alerts ?? 0), icon: Activity, color: "text-blue-400", border: "border-blue-500/20", bg: "from-blue-500/10 to-blue-500/5" },
 
227
  ].map((stat) => (
228
  <motion.div key={stat.label} variants={itemVariants} className={`rounded-2xl border ${stat.border} bg-gradient-to-br ${stat.bg} p-5`}>
229
  <div className="flex items-start justify-between">
230
+ <div><p className="text-xs text-zinc-400 uppercase tracking-wider">{stat.label}</p><p className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</p></div>
 
 
 
231
  <stat.icon className={`h-5 w-5 ${stat.color} opacity-60`} />
232
  </div>
233
  </motion.div>
234
  ))}
235
  </div>
236
 
 
237
  <motion.div variants={itemVariants} className="glass-card p-6">
238
  <h2 className="text-base font-semibold text-white mb-4">Active Detection Rules</h2>
239
  <div className="grid gap-3 sm:grid-cols-2">
 
244
  { icon: Eye, label: "Duplicate Payment Check", desc: "Catches same merchant + amount within 10 minutes" },
245
  ].map((rule) => (
246
  <div key={rule.label} className="flex items-start gap-3 rounded-xl border border-white/8 bg-white/[0.02] p-4">
247
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex-shrink-0"><rule.icon className="h-4 w-4 text-emerald-400" /></div>
 
 
248
  <div className="flex-1 min-w-0">
249
  <div className="flex items-center justify-between gap-2">
250
  <p className="text-sm font-medium text-white">{rule.label}</p>
251
+ <div className="flex items-center gap-1 flex-shrink-0"><div className="h-1.5 w-1.5 rounded-full bg-emerald-400" /><span className="text-[10px] text-emerald-400">ON</span></div>
 
 
 
252
  </div>
253
  <p className="text-xs text-zinc-500 mt-0.5">{rule.desc}</p>
254
  </div>
 
257
  </div>
258
  </motion.div>
259
 
 
260
  <motion.div variants={itemVariants} className="glass-card overflow-hidden">
261
  <div className="flex items-center justify-between px-6 py-4 border-b border-white/8">
262
  <div>
263
  <h2 className="text-base font-semibold text-white">Fraud Alerts</h2>
264
+ <p className="text-xs text-zinc-400 mt-0.5">{loading ? "Loading..." : `${filtered.length} alert${filtered.length !== 1 ? "s" : ""} · Click any row for AI explanation`}</p>
 
 
265
  </div>
266
  <div className="flex items-center gap-1 rounded-xl border border-white/10 bg-white/5 p-1">
267
  {(["all", "high", "pending"] as const).map((f) => (
268
+ <button key={f} onClick={() => setFilter(f)} className={`rounded-lg px-3 py-1.5 text-xs font-medium capitalize transition-all ${filter === f ? "bg-white/10 text-white" : "text-zinc-500 hover:text-zinc-300"}`}>
 
269
  {f === "all" ? "All" : f === "high" ? "High Risk" : "Pending"}
270
  </button>
271
  ))}
 
273
  </div>
274
 
275
  {loading ? (
276
+ <div className="space-y-px p-4">{[1,2,3].map((i) => <Skeleton key={i} className="h-16 w-full rounded-xl" />)}</div>
 
 
277
  ) : filtered.length === 0 ? (
278
  <div className="flex flex-col items-center gap-3 py-16 text-center">
279
  <CheckCircle className="h-12 w-12 text-emerald-500/40" />
280
+ <p className="text-sm font-medium text-zinc-400">{alerts.length === 0 ? "No fraud alerts detected" : "No alerts match this filter"}</p>
281
+ <p className="text-xs text-zinc-600">{alerts.length === 0 ? "All transactions look clean. AI Shield is monitoring in real-time." : "Try a different filter."}</p>
 
 
 
 
282
  </div>
283
  ) : (
284
+ <div>{filtered.map((alert, i) => <AlertRow key={alert.id} alert={alert} index={i} />)}</div>
 
 
 
 
285
  )}
286
  </motion.div>
287
 
 
289
  <motion.div variants={itemVariants} className="flex items-center gap-3 rounded-2xl border border-emerald-500/20 bg-emerald-500/5 px-5 py-3">
290
  <CheckCircle className="h-4 w-4 text-emerald-400 flex-shrink-0" />
291
  <p className="text-xs text-zinc-400">
292
+ <span className="text-emerald-400 font-medium">{safeCount} transaction{safeCount !== 1 ? "s" : ""} verified safe</span>{" "}— risk score below threshold.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  </p>
294
  </motion.div>
295
  )}