Add Vue 3 dashboard frontend with nginx reverse proxy, charts and LLM analysis UI
This commit is contained in:
@@ -1,21 +1,22 @@
|
||||
# Log Analyzer Backend
|
||||
# Log Analyzer Backend + Frontend
|
||||
|
||||
CPU-LLM-gestütztes Backend zur Auswertung von Firewall- und Proxy-Logs. Parst Dateien, aggregiert Statistiken (Top-Hits, Quellen, Ziele, Ports, URLs) und nutzt ein lokales LLM (via Ollama) für die Analyse.
|
||||
CPU-LLM-gestütztes Dashboard zur Auswertung von Firewall- und Proxy-Logs. Parst Dateien, zeigt übersichtliche Statistiken (Top-Hits, Quellen, Ziele, Ports, URLs, Zeitverlauf) und nutzt ein lokales LLM (via Ollama) für die Analyse.
|
||||
|
||||
## Features
|
||||
- **Log-Parsing:** Unterstützt iptables, pfSense, Cisco ASA, Squid, nginx
|
||||
- **Statistiken:** Top Quellen, Ziele, Ports, URLs, Actions, Timeline
|
||||
- **LLM-Analyse:** Zusammenfassung und Anomalie-Erkennung via lokalem Ollama-Modell
|
||||
- **REST-API:** FastAPI mit automatischer OpenAPI-Doku unter `/docs`
|
||||
- **Docker-Compose:** Schneller Start mit Backend + nginx Reverse Proxy
|
||||
- **Web-Dashboard:** Vue 3 + Chart.js SPA, erreichbar auf Port 80
|
||||
- **Docker-Compose:** Ein Befehl zum Starten von Backend + Frontend
|
||||
|
||||
## Architektur
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
|
||||
│ Client │──────▶│ nginx:80 │──────▶│ FastAPI Backend:8000 │
|
||||
└──────────────┘ └──────────────┘ │ - SQLite (Volumen) │
|
||||
│ - Log-Parser │
|
||||
│ - Ollama-Client │
|
||||
│ Browser │──────▶│ Frontend:80 │──────▶│ FastAPI Backend:8000 │
|
||||
└──────────────┘ │ (nginx + │ │ - SQLite (Volumen) │
|
||||
│ Vue SPA) │ │ - Log-Parser │
|
||||
└──────────────┘ │ - Ollama-Client │
|
||||
└───────────────────────┘
|
||||
│
|
||||
▼
|
||||
@@ -29,7 +30,7 @@ CPU-LLM-gestütztes Backend zur Auswertung von Firewall- und Proxy-Logs. Parst D
|
||||
## Voraussetzungen
|
||||
- Docker + Docker Compose
|
||||
- Laufende Ollama-Instanz auf dem Host (Port 11434) mit Modell `llava:7b`
|
||||
- Ports: 8080 (nginx), 11434 (Ollama auf Host)
|
||||
- Ports: 80 (Dashboard), 11434 (Ollama auf Host)
|
||||
|
||||
## Installation & Start
|
||||
|
||||
@@ -42,30 +43,38 @@ cd log-analyzer-backend
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
Die API ist dann unter `http://localhost:8080/api` erreichbar.
|
||||
Das Dashboard ist dann unter `http://localhost` erreichbar. Die API-Doku unter `http://localhost/docs`.
|
||||
|
||||
## Nutzung
|
||||
|
||||
### Log-Datei hochladen
|
||||
### Dashboard
|
||||
1. Browser öffnen: `http://localhost`
|
||||
2. Log-Datei über das Upload-Feld hochladen
|
||||
3. Statistiken und Charts werden automatisch geladen (alle 15 Sek. refreshed)
|
||||
4. LLM-Analyse über den Bereich unten starten
|
||||
|
||||
### API (cURL)
|
||||
|
||||
#### Log-Datei hochladen
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/upload \
|
||||
curl -X POST http://localhost/api/upload \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "file=@/var/log/iptables.log"
|
||||
```
|
||||
|
||||
### Statistiken abrufen
|
||||
#### Statistiken abrufen
|
||||
```bash
|
||||
curl "http://localhost:8080/api/stats?limit=20"
|
||||
curl "http://localhost/api/stats?limit=20"
|
||||
```
|
||||
|
||||
### LLM-Analyse starten
|
||||
#### LLM-Analyse starten
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/api/analyze?log_type=firewall&limit=100"
|
||||
curl -X POST "http://localhost/api/analyze?log_type=firewall&limit=100"
|
||||
```
|
||||
|
||||
### Gesundheitscheck
|
||||
#### Gesundheitscheck
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
curl http://localhost/health
|
||||
```
|
||||
|
||||
## API-Endpunkte
|
||||
@@ -79,6 +88,7 @@ curl http://localhost:8080/health
|
||||
| GET | `/api/stats/ports` | Top Ports |
|
||||
| POST | `/api/analyze` | LLM-Analyse der Logs |
|
||||
| GET | `/health` | Healthcheck |
|
||||
| GET | `/docs` | Swagger UI (FastAPI) |
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
@@ -103,6 +113,7 @@ curl http://localhost:8080/health
|
||||
- Das Backend erwartet Ollama auf dem **Host** (nicht im Container). Für Linux ggf. `extra_hosts: ["host.docker.internal:host-gateway"]` nutzen.
|
||||
- Große Logdateien werden in Batches von 500 Zeilen verarbeitet.
|
||||
- Die SQLite-Datenbank wird im Docker-Volumen `logdata` persistiert.
|
||||
- Das Frontend aktualisiert die Statistiken automatisch alle 15 Sekunden.
|
||||
|
||||
## Lizenz
|
||||
MIT
|
||||
|
||||
+4
-6
@@ -14,13 +14,11 @@ services:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: log-analyzer-nginx
|
||||
frontend:
|
||||
build: ./frontend
|
||||
container_name: log-analyzer-frontend
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM nginx:alpine
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY . /usr/share/nginx/html/
|
||||
@@ -0,0 +1,141 @@
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--panel: #181b23;
|
||||
--accent: #38bdf8;
|
||||
--accent2: #22d3ee;
|
||||
--text: #e2e8f0;
|
||||
--muted: #94a3b8;
|
||||
--border: #27303f;
|
||||
--success: #22c55e;
|
||||
--danger: #ef4444;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: linear-gradient(90deg, rgba(56,189,248,0.15), transparent);
|
||||
}
|
||||
header h1 { margin: 0; font-size: 1.8rem; }
|
||||
.subtitle { color: var(--muted); font-size: 0.95rem; }
|
||||
|
||||
main { padding: 1.5rem 2rem; max-width: 1400px; margin: 0 auto; }
|
||||
|
||||
.upload-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.upload-card h2 { margin-top: 0; font-size: 1.15rem; }
|
||||
.upload-card input[type=file] { margin-right: 0.75rem; }
|
||||
|
||||
button {
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
||||
color: #0f1117;
|
||||
border: none;
|
||||
padding: 0.55rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.msg { margin-top: 0.75rem; padding: 0.5rem 0.75rem; border-radius: 0.5rem; }
|
||||
.msg.ok { background: rgba(34,197,94,0.15); color: var(--success); border: 1px solid rgba(34,197,94,0.3); }
|
||||
.msg.err { background: rgba(239,68,68,0.15); color: var(--danger); border: 1px solid rgba(239,68,68,0.3); }
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.stat-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-label { color: var(--muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.stat-value { font-size: 2rem; font-weight: 800; margin-top: 0.25rem; color: var(--accent); }
|
||||
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.chart-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
.chart-card.wide { grid-column: 1 / -1; }
|
||||
.chart-card h3 { margin-top: 0; font-size: 1rem; color: var(--muted); }
|
||||
|
||||
canvas { max-height: 260px; }
|
||||
|
||||
.tables h3, .actions h3 { margin: 0 0 0.5rem; }
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
th, td { padding: 0.6rem 0.8rem; text-align: left; border-bottom: 1px solid var(--border); }
|
||||
th { color: var(--muted); font-weight: 600; font-size: 0.85rem; text-transform: uppercase; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
.url { word-break: break-all; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; color: var(--accent2); }
|
||||
|
||||
.action-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.tag {
|
||||
background: rgba(56,189,248,0.12);
|
||||
color: var(--accent);
|
||||
border: 1px solid rgba(56,189,248,0.25);
|
||||
padding: 0.35rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.analyze {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.analyze-controls { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; margin-bottom: 1rem; }
|
||||
.analyze-controls label { display: flex; align-items: center; gap: 0.5rem; color: var(--muted); }
|
||||
.analyze-controls input, .analyze-controls select {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
|
||||
.analysis-result {
|
||||
background: #0b0d12;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.analysis-result pre { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; color: #c7d2e2; }
|
||||
|
||||
footer { text-align: center; padding: 2rem; color: var(--muted); }
|
||||
footer a { color: var(--accent); }
|
||||
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Log Analyzer Dashboard</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<header>
|
||||
<h1>🔥 Log Analyzer</h1>
|
||||
<span class="subtitle">Firewall & Proxy Log Dashboard</span>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="upload-card">
|
||||
<h2>📤 Log-Datei hochladen</h2>
|
||||
<input type="file" @change="handleFile" accept=".log,.txt,.csv">
|
||||
<button @click="uploadFile" :disabled="!selectedFile || uploading">
|
||||
{{ uploading ? 'Hochladen...' : 'Hochladen & Parsen' }}
|
||||
</button>
|
||||
<div v-if="uploadMsg" :class="['msg', uploadOk ? 'ok' : 'err']">{{ uploadMsg }}</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-grid" v-if="stats.overview.total_entries > 0">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Gesamt Logs</div>
|
||||
<div class="stat-value">{{ stats.overview.total_entries }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Firewall</div>
|
||||
<div class="stat-value">{{ stats.overview.firewall_entries }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Proxy</div>
|
||||
<div class="stat-value">{{ stats.overview.proxy_entries }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Unique Sources</div>
|
||||
<div class="stat-value">{{ stats.unique_counts.unique_sources }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Unique Destinations</div>
|
||||
<div class="stat-value">{{ stats.unique_counts.unique_destinations }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="charts-grid" v-if="stats.overview.total_entries > 0">
|
||||
<div class="chart-card">
|
||||
<h3>Top Quellen</h3>
|
||||
<canvas id="chartSources"></canvas>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Top Ziele</h3>
|
||||
<canvas id="chartDestinations"></canvas>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Top Ports</h3>
|
||||
<canvas id="chartPorts"></canvas>
|
||||
</div>
|
||||
<div class="chart-card wide">
|
||||
<h3>Zeitverlauf (pro Stunde)</h3>
|
||||
<canvas id="chartTimeline"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tables" v-if="stats.top_urls.length > 0">
|
||||
<h3>🔝 Top URLs</h3>
|
||||
<table>
|
||||
<thead><tr><th>URL</th><th>Hits</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="u in stats.top_urls.slice(0,20)" :key="u.url">
|
||||
<td class="url">{{ u.url }}</td>
|
||||
<td>{{ u.count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="actions" v-if="stats.actions.length > 0">
|
||||
<h3>⚡ Actions</h3>
|
||||
<div class="action-tags">
|
||||
<span v-for="a in stats.actions" :key="a.action" class="tag">
|
||||
{{ a.action }}: {{ a.count }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="analyze">
|
||||
<h2>🤖 LLM-Analyse</h2>
|
||||
<div class="analyze-controls">
|
||||
<label>Log-Typ:
|
||||
<select v-model="analyzeType">
|
||||
<option value="all">Alle</option>
|
||||
<option value="firewall">Firewall</option>
|
||||
<option value="proxy">Proxy</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Zeilen:
|
||||
<input type="number" v-model.number="analyzeLimit" min="10" max="500" step="10">
|
||||
</label>
|
||||
<button @click="runAnalysis" :disabled="analyzing || stats.overview.total_entries === 0">
|
||||
{{ analyzing ? 'Analysiere...' : 'Analyse starten' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="analysisResult" class="analysis-result">
|
||||
<pre>{{ analysisResult }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<small>Log Analyzer Backend — <a href="/docs">API Docs</a></small>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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');
|
||||
@@ -3,6 +3,10 @@ events {
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
|
||||
upstream backend {
|
||||
server backend:8000;
|
||||
}
|
||||
@@ -10,13 +14,22 @@ http {
|
||||
server {
|
||||
listen 80;
|
||||
client_max_body_size 100M;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
proxy_pass http://backend;
|
||||
location /api/ {
|
||||
proxy_pass http://backend/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user