secubox-openwrt/package/secubox/luci-app-ai-gateway/htdocs/luci-static/resources/view/ai-gateway/audit.js
CyberMind-FR 59dbd714a5 fix(tools): Add curl redirect handling to image builder scripts
Validated secubox-image.sh and secubox-sysupgrade.sh scripts:
- Fixed curl redirect issue: ASU API returns 301 redirects
- Added -L flag to 9 curl calls across both scripts
- Verified all device profiles valid (mochabin, espressobin, x86-64)
- Confirmed POSIX sh compatibility for sysupgrade script
- Validated first-boot script syntax

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-03 09:44:04 +01:00

242 lines
8.8 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require poll';
'require fs';
var callGetAuditStats = rpc.declare({
object: 'luci.ai-gateway',
method: 'get_audit_stats',
expect: {}
});
var kissCSS = `
.audit-container { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.audit-container h2 { margin: 0 0 8px 0; }
.audit-container .subtitle { color: var(--text-secondary, #64748b); margin-bottom: 24px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: var(--bg-secondary, #f8fafc); border: 1px solid var(--border-color, #e2e8f0); border-radius: 12px; padding: 20px; text-align: center; }
.stat-card .value { font-size: 2em; font-weight: 700; }
.stat-card .label { color: var(--text-secondary, #64748b); font-size: 0.9em; margin-top: 4px; }
.stat-local { color: #22c55e; }
.stat-sanitized { color: #f59e0b; }
.stat-cloud { color: #3b82f6; }
.chart-section { margin-bottom: 24px; }
.chart-section h3 { margin-bottom: 16px; font-size: 1.1em; }
.chart-bar { display: flex; height: 32px; border-radius: 8px; overflow: hidden; background: #e2e8f0; }
.chart-bar .segment { display: flex; align-items: center; justify-content: center; color: white; font-weight: 500; font-size: 0.85em; transition: width 0.3s; }
.segment-local { background: #22c55e; }
.segment-sanitized { background: #f59e0b; }
.segment-cloud { background: #3b82f6; }
.chart-legend { display: flex; gap: 24px; margin-top: 12px; }
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 0.9em; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; }
.info-box { padding: 16px 20px; background: #f0fdf4; border: 1px solid #86efac; border-radius: 8px; margin-bottom: 24px; }
.info-box h4 { margin: 0 0 8px 0; color: #166534; }
.info-box p { margin: 0; color: #14532d; font-size: 0.9em; }
.info-box code { background: #dcfce7; padding: 2px 6px; border-radius: 4px; font-size: 0.85em; }
.log-section { margin-top: 24px; }
.log-section h3 { margin-bottom: 12px; }
.log-info { color: var(--text-secondary, #64748b); font-size: 0.9em; margin-bottom: 12px; }
.log-viewer { background: #1e293b; border-radius: 8px; padding: 16px; font-family: monospace; font-size: 0.85em; color: #e2e8f0; max-height: 400px; overflow-y: auto; }
.log-line { padding: 4px 0; border-bottom: 1px solid #334155; }
.log-line:last-child { border-bottom: none; }
.log-time { color: #64748b; }
.log-local { color: #4ade80; }
.log-sanitized { color: #fbbf24; }
.log-cloud { color: #60a5fa; }
.btn { padding: 8px 16px; border-radius: 6px; font-weight: 500; cursor: pointer; border: none; }
.btn-secondary { background: #64748b; color: white; }
.btn-secondary:hover { background: #475569; }
@media (prefers-color-scheme: dark) {
.stat-card { background: #1e293b; border-color: #334155; }
.info-box { background: #14532d; border-color: #22c55e; }
.info-box h4, .info-box p { color: #bbf7d0; }
.info-box code { background: #166534; color: #dcfce7; }
}
`;
return view.extend({
title: 'Audit Log',
load: function() {
return Promise.all([
callGetAuditStats(),
fs.read('/var/log/ai-gateway-audit.jsonl').catch(function() { return ''; })
]);
},
render: function(data) {
var stats = data[0].result || data[0] || {};
var logContent = data[1] || '';
var container = E('div', { 'class': 'audit-container' });
container.appendChild(E('style', {}, kissCSS));
container.appendChild(E('h2', {}, 'Audit Log'));
container.appendChild(E('p', { 'class': 'subtitle' },
'ANSSI CSPN compliance audit trail. All AI Gateway classification decisions are logged.'));
// ANSSI Info Box
container.appendChild(E('div', { 'class': 'info-box' }, [
E('h4', {}, 'ANSSI CSPN Compliance'),
E('p', {}, [
'Audit logs are stored at ',
E('code', {}, '/var/log/ai-gateway-audit.jsonl'),
' in JSON Lines format. Export for compliance review with: ',
E('code', {}, 'aigatewayctl audit export')
])
]));
// Stats Grid
var localOnly = stats.local_only || 0;
var sanitized = stats.sanitized || 0;
var cloudDirect = stats.cloud_direct || 0;
var total = localOnly + sanitized + cloudDirect;
var statsGrid = E('div', { 'class': 'stats-grid' });
statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [
E('div', { 'class': 'value' }, String(total)),
E('div', { 'class': 'label' }, 'Total Requests')
]));
statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [
E('div', { 'class': 'value stat-local' }, String(localOnly)),
E('div', { 'class': 'label' }, 'LOCAL_ONLY')
]));
statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [
E('div', { 'class': 'value stat-sanitized' }, String(sanitized)),
E('div', { 'class': 'label' }, 'SANITIZED')
]));
statsGrid.appendChild(E('div', { 'class': 'stat-card' }, [
E('div', { 'class': 'value stat-cloud' }, String(cloudDirect)),
E('div', { 'class': 'label' }, 'CLOUD_DIRECT')
]));
container.appendChild(statsGrid);
// Distribution Chart
if (total > 0) {
var chartSection = E('div', { 'class': 'chart-section' });
chartSection.appendChild(E('h3', {}, 'Classification Distribution'));
var localPct = Math.round((localOnly / total) * 100);
var sanitizedPct = Math.round((sanitized / total) * 100);
var cloudPct = 100 - localPct - sanitizedPct;
var chartBar = E('div', { 'class': 'chart-bar' });
if (localPct > 0) {
chartBar.appendChild(E('div', {
'class': 'segment segment-local',
'style': 'width: ' + localPct + '%;'
}, localPct + '%'));
}
if (sanitizedPct > 0) {
chartBar.appendChild(E('div', {
'class': 'segment segment-sanitized',
'style': 'width: ' + sanitizedPct + '%;'
}, sanitizedPct + '%'));
}
if (cloudPct > 0) {
chartBar.appendChild(E('div', {
'class': 'segment segment-cloud',
'style': 'width: ' + cloudPct + '%;'
}, cloudPct + '%'));
}
chartSection.appendChild(chartBar);
chartSection.appendChild(E('div', { 'class': 'chart-legend' }, [
E('div', { 'class': 'legend-item' }, [
E('span', { 'class': 'legend-dot', 'style': 'background: #22c55e;' }),
E('span', {}, 'Local Only (' + localOnly + ')')
]),
E('div', { 'class': 'legend-item' }, [
E('span', { 'class': 'legend-dot', 'style': 'background: #f59e0b;' }),
E('span', {}, 'Sanitized (' + sanitized + ')')
]),
E('div', { 'class': 'legend-item' }, [
E('span', { 'class': 'legend-dot', 'style': 'background: #3b82f6;' }),
E('span', {}, 'Cloud Direct (' + cloudDirect + ')')
])
]));
container.appendChild(chartSection);
}
// Log Viewer
var logSection = E('div', { 'class': 'log-section' });
logSection.appendChild(E('h3', {}, 'Recent Audit Entries'));
logSection.appendChild(E('p', { 'class': 'log-info' },
'Last 50 classification decisions (newest first)'));
var logViewer = E('div', { 'class': 'log-viewer', 'id': 'log-viewer' });
if (logContent) {
var lines = logContent.trim().split('\n').slice(-50).reverse();
lines.forEach(function(line) {
if (!line.trim()) return;
try {
var entry = JSON.parse(line);
var classClass = 'log-' + (entry.classification || 'local').replace('_only', '').replace('_direct', '');
var time = entry.timestamp ? entry.timestamp.split('T')[1].split('.')[0] : '';
logViewer.appendChild(E('div', { 'class': 'log-line' }, [
E('span', { 'class': 'log-time' }, '[' + time + '] '),
E('span', { 'class': classClass }, (entry.classification || 'unknown').toUpperCase()),
E('span', {}, ' - ' + (entry.reason || entry.classification_reason || 'classified')),
entry.provider ? E('span', { 'style': 'color: #94a3b8;' }, ' → ' + entry.provider) : ''
]));
} catch (e) {
logViewer.appendChild(E('div', { 'class': 'log-line' }, line));
}
});
} else {
logViewer.appendChild(E('div', { 'class': 'log-line', 'style': 'color: #64748b;' },
'No audit log entries yet. Entries appear when requests are processed through the AI Gateway.'));
}
logSection.appendChild(logViewer);
logSection.appendChild(E('div', { 'style': 'margin-top: 12px;' }, [
E('button', {
'class': 'btn btn-secondary',
'click': function() { window.location.reload(); }
}, 'Refresh')
]));
container.appendChild(logSection);
// Setup polling
poll.add(this.pollStats.bind(this), 30);
return container;
},
pollStats: function() {
return callGetAuditStats().then(function(stats) {
var s = stats.result || stats || {};
var cards = document.querySelectorAll('.stat-card .value');
if (cards.length >= 4) {
var total = (s.local_only || 0) + (s.sanitized || 0) + (s.cloud_direct || 0);
cards[0].textContent = String(total);
cards[1].textContent = String(s.local_only || 0);
cards[2].textContent = String(s.sanitized || 0);
cards[3].textContent = String(s.cloud_direct || 0);
}
});
}
});