- 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>
167 lines
6.3 KiB
TypeScript
167 lines
6.3 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, use } 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 { AnalyzeButton } from '@/components/analyzer/analyze-button';
|
|
import { RelatedTicketsPanel } from '@/components/analyzer/related-tickets-panel';
|
|
import {
|
|
ProviderToggle,
|
|
type AnalyzerProvider,
|
|
} from '@/components/analyzer/provider-toggle';
|
|
import { Sparkles, Zap } from 'lucide-react';
|
|
import type { PersistedAnalysis } from '@/lib/types/analyzer';
|
|
|
|
export default function TicketAnalyzerPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ ticketNumber: string }>;
|
|
}) {
|
|
const { ticketNumber } = use(params);
|
|
const [analyses, setAnalyses] = useState<PersistedAnalysis[] | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [provider, setProvider] = useState<AnalyzerProvider>('anthropic');
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const res = await fetch(
|
|
`/api/analyzer/tickets/${encodeURIComponent(ticketNumber)}/analyses`
|
|
);
|
|
if (!res.ok) {
|
|
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
|
throw new Error(data.error ?? `Request failed: ${res.status}`);
|
|
}
|
|
const { analyses } = (await res.json()) as { analyses: PersistedAnalysis[] };
|
|
if (!cancelled) setAnalyses(analyses);
|
|
} catch (err) {
|
|
if (!cancelled) setError(err instanceof Error ? err.message : 'Unknown error');
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [ticketNumber]);
|
|
|
|
const latest = analyses?.[0];
|
|
|
|
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-4 flex-wrap">
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">Ticket</p>
|
|
<CardTitle className="font-mono">{ticketNumber}</CardTitle>
|
|
</div>
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<ProviderToggle value={provider} onChange={setProvider} size="sm" />
|
|
<AnalyzeButton ticketNumber={ticketNumber} provider={provider} />
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Click <strong>Analyze</strong> to run the AI pipeline using the
|
|
selected provider. Each provider keeps its own analysis history,
|
|
so you can compare Claude and DeepSeek output side-by-side. A run
|
|
with the same content hash on the same provider returns instantly.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertTitle>Couldn’t load analysis history</AlertTitle>
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<RelatedTicketsPanel ticketNumber={ticketNumber} provider={provider} />
|
|
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Analysis history</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{analyses === null && !error ? (
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-12 w-full" />
|
|
<Skeleton className="h-12 w-full" />
|
|
</div>
|
|
) : analyses && analyses.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">
|
|
No analyses yet. Run one above.
|
|
</p>
|
|
) : (
|
|
<ul className="divide-y">
|
|
{(analyses ?? []).map((a) => {
|
|
const isOpenRouter = a.provider === 'openrouter';
|
|
const tierLabel = isOpenRouter
|
|
? a.opusUsed
|
|
? 'V4 Flash → V4 Pro → R1'
|
|
: a.sonnetUsed
|
|
? 'V4 Flash → V4 Pro'
|
|
: 'V4 Flash'
|
|
: a.opusUsed
|
|
? 'Haiku → Sonnet → Opus'
|
|
: a.sonnetUsed
|
|
? 'Haiku → Sonnet'
|
|
: 'Haiku';
|
|
const ProviderIcon = isOpenRouter ? Zap : Sparkles;
|
|
return (
|
|
<li
|
|
key={a.id}
|
|
className="py-3 flex items-center justify-between gap-4"
|
|
>
|
|
<div className="min-w-0">
|
|
<Link
|
|
href={`/analyzer/analysis/${a.id}`}
|
|
className="font-medium hover:underline flex items-center gap-2 flex-wrap"
|
|
>
|
|
<ProviderIcon className="w-4 h-4" />
|
|
Version {a.analysisVersion}
|
|
<Badge
|
|
variant={isOpenRouter ? 'default' : 'secondary'}
|
|
className="text-[10px] py-0"
|
|
>
|
|
{isOpenRouter ? 'DeepSeek' : 'Claude'}
|
|
</Badge>
|
|
{latest?.id === a.id && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
latest
|
|
</Badge>
|
|
)}
|
|
{a.needsHumanReview && (
|
|
<Badge variant="destructive" className="text-xs">
|
|
Needs review
|
|
</Badge>
|
|
)}
|
|
</Link>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{new Date(a.triggeredAt).toLocaleString()}
|
|
{' · '}
|
|
{tierLabel}
|
|
{' · '}${a.estimatedCostUsd.toFixed(4)}
|
|
</p>
|
|
</div>
|
|
{a.confidenceScore !== null && (
|
|
<Badge variant="outline">
|
|
{Math.round(a.confidenceScore * 100)}%
|
|
</Badge>
|
|
)}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|