Add Vue 3 dashboard frontend with nginx reverse proxy, charts and LLM analysis UI
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
const { createApp } = Vue;
|
||||
|
||||
const api = axios.create({ baseURL: '/api' });
|
||||
|
||||
const chartColors = [
|
||||
'#38bdf8','#22d3ee','#818cf8','#c084fc','#f472b6','#fb7185','#34d399','#a3e635','#fbbf24','#f87171'
|
||||
];
|
||||
|
||||
function destroyChart(chart) { if(chart) { chart.destroy(); } }
|
||||
|
||||
function makeBarConfig(label, labels, data) {
|
||||
return {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label,
|
||||
data,
|
||||
backgroundColor: chartColors,
|
||||
borderRadius: 4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { color: '#27303f' }, ticks: { color: '#94a3b8' } },
|
||||
x: { grid: { display: false }, ticks: { color: '#94a3b8' } }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function makeLineConfig(labels, data) {
|
||||
return {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Events',
|
||||
data,
|
||||
borderColor: '#38bdf8',
|
||||
backgroundColor: 'rgba(56,189,248,0.15)',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
pointRadius: 3,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { color: '#27303f' }, ticks: { color: '#94a3b8' } },
|
||||
x: { grid: { color: '#27303f' }, ticks: { color: '#94a3b8' } }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
selectedFile: null,
|
||||
uploading: false,
|
||||
uploadMsg: '',
|
||||
uploadOk: false,
|
||||
stats: {
|
||||
overview: { total_entries: 0, firewall_entries: 0, proxy_entries: 0 },
|
||||
top_sources: [],
|
||||
top_destinations: [],
|
||||
top_ports: [],
|
||||
top_urls: [],
|
||||
actions: [],
|
||||
timeline: [],
|
||||
unique_counts: { unique_sources: 0, unique_destinations: 0 }
|
||||
},
|
||||
analyzeType: 'all',
|
||||
analyzeLimit: 100,
|
||||
analyzing: false,
|
||||
analysisResult: '',
|
||||
charts: {}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetchStats();
|
||||
setInterval(() => this.fetchStats(), 15000);
|
||||
},
|
||||
methods: {
|
||||
handleFile(e) {
|
||||
this.selectedFile = e.target.files[0];
|
||||
},
|
||||
async uploadFile() {
|
||||
if (!this.selectedFile) return;
|
||||
this.uploading = true;
|
||||
this.uploadMsg = '';
|
||||
const form = new FormData();
|
||||
form.append('file', this.selectedFile);
|
||||
try {
|
||||
const res = await api.post('/upload', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
this.uploadOk = true;
|
||||
this.uploadMsg = res.data.message;
|
||||
this.selectedFile = null;
|
||||
this.$el.querySelector('input[type=file]').value = '';
|
||||
this.fetchStats();
|
||||
} catch (e) {
|
||||
this.uploadOk = false;
|
||||
this.uploadMsg = e.response?.data?.detail || 'Upload fehlgeschlagen.';
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
}
|
||||
},
|
||||
async fetchStats() {
|
||||
try {
|
||||
const res = await api.get('/stats', { params: { limit: 20 } });
|
||||
this.stats = res.data;
|
||||
this.$nextTick(() => this.renderCharts());
|
||||
} catch (e) {
|
||||
console.error('Stats fetch error', e);
|
||||
}
|
||||
},
|
||||
renderCharts() {
|
||||
const s = this.stats;
|
||||
|
||||
destroyChart(this.charts.sources);
|
||||
if (s.top_sources.length) {
|
||||
const cfg = makeBarConfig('Hits', s.top_sources.map(x=>x.source_ip||'N/A'), s.top_sources.map(x=>x.count));
|
||||
this.charts.sources = new Chart(document.getElementById('chartSources'), cfg);
|
||||
}
|
||||
|
||||
destroyChart(this.charts.dests);
|
||||
if (s.top_destinations.length) {
|
||||
const cfg = makeBarConfig('Hits', s.top_destinations.map(x=>x.destination_ip||'N/A'), s.top_destinations.map(x=>x.count));
|
||||
this.charts.dests = new Chart(document.getElementById('chartDestinations'), cfg);
|
||||
}
|
||||
|
||||
destroyChart(this.charts.ports);
|
||||
if (s.top_ports.length) {
|
||||
const cfg = makeBarConfig('Hits', s.top_ports.map(x=>x.destination_port||'N/A'), s.top_ports.map(x=>x.count));
|
||||
this.charts.ports = new Chart(document.getElementById('chartPorts'), cfg);
|
||||
}
|
||||
|
||||
destroyChart(this.charts.timeline);
|
||||
if (s.timeline.length) {
|
||||
const cfg = makeLineConfig(s.timeline.map(x=>x.time_bucket), s.timeline.map(x=>x.count));
|
||||
this.charts.timeline = new Chart(document.getElementById('chartTimeline'), cfg);
|
||||
}
|
||||
},
|
||||
async runAnalysis() {
|
||||
this.analyzing = true;
|
||||
this.analysisResult = '';
|
||||
try {
|
||||
const res = await api.post('/analyze', null, {
|
||||
params: { log_type: this.analyzeType, limit: this.analyzeLimit }
|
||||
});
|
||||
this.analysisResult = res.data.analysis;
|
||||
} catch (e) {
|
||||
this.analysisResult = e.response?.data?.detail || 'Analyse fehlgeschlagen.';
|
||||
} finally {
|
||||
this.analyzing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).mount('#app');
|
||||
Reference in New Issue
Block a user