- 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>
115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
'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>
|
||
);
|
||
}
|