'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Skeleton } from '@/components/ui/skeleton'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell, } from 'recharts'; import { Brain, Play, RefreshCw, CheckCircle2, AlertTriangle, Clock, WifiOff, ChevronDown, ChevronRight, Wrench, RotateCcw, Sparkles, Zap, BookOpen, GraduationCap, } from 'lucide-react'; // ── Types ───────────────────────────────────────────────────────────────────── interface Stats { total_analyzed: string; total_ytd: string; avg_hours: string; same_day_pct: string; preventable_pct: string; offline_pct: string; auto_resolved_pct: string; } interface CategoryRow { problem_category: string; count: string; avg_hours: string; same_day_pct: string } interface ResolutionRow { resolution_type: string; count: string } interface SkillRow { skill: string; count: string } interface ComplexityRow { complexity: string; count: string } interface AnalysisData { stats: Stats; by_category: CategoryRow[]; by_resolution: ResolutionRow[]; skills: SkillRow[]; by_complexity: ComplexityRow[]; tickets: TicketRow[]; total: number; limit: number; page: number; } interface TicketRow { ticket_number: string; company_name: string | null; device_hostname: string | null; ticket_created_at: string | null; ticket_closed_at: string | null; same_day_close: boolean; hours_worked: string; problem_category: string; resolution_type: string; complexity: string; device_was_offline: boolean | null; backup_completed_before_tech: boolean | null; preventable: boolean | null; work_summary: string | null; recommended_procedure: string | null; } interface RunStatus { is_running: boolean; run_total: number; run_done: number; run_errors: number; total_analyzed: number; total_eligible: number; last_analyzed_at: string | null; } // ── Config ──────────────────────────────────────────────────────────────────── const CATEGORY_CFG: Record = { device_offline: { label: 'Device Offline', color: '#94a3b8' }, agent_issue: { label: 'Agent Issue', color: '#f97316' }, job_failed: { label: 'Job Failed', color: '#ef4444' }, storage_issue: { label: 'Storage Issue', color: '#f59e0b' }, network_issue: { label: 'Network Issue', color: '#3b82f6' }, authentication: { label: 'Authentication', color: '#8b5cf6' }, software_error: { label: 'Software Error', color: '#ec4899' }, self_resolved: { label: 'Self-Resolved', color: '#22c55e' }, configuration: { label: 'Configuration', color: '#06b6d4' }, other: { label: 'Other', color: '#6b7280' }, }; const RESOLUTION_CFG: Record = { no_action_needed: 'No Action Needed', device_powered_on: 'Device Powered On', backup_restarted: 'Backup Restarted', agent_reinstalled: 'Agent Reinstalled', storage_cleared: 'Storage Cleared', settings_updated: 'Settings Updated', escalated: 'Escalated', other: 'Other', }; const COMPLEXITY_COLOR: Record = { trivial: '#22c55e', low: '#84cc16', medium: '#f59e0b', high: '#ef4444', }; interface SummaryAnalysis { headline: string; key_findings: string[]; issue_breakdown: { category: string; insight: string; sop: string }[]; skills_assessment: string; quick_wins: string[]; automation_opportunities: string; training_priority: string; } interface SummaryData { analysis: SummaryAnalysis; model: string; generated_at: string; } function catLabel(k: string) { return CATEGORY_CFG[k]?.label ?? k; } function catColor(k: string) { return CATEGORY_CFG[k]?.color ?? '#6b7280'; } function resLabel(k: string) { return RESOLUTION_CFG[k] ?? k; } function timeAgo(d: string | null) { if (!d) return '—'; const h = Math.floor((Date.now() - new Date(d).getTime()) / 3_600_000); if (h < 1) return 'Just now'; if (h < 24) return `${h}h ago`; return `${Math.floor(h / 24)}d ago`; } // ── Expandable ticket row ───────────────────────────────────────────────────── function TicketRow({ t, categoryFilter, onFilter }: { t: TicketRow; categoryFilter: string; onFilter: (cat: string) => void; }) { const [open, setOpen] = useState(false); const catCfg = CATEGORY_CFG[t.problem_category]; return ( <> setOpen(o => !o)} > {open ? : } {t.ticket_number} {t.company_name ?? '—'} {t.device_hostname ?? '—'} {catLabel(t.problem_category)} {resLabel(t.resolution_type)} {t.same_day_close ? : } {parseFloat(t.hours_worked).toFixed(2)}h {t.complexity} {timeAgo(t.ticket_created_at)} {open && (
{t.work_summary && (
Summary

{t.work_summary}

)}
{t.device_was_offline != null && ( {t.device_was_offline ? 'Device was offline' : 'Device was online'} )} {t.backup_completed_before_tech != null && ( {t.backup_completed_before_tech ? <> Backup auto-completed : <> Tech action required} )} {t.preventable != null && ( {t.preventable ? '⚠ Preventable' : '✓ Not preventable'} )}
{t.recommended_procedure && (
Recommended SOP

{t.recommended_procedure}

)}
)} ); } // ── Page ────────────────────────────────────────────────────────────────────── export default function VeeamAnalysisPage() { const [data, setData] = useState(null); const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); const [catFilter, setCatFilter] = useState(''); const [page, setPage] = useState(1); const [summary, setSummary] = useState(null); const [summaryLoading, setSummaryLoading] = useState(false); const [summaryError, setSummaryError] = useState(null); const pollRef = useRef | null>(null); const fetchData = useCallback(async (cat = catFilter, p = page) => { const params = new URLSearchParams({ page: String(p) }); if (cat) params.set('category', cat); const res = await fetch(`/api/veeam/ticket-analysis?${params}`); setData(await res.json()); setLoading(false); }, [catFilter, page]); const fetchStatus = useCallback(async () => { const res = await fetch('/api/veeam/ticket-analysis/status'); const s: RunStatus = await res.json(); setStatus(s); return s; }, []); useEffect(() => { fetchData(); fetchStatus(); }, []); const startPolling = useCallback(() => { if (pollRef.current) return; pollRef.current = setInterval(async () => { const s = await fetchStatus(); if (!s.is_running) { clearInterval(pollRef.current!); pollRef.current = null; fetchData(); } }, 2000); }, [fetchStatus, fetchData]); useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []); const handleRun = async (reanalyze = false) => { const res = await fetch('/api/veeam/ticket-analysis/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reanalyze }), }); const json = await res.json(); if (json.started) { await fetchStatus(); startPolling(); } }; const handleSummary = async () => { setSummaryLoading(true); setSummaryError(null); try { const res = await fetch('/api/veeam/ticket-analysis/summary', { method: 'POST' }); const json = await res.json(); if (json.error) setSummaryError(json.raw ? `${json.error}\n\n${json.raw}` : json.error); else setSummary(json); } catch (e: any) { setSummaryError(e.message); } finally { setSummaryLoading(false); } }; const handleCatFilter = (cat: string) => { const next = catFilter === cat ? '' : cat; setCatFilter(next); setPage(1); setLoading(true); fetchData(next, 1); }; const totalPages = data ? Math.ceil(data.total / (data.limit ?? 50)) : 1; const runPct = status?.is_running && status.run_total > 0 ? Math.round(100 * status.run_done / status.run_total) : null; return (
{/* Header */}

Veeam Backup — Ticket Analysis

AI classification of YTD Veeam tickets to identify failure patterns, required skills, and SOP gaps.

{status && !status.is_running && status.total_analyzed > 0 && ( )}
{/* Progress bar */} {status?.is_running && runPct !== null && (

{status.run_done} / {status.run_total} tickets analyzed {status.run_errors > 0 && ` · ${status.run_errors} errors`}

)} {/* Status line */} {status && !status.is_running && (

{status.total_analyzed} of {status.total_eligible} eligible tickets analyzed {status.last_analyzed_at && ` · Last run ${timeAgo(status.last_analyzed_at)}`} {status.total_eligible === 0 && ' — no Veeam tickets with time entries found YTD'}

)} {status?.total_analyzed === 0 && !status.is_running ? (

No analysis data yet.

Click Run Analysis to classify {status.total_eligible} tickets with time entries.

) : ( <> {/* Summary cards */} {loading && !data ? (
{[...Array(4)].map((_, i) => )}
) : data && (
{[ { label: 'Tickets Analyzed', value: data.stats.total_analyzed, sub: `of ${data.stats.total_ytd} YTD`, icon: Brain, cls: '', }, { label: 'Same-Day Close', value: `${data.stats.same_day_pct ?? 0}%`, sub: 'Opened & closed same day', icon: CheckCircle2, cls: 'text-green-500', }, { label: 'Avg Hours/Ticket', value: `${data.stats.avg_hours ?? 0}h`, sub: `${data.stats.auto_resolved_pct ?? 0}% auto-resolved before tech`, icon: Clock, cls: '', }, { label: 'Preventable', value: `${data.stats.preventable_pct ?? 0}%`, sub: `${data.stats.offline_pct ?? 0}% device was offline`, icon: AlertTriangle, cls: 'text-amber-500', }, ].map(({ label, value, sub, icon: Icon, cls }) => ( {label}
{value}

{sub}

))}
)} {/* Charts row */} {data && (
{/* Problem categories */} Problem Categories

Click a bar to filter the ticket list

({ name: catLabel(r.problem_category), key: r.problem_category, count: parseInt(r.count), avg_hours: parseFloat(r.avg_hours), }))} layout="vertical" margin={{ left: 10, right: 20, top: 0, bottom: 0 }} > handleCatFilter(String(d.key ?? ''))}> {data.by_category.map((r, i) => ( ))}
{/* Right column: Resolution + Skills */}
{/* Resolution types */} Resolution Types
{data.by_resolution.map(r => { const total = data.by_resolution.reduce((s, x) => s + parseInt(x.count), 0); const pct = total > 0 ? Math.round(100 * parseInt(r.count) / total) : 0; return (
{resLabel(r.resolution_type)}
{r.count}
); })}
{/* Skills */} Skills Required
{data.skills.map(s => ( {s.skill} {s.count} ))}
)} {/* Sonnet Summary */} {data && parseInt(data.stats.total_analyzed) > 0 && (
Operations Summary

Sonnet analysis of aggregate patterns — issue types, skills, and SOPs

{summaryError && (

{summaryError}

)} {summaryLoading && !summary && ( )} {summary && ( {/* Headline */}

{summary.analysis.headline}

{/* Key findings */}

Key Findings

    {summary.analysis.key_findings?.map((f, i) => (
  • {f}
  • ))}
{/* Issue breakdown */}

Issue Breakdown & SOPs

{summary.analysis.issue_breakdown?.map((item, i) => (
{catLabel(item.category)}

{item.insight}

SOP: {item.sop}
))}
{/* Bottom row: skills + quick wins + automation */}

Skills Assessment

{summary.analysis.skills_assessment}

{summary.analysis.training_priority && (

{summary.analysis.training_priority}

)}

Quick Wins

    {summary.analysis.quick_wins?.map((w, i) => (
  • {w}
  • ))}

Automation Opportunities

{summary.analysis.automation_opportunities}

Generated by {summary.model} · {new Date(summary.generated_at).toLocaleString()}

)}
)} {/* Ticket table */} {data && (
Tickets {catFilter && ( {catLabel(catFilter)} )}

{data.total} tickets

Ticket Client Device Category Resolution Same-day Hours Complexity Age {data.tickets.length > 0 ? data.tickets.map(t => ( )) : ( No analyzed tickets yet — run the analysis above. )}
{/* Pagination */} {totalPages > 1 && (
Page {page} of {totalPages}
)}
)} )}
); }