wulf-pulse/components/analyzer/analyze-button.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

115 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { Sparkles, Loader2 } from 'lucide-react';
import type { JobStatus } from '@/lib/types/analyzer';
import type { AnalyzerProvider } from './provider-toggle';
interface AnalyzeButtonProps {
ticketNumber: string;
/** Force a re-run even if the content hash matches an existing analysis. */
force?: boolean;
variant?: 'default' | 'outline' | 'secondary';
label?: string;
/** LLM provider (anthropic = Claude default; openrouter = DeepSeek). */
provider?: AnalyzerProvider;
}
const STAGE_LABEL: Record<JobStatus, string> = {
queued: 'Queued…',
fetching: 'Fetching…',
triaging: 'Triaging…',
itglue: 'Searching IT Glue…',
analyzing: 'Analyzing…',
deep_review: 'Deep review…',
complete: 'Done',
failed: 'Failed',
};
export function AnalyzeButton({
ticketNumber,
force = false,
variant = 'default',
label = 'Analyze',
provider = 'anthropic',
}: AnalyzeButtonProps) {
const router = useRouter();
const [status, setStatus] = useState<JobStatus | 'idle'>('idle');
async function pollJob(jobId: string) {
const start = Date.now();
// DeepSeek runs (especially V4 Pro deep analysis) take 4-6× longer than
// Claude — observed ~5min on a typical ticket. 12min keeps headroom.
const TIMEOUT_MS = 12 * 60 * 1000;
while (Date.now() - start < TIMEOUT_MS) {
await new Promise((r) => setTimeout(r, 2000));
const res = await fetch(`/api/analyzer/jobs/${jobId}`);
if (!res.ok) throw new Error(`Job poll failed: ${res.status}`);
const { job } = (await res.json()) as {
job: { status: JobStatus; resultAnalysisId: string | null; errorMessage: string | null };
};
setStatus(job.status);
if (job.status === 'complete' && job.resultAnalysisId) {
return job.resultAnalysisId;
}
if (job.status === 'failed') {
throw new Error(job.errorMessage ?? 'Analysis failed');
}
}
throw new Error('Analysis timed out after 5 minutes');
}
async function handleClick() {
setStatus('queued');
try {
const res = await fetch(
`/api/analyzer/tickets/${encodeURIComponent(ticketNumber)}/analyze`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ force, provider }),
}
);
if (!res.ok) {
const error = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(error.error ?? `Request failed: ${res.status}`);
}
const data = (await res.json()) as
| { status: 'complete'; existingAnalysisId: string }
| { status: 'queued'; jobId: string };
if (data.status === 'complete') {
router.push(`/analyzer/analysis/${data.existingAnalysisId}`);
return;
}
const analysisId = await pollJob(data.jobId);
router.push(`/analyzer/analysis/${analysisId}`);
} catch (err) {
const msg = err instanceof Error ? err.message : 'Unknown error';
toast.error(msg);
setStatus('idle');
}
}
const isRunning = status !== 'idle' && status !== 'failed';
return (
<Button onClick={handleClick} disabled={isRunning} variant={variant}>
{isRunning ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{STAGE_LABEL[status as JobStatus] ?? 'Working…'}
</>
) : (
<>
<Sparkles className="w-4 h-4 mr-2" />
{force ? 'Re-analyze' : label}
</>
)}
</Button>
);
}