Files

745 lines
33 KiB
TypeScript
Raw Permalink Normal View History

"use client";
import React, { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import {
Lock,
Eye,
EyeOff,
Calendar,
Clock,
Sparkles,
ExternalLink,
Download,
RefreshCw,
AlertTriangle,
ShieldAlert,
CheckCircle2,
XCircle,
BarChart3,
Layers,
LogOut,
ArrowLeft,
Cpu,
Image as ImageIcon,
Activity,
Filter,
} from "lucide-react";
import { StatsSummary } from "@/lib/storage";
export default function PortalPage() {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [authError, setAuthError] = useState("");
const [isLoggingIn, setIsLoggingIn] = useState(false);
// Filter State
const [period, setPeriod] = useState<"day" | "week" | "month" | "all" | "custom">("week");
const [fromDate, setFromDate] = useState("");
const [toDate, setToDate] = useState("");
const [selectedStyleFilter, setSelectedStyleFilter] = useState("all");
// Dashboard Data State
const [stats, setStats] = useState<StatsSummary | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState<"gallery" | "errors">("gallery");
// Check initial authentication
const checkAuth = useCallback(async () => {
try {
const res = await fetch("/api/portal/auth");
if (res.ok) {
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
} catch {
setIsAuthenticated(false);
}
}, []);
useEffect(() => {
checkAuth();
}, [checkAuth]);
// Fetch statistics
const fetchStats = useCallback(async () => {
if (!isAuthenticated) return;
setIsLoading(true);
try {
let url = `/api/portal/stats?period=${period}`;
if (period === "custom") {
if (fromDate) url += `&from=${fromDate}`;
if (toDate) url += `&to=${toDate}`;
}
const res = await fetch(url);
if (res.status === 401) {
setIsAuthenticated(false);
return;
}
const data = await res.json();
if (data.success && data.stats) {
setStats(data.stats);
}
} catch (err) {
console.error("Failed to fetch stats:", err);
} finally {
setIsLoading(false);
}
}, [isAuthenticated, period, fromDate, toDate]);
useEffect(() => {
if (isAuthenticated) {
fetchStats();
}
}, [isAuthenticated, fetchStats]);
// Handle Login
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setAuthError("");
setIsLoggingIn(true);
try {
const res = await fetch("/api/portal/auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password }),
});
const data = await res.json();
if (res.ok && data.success) {
setIsAuthenticated(true);
setPassword("");
} else {
setAuthError(data.error || "Incorrect password");
}
} catch {
setAuthError("Network error. Please try again.");
} finally {
setIsLoggingIn(false);
}
};
// Handle Logout
const handleLogout = async () => {
await fetch("/api/portal/auth", { method: "DELETE" });
setIsAuthenticated(false);
setStats(null);
};
// Filter gallery images by selected style dropdown
const displayedImages = (stats?.images || []).filter((img) => {
if (selectedStyleFilter === "all") return true;
return img.styleId === selectedStyleFilter;
});
// Loading Initial Auth State
if (isAuthenticated === null) {
return (
<div className="min-h-screen bg-[#0d0716] flex items-center justify-center text-purple-200">
<div className="flex items-center space-x-3">
<RefreshCw className="w-6 h-6 animate-spin text-orange-500" />
<span className="font-semibold text-lg">Authenticating...</span>
</div>
</div>
);
}
// Login Screen
if (!isAuthenticated) {
return (
<div className="fixed inset-0 overflow-y-auto bg-gradient-to-br from-[#0c0617] via-[#150a26] to-[#0c0617] flex items-center justify-center p-4">
<div className="w-full max-w-md bg-[#1a0e30]/90 backdrop-blur-xl border border-purple-900/40 rounded-3xl p-8 shadow-[0_20px_60px_-15px_rgba(0,0,0,0.8)] flex flex-col items-center text-center">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-tr from-orange-600 to-amber-400 p-0.5 shadow-lg shadow-orange-500/30 flex items-center justify-center mb-6">
<div className="w-full h-full bg-[#160b2b] rounded-2xl flex items-center justify-center">
<Lock className="w-8 h-8 text-orange-400" />
</div>
</div>
<h1 className="text-2xl font-black text-white tracking-wide">
万圣空手 · 管理后台
</h1>
<p className="text-sm text-purple-300/80 mt-1 mb-6">
上海健志空手道馆 · Halloween Management & Gallery
</p>
<form onSubmit={handleLogin} className="w-full space-y-4">
<div className="relative text-left">
<input
type={showPassword ? "text" : "password"}
placeholder="Enter password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoFocus
className="w-full px-4 py-3.5 bg-[#0e071c] border border-purple-800/50 rounded-2xl text-white placeholder-purple-400/40 focus:outline-none focus:border-orange-500 focus:ring-2 focus:ring-orange-500/20 text-sm font-mono pr-12 transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-3.5 text-purple-400 hover:text-white transition-colors"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
{authError && (
<div className="p-3 bg-red-950/60 border border-red-800/60 rounded-xl text-red-300 text-xs font-semibold flex items-center justify-center space-x-2 animate-shake">
<AlertTriangle size={14} />
<span>{authError}</span>
</div>
)}
<button
type="submit"
disabled={isLoggingIn || !password}
className="w-full py-3.5 px-4 bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 text-black font-extrabold text-sm rounded-2xl transition-all shadow-lg shadow-orange-500/25 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
>
{isLoggingIn ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
<span>Unlocking...</span>
</>
) : (
<span>Access Portal</span>
)}
</button>
</form>
<div className="mt-6 pt-4 border-t border-purple-900/30 w-full">
<Link
href="/"
className="text-xs text-purple-400 hover:text-white flex items-center justify-center space-x-1 transition-colors"
>
<ArrowLeft size={14} />
<span>Back to Photo Booth</span>
</Link>
</div>
</div>
</div>
);
}
// Main Portal Dashboard
return (
<div className="fixed inset-0 overflow-y-auto overflow-x-hidden bg-[#0c0617] text-white flex flex-col font-sans">
{/* Top Navigation Bar */}
<header className="sticky top-0 z-30 flex-shrink-0 bg-[#140a27]/95 backdrop-blur-md border-b border-purple-900/40 px-4 sm:px-8 py-3.5 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-orange-500 to-amber-400 p-0.5 shadow-md flex items-center justify-center">
<div className="w-full h-full bg-[#180d2f] rounded-[10px] flex items-center justify-center">
<Sparkles className="w-5 h-5 text-orange-400" />
</div>
</div>
<div>
<h1 className="text-base sm:text-lg font-black tracking-wide flex items-center space-x-2">
<span>万圣空手道馆 · Spooky Hand</span>
<span className="text-[10px] px-2 py-0.5 bg-orange-500/20 border border-orange-500/40 text-orange-300 rounded-full font-bold uppercase tracking-wider">
Portal
</span>
</h1>
<p className="text-[11px] text-purple-300/70">
上海健志空手道馆 · Live statistics, error tracking & gallery
</p>
</div>
</div>
<div className="flex items-center space-x-2 sm:space-x-3">
<button
onClick={() => fetchStats()}
disabled={isLoading}
title="Refresh statistics"
className="p-2.5 bg-purple-900/30 hover:bg-purple-800/40 border border-purple-700/40 rounded-xl text-purple-300 hover:text-white transition-all disabled:opacity-50"
>
<RefreshCw size={16} className={isLoading ? "animate-spin" : ""} />
</button>
<Link
href="/"
className="px-3.5 py-2 bg-purple-900/30 hover:bg-purple-800/40 border border-purple-700/40 rounded-xl text-purple-200 hover:text-white text-xs font-bold transition-all flex items-center space-x-1.5"
>
<ArrowLeft size={14} />
<span className="hidden sm:inline">Photo Booth</span>
</Link>
<button
onClick={handleLogout}
className="px-3.5 py-2 bg-red-950/40 hover:bg-red-900/60 border border-red-800/50 rounded-xl text-red-300 hover:text-white text-xs font-bold transition-all flex items-center space-x-1.5"
>
<LogOut size={14} />
<span>Logout</span>
</button>
</div>
</header>
{/* Main Container */}
<main className="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 lg:p-8 space-y-6 pb-24">
{/* Period Filter Bar */}
<section className="bg-[#150a29]/80 border border-purple-900/40 rounded-2xl p-4 shadow-lg flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center space-x-2">
<Calendar size={16} className="text-orange-400" />
<span className="text-xs font-bold text-purple-200 uppercase tracking-wider">
Time Period:
</span>
</div>
<div className="flex flex-wrap items-center gap-1.5">
{(
[
{ id: "day", label: "Today" },
{ id: "week", label: "Last 7 Days" },
{ id: "month", label: "Last 30 Days" },
{ id: "all", label: "All Time" },
{ id: "custom", label: "Custom Range" },
] as const
).map((item) => (
<button
key={item.id}
onClick={() => setPeriod(item.id)}
className={`px-3 py-1.5 rounded-xl text-xs font-bold transition-all ${
period === item.id
? "bg-gradient-to-r from-orange-500 to-amber-500 text-black shadow-md shadow-orange-500/20 font-black"
: "bg-[#100620] hover:bg-purple-900/40 text-purple-300 border border-purple-900/40"
}`}
>
{item.label}
</button>
))}
</div>
{/* Custom Date Pickers */}
{period === "custom" && (
<div className="w-full sm:w-auto flex items-center space-x-2 pt-2 sm:pt-0 border-t sm:border-t-0 border-purple-900/40">
<input
type="date"
value={fromDate}
onChange={(e) => setFromDate(e.target.value)}
className="bg-[#0e071c] border border-purple-800/60 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-orange-500"
/>
<span className="text-purple-400 text-xs">to</span>
<input
type="date"
value={toDate}
onChange={(e) => setToDate(e.target.value)}
className="bg-[#0e071c] border border-purple-800/60 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-orange-500"
/>
</div>
)}
</section>
{/* KPI Summary Cards */}
<section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Card 1: Total Generations */}
<div className="bg-[#150a29]/80 border border-purple-900/40 rounded-2xl p-5 shadow-lg relative overflow-hidden group">
<div className="flex items-center justify-between text-purple-300 mb-2">
<span className="text-xs font-bold uppercase tracking-wider">Total Generations</span>
<div className="p-2 bg-orange-500/10 rounded-xl border border-orange-500/20">
<BarChart3 className="w-4 h-4 text-orange-400" />
</div>
</div>
<div className="text-3xl font-black text-white">
{stats?.totalAttempts ?? 0}
</div>
<div className="mt-2 text-xs flex items-center space-x-2 text-purple-300/80">
<span className="text-emerald-400 font-bold flex items-center space-x-0.5">
<CheckCircle2 size={12} />
<span>{stats?.successCount ?? 0} success</span>
</span>
<span>•</span>
<span className="text-purple-400">
{stats?.successRate ?? 0}% rate
</span>
</div>
</div>
{/* Card 2: Average Generation Speed */}
<div className="bg-[#150a29]/80 border border-purple-900/40 rounded-2xl p-5 shadow-lg relative overflow-hidden group">
<div className="flex items-center justify-between text-purple-300 mb-2">
<span className="text-xs font-bold uppercase tracking-wider">Avg Latency</span>
<div className="p-2 bg-cyan-500/10 rounded-xl border border-cyan-500/20">
<Clock className="w-4 h-4 text-cyan-400" />
</div>
</div>
<div className="text-3xl font-black text-white">
{stats?.avgDurationMs ? `${(stats.avgDurationMs / 1000).toFixed(1)}s` : "0s"}
</div>
<div className="mt-2 text-xs text-purple-300/80">
End-to-end transformation time
</div>
</div>
{/* Card 3: Safety Refusals */}
<div className="bg-[#150a29]/80 border border-purple-900/40 rounded-2xl p-5 shadow-lg relative overflow-hidden group">
<div className="flex items-center justify-between text-purple-300 mb-2">
<span className="text-xs font-bold uppercase tracking-wider">Content Refusals</span>
<div className="p-2 bg-amber-500/10 rounded-xl border border-amber-500/20">
<ShieldAlert className="w-4 h-4 text-amber-400" />
</div>
</div>
<div className="text-3xl font-black text-amber-400">
{stats?.refusedCount ?? 0}
</div>
<div className="mt-2 text-xs text-purple-300/80">
Filtered by SFW / safety guardrails
</div>
</div>
{/* Card 4: Server / Provider Errors */}
<div className="bg-[#150a29]/80 border border-purple-900/40 rounded-2xl p-5 shadow-lg relative overflow-hidden group">
<div className="flex items-center justify-between text-purple-300 mb-2">
<span className="text-xs font-bold uppercase tracking-wider">Server Errors</span>
<div className="p-2 bg-red-500/10 rounded-xl border border-red-500/20">
<XCircle className="w-4 h-4 text-red-400" />
</div>
</div>
<div className="text-3xl font-black text-red-400">
{stats?.errorCount ?? 0}
</div>
<div className="mt-2 text-xs text-purple-300/80">
API timeouts & network failures
</div>
</div>
</section>
{/* Analytics Breakdown Row: Daily Trend & Disguise Popularity */}
<section className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Daily Trend Histogram */}
<div className="lg:col-span-2 bg-[#150a29]/80 border border-purple-900/40 rounded-2xl p-5 shadow-lg flex flex-col justify-between">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<Activity size={16} className="text-orange-400" />
<h3 className="text-sm font-bold uppercase tracking-wider text-purple-200">
Generations by Day
</h3>
</div>
<span className="text-xs text-purple-400 font-mono">
{stats?.dailyTrends?.length || 0} days shown
</span>
</div>
{stats?.dailyTrends && stats.dailyTrends.length > 0 ? (
<div className="h-44 flex items-end gap-2 pt-6 pb-2 overflow-x-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
{(() => {
const maxCount = Math.max(
1,
...stats.dailyTrends.map((d) => d.total)
);
return stats.dailyTrends.map((day) => {
const heightPercent = Math.max(
8,
Math.round((day.total / maxCount) * 100)
);
const formattedDate = day.date.slice(5); // MM-DD
return (
<div
key={day.date}
className="flex-1 min-w-[34px] h-full flex flex-col items-center gap-1 group relative"
title={`${day.date}: ${day.total} total (${day.success} ok, ${day.refused} refused, ${day.error} err)`}
>
{/* Count (always visible, never clipped) */}
<span className="text-[11px] font-bold font-mono text-orange-300 leading-none">
{day.total}
</span>
{/* Bar */}
<div className="w-full flex-1 min-h-0 bg-[#1e0e38] rounded-t-lg overflow-hidden flex flex-col justify-end">
<div
style={{ height: `${heightPercent}%` }}
className="w-full bg-gradient-to-t from-orange-600 to-amber-400 rounded-t-lg transition-all group-hover:brightness-125"
/>
</div>
{/* Label */}
<span className="text-[10px] text-purple-400 font-mono">
{formattedDate}
</span>
</div>
);
});
})()}
</div>
) : (
<div className="h-44 flex items-center justify-center text-purple-400/60 text-xs italic">
No generations recorded for this time range yet.
</div>
)}
</div>
{/* Popular Disguise Breakdown */}
<div className="bg-[#150a29]/80 border border-purple-900/40 rounded-2xl p-5 shadow-lg flex flex-col">
<div className="flex items-center space-x-2 mb-4">
<Layers size={16} className="text-orange-400" />
<h3 className="text-sm font-bold uppercase tracking-wider text-purple-200">
Top Disguise Styles
</h3>
</div>
<div className="flex-1 space-y-3 overflow-y-auto max-h-52 pr-1">
{stats?.styleBreakdown && stats.styleBreakdown.length > 0 ? (
stats.styleBreakdown.map((style) => (
<div key={style.styleId} className="space-y-1">
<div className="flex items-center justify-between text-xs font-semibold">
<span className="text-purple-200 truncate pr-2">
{style.styleName}
</span>
<span className="text-orange-400 font-mono">
{style.count} ({style.percentage}%)
</span>
</div>
<div className="w-full bg-[#100620] h-2 rounded-full overflow-hidden">
<div
className="bg-gradient-to-r from-orange-500 to-amber-400 h-full rounded-full transition-all"
style={{ width: `${style.percentage}%` }}
/>
</div>
</div>
))
) : (
<div className="h-32 flex items-center justify-center text-purple-400/60 text-xs italic">
No styles used in this period.
</div>
)}
</div>
{/* Provider Health Chips */}
<div className="pt-4 mt-2 border-t border-purple-900/40 flex flex-wrap gap-2">
{stats?.providerBreakdown?.map((p) => (
<span
key={`${p.provider}-${p.model}`}
className="px-2.5 py-1 bg-purple-950/60 border border-purple-800/50 rounded-lg text-[10px] font-mono text-purple-300 flex items-center space-x-1"
>
<Cpu size={12} className="text-orange-400" />
<span>{p.provider}: {p.count}</span>
</span>
))}
</div>
</div>
</section>
{/* View Tabs: Gallery vs Errors */}
<section className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-purple-900/40 pb-3">
<div className="flex items-center space-x-3">
<button
onClick={() => setActiveTab("gallery")}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-xs font-black transition-all ${
activeTab === "gallery"
? "bg-purple-800/60 text-white border border-purple-600/60 shadow-md"
: "text-purple-300 hover:text-white bg-transparent"
}`}
>
<ImageIcon size={16} />
<span>Generated Images Gallery ({stats?.images?.length || 0})</span>
</button>
<button
onClick={() => setActiveTab("errors")}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-xs font-black transition-all ${
activeTab === "errors"
? "bg-red-950/70 text-red-200 border border-red-800/70 shadow-md"
: "text-purple-300 hover:text-white bg-transparent"
}`}
>
<AlertTriangle size={16} />
<span>Error & Refusal Logs ({stats?.errorLogs?.length || 0})</span>
</button>
</div>
{/* Style Filter for Gallery */}
{activeTab === "gallery" && (
<div className="flex items-center space-x-2">
<Filter size={14} className="text-purple-400" />
<select
value={selectedStyleFilter}
onChange={(e) => setSelectedStyleFilter(e.target.value)}
className="bg-[#0e071c] border border-purple-800/60 rounded-xl px-3 py-1.5 text-xs text-purple-200 focus:outline-none focus:border-orange-500"
>
<option value="all">All Disguises</option>
{stats?.styleBreakdown?.map((s) => (
<option key={s.styleId} value={s.styleId}>
{s.styleName} ({s.count})
</option>
))}
</select>
</div>
)}
</div>
{/* TAB 1: Generated Images Gallery */}
{activeTab === "gallery" && (
<div>
{displayedImages.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
{displayedImages.map((img) => (
<div
key={img.id}
className="group relative bg-[#150a29] border border-purple-900/40 hover:border-orange-500/60 rounded-2xl overflow-hidden shadow-lg transition-all hover:-translate-y-1"
>
{/* Thumbnail Container */}
<a
href={img.imageUrl}
target="_blank"
rel="noopener noreferrer"
title="Click to open image in new tab"
className="block aspect-[9/16] bg-black overflow-hidden relative"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={img.imageUrl}
alt={img.styleName}
loading="lazy"
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
{/* Hover Overlay with Open in New Tab Icon */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<div className="p-2.5 bg-orange-500/90 rounded-full text-black shadow-lg">
<ExternalLink size={18} />
</div>
</div>
{/* Provider Badge */}
<div className="absolute top-2 left-2 z-10">
<span
className={`px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider shadow-md ${
img.provider === "gemini"
? "bg-cyan-500 text-black"
: img.provider === "openrouter"
? "bg-purple-500 text-white"
: "bg-amber-400 text-black"
}`}
>
{img.provider === "gemini" ? "Google Gemini" : img.provider === "openrouter" ? "OpenRouter" : "Demo Mode"}
</span>
</div>
{/* Latency Badge */}
{img.durationMs ? (
<div className="absolute top-2 right-2 z-10 px-1.5 py-0.5 rounded-md bg-black/70 text-[9px] font-mono text-purple-200">
{(img.durationMs / 1000).toFixed(1)}s
</div>
) : null}
</a>
{/* Card Info */}
<div className="p-2.5 text-left space-y-1">
<div className="text-xs font-bold text-white truncate" title={img.styleName}>
{img.styleName}
</div>
<div className="text-[10px] text-purple-400 font-mono">
{new Date(img.timestamp).toLocaleString("en-CA", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</div>
<div className="pt-1 flex items-center justify-between text-[10px] border-t border-purple-900/30">
<span className="text-purple-400/80 font-mono truncate max-w-[100px]" title={img.model}>
{img.model}
</span>
<a
href={img.imageUrl}
download={`portrait-${img.id}.png`}
title="Download image"
className="text-purple-400 hover:text-orange-400 transition-colors p-1"
>
<Download size={13} />
</a>
</div>
</div>
</div>
))}
</div>
) : (
<div className="bg-[#150a29]/60 border border-purple-900/40 rounded-2xl p-12 text-center">
<ImageIcon className="w-12 h-12 text-purple-500/40 mx-auto mb-3" />
<p className="text-purple-300 font-semibold text-sm">
No generated portraits found for this selection.
</p>
<p className="text-purple-400/60 text-xs mt-1">
Snap photos in the booth to see them appear here live!
</p>
</div>
)}
</div>
)}
{/* TAB 2: Error & Content Refusal Logs */}
{activeTab === "errors" && (
<div className="space-y-3">
{stats?.errorLogs && stats.errorLogs.length > 0 ? (
<div className="bg-[#150a29]/80 border border-purple-900/40 rounded-2xl overflow-hidden shadow-lg">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs font-sans">
<thead className="bg-[#0e061e] text-purple-300 uppercase tracking-wider font-mono text-[10px] border-b border-purple-900/40">
<tr>
<th className="py-3 px-4">Time</th>
<th className="py-3 px-4">Type</th>
<th className="py-3 px-4">Disguise</th>
<th className="py-3 px-4">Provider / Model</th>
<th className="py-3 px-4">Reason / Error Message</th>
<th className="py-3 px-4">Client ID</th>
</tr>
</thead>
<tbody className="divide-y divide-purple-900/30 font-mono text-purple-200">
{stats.errorLogs.map((log) => {
const isSafety = log.errorType === "safety_refusal" || log.status === "refused";
return (
<tr key={log.id} className="hover:bg-purple-900/20 transition-colors">
<td className="py-3 px-4 whitespace-nowrap text-purple-400">
{new Date(log.timestamp).toLocaleString("en-CA", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}
</td>
<td className="py-3 px-4 whitespace-nowrap">
<span
className={`px-2 py-0.5 rounded-full text-[10px] font-bold ${
isSafety
? "bg-amber-950/80 border border-amber-600/50 text-amber-300"
: "bg-red-950/80 border border-red-600/50 text-red-300"
}`}
>
{isSafety ? "Safety Refusal" : "Server / API Error"}
</span>
</td>
<td className="py-3 px-4 font-sans font-semibold text-white">
{log.styleName}
</td>
<td className="py-3 px-4 whitespace-nowrap text-purple-300">
{log.provider} ({log.model})
</td>
<td className="py-3 px-4 text-purple-200 font-sans max-w-md break-words">
{log.refusalReason || log.errorMessage || "Unknown error"}
</td>
<td className="py-3 px-4 text-purple-400/80">
{log.clientId || "127.0.0.1"}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
) : (
<div className="bg-[#150a29]/60 border border-purple-900/40 rounded-2xl p-12 text-center">
<CheckCircle2 className="w-12 h-12 text-emerald-400/40 mx-auto mb-3" />
<p className="text-purple-300 font-semibold text-sm">
No errors or content refusals recorded!
</p>
<p className="text-purple-400/60 text-xs mt-1">
Your photo booth is running smoothly.
</p>
</div>
)}
</div>
)}
</section>
</main>
</div>
);
}