'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)}
onSync('incremental')} disabled={syncing || !data.connected}>
{syncing ? : }
Incremental
onSync('full')} disabled={syncing || !data.connected}>
{syncing ? : }
Full Sync (120d)
{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"
/>
setDirection(e.target.value)}
className="border rounded-md px-3 py-1.5 text-sm bg-background">
All directions
Inbound
Outbound
setStatus(e.target.value)}
className="border rounded-md px-3 py-1.5 text-sm bg-background">
All statuses
Delivered
Rejected
Held
Bounced
Spam
setDays(e.target.value)}
className="border rounded-md px-3 py-1.5 text-sm bg-background">
Last 24h
Last 7 days
Last 30 days
Last 90 days
Last 120 days
Search
{loading ? (
) : !rows.length ? (
No messages found — run a sync first or adjust filters
) : (
From
To
Subject
Direction
Status
Sent
{rows.map((r: any) => (
{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 (
Type
Level
Actor
Verdict
URL / File
When
{rows.map((r: any) => (
{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 (
Type
Status
Messages
Threats
Started
Duration
{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 (
{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 */}
Integrations
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
);
}