wulf-pulse/app/analyzer/itglue/sites/[companyId]/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

234 lines
8.3 KiB
TypeScript

'use client';
import { useEffect, useMemo, useState, use } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { RmmScriptPicker } from '@/components/rmm/rmm-script-picker';
import { Server } from 'lucide-react';
interface SiteInfo {
companyId: string;
companyName: string | null;
itglueOrgId: string | null;
itglueOrgName: string | null;
dattoSiteId: number | null;
wnpHostname: string | null;
wnpDeviceUid: string | null;
wnpOnline: boolean | null;
sites: Array<{
device_uid: string;
hostname: string | null;
online: boolean;
site_id: number;
site_name: string;
site_device_count: number;
}>;
}
interface ExecRow {
id: string;
scriptId: string;
jobName: string;
targetHostname: string | null;
status: 'queued' | 'running' | 'complete' | 'failed' | 'timeout';
exitCode: number | null;
parsedEvidence: unknown;
queuedAt: string;
completedAt: string | null;
}
export default function SiteDiscoveryPage({
params,
}: {
params: Promise<{ companyId: string }>;
}) {
const { companyId } = use(params);
const [info, setInfo] = useState<SiteInfo | null>(null);
const [executions, setExecutions] = useState<ExecRow[] | null>(null);
const [error, setError] = useState<string | null>(null);
async function loadAll(): Promise<void> {
try {
const [s, e] = await Promise.all([
fetch(`/api/analyzer/itglue/sites/${companyId}`).then((r) => r.json()),
fetch(`/api/rmm/executions?companyId=${companyId}&limit=50`).then((r) =>
r.json()
),
]);
if (s.error) throw new Error(s.error);
setInfo(s.site);
setExecutions(e.executions ?? []);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
}
useEffect(() => {
void loadAll();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [companyId]);
const recent = useMemo(() => executions ?? [], [executions]);
if (error) {
return (
<div className="container mx-auto px-6 py-6 max-w-4xl">
<Alert variant="destructive">
<AlertTitle>Couldn&rsquo;t load this site</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
</div>
);
}
if (!info) {
return (
<div className="container mx-auto px-6 py-6 max-w-4xl space-y-4">
<Skeleton className="h-32 w-full" />
<Skeleton className="h-64 w-full" />
</div>
);
}
return (
<div className="container mx-auto px-6 py-6 max-w-4xl space-y-6">
<Card>
<CardHeader>
<div className="flex items-start justify-between gap-3 flex-wrap">
<div className="space-y-1 min-w-0">
<p className="text-sm text-muted-foreground">Site discovery</p>
<CardTitle className="text-2xl truncate">
{info.companyName ?? `Company ${info.companyId}`}
</CardTitle>
<p className="text-xs text-muted-foreground">
{info.wnpHostname ? (
<>
<Server className="inline w-3 h-3 mr-1" />
Primary target:{' '}
<span className="font-mono">{info.wnpHostname}</span>
{info.wnpOnline === false && (
<Badge variant="destructive" className="ml-2 text-[10px]">
offline
</Badge>
)}
{info.sites.length > 1 && (
<span className="ml-2">
({info.sites.length} WNP endpoints across this client&rsquo;s sites)
</span>
)}
</>
) : (
<span className="text-amber-600">
No Wulf Nurse Production endpoint registered for this client.
</span>
)}
</p>
</div>
<RmmScriptPicker
filter="site_anchor"
companyId={info.companyId}
onComplete={() => loadAll()}
/>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Site-anchored discovery scripts run from the Wulf Nurse Production
endpoint and use native PowerShell + AD/DHCP/DNS cmdlets to gather
facts about the environment. Output is captured and made available
to subsequent IT Glue audits as live evidence.
</p>
{info.sites.length > 1 && (
<div className="mt-4">
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-1">
All sites for this client
</p>
<ul className="text-xs space-y-1">
{info.sites.map((s) => (
<li
key={s.device_uid}
className="flex items-center justify-between gap-3"
>
<span>
<span className="font-mono">{s.hostname}</span>
<span className="ml-2 text-muted-foreground">
{s.site_name} · {s.site_device_count} devices
</span>
{s.hostname === info.wnpHostname && (
<Badge variant="secondary" className="ml-2 text-[10px]">
primary
</Badge>
)}
</span>
{!s.online && (
<Badge variant="destructive" className="text-[10px]">
offline
</Badge>
)}
</li>
))}
</ul>
<p className="text-xs text-muted-foreground mt-2">
v1 dispatches site-anchored scripts to the primary target only.
A future version will let you pick a specific site here.
</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Recent runs ({recent.length})</CardTitle>
</CardHeader>
<CardContent>
{recent.length === 0 ? (
<p className="text-sm text-muted-foreground">No runs yet.</p>
) : (
<ul className="divide-y">
{recent.map((e) => (
<li key={e.id} className="py-3">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">
{e.scriptId}{' '}
<Badge
variant={
e.status === 'complete'
? 'default'
: e.status === 'failed' || e.status === 'timeout'
? 'destructive'
: 'outline'
}
className="ml-1 text-[10px]"
>
{e.status}
</Badge>
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{e.targetHostname ?? '—'} ·{' '}
{new Date(e.queuedAt).toLocaleString()}
</p>
</div>
</div>
{e.parsedEvidence !== null && e.parsedEvidence !== undefined && (
<details className="mt-2">
<summary className="text-xs text-muted-foreground cursor-pointer">
Show parsed evidence
</summary>
<pre className="text-[11px] bg-muted/50 rounded p-2 max-h-72 overflow-auto whitespace-pre-wrap mt-1">
{JSON.stringify(e.parsedEvidence, null, 2)}
</pre>
</details>
)}
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
);
}