'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ArrowLeft, Activity, History, Calendar, Mail, RefreshCw, Loader2, CheckCircle2, XCircle, AlertTriangle, Shield, Inbox, Send, Clock, ChevronDown, ChevronRight, } from 'lucide-react'; import SyncScheduler from '@/components/admin/SyncScheduler'; function fmtDate(d: string | null | undefined) { if (!d) return 'Never'; return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } function fmtNum(n: number | null | undefined) { if (n == null) return '—'; return n.toLocaleString(); } function StatCard({ label, value, sub, icon: Icon, cls }: { label: string; value: string | number; sub?: string; icon?: React.ElementType; cls?: string; }) { return (
{Icon && }{label}
{value}
{sub &&
{sub}
}
); } function StatusBadge({ status }: { status: string }) { const cls = status === 'delivered' ? 'bg-green-500/15 text-green-700' : status === 'rejected' ? 'bg-red-500/15 text-red-600' : status === 'held' ? 'bg-yellow-500/15 text-yellow-700' : status === 'bounced' ? 'bg-orange-500/15 text-orange-700' : status === 'spam' ? 'bg-purple-500/15 text-purple-700' : 'bg-muted text-muted-foreground'; return ( {status || '—'} ); } function ThreatBadge({ level }: { level: string }) { const cls = level === 'high' ? 'bg-red-500/15 text-red-600' : level === 'medium' ? 'bg-orange-500/15 text-orange-700' : level === 'low' ? 'bg-yellow-500/15 text-yellow-700' : 'bg-muted text-muted-foreground'; return ( {level || 'info'} ); } // ── Status Tab ──────────────────────────────────────────────────────────────── function StatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) => void; syncing: boolean }) { if (!data) return (
); const stats = data.stats ?? {}; return (
{/* Connection banner */}
{data.connected ? : }

{data.connected ? `Connected — ${data.accountName ?? 'Mimecast'}` : 'Not connected'}

{data.packageName && ( ({data.packageName}) )}

Last sync: {fmtDate(stats.lastSync)} · Oldest message: {fmtDate(stats.oldestMessage)}

{data.error && (
{data.error}
)} {/* Stats grid */}
0 ? 'border-red-500/20 bg-red-500/5' : ''} />

Data Retention

120-day rolling window. Messages older than 120 days are automatically purged on each sync. Message bodies are fetched for delivered inbound messages (up to 500 per sync run).

); } // ── Messages Tab ────────────────────────────────────────────────────────────── function MessagesTab() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [direction, setDirection] = useState(''); const [status, setStatus] = useState(''); const [days, setDays] = useState('7'); const load = () => { setLoading(true); const params = new URLSearchParams({ days, limit: '200' }); if (search) params.set('search', search); if (direction) params.set('direction', direction); if (status) params.set('status', status); fetch(`/api/mimecast/messages?${params}`) .then(r => r.json()) .then(d => setRows(d.messages ?? [])) .catch(() => setRows([])) .finally(() => setLoading(false)); }; useEffect(() => { load(); }, [days, direction, status]); return (
{/* Filters */}
setSearch(e.target.value)} onKeyDown={e => e.key === 'Enter' && load()} className="flex-1 min-w-48 border rounded-md px-3 py-1.5 text-sm bg-background" />
{loading ? (
) : !rows.length ? (
No messages found — run a sync first or adjust filters
) : (
{rows.map((r: any) => ( ))}
From To Subject Direction Status Sent
{r.sender_address ?? '—'} {r.recipient_address ?? '—'} {r.subject ?? '—'} {r.direction ?? '—'} {fmtDate(r.sent_datetime)}
)}
); } // ── Threats Tab ─────────────────────────────────────────────────────────────── function ThreatsTab() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch('/api/mimecast/threats?limit=200') .then(r => r.json()) .then(d => setRows(d.threats ?? [])) .catch(() => setRows([])) .finally(() => setLoading(false)); }, []); if (loading) return
; if (!rows.length) return
No threat events — run a sync first
; return (
{rows.map((r: any) => ( ))}
Type Level Actor Verdict URL / File When
{r.event_type ?? '—'} {r.actor_email ?? '—'} {r.verdict ?? '—'} {r.url ?? r.file_name ?? '—'} {fmtDate(r.event_datetime)}
); } // ── History Tab ─────────────────────────────────────────────────────────────── function HistoryTab() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch('/api/sync/history?entityType=mimecast&limit=30') .then(r => r.json()) .then(d => setRows(d.history ?? [])) .catch(() => setRows([])) .finally(() => setLoading(false)); }, []); if (loading) return
; if (!rows.length) return
No sync history yet
; return (
{rows.map((r: any, i: number) => { const dur = r.completed_at && r.started_at ? new Date(r.completed_at).getTime() - new Date(r.started_at).getTime() : null; const durStr = dur == null ? '—' : dur < 60000 ? `${Math.round(dur / 1000)}s` : `${Math.floor(dur / 60000)}m ${Math.round((dur % 60000) / 1000)}s`; const statusCls = r.status === 'completed' ? 'bg-green-500/15 text-green-700' : r.status === 'failed' ? 'bg-red-500/15 text-red-600' : 'bg-muted text-muted-foreground'; const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata ?? {}); return ( ); })}
Type Status Messages Threats Started Duration
{r.sync_type ?? '—'} {r.status} {fmtNum(meta.messagesUpserted ?? r.records_added)} {fmtNum(meta.threatsUpserted)} {fmtDate(r.started_at)} {durStr}
); } // ── Page ────────────────────────────────────────────────────────────────────── export default function MimecastSyncPage() { const [statusData, setStatusData] = useState(null); const [syncing, setSyncing] = useState(false); const [lastResult, setLastResult] = useState(null); const fetchStatus = () => { fetch('/api/mimecast/status') .then(r => r.json()) .then(d => setStatusData(d)) .catch(() => setStatusData({ configured: false, connected: false })); }; useEffect(() => { fetchStatus(); }, []); const handleSync = async (syncType: string) => { setSyncing(true); setLastResult(null); try { const res = await fetch('/api/sync/mimecast', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ syncType }), }); const data = await res.json(); setLastResult(data); fetchStatus(); } catch (err: any) { setLastResult({ error: err.message }); } finally { setSyncing(false); } }; return (
{/* Header */}

Email Security — Mimecast

Message logs, threat events, 120-day retention

{/* Last sync result banner */} {lastResult && (
{lastResult.error ? `Sync failed: ${lastResult.error}` : `Sync complete — ${fmtNum(lastResult.messagesUpserted)} messages, ${fmtNum(lastResult.threatsUpserted)} threats, ${fmtNum(lastResult.bodiesFetched)} bodies fetched in ${Math.round((lastResult.durationMs ?? 0) / 1000)}s` } {lastResult.errors?.length > 0 && (
{lastResult.errors.slice(0, 3).join(' · ')}
)}
)} Status Messages Threats History Schedules
); }