'use strict'; 'require view'; 'require dom'; 'require poll'; 'require ui'; 'require cve-triage.api as api'; return view.extend({ handleSaveApply: null, handleSave: null, handleReset: null, load: function() { return Promise.all([ api.callStatus(), api.callGetPending(), api.callGetAlerts(), api.callGetSummary() ]); }, renderStatusCard: function(status) { var localaiClass = status.localai_status === 'online' ? 'success' : 'danger'; return E('div', { 'class': 'cbi-section' }, [ E('h3', {}, 'Agent Status'), E('div', { 'class': 'cve-status-grid' }, [ E('div', { 'class': 'cve-stat' }, [ E('span', { 'class': 'cve-stat-value' }, status.enabled ? 'ON' : 'OFF'), E('span', { 'class': 'cve-stat-label' }, 'Agent') ]), E('div', { 'class': 'cve-stat' }, [ E('span', { 'class': 'cve-stat-value cve-' + localaiClass }, status.localai_status.toUpperCase()), E('span', { 'class': 'cve-stat-label' }, 'LocalAI') ]), E('div', { 'class': 'cve-stat' }, [ E('span', { 'class': 'cve-stat-value' }, status.pending_count), E('span', { 'class': 'cve-stat-label' }, 'Pending') ]), E('div', { 'class': 'cve-stat' }, [ E('span', { 'class': 'cve-stat-value cve-warning' }, status.alert_count), E('span', { 'class': 'cve-stat-label' }, 'Alerts') ]) ]), E('div', { 'class': 'cve-packages' }, [ E('small', {}, 'Monitored: ' + status.packages.opkg + ' opkg, ' + status.packages.lxc + ' LXC, ' + status.packages.docker + ' Docker') ]), status.last_run ? E('div', { 'class': 'cve-lastrun' }, [ E('small', {}, 'Last run: ' + status.last_run) ]) : null ]); }, renderSummaryCard: function(summaryData) { var summary = summaryData.summary || {}; var riskScore = summary.risk_score || 0; var riskClass = riskScore >= 70 ? 'danger' : (riskScore >= 40 ? 'warning' : 'success'); return E('div', { 'class': 'cbi-section' }, [ E('h3', {}, 'Security Summary'), E('div', { 'class': 'cve-risk-score cve-' + riskClass }, [ E('span', { 'class': 'cve-risk-value' }, riskScore), E('span', { 'class': 'cve-risk-label' }, '/100 Risk Score') ]), E('p', { 'class': 'cve-summary-text' }, summary.summary || 'No analysis available. Run a triage cycle.') ]); }, renderAlertsCard: function(alertsData) { var alerts = alertsData.alerts || []; var activeAlerts = alerts.filter(function(a) { return !a.acknowledged; }); if (activeAlerts.length === 0) { return E('div', { 'class': 'cbi-section' }, [ E('h3', {}, 'Alerts'), E('p', { 'class': 'cve-no-data' }, 'No active alerts') ]); } var self = this; return E('div', { 'class': 'cbi-section' }, [ E('h3', {}, 'Active Alerts (' + activeAlerts.length + ')'), E('div', { 'class': 'cve-alerts' }, activeAlerts.slice(0, 5).map(function(alert) { return E('div', { 'class': 'cve-alert cve-alert-' + api.getSeverityClass(alert.severity) }, [ E('div', { 'class': 'cve-alert-header' }, [ E('span', { 'class': 'cve-severity' }, api.getSeverityIcon(alert.severity)), api.formatCVE(alert.cve), E('button', { 'class': 'cbi-button cbi-button-action cve-btn-ack', 'click': function() { api.callAckAlert(alert.id).then(function() { window.location.reload(); }); } }, 'Ack') ]), E('div', { 'class': 'cve-alert-message' }, alert.message) ]); }) ) ]); }, renderPendingCard: function(pendingData) { var pending = pendingData.pending || []; var self = this; return E('div', { 'class': 'cbi-section' }, [ E('h3', {}, 'Pending Recommendations (' + pending.length + ')'), pending.length > 0 ? E('div', { 'class': 'cve-actions-bar' }, [ E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { if (confirm('Approve all ' + pending.length + ' recommendations?')) { api.callApproveAll().then(function() { window.location.reload(); }); } } }, 'Approve All'), E('button', { 'class': 'cbi-button cbi-button-negative', 'click': function() { if (confirm('Clear all pending recommendations?')) { api.callClearPending().then(function() { window.location.reload(); }); } } }, 'Clear All') ]) : null, pending.length === 0 ? E('p', { 'class': 'cve-no-data' }, 'No pending recommendations') : E('table', { 'class': 'table cve-table' }, [ E('thead', {}, E('tr', {}, [ E('th', {}, 'Severity'), E('th', {}, 'CVE'), E('th', {}, 'Package'), E('th', {}, 'Action'), E('th', {}, 'Urgency'), E('th', {}, 'Actions') ])), E('tbody', {}, pending.map(function(rec) { return E('tr', { 'class': 'cve-row-' + api.getSeverityClass(rec.severity) }, [ E('td', {}, E('span', { 'class': 'cve-badge cve-badge-' + api.getSeverityClass(rec.severity) }, rec.severity.toUpperCase())), E('td', {}, api.formatCVE(rec.cve)), E('td', {}, rec.affected_package || '-'), E('td', {}, rec.action), E('td', {}, rec.urgency), E('td', {}, [ E('button', { 'class': 'cbi-button cbi-button-positive cve-btn-sm', 'click': function() { api.callApprove(rec.id).then(function() { window.location.reload(); }); } }, '\u2713'), ' ', E('button', { 'class': 'cbi-button cbi-button-negative cve-btn-sm', 'click': function() { api.callReject(rec.id, 'Manual rejection').then(function() { window.location.reload(); }); } }, '\u2717') ]) ]); }) ) ]) ]); }, render: function(data) { var status = data[0] || {}; var pending = data[1] || {}; var alerts = data[2] || {}; var summary = data[3] || {}; var self = this; // Inject CSS var css = E('style', {}, ` .cve-status-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin: 1rem 0; } .cve-stat { text-align: center; padding: 1rem; background: var(--background-color-low, #f5f5f5); border-radius: 8px; } .cve-stat-value { display: block; font-size: 1.5rem; font-weight: bold; } .cve-stat-label { font-size: 0.85rem; color: var(--text-color-medium, #666); } .cve-success { color: #28a745; } .cve-warning { color: #ffc107; } .cve-danger { color: #dc3545; } .cve-risk-score { text-align: center; padding: 2rem; margin: 1rem 0; border-radius: 12px; } .cve-risk-score.cve-success { background: rgba(40, 167, 69, 0.1); } .cve-risk-score.cve-warning { background: rgba(255, 193, 7, 0.1); } .cve-risk-score.cve-danger { background: rgba(220, 53, 69, 0.1); } .cve-risk-value { font-size: 3rem; font-weight: bold; } .cve-risk-label { display: block; font-size: 0.9rem; } .cve-summary-text { margin-top: 1rem; padding: 1rem; background: var(--background-color-low, #f5f5f5); border-radius: 8px; } .cve-alert { padding: 0.75rem; margin-bottom: 0.5rem; border-radius: 6px; border-left: 4px solid; } .cve-alert-danger { border-color: #dc3545; background: rgba(220, 53, 69, 0.05); } .cve-alert-warning { border-color: #ffc107; background: rgba(255, 193, 7, 0.05); } .cve-alert-header { display: flex; align-items: center; gap: 0.5rem; } .cve-alert-message { margin-top: 0.5rem; font-size: 0.9rem; } .cve-severity { font-size: 1.2rem; } .cve-link { color: #0d6efd; text-decoration: none; font-family: monospace; } .cve-link:hover { text-decoration: underline; } .cve-no-data { color: var(--text-color-medium, #666); font-style: italic; } .cve-table { width: 100%; } .cve-badge { padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: bold; } .cve-badge-danger { background: #dc3545; color: white; } .cve-badge-warning { background: #ffc107; color: black; } .cve-badge-primary { background: #0d6efd; color: white; } .cve-badge-secondary { background: #6c757d; color: white; } .cve-row-danger { background: rgba(220, 53, 69, 0.05); } .cve-row-warning { background: rgba(255, 193, 7, 0.05); } .cve-actions-bar { margin-bottom: 1rem; display: flex; gap: 0.5rem; } .cve-btn-sm { padding: 0.25rem 0.5rem; font-size: 0.85rem; } .cve-btn-ack { padding: 0.2rem 0.5rem; font-size: 0.75rem; margin-left: auto; } .cve-packages, .cve-lastrun { margin-top: 0.5rem; color: var(--text-color-medium, #666); } .cve-header-actions { float: right; } `); return E('div', { 'class': 'cbi-map' }, [ css, E('h2', {}, [ 'CVE Triage', E('span', { 'class': 'cve-header-actions' }, [ E('button', { 'class': 'cbi-button cbi-button-action', 'click': function() { ui.showModal('Running Triage...', [ E('p', { 'class': 'spinning' }, 'Starting CVE triage cycle...') ]); api.callRun().then(function() { setTimeout(function() { ui.hideModal(); window.location.reload(); }, 3000); }); } }, 'Run Triage') ]) ]), E('div', { 'class': 'cve-grid', 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;' }, [ this.renderStatusCard(status), this.renderSummaryCard(summary) ]), this.renderAlertsCard(alerts), this.renderPendingCard(pending) ]); } });