import { Component, createEffect, createSignal, For, Show } from 'solid-js'; import { AIAPI } from '@/api/ai'; import type { FailurePrediction, InfrastructureChange, RemediationRecord, RemediationStats, ResourceCorrelation } from '@/types/aiIntelligence'; const DEFAULT_UPGRADE_URL = 'https://pulsemonitor.app/pro'; interface InsightRow { id: string; type: 'prediction' | 'impact' | 'memory'; typeBadge: string; typeBadgeClass: string; title: string; subtitle: string; timestamp: string; confidence?: number; locked: boolean; badgeClass: string; } /** * AIOverviewTable displays AI Insights, Pulse AI Impact, and Operational Memory * in a unified, scannable table format */ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) => { const [predictions, setPredictions] = createSignal([]); const [correlations, setCorrelations] = createSignal([]); const [remediations, setRemediations] = createSignal([]); const [remediationStats, setRemediationStats] = createSignal(null); const [changes, setChanges] = createSignal([]); const [loading, setLoading] = createSignal(false); const [error, setError] = createSignal(''); // Locked states const [insightsLocked, setInsightsLocked] = createSignal(false); const [impactLocked, setImpactLocked] = createSignal(false); const [memoryLocked, setMemoryLocked] = createSignal(false); const [upgradeUrl, setUpgradeUrl] = createSignal(DEFAULT_UPGRADE_URL); // Locked counts (for locked state display) const [insightsLockedCount, setInsightsLockedCount] = createSignal(0); const [memoryLockedCount, setMemoryLockedCount] = createSignal(0); const showWhenEmpty = () => Boolean(props.showWhenEmpty); const loadData = async () => { setLoading(true); setError(''); try { const [predResp, corrResp, remResp, changesResp] = await Promise.all([ AIAPI.getPredictions(), AIAPI.getCorrelations(), AIAPI.getRemediations({ hours: 168, limit: 6 }), AIAPI.getRecentChanges(24), ]); // Handle insights lock const insightsLockedState = Boolean(predResp.license_required || corrResp.license_required); setInsightsLocked(insightsLockedState); if (insightsLockedState) { const predCount = predResp.count || 0; const corrCount = corrResp.count || 0; setInsightsLockedCount(predCount + corrCount); setPredictions([]); setCorrelations([]); } else { setInsightsLockedCount(0); setPredictions(predResp.predictions || []); setCorrelations(corrResp.correlations || []); } // Handle impact lock setImpactLocked(Boolean(remResp.license_required)); setRemediationStats(remResp.stats || null); setRemediations(remResp.remediations || []); // Handle memory lock const memoryLockedState = Boolean(changesResp.license_required); setMemoryLocked(memoryLockedState); if (memoryLockedState) { setMemoryLockedCount(changesResp.count || 0); setChanges([]); } else { setMemoryLockedCount(0); setChanges(changesResp.changes || []); } // Set upgrade URL from any response setUpgradeUrl( predResp.upgrade_url || corrResp.upgrade_url || remResp.upgrade_url || changesResp.upgrade_url || DEFAULT_UPGRADE_URL ); } catch (e) { console.error('Failed to load AI overview data:', e); setError('Failed to load AI overview data.'); } finally { setLoading(false); } }; createEffect(() => { void loadData(); }); // Format relative time const formatRelativeTime = (ts: string) => { const date = new Date(ts); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); }; // Format days until const formatDaysUntil = (days: number) => { if (days < 0) return 'Overdue'; if (days < 1) return 'Today'; if (days < 2) return 'Tomorrow'; return `In ${Math.round(days)} days`; }; // Build unified rows const unifiedRows = (): InsightRow[] => { const rows: InsightRow[] = []; // Predictions for (const pred of predictions()) { const colorClass = pred.is_overdue || pred.days_until < 0 ? 'text-red-600 dark:text-red-400' : pred.days_until < 3 ? 'text-amber-600 dark:text-amber-400' : pred.days_until < 7 ? 'text-yellow-600 dark:text-yellow-400' : 'text-blue-600 dark:text-blue-400'; rows.push({ id: `pred-${pred.resource_id}-${pred.event_type}`, type: 'prediction', typeBadge: 'Prediction', typeBadgeClass: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300', title: formatEventName(pred.event_type), subtitle: pred.basis, timestamp: formatDaysUntil(pred.days_until), confidence: pred.confidence, locked: false, badgeClass: colorClass, }); } // Correlations for (const corr of correlations()) { rows.push({ id: `corr-${corr.source_id}-${corr.target_id}`, type: 'prediction', typeBadge: 'Dependency', typeBadgeClass: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300', title: `${corr.source_name || corr.source_id} → ${corr.target_name || corr.target_id}`, subtitle: corr.description || `${corr.event_pattern} (${corr.occurrences} observations)`, timestamp: `Delay: ${corr.avg_delay}`, confidence: corr.confidence, locked: false, badgeClass: 'text-indigo-600 dark:text-indigo-400', }); } // Remediations - ONLY show actual ACTIONS (restarts, resizes, cleanups, fixes) // Skip diagnostic commands (df, grep, cat, tail, ps) - they don't provide lasting value const isActionableCommand = (action: string): boolean => { const cmd = action.trim().replace(/^\[[^\]]+\]\s*/, ''); // Strip [host] prefix const actionPatterns = [ 'docker restart', 'docker start', 'docker stop', 'docker rm', 'docker compose up', 'docker compose down', 'docker compose restart', 'systemctl restart', 'systemctl start', 'systemctl stop', 'systemctl enable', 'systemctl disable', 'service restart', 'service start', 'service stop', 'pct resize', 'pct start', 'pct stop', 'pct shutdown', 'pct reboot', 'qm resize', 'qm start', 'qm stop', 'qm shutdown', 'qm reboot', 'rm -', 'rm /', 'chmod', 'chown', 'mkdir', 'mv ', 'cp ', 'apt install', 'apt upgrade', 'apt remove', 'yum install', 'dnf install', 'pip install', 'npm install', 'kill ', 'pkill ', 'killall ', 'reboot', 'shutdown', ]; return actionPatterns.some(pattern => cmd.includes(pattern)); }; const actionableRemediations = remediations().filter(rem => isActionableCommand(rem.action)); for (const rem of actionableRemediations) { const outcomeBadgeClass: Record = { resolved: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300', partial: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', failed: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', }; rows.push({ id: `rem-${rem.finding_id}-${rem.timestamp}`, type: 'impact', typeBadge: rem.outcome === 'resolved' ? 'Fixed' : rem.outcome === 'failed' ? 'Failed' : 'Action', typeBadgeClass: outcomeBadgeClass[rem.outcome] || 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300', title: rem.summary || summarizeValue(rem.problem, rem.action), // Use backend summary or fallback subtitle: rem.resource_name || '', timestamp: formatRelativeTime(rem.timestamp), locked: false, badgeClass: 'text-emerald-600 dark:text-emerald-400', }); } // Changes - only show meaningful changes, not just "created" (infrastructure detection) // Valuable changes: config, status, migrated, restarted, deleted, backed_up // Skip: created (just means AI patrol discovered the resource) const meaningfulChanges = changes().filter(c => c.change_type !== 'created'); for (const change of meaningfulChanges.slice(0, 6)) { const changeTypeBadgeClass: Record = { deleted: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', config: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300', status: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', migrated: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300', restarted: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300', backed_up: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300', }; rows.push({ id: `change-${change.resource_id}-${change.detected_at}`, type: 'memory', typeBadge: formatChangeType(change.change_type), typeBadgeClass: changeTypeBadgeClass[change.change_type] || 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300', title: change.resource_name || change.resource_id, subtitle: change.description || 'Change detected by AI Patrol.', timestamp: formatRelativeTime(change.detected_at), locked: false, badgeClass: 'text-teal-600 dark:text-teal-400', }); } return rows; }; const formatEventName = (eventType: string) => { const names: Record = { high_memory: 'High Memory', high_cpu: 'High CPU', disk_full: 'Disk Full', oom: 'Out of Memory', restart: 'Restart', unresponsive: 'Unresponsive', backup_failed: 'Backup Failure', }; if (names[eventType]) return names[eventType]; return eventType .split(/[_-]/) .map(part => { if (part === 'cpu') return 'CPU'; if (part === 'vm') return 'VM'; if (part === 'pbs') return 'PBS'; if (part === 'raid') return 'RAID'; return part.charAt(0).toUpperCase() + part.slice(1); }) .join(' '); }; const formatChangeType = (changeType: string) => { const labels: Record = { created: 'Created', deleted: 'Deleted', config: 'Config', status: 'Status', migrated: 'Migrated', restarted: 'Restarted', backed_up: 'Backup', }; if (labels[changeType]) return labels[changeType]; return changeType.split(/[_-]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' '); }; // Summarize a shell command action into a human-readable description // Prefixed with underscore - not currently used but kept for future expansion const _summarizeAction = (action: string): { title: string; subtitle: string } => { const cmd = action.trim(); // Common command patterns and their summaries if (cmd.includes('df -h') || cmd.includes('du -s')) { const pathMatch = cmd.match(/\/([\w/.-]+)/); const path = pathMatch ? pathMatch[0] : 'storage'; return { title: 'Checked disk usage', subtitle: `Analyzed space on ${path}` }; } if (cmd.includes('docker ps') || cmd.includes('docker status')) { const containerMatch = cmd.match(/name[=:]?\s*(\w+)/i); const container = containerMatch ? containerMatch[1] : 'containers'; return { title: 'Checked container status', subtitle: `Verified ${container} is running` }; } if (cmd.includes('docker restart') || cmd.includes('docker start')) { const containerMatch = cmd.match(/(?:restart|start)\s+(\w+)/i); const container = containerMatch ? containerMatch[1] : 'container'; return { title: 'Restarted container', subtitle: `Restored ${container} service` }; } if (cmd.includes('systemctl restart') || (cmd.includes('service') && cmd.includes('restart'))) { const serviceMatch = cmd.match(/(?:restart|start)\s+(\S+)/i); const service = serviceMatch ? serviceMatch[1] : 'service'; return { title: 'Restarted service', subtitle: `Restored ${service}` }; } if (cmd.includes('grep') && cmd.includes('config')) { return { title: 'Checked configuration', subtitle: 'Verified config settings' }; } if (cmd.includes('grep')) { return { title: 'Searched logs/config', subtitle: 'Retrieved diagnostic information' }; } if (cmd.includes('tail') || cmd.includes('cat') || cmd.includes('head')) { return { title: 'Inspected logs/files', subtitle: 'Retrieved diagnostic information' }; } if (cmd.includes('ps aux') || cmd.includes('top') || cmd.includes('htop')) { return { title: 'Checked processes', subtitle: 'Analyzed running processes' }; } if (cmd.includes('free') || cmd.includes('memory')) { return { title: 'Checked memory usage', subtitle: 'Analyzed memory allocation' }; } if (cmd.includes('ping') || cmd.includes('curl') || cmd.includes('wget')) { return { title: 'Tested connectivity', subtitle: 'Verified network connection' }; } if (cmd.startsWith('[host]') || cmd.startsWith('[')) { // Agent command - extract the actual command const innerCmd = cmd.replace(/^\[[\w\s]+\]\s*/, ''); const inner = _summarizeAction(innerCmd); return { title: inner.title, subtitle: `${inner.subtitle} (via agent)` }; } // Fallback: truncate and show the command const truncated = cmd.length > 50 ? cmd.substring(0, 47) + '...' : cmd; return { title: 'Executed command', subtitle: truncated }; }; // Create a meaningful summary from the ACTION (command) only // The problem field is just chat context and not useful here const summarizeValue = (_problem: string, action: string): string => { const cmd = action.trim(); // Extract target/context from the command (container names, paths, services) const extractTarget = (): string | null => { // Container name from docker commands const containerMatch = cmd.match(/name[=:]?\s*(\w+)/i) || cmd.match(/filter.*?(\w+)/); if (containerMatch) return containerMatch[1]; // Service name from systemctl const serviceMatch = cmd.match(/systemctl\s+\w+\s+(\S+)/); if (serviceMatch) return serviceMatch[1]; // Path for disk commands - get the meaningful part const pathMatch = cmd.match(/\/([\w]+)(?:\/[\w_-]+)*(?:\s|$)/); if (pathMatch) { const fullPath = cmd.match(/\/[\w\/_-]+/)?.[0] || ''; // Extract meaningful name from path if (fullPath.includes('frigate')) return 'Frigate'; if (fullPath.includes('plex')) return 'Plex'; if (fullPath.includes('docker')) return 'Docker'; if (fullPath.includes('recordings')) return 'recordings'; if (fullPath.includes('media')) return 'media'; if (fullPath.includes('config')) return 'config'; return fullPath.split('/').filter(Boolean).pop() || null; } // Config file reference if (cmd.includes('config.yml') || cmd.includes('config.yaml')) return 'config'; return null; }; const target = extractTarget(); const targetStr = target ? ` (${target})` : ''; // Generate meaningful descriptions based on command type if (cmd.includes('docker restart') || cmd.includes('docker start')) { return `Restarted container${targetStr}`; } if (cmd.includes('docker stop')) { return `Stopped container${targetStr}`; } if (cmd.includes('docker ps') || cmd.includes('docker status')) { return `Verified container status${targetStr}`; } if (cmd.includes('docker logs')) { return `Retrieved container logs${targetStr}`; } if (cmd.includes('systemctl restart') || cmd.includes('service') && cmd.includes('restart')) { return `Restarted service${targetStr}`; } if (cmd.includes('systemctl status')) { return `Checked service status${targetStr}`; } if ((cmd.includes('df') || cmd.includes('du')) && target) { return `Analyzed storage usage${targetStr}`; } if (cmd.includes('df') || cmd.includes('du')) { return 'Analyzed disk space'; } if (cmd.includes('grep') && (cmd.includes('config') || cmd.includes('.yml') || cmd.includes('.yaml'))) { return `Inspected configuration${targetStr}`; } if (cmd.includes('grep') || cmd.includes('tail -f') || cmd.includes('journalctl')) { return `Reviewed logs${targetStr}`; } if (cmd.includes('tail') || cmd.includes('cat') || cmd.includes('head')) { return `Retrieved file contents${targetStr}`; } if (cmd.includes('ping') || cmd.includes('curl') || cmd.includes('wget')) { return 'Tested network connectivity'; } if (cmd.includes('free') || cmd.includes('/proc/meminfo')) { return 'Checked memory usage'; } if (cmd.includes('ps aux') || cmd.includes('top') || cmd.includes('htop')) { return 'Analyzed running processes'; } if (cmd.includes('chmod') || cmd.includes('chown')) { return `Fixed permissions${targetStr}`; } if (cmd.includes('rm ') || cmd.includes('rm -')) { return `Cleaned up files${targetStr}`; } if (cmd.includes('mkdir')) { return `Created directory${targetStr}`; } if (cmd.startsWith('[host]') || cmd.startsWith('[')) { // Agent command - recurse with inner command const innerCmd = cmd.replace(/^\[[\w\s]+\]\s*/, ''); return summarizeValue('', innerCmd); } return 'Ran diagnostic'; }; const totalItems = () => unifiedRows().length; const anyLocked = () => insightsLocked() || impactLocked() || memoryLocked(); const shouldShow = () => showWhenEmpty() || loading() || totalItems() > 0 || anyLocked(); // Stats for quick summary const statsDisplay = () => { const stats = remediationStats(); if (!stats) return null; return stats; }; return (
{/* Header */}
AI Intelligence Summary
0}> {totalItems()} items Some Locked
{/* Quick Stats */}
{statsDisplay()!.resolved} resolved
{statsDisplay()!.automatic} auto-fix
{/* Loading */}
Loading AI intelligence...
{/* Error */}
{error()}
{/* Locked notice (if all are locked) */}

Pulse Pro required

Full AI intelligence features including predictions, impact tracking, and operational memory require Pulse Pro.

Upgrade
{/* Table */} 0 || !anyLocked())}> 0} fallback={

No AI insights yet. The AI will learn patterns and surface intelligence over time.

} >
{(row) => ( )}
Type Details When
{row.typeBadge}
{row.title} {row.subtitle}
{Math.round(row.confidence! * 100)}% confidence
{row.timestamp}
{/* Section locked notices (partial lock) */}
{insightsLockedCount()} AI Insights locked
Impact details locked
{memoryLockedCount()} changes locked
Upgrade to Pulse Pro
); };