wulf-pulse/app/admin/sync/datto-rmm/page.tsx

243 lines
11 KiB
TypeScript

'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, Monitor, Loader2, RefreshCw,
ExternalLink, Server, Wifi, WifiOff, AlertTriangle, Bell, XCircle, CheckCircle2, Clock,
} from 'lucide-react';
function fmtDate(d: string | null) {
if (!d) return 'Never';
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
}
function StatCard({ label, value, sub, icon: Icon, cls }: { label: string; value: string | number; sub?: string; icon?: React.ElementType; cls?: string }) {
return (
<div className={`rounded-lg border p-4 flex flex-col gap-1 ${cls ?? ''}`}>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{Icon && <Icon className="w-3.5 h-3.5" />}{label}
</div>
<div className="text-2xl font-bold tabular-nums">{value}</div>
{sub && <div className="text-xs text-muted-foreground">{sub}</div>}
</div>
);
}
function StatusTab({ data, onSync, syncing }: { data: any; onSync: () => void; syncing: boolean }) {
if (!data) return <div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /></div>;
const devs = data.devices ?? {};
const alerts = data.openAlerts ?? {};
return (
<div className="space-y-6">
<div className="flex items-center justify-between rounded-lg border p-4 bg-muted/30">
<div className="space-y-0.5">
<p className="text-sm font-medium">{data.configured ? 'Connected' : 'Not configured'}</p>
<p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync)}</p>
</div>
<div className="flex gap-2">
<a href="https://concord.rmm.datto.com" target="_blank" rel="noopener noreferrer">
<Button variant="outline" size="sm" className="gap-2">
<ExternalLink className="w-4 h-4" />Portal
</Button>
</a>
<Button size="sm" onClick={onSync} disabled={syncing || !data.configured}>
{syncing ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <RefreshCw className="w-4 h-4 mr-2" />}
Full Sync
</Button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<StatCard label="Sites" value={data.sites ?? 0} icon={Server} />
<StatCard label="Devices" value={devs.total ?? 0} icon={Monitor} />
<StatCard label="Online" value={devs.online ?? 0} icon={Wifi}
cls="border-green-500/30 bg-green-500/5" />
<StatCard label="Offline" value={devs.offline ?? 0} icon={WifiOff}
cls={(devs.offline ?? 0) > 0 ? 'border-yellow-500/30 bg-yellow-500/5' : ''} />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<StatCard label="Open Alerts" value={alerts.total ?? 0} icon={Bell}
cls={(alerts.total ?? 0) > 0 ? 'border-red-500/30 bg-red-500/5' : ''} />
<StatCard label="Critical / High" value={`${alerts.critical ?? 0} / ${alerts.high ?? 0}`} icon={XCircle}
cls={(alerts.critical ?? 0) > 0 ? 'border-red-500/30 bg-red-500/5' : ''} />
<StatCard label="Moderate" value={alerts.moderate ?? 0} icon={AlertTriangle} />
<StatCard label="With Ticket" value={alerts.withTicket ?? 0} icon={CheckCircle2}
sub="linked to Autotask" />
</div>
{(alerts.critical > 0 || alerts.high > 0) && (
<div className="rounded-lg border p-4 space-y-2">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Attention Required</p>
{alerts.critical > 0 && (
<div className="flex items-center gap-2 text-sm text-red-600">
<XCircle className="w-4 h-4" />{alerts.critical} critical alert{alerts.critical !== 1 ? 's' : ''}
</div>
)}
{alerts.high > 0 && (
<div className="flex items-center gap-2 text-sm text-orange-600">
<AlertTriangle className="w-4 h-4" />{alerts.high} high priority alert{alerts.high !== 1 ? 's' : ''}
</div>
)}
</div>
)}
</div>
);
}
function HistoryTab({ refreshKey }: { refreshKey: number }) {
const [rows, setRows] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch('/api/sync/history?entityType=datto_rmm&limit=50')
.then(r => r.json())
.then(d => setRows(d.history ?? []))
.catch(() => setRows([]))
.finally(() => setLoading(false));
}, [refreshKey]);
if (loading) return <div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /></div>;
if (!rows.length) return <div className="text-center py-12 text-muted-foreground text-sm">No sync history yet run a sync to populate</div>;
return (
<div className="rounded-lg border overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-muted/50 border-b">
<tr>
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Type</th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Status</th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Records</th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Started</th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Duration</th>
</tr>
</thead>
<tbody>
{rows.map((row: any, i: number) => {
const dur = row.completed_at && row.started_at
? Math.round((new Date(row.completed_at).getTime() - new Date(row.started_at).getTime()) / 1000)
: null;
return (
<tr key={i} className="border-b last:border-0 hover:bg-muted/30">
<td className="px-4 py-2 capitalize">{row.sync_type}</td>
<td className="px-4 py-2">
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${
row.status === 'completed' ? 'bg-green-500/15 text-green-700' :
row.status === 'failed' ? 'bg-red-500/15 text-red-600' :
'bg-yellow-500/15 text-yellow-700'
}`}>{row.status}</span>
</td>
<td className="px-4 py-2 tabular-nums">{row.records_added ?? 0}</td>
<td className="px-4 py-2 text-muted-foreground">{fmtDate(row.started_at)}</td>
<td className="px-4 py-2 text-muted-foreground">{dur != null ? `${dur}s` : '—'}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
export default function DattoRmmPage() {
const [status, setStatus] = useState<any>(null);
const [syncing, setSyncing] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const fetchStatus = async () => {
const res = await fetch('/api/integrations/status');
if (res.ok) { const d = await res.json(); setStatus(d.dattoRmm); }
};
useEffect(() => { fetchStatus(); }, [refreshKey]);
const handleSync = async () => {
setSyncing(true);
try {
await fetch('/api/datto-rmm/sync', {
method: 'POST',
body: JSON.stringify({ syncType: 'full' }),
headers: { 'Content-Type': 'application/json' },
});
const poll = setInterval(async () => {
const r = await fetch('/api/datto-rmm/sync');
if (r.ok) {
const d = await r.json();
if (!d.isSyncing) {
clearInterval(poll);
setSyncing(false);
setRefreshKey(k => k + 1);
}
}
}, 5000);
} catch {
setSyncing(false);
}
};
return (
<div className="container mx-auto py-4 md:py-8 px-4 space-y-6">
<div className="flex items-center gap-4">
<Link href="/admin/sync">
<Button variant="outline" size="sm" className="gap-2">
<ArrowLeft className="w-4 h-4" />
Integrations
</Button>
</Link>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg border border-orange-500/30 bg-orange-500/5">
<Monitor className="w-5 h-5 text-orange-500" />
</div>
<div>
<h1 className="text-2xl font-bold">RMM Datto RMM</h1>
<p className="text-sm text-muted-foreground">Sites, devices, alerts, patch management</p>
</div>
</div>
</div>
<Tabs defaultValue="status" className="w-full">
<TabsList className="grid w-full max-w-md grid-cols-3">
<TabsTrigger value="status" className="gap-2">
<Activity className="h-4 w-4" />Status
</TabsTrigger>
<TabsTrigger value="history" className="gap-2">
<History className="h-4 w-4" />History
</TabsTrigger>
<TabsTrigger value="about" className="gap-2">
<Monitor className="h-4 w-4" />About
</TabsTrigger>
</TabsList>
<TabsContent value="status" className="mt-6">
<StatusTab data={status} onSync={handleSync} syncing={syncing} />
</TabsContent>
<TabsContent value="history" className="mt-6">
<HistoryTab refreshKey={refreshKey} />
</TabsContent>
<TabsContent value="about" className="mt-6">
<div className="space-y-4 text-sm text-muted-foreground">
<div className="rounded-lg border p-4 space-y-2">
<p className="font-medium text-foreground">Synced Entities</p>
<ul className="space-y-1 list-disc list-inside">
<li><strong>Sites</strong> RMM sites with device counts, mapped to Autotask companies</li>
<li><strong>Devices</strong> all managed devices with OS, IP, AV, patch status, UDFs</li>
<li><strong>Open Alerts</strong> active alerts with priority, device context, ticket linkage</li>
<li><strong>Resolved Alerts</strong> recent resolved alerts with response action history</li>
</ul>
</div>
<div className="rounded-lg border p-4 space-y-2">
<p className="font-medium text-foreground">Authentication</p>
<p>OAuth2 password grant API key + secret Bearer token (100h TTL, refreshed at 50min)</p>
<p>Rate limit: 600 requests / 60 seconds across the account</p>
</div>
</div>
</TabsContent>
</Tabs>
</div>
);
}