wulf-pulse/app/admin/rmm-overshell/page.tsx
lorentz 1112a06afe feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
  resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
  receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
  xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
  link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
  panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
  admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00

272 lines
10 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Loader2, RefreshCw, Terminal } from 'lucide-react';
import { toast } from 'sonner';
interface Settings {
overshellComponentUid: string | null;
overshellComponentName: string | null;
overshellVariableName: string;
discoveredAt: string | null;
logliftComponentUid: string | null;
logliftComponentName: string | null;
logliftDiscoveredAt: string | null;
updatedAt: string;
}
interface ExecRow {
id: string;
scriptId: string;
jobName: string;
targetHostname: string | null;
status: 'queued' | 'running' | 'complete' | 'failed' | 'timeout';
exitCode: number | null;
errorMessage: string | null;
performedByUserId: string | null;
queuedAt: string;
completedAt: string | null;
}
export default function RmmOvershellAdminPage() {
const [settings, setSettings] = useState<Settings | null>(null);
const [counts, setCounts] = useState<{ total: string; running: string; failed_24h: string } | null>(null);
const [executions, setExecutions] = useState<ExecRow[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [discovering, setDiscovering] = useState(false);
const [discoveringLoglift, setDiscoveringLoglift] = useState(false);
async function loadAll() {
try {
const [s, e] = await Promise.all([
fetch('/api/admin/rmm/settings').then((r) => r.json()),
fetch('/api/rmm/executions?limit=50').then((r) => r.json()),
]);
if (s.error) throw new Error(s.error);
setSettings(s.settings);
setCounts(s.counts);
setExecutions(e.executions ?? []);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
}
useEffect(() => {
void loadAll();
}, []);
async function discover() {
setDiscovering(true);
try {
const res = await fetch('/api/admin/rmm/settings/discover', { method: 'POST' });
const data = await res.json();
if (!res.ok) throw new Error(data.message ?? data.error ?? 'Discovery failed');
toast.success(`Found component: ${data.discovered?.name ?? 'unknown'}`);
void loadAll();
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Discovery failed');
} finally {
setDiscovering(false);
}
}
async function discoverLoglift() {
setDiscoveringLoglift(true);
try {
const res = await fetch('/api/admin/rmm/settings/discover-loglift', {
method: 'POST',
});
const data = await res.json();
if (!res.ok) throw new Error(data.message ?? data.error ?? 'Discovery failed');
toast.success(`Found LogLift component: ${data.discovered?.name ?? 'unknown'}`);
void loadAll();
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Discovery failed');
} finally {
setDiscoveringLoglift(false);
}
}
return (
<div className="container mx-auto px-6 py-6 max-w-5xl space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="w-5 h-5" />
RMM Overshell
</CardTitle>
<p className="text-sm text-muted-foreground">
Datto RMM PowerShell evidence pipeline. Pulse dispatches scripts via
the configured Overshell component; the worker polls for results
and the audit pipeline pulls them in as live evidence.
</p>
</CardHeader>
<CardContent className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertTitle>Couldn&rsquo;t load settings</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{settings === null && !error ? (
<Skeleton className="h-32 w-full" />
) : settings ? (
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="font-medium">Overshell component</p>
{settings.overshellComponentUid ? (
<>
<p className="text-xs text-muted-foreground mt-0.5">
{settings.overshellComponentName}
</p>
<p className="text-[10px] font-mono text-muted-foreground">
{settings.overshellComponentUid}
</p>
{settings.discoveredAt && (
<p className="text-[10px] text-muted-foreground mt-1">
discovered {new Date(settings.discoveredAt).toLocaleString()}
</p>
)}
</>
) : (
<p className="text-xs text-amber-600 mt-0.5">
No component cached. Click Discover to scan Datto RMM.
</p>
)}
</div>
<div>
<p className="font-medium">Variable name</p>
<p className="text-xs font-mono text-muted-foreground mt-0.5">
{settings.overshellVariableName}
</p>
<p className="text-[10px] text-muted-foreground mt-1">
Adjust if your component uses a different variable.
</p>
</div>
<div>
<p className="font-medium">Activity (24h)</p>
<p className="text-xs text-muted-foreground mt-0.5">
{counts?.total ?? '0'} total · {counts?.running ?? '0'} running ·
<span className="text-destructive">
{' '}
{counts?.failed_24h ?? '0'} failed
</span>
</p>
</div>
<div className="flex items-end gap-2">
<Button onClick={discover} disabled={discovering}>
{discovering ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Discovering
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-2" />
Re-discover Overshell
</>
)}
</Button>
</div>
<div className="col-span-2 border-t pt-4">
<p className="font-medium">LogLift component</p>
{settings.logliftComponentUid ? (
<>
<p className="text-xs text-muted-foreground mt-0.5">
{settings.logliftComponentName}
</p>
<p className="text-[10px] font-mono text-muted-foreground">
{settings.logliftComponentUid}
</p>
{settings.logliftDiscoveredAt && (
<p className="text-[10px] text-muted-foreground mt-1">
discovered{' '}
{new Date(settings.logliftDiscoveredAt).toLocaleString()}
</p>
)}
</>
) : (
<p className="text-xs text-amber-600 mt-0.5">
No LogLift component cached. Click below to scan Datto RMM
for one named &ldquo;loglift&rdquo; or &ldquo;eventlog&rdquo;.
</p>
)}
<div className="mt-3">
<Button
variant="outline"
onClick={discoverLoglift}
disabled={discoveringLoglift}
>
{discoveringLoglift ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Discovering
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-2" />
Re-discover LogLift
</>
)}
</Button>
</div>
</div>
</div>
) : null}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Recent executions</CardTitle>
</CardHeader>
<CardContent>
{executions === null ? (
<Skeleton className="h-24 w-full" />
) : executions.length === 0 ? (
<p className="text-sm text-muted-foreground">No executions yet.</p>
) : (
<ul className="divide-y">
{executions.map((e) => (
<li key={e.id} className="py-2 grid grid-cols-12 gap-2 text-sm">
<span className="col-span-3 font-mono truncate">{e.scriptId}</span>
<span className="col-span-3 truncate">{e.targetHostname ?? '—'}</span>
<span className="col-span-2">
<Badge
variant={
e.status === 'complete'
? 'default'
: e.status === 'failed' || e.status === 'timeout'
? 'destructive'
: 'outline'
}
className="text-[10px]"
>
{e.status}
{e.exitCode !== null ? ` · exit ${e.exitCode}` : ''}
</Badge>
</span>
<span className="col-span-3 text-xs text-muted-foreground truncate">
{new Date(e.queuedAt).toLocaleString()}
</span>
<span className="col-span-1 text-right">
{e.errorMessage && (
<span className="text-xs text-destructive truncate">!</span>
)}
</span>
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
);
}