wulf-pulse/app/analyzer/itglue/applications/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

155 lines
5.2 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
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 { Input } from '@/components/ui/input';
interface ApplicationRow {
id: string;
name: string | null;
organizationId: string | null;
organizationName: string | null;
traitCount: number;
latestAudit: {
id: string;
generatedAt: string | null;
overallScore: number | null;
provider: 'anthropic' | 'openrouter' | null;
} | null;
}
function scoreBadgeVariant(
score: number | null
): 'default' | 'secondary' | 'destructive' | 'outline' {
if (score === null) return 'outline';
if (score > 0.8) return 'default';
if (score > 0.5) return 'secondary';
return 'destructive';
}
export default function ApplicationsListPage() {
const [rows, setRows] = useState<ApplicationRow[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState('');
useEffect(() => {
let cancelled = false;
(async () => {
try {
const res = await fetch('/api/analyzer/itglue/applications');
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const data = (await res.json()) as { applications: ApplicationRow[] };
if (!cancelled) setRows(data.applications);
} catch (err) {
if (!cancelled)
setError(err instanceof Error ? err.message : 'Unknown error');
}
})();
return () => {
cancelled = true;
};
}, []);
const visible = rows
? rows.filter((r) => {
if (!filter.trim()) return true;
const q = filter.toLowerCase();
return (
(r.name ?? '').toLowerCase().includes(q) ||
(r.organizationName ?? '').toLowerCase().includes(q)
);
})
: [];
const auditedCount = rows
? rows.filter((r) => r.latestAudit !== null).length
: 0;
return (
<div className="container mx-auto px-6 py-6 max-w-5xl space-y-6">
<Card>
<CardHeader>
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
<CardTitle>IT Glue applications audit</CardTitle>
<p className="text-sm text-muted-foreground mt-1">
{rows === null
? 'Loading…'
: `${rows.length} application records · ${auditedCount} audited`}
</p>
</div>
<Input
placeholder="Filter by name or client…"
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="max-w-xs"
/>
</div>
</CardHeader>
<CardContent>
{error && (
<Alert variant="destructive">
<AlertTitle>Couldn&rsquo;t load applications</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{rows === null && !error ? (
<div className="space-y-2">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</div>
) : (
<ul className="divide-y">
{visible.map((r) => (
<li
key={r.id}
className="py-3 flex items-center justify-between gap-3"
>
<div className="min-w-0 flex-1">
<Link
href={`/analyzer/itglue/applications/${r.id}`}
className="font-medium hover:underline"
>
{r.name ?? r.id}
</Link>
<p className="text-xs text-muted-foreground mt-0.5">
{r.organizationName ?? '—'}
{' · '}
{r.traitCount} field{r.traitCount === 1 ? '' : 's'} populated
{r.latestAudit && (
<>
{' · '}
last audited{' '}
{r.latestAudit.generatedAt
? new Date(r.latestAudit.generatedAt).toLocaleDateString()
: 'unknown'}
{' '}
(
{r.latestAudit.provider === 'openrouter'
? 'DeepSeek'
: 'Claude'}
)
</>
)}
</p>
</div>
<Badge variant={scoreBadgeVariant(r.latestAudit?.overallScore ?? null)}>
{r.latestAudit?.overallScore !== null &&
r.latestAudit?.overallScore !== undefined
? Math.round(r.latestAudit.overallScore * 100) + '%'
: 'No audit'}
</Badge>
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
);
}