A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1094 lines
31 KiB
Python
1094 lines
31 KiB
Python
"""
|
|
BreakPilot Developer Admin Frontend
|
|
|
|
Ein separates Admin-Frontend fuer Entwickler mit DevSecOps Dashboard,
|
|
SBOM-Viewer, Security-Scans und anderen Entwickler-Tools.
|
|
|
|
URL: /dev-admin
|
|
"""
|
|
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
# Import der Security Module
|
|
from .modules.security import SecurityModule
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def get_dev_admin_css() -> str:
|
|
"""CSS fuer das Developer Admin Frontend."""
|
|
return """
|
|
/* ==========================================
|
|
DEVELOPER ADMIN - CSS VARIABLES
|
|
========================================== */
|
|
|
|
:root {
|
|
/* Primary Colors - Entwickler Blau */
|
|
--da-primary: #3b82f6;
|
|
--da-primary-hover: #2563eb;
|
|
--da-primary-soft: rgba(59, 130, 246, 0.1);
|
|
|
|
/* Background */
|
|
--da-bg: #0f172a;
|
|
--da-surface: #1e293b;
|
|
--da-surface-elevated: #334155;
|
|
|
|
/* Borders */
|
|
--da-border: #475569;
|
|
--da-border-subtle: rgba(255, 255, 255, 0.1);
|
|
|
|
/* Text */
|
|
--da-text: #e5e7eb;
|
|
--da-text-muted: #9ca3af;
|
|
|
|
/* Status Colors */
|
|
--da-danger: #ef4444;
|
|
--da-warning: #f59e0b;
|
|
--da-success: #22c55e;
|
|
--da-info: #3b82f6;
|
|
|
|
/* Sidebar */
|
|
--sidebar-width: 260px;
|
|
}
|
|
|
|
/* Light Theme */
|
|
[data-theme="light"] {
|
|
--da-bg: #f8fafc;
|
|
--da-surface: #ffffff;
|
|
--da-surface-elevated: #f1f5f9;
|
|
--da-border: #e2e8f0;
|
|
--da-border-subtle: rgba(0, 0, 0, 0.1);
|
|
--da-text: #1e293b;
|
|
--da-text-muted: #64748b;
|
|
}
|
|
|
|
/* ==========================================
|
|
RESET & BASE
|
|
========================================== */
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: 'JetBrains Mono', 'SF Mono', 'Monaco', monospace;
|
|
background: var(--da-bg);
|
|
color: var(--da-text);
|
|
min-height: 100vh;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* ==========================================
|
|
LAYOUT
|
|
========================================== */
|
|
|
|
.dev-admin-root {
|
|
display: flex;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* Sidebar */
|
|
.dev-admin-sidebar {
|
|
width: var(--sidebar-width);
|
|
background: var(--da-surface);
|
|
border-right: 1px solid var(--da-border);
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
height: 100vh;
|
|
overflow-y: auto;
|
|
z-index: 100;
|
|
}
|
|
|
|
.dev-admin-sidebar-header {
|
|
padding: 20px;
|
|
border-bottom: 1px solid var(--da-border);
|
|
}
|
|
|
|
.dev-admin-brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.dev-admin-logo {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.dev-admin-title {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: var(--da-text);
|
|
}
|
|
|
|
.dev-admin-subtitle {
|
|
font-size: 11px;
|
|
color: var(--da-text-muted);
|
|
}
|
|
|
|
/* Sidebar Navigation */
|
|
.dev-admin-nav {
|
|
padding: 16px 12px;
|
|
}
|
|
|
|
.dev-admin-nav-section {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.dev-admin-nav-label {
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
color: var(--da-text-muted);
|
|
padding: 0 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.dev-admin-nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
color: var(--da-text-muted);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.dev-admin-nav-item:hover {
|
|
background: var(--da-surface-elevated);
|
|
color: var(--da-text);
|
|
}
|
|
|
|
.dev-admin-nav-item.active {
|
|
background: var(--da-primary-soft);
|
|
color: var(--da-primary);
|
|
}
|
|
|
|
.dev-admin-nav-icon {
|
|
font-size: 18px;
|
|
width: 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.dev-admin-nav-badge {
|
|
margin-left: auto;
|
|
padding: 2px 8px;
|
|
font-size: 10px;
|
|
border-radius: 10px;
|
|
background: var(--da-primary-soft);
|
|
color: var(--da-primary);
|
|
}
|
|
|
|
/* Main Content */
|
|
.dev-admin-main {
|
|
flex: 1;
|
|
margin-left: var(--sidebar-width);
|
|
padding: 24px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* Header */
|
|
.dev-admin-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 24px;
|
|
padding-bottom: 16px;
|
|
border-bottom: 1px solid var(--da-border);
|
|
}
|
|
|
|
.dev-admin-header h1 {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.dev-admin-header-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
}
|
|
|
|
/* Theme Toggle */
|
|
.theme-toggle-dev {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--da-border);
|
|
background: var(--da-surface);
|
|
color: var(--da-text);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.theme-toggle-dev:hover {
|
|
background: var(--da-surface-elevated);
|
|
border-color: var(--da-primary);
|
|
}
|
|
|
|
/* Panels */
|
|
.dev-admin-panel {
|
|
display: none;
|
|
}
|
|
|
|
.dev-admin-panel.active {
|
|
display: block;
|
|
}
|
|
|
|
/* Back Link */
|
|
.dev-admin-back {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--da-text-muted);
|
|
text-decoration: none;
|
|
font-size: 13px;
|
|
padding: 8px 12px;
|
|
margin: 16px 12px;
|
|
border-radius: 8px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.dev-admin-back:hover {
|
|
background: var(--da-surface-elevated);
|
|
color: var(--da-text);
|
|
}
|
|
|
|
/* ==========================================
|
|
LOGS STYLES
|
|
========================================== */
|
|
.log-controls {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
}
|
|
|
|
.log-controls select {
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--da-border);
|
|
background: var(--da-surface);
|
|
color: var(--da-text);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.logs-list {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 12px;
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.log-entry {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 8px 12px;
|
|
border-bottom: 1px solid var(--da-border-subtle);
|
|
align-items: baseline;
|
|
}
|
|
|
|
.log-entry:hover {
|
|
background: var(--da-surface-elevated);
|
|
}
|
|
|
|
.log-time {
|
|
color: var(--da-text-muted);
|
|
min-width: 80px;
|
|
}
|
|
|
|
.log-level {
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
min-width: 60px;
|
|
text-align: center;
|
|
}
|
|
|
|
.log-error .log-level { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
|
.log-warning .log-level { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
|
.log-info .log-level { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
|
|
.log-debug .log-level { background: rgba(156, 163, 175, 0.2); color: #9ca3af; }
|
|
|
|
.log-service {
|
|
color: var(--da-primary);
|
|
min-width: 120px;
|
|
}
|
|
|
|
.log-message {
|
|
color: var(--da-text);
|
|
flex: 1;
|
|
}
|
|
|
|
/* ==========================================
|
|
METRICS STYLES
|
|
========================================== */
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.metric-card {
|
|
background: var(--da-surface-elevated);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
position: relative;
|
|
}
|
|
|
|
.metric-name {
|
|
font-size: 12px;
|
|
color: var(--da-text-muted);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: var(--da-text);
|
|
}
|
|
|
|
.metric-unit {
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
color: var(--da-text-muted);
|
|
}
|
|
|
|
.metric-trend {
|
|
position: absolute;
|
|
top: 16px;
|
|
right: 16px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.trend-up { color: #22c55e; }
|
|
.trend-down { color: #ef4444; }
|
|
.trend-stable { color: var(--da-text-muted); }
|
|
|
|
/* ==========================================
|
|
CONTAINERS STYLES
|
|
========================================== */
|
|
.containers-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.container-card {
|
|
background: var(--da-surface-elevated);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
border-left: 4px solid var(--da-success);
|
|
}
|
|
|
|
.container-card.status-unhealthy {
|
|
border-left-color: var(--da-danger);
|
|
}
|
|
|
|
.container-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.container-name {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: var(--da-text);
|
|
}
|
|
|
|
.container-status {
|
|
font-size: 12px;
|
|
color: var(--da-success);
|
|
}
|
|
|
|
.status-unhealthy .container-status {
|
|
color: var(--da-danger);
|
|
}
|
|
|
|
.container-stats {
|
|
display: flex;
|
|
gap: 16px;
|
|
}
|
|
|
|
.container-stats .stat {
|
|
flex: 1;
|
|
text-align: center;
|
|
}
|
|
|
|
.container-stats .stat-label {
|
|
display: block;
|
|
font-size: 10px;
|
|
color: var(--da-text-muted);
|
|
text-transform: uppercase;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.container-stats .stat-value {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--da-text);
|
|
}
|
|
|
|
/* ==========================================
|
|
SERVICES STYLES
|
|
========================================== */
|
|
.services-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.service-row {
|
|
display: grid;
|
|
grid-template-columns: 200px 1fr 100px;
|
|
gap: 16px;
|
|
align-items: center;
|
|
padding: 16px;
|
|
background: var(--da-surface-elevated);
|
|
border-radius: 8px;
|
|
border-left: 4px solid var(--da-success);
|
|
}
|
|
|
|
.service-row.status-unhealthy {
|
|
border-left-color: var(--da-danger);
|
|
}
|
|
|
|
.service-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.service-status {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.status-healthy .service-status { color: var(--da-success); }
|
|
.status-unhealthy .service-status { color: var(--da-danger); }
|
|
|
|
.service-name {
|
|
font-weight: 600;
|
|
color: var(--da-text);
|
|
}
|
|
|
|
.service-url {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 12px;
|
|
color: var(--da-text-muted);
|
|
}
|
|
|
|
.service-response {
|
|
font-weight: 600;
|
|
text-align: right;
|
|
}
|
|
|
|
.response-fast { color: var(--da-success); }
|
|
.response-medium { color: var(--da-warning); }
|
|
.response-slow { color: var(--da-danger); }
|
|
|
|
/* ==========================================
|
|
SBOM EXTRAS
|
|
========================================== */
|
|
.stat-card {
|
|
background: var(--da-surface-elevated);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-card .stat-value {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: var(--da-primary);
|
|
}
|
|
|
|
.stat-card .stat-label {
|
|
font-size: 11px;
|
|
color: var(--da-text-muted);
|
|
text-transform: uppercase;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.license-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
background: var(--da-primary-soft);
|
|
color: var(--da-primary);
|
|
}
|
|
"""
|
|
|
|
|
|
def get_dev_admin_html() -> str:
|
|
"""HTML-Struktur fuer das Developer Admin Frontend."""
|
|
return """
|
|
<!DOCTYPE html>
|
|
<html lang="de" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>BreakPilot Developer Admin</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
{css}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="dev-admin-root">
|
|
<!-- Sidebar -->
|
|
<aside class="dev-admin-sidebar">
|
|
<div class="dev-admin-sidebar-header">
|
|
<div class="dev-admin-brand">
|
|
<div class="dev-admin-logo">🛠</div>
|
|
<div>
|
|
<div class="dev-admin-title">Developer Admin</div>
|
|
<div class="dev-admin-subtitle">BreakPilot DevOps</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="dev-admin-nav">
|
|
<div class="dev-admin-nav-section">
|
|
<div class="dev-admin-nav-label">DevSecOps</div>
|
|
<div class="dev-admin-nav-item active" data-panel="security" onclick="switchPanel('security')">
|
|
<span class="dev-admin-nav-icon">🛡</span>
|
|
<span>Security Dashboard</span>
|
|
</div>
|
|
<div class="dev-admin-nav-item" data-panel="sbom" onclick="switchPanel('sbom')">
|
|
<span class="dev-admin-nav-icon">📜</span>
|
|
<span>SBOM Viewer</span>
|
|
</div>
|
|
<div class="dev-admin-nav-item" data-panel="scans" onclick="switchPanel('scans')">
|
|
<span class="dev-admin-nav-icon">🔍</span>
|
|
<span>Security Scans</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dev-admin-nav-section">
|
|
<div class="dev-admin-nav-label">Monitoring</div>
|
|
<div class="dev-admin-nav-item" data-panel="logs" onclick="switchPanel('logs')">
|
|
<span class="dev-admin-nav-icon">📄</span>
|
|
<span>Logs</span>
|
|
</div>
|
|
<div class="dev-admin-nav-item" data-panel="metrics" onclick="switchPanel('metrics')">
|
|
<span class="dev-admin-nav-icon">📈</span>
|
|
<span>Metrics</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dev-admin-nav-section">
|
|
<div class="dev-admin-nav-label">Infrastruktur</div>
|
|
<div class="dev-admin-nav-item" data-panel="containers" onclick="switchPanel('containers')">
|
|
<span class="dev-admin-nav-icon">📦</span>
|
|
<span>Containers</span>
|
|
</div>
|
|
<div class="dev-admin-nav-item" data-panel="services" onclick="switchPanel('services')">
|
|
<span class="dev-admin-nav-icon">⚙</span>
|
|
<span>Services</span>
|
|
</div>
|
|
</div>
|
|
|
|
<a href="/app" class="dev-admin-back">
|
|
<span>←</span>
|
|
<span>Zurueck zum Studio</span>
|
|
</a>
|
|
</nav>
|
|
</aside>
|
|
|
|
<!-- Main Content -->
|
|
<main class="dev-admin-main">
|
|
<header class="dev-admin-header">
|
|
<h1 id="panel-title">Security Dashboard</h1>
|
|
<div class="dev-admin-header-actions">
|
|
<button class="theme-toggle-dev" onclick="toggleTheme()" title="Theme wechseln">
|
|
🌙
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Security Panel (Default) -->
|
|
<div id="panel-security" class="dev-admin-panel active">
|
|
{security_content}
|
|
</div>
|
|
|
|
<!-- SBOM Panel -->
|
|
<div id="panel-sbom" class="dev-admin-panel">
|
|
<div class="security-card">
|
|
<div class="security-card-header">
|
|
<h3>📜 Software Bill of Materials (SBOM)</h3>
|
|
</div>
|
|
<div class="security-card-body">
|
|
<p style="color: var(--da-text-muted); margin-bottom: 16px;">
|
|
Vollstaendige Liste aller Abhaengigkeiten und deren Versionen.
|
|
</p>
|
|
<div id="sbom-content">Lade SBOM-Daten...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scans Panel -->
|
|
<div id="panel-scans" class="dev-admin-panel">
|
|
<div class="security-card">
|
|
<div class="security-card-header">
|
|
<h3>🔍 Security Scans ausfuehren</h3>
|
|
</div>
|
|
<div class="security-card-body">
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
|
|
<button class="scan-btn" onclick="runScan('secrets')">
|
|
<span>🔐</span>
|
|
<span>Secrets Scan</span>
|
|
</button>
|
|
<button class="scan-btn" onclick="runScan('sast')">
|
|
<span>💻</span>
|
|
<span>SAST Scan</span>
|
|
</button>
|
|
<button class="scan-btn" onclick="runScan('deps')">
|
|
<span>📦</span>
|
|
<span>Dependency Scan</span>
|
|
</button>
|
|
<button class="scan-btn" onclick="runScan('containers')">
|
|
<span>🐳</span>
|
|
<span>Container Scan</span>
|
|
</button>
|
|
<button class="scan-btn scan-btn-all" onclick="runScan('all')">
|
|
<span>🚀</span>
|
|
<span>Vollstaendiger Scan</span>
|
|
</button>
|
|
</div>
|
|
<div id="scan-results" style="margin-top: 24px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logs Panel -->
|
|
<div id="panel-logs" class="dev-admin-panel">
|
|
<div class="security-card">
|
|
<div class="security-card-header">
|
|
<h3>📄 Application Logs</h3>
|
|
<div class="log-controls">
|
|
<select id="log-service-filter" onchange="loadLogs()">
|
|
<option value="">Alle Services</option>
|
|
<option value="backend">Backend</option>
|
|
<option value="consent-service">Consent Service</option>
|
|
<option value="postgres">PostgreSQL</option>
|
|
<option value="mailpit">Mailpit</option>
|
|
</select>
|
|
<select id="log-level-filter" onchange="loadLogs()">
|
|
<option value="">Alle Level</option>
|
|
<option value="ERROR">Error</option>
|
|
<option value="WARNING">Warning</option>
|
|
<option value="INFO">Info</option>
|
|
<option value="DEBUG">Debug</option>
|
|
</select>
|
|
<button class="scan-btn" onclick="loadLogs()" style="margin-left: 8px;">
|
|
🔄 Aktualisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="security-card-body" id="logs-content">
|
|
<div style="color: var(--da-text-muted);">Lade Logs...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metrics Panel -->
|
|
<div id="panel-metrics" class="dev-admin-panel">
|
|
<div class="security-card">
|
|
<div class="security-card-header">
|
|
<h3>📈 System Metrics</h3>
|
|
<button class="scan-btn" onclick="loadMetrics()">🔄 Aktualisieren</button>
|
|
</div>
|
|
<div class="security-card-body" id="metrics-content">
|
|
<div style="color: var(--da-text-muted);">Lade Metriken...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Containers Panel -->
|
|
<div id="panel-containers" class="dev-admin-panel">
|
|
<div class="security-card">
|
|
<div class="security-card-header">
|
|
<h3>📦 Container Status</h3>
|
|
<button class="scan-btn" onclick="loadContainers()">🔄 Aktualisieren</button>
|
|
</div>
|
|
<div class="security-card-body" id="containers-content">
|
|
<div style="color: var(--da-text-muted);">Lade Container-Status...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Services Panel -->
|
|
<div id="panel-services" class="dev-admin-panel">
|
|
<div class="security-card">
|
|
<div class="security-card-header">
|
|
<h3>⚙ Service Health</h3>
|
|
<button class="scan-btn" onclick="loadServices()">🔄 Aktualisieren</button>
|
|
</div>
|
|
<div class="security-card-body" id="services-content">
|
|
<div style="color: var(--da-text-muted);">Lade Service-Status...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
{js}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def get_dev_admin_js() -> str:
|
|
"""JavaScript fuer das Developer Admin Frontend."""
|
|
return """
|
|
// ==========================================
|
|
// DEVELOPER ADMIN - JAVASCRIPT
|
|
// ==========================================
|
|
|
|
const panelTitles = {
|
|
'security': 'Security Dashboard',
|
|
'sbom': 'SBOM Viewer',
|
|
'scans': 'Security Scans',
|
|
'logs': 'Application Logs',
|
|
'metrics': 'System Metrics',
|
|
'containers': 'Container Status',
|
|
'services': 'Service Health'
|
|
};
|
|
|
|
function switchPanel(panelId) {
|
|
// Alle Panels ausblenden
|
|
document.querySelectorAll('.dev-admin-panel').forEach(panel => {
|
|
panel.classList.remove('active');
|
|
});
|
|
|
|
// Alle Nav-Items deaktivieren
|
|
document.querySelectorAll('.dev-admin-nav-item').forEach(item => {
|
|
item.classList.remove('active');
|
|
});
|
|
|
|
// Gewaehltes Panel anzeigen
|
|
const panel = document.getElementById('panel-' + panelId);
|
|
if (panel) {
|
|
panel.classList.add('active');
|
|
}
|
|
|
|
// Nav-Item aktivieren
|
|
const navItem = document.querySelector('[data-panel="' + panelId + '"]');
|
|
if (navItem) {
|
|
navItem.classList.add('active');
|
|
}
|
|
|
|
// Titel aktualisieren
|
|
document.getElementById('panel-title').textContent = panelTitles[panelId] || 'Developer Admin';
|
|
|
|
// Panel-spezifische Initialisierung
|
|
if (panelId === 'security' && typeof SecurityDashboard !== 'undefined') {
|
|
SecurityDashboard.init();
|
|
} else if (panelId === 'sbom') {
|
|
loadSBOM();
|
|
} else if (panelId === 'logs') {
|
|
loadLogs();
|
|
} else if (panelId === 'metrics') {
|
|
loadMetrics();
|
|
} else if (panelId === 'containers') {
|
|
loadContainers();
|
|
} else if (panelId === 'services') {
|
|
loadServices();
|
|
}
|
|
}
|
|
|
|
function toggleTheme() {
|
|
const html = document.documentElement;
|
|
const currentTheme = html.getAttribute('data-theme');
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
html.setAttribute('data-theme', newTheme);
|
|
localStorage.setItem('dev-admin-theme', newTheme);
|
|
}
|
|
|
|
function loadSBOM() {
|
|
const container = document.getElementById('sbom-content');
|
|
container.innerHTML = '<div style="color: var(--da-text-muted);">Lade SBOM-Daten...</div>';
|
|
|
|
// Nutze /demo/sbom - gibt Mock-Daten wenn keine echten verfuegbar
|
|
fetch('/api/v1/security/demo/sbom')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.components && data.components.length > 0) {
|
|
let html = '<div class="sbom-summary" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px;">';
|
|
html += '<div class="stat-card"><div class="stat-value">' + data.components.length + '</div><div class="stat-label">Komponenten</div></div>';
|
|
|
|
// Lizenzen zaehlen
|
|
const licenses = {};
|
|
data.components.forEach(c => {
|
|
const lic = c.licenses?.[0]?.license?.id || 'Unknown';
|
|
licenses[lic] = (licenses[lic] || 0) + 1;
|
|
});
|
|
html += '<div class="stat-card"><div class="stat-value">' + Object.keys(licenses).length + '</div><div class="stat-label">Lizenzen</div></div>';
|
|
html += '<div class="stat-card"><div class="stat-value">' + (data.metadata?.component?.version || '-') + '</div><div class="stat-label">App Version</div></div>';
|
|
html += '<div class="stat-card"><div class="stat-value">' + (data.specVersion || '-') + '</div><div class="stat-label">SBOM Spec</div></div>';
|
|
html += '</div>';
|
|
|
|
html += '<table class="security-table"><thead><tr>';
|
|
html += '<th>Name</th><th>Version</th><th>Typ</th><th>Lizenz</th><th>PURL</th>';
|
|
html += '</tr></thead><tbody>';
|
|
|
|
data.components.slice(0, 100).forEach(comp => {
|
|
const license = comp.licenses?.[0]?.license?.id || '-';
|
|
html += '<tr>';
|
|
html += '<td><strong>' + (comp.name || '-') + '</strong></td>';
|
|
html += '<td><code>' + (comp.version || '-') + '</code></td>';
|
|
html += '<td>' + (comp.type || '-') + '</td>';
|
|
html += '<td><span class="license-badge">' + license + '</span></td>';
|
|
html += '<td style="font-size: 11px; color: var(--da-text-muted);">' + (comp.purl || '-') + '</td>';
|
|
html += '</tr>';
|
|
});
|
|
|
|
html += '</tbody></table>';
|
|
if (data.components.length > 100) {
|
|
html += '<p style="color: var(--da-text-muted); margin-top: 16px;">';
|
|
html += 'Zeige 100 von ' + data.components.length + ' Komponenten</p>';
|
|
}
|
|
container.innerHTML = html;
|
|
} else {
|
|
container.innerHTML = '<p style="color: var(--da-text-muted);">Keine SBOM-Daten verfuegbar. Fuehren Sie "syft" aus.</p>';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
container.innerHTML = '<p style="color: var(--da-danger);">Fehler beim Laden: ' + err.message + '</p>';
|
|
});
|
|
}
|
|
|
|
async function runScan(scanType) {
|
|
const resultsDiv = document.getElementById('scan-results');
|
|
resultsDiv.innerHTML = '<div style="color: var(--da-primary);">Starte ' + scanType + ' Scan...</div>';
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/security/scan/' + scanType, {
|
|
method: 'POST'
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
resultsDiv.innerHTML = '<div style="color: var(--da-success);">' +
|
|
'✔ Scan gestartet: ' + data.scan_type +
|
|
'<br>Status: ' + data.status +
|
|
'<br>Message: ' + (data.message || 'Scan laeuft im Hintergrund') +
|
|
'</div>';
|
|
} else {
|
|
resultsDiv.innerHTML = '<div style="color: var(--da-danger);">' +
|
|
'✘ Fehler: ' + (data.detail || 'Unbekannter Fehler') +
|
|
'</div>';
|
|
}
|
|
} catch (err) {
|
|
resultsDiv.innerHTML = '<div style="color: var(--da-danger);">' +
|
|
'✘ Netzwerkfehler: ' + err.message +
|
|
'</div>';
|
|
}
|
|
}
|
|
|
|
// ===================
|
|
// LOGS VIEWER
|
|
// ===================
|
|
function loadLogs() {
|
|
const container = document.getElementById('logs-content');
|
|
const serviceFilter = document.getElementById('log-service-filter')?.value || '';
|
|
const levelFilter = document.getElementById('log-level-filter')?.value || '';
|
|
|
|
let url = '/api/v1/security/monitoring/logs?limit=50';
|
|
if (serviceFilter) url += '&service=' + serviceFilter;
|
|
if (levelFilter) url += '&level=' + levelFilter;
|
|
|
|
container.innerHTML = '<div style="color: var(--da-text-muted);">Lade Logs...</div>';
|
|
|
|
fetch(url)
|
|
.then(res => res.json())
|
|
.then(logs => {
|
|
if (logs.length === 0) {
|
|
container.innerHTML = '<p style="color: var(--da-text-muted);">Keine Logs gefunden.</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="logs-list">';
|
|
logs.forEach(log => {
|
|
const levelClass = 'log-' + log.level.toLowerCase();
|
|
const time = new Date(log.timestamp).toLocaleTimeString('de-DE');
|
|
html += '<div class="log-entry ' + levelClass + '">';
|
|
html += '<span class="log-time">' + time + '</span>';
|
|
html += '<span class="log-level">' + log.level + '</span>';
|
|
html += '<span class="log-service">[' + log.service + ']</span>';
|
|
html += '<span class="log-message">' + log.message + '</span>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
})
|
|
.catch(err => {
|
|
container.innerHTML = '<p style="color: var(--da-danger);">Fehler: ' + err.message + '</p>';
|
|
});
|
|
}
|
|
|
|
// ===================
|
|
// METRICS DASHBOARD
|
|
// ===================
|
|
function loadMetrics() {
|
|
const container = document.getElementById('metrics-content');
|
|
container.innerHTML = '<div style="color: var(--da-text-muted);">Lade Metriken...</div>';
|
|
|
|
fetch('/api/v1/security/monitoring/metrics')
|
|
.then(res => res.json())
|
|
.then(metrics => {
|
|
let html = '<div class="metrics-grid">';
|
|
metrics.forEach(metric => {
|
|
const trendIcon = metric.trend === 'up' ? '▲' : (metric.trend === 'down' ? '▼' : '●');
|
|
const trendClass = metric.trend === 'up' ? 'trend-up' : (metric.trend === 'down' ? 'trend-down' : 'trend-stable');
|
|
|
|
html += '<div class="metric-card">';
|
|
html += '<div class="metric-name">' + metric.name + '</div>';
|
|
html += '<div class="metric-value">' + metric.value + ' <span class="metric-unit">' + metric.unit + '</span></div>';
|
|
html += '<div class="metric-trend ' + trendClass + '">' + trendIcon + '</div>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
})
|
|
.catch(err => {
|
|
container.innerHTML = '<p style="color: var(--da-danger);">Fehler: ' + err.message + '</p>';
|
|
});
|
|
}
|
|
|
|
// ===================
|
|
// CONTAINER STATUS
|
|
// ===================
|
|
function loadContainers() {
|
|
const container = document.getElementById('containers-content');
|
|
container.innerHTML = '<div style="color: var(--da-text-muted);">Lade Container...</div>';
|
|
|
|
fetch('/api/v1/security/monitoring/containers')
|
|
.then(res => res.json())
|
|
.then(containers => {
|
|
let html = '<div class="containers-grid">';
|
|
containers.forEach(c => {
|
|
const healthClass = c.health === 'healthy' ? 'status-healthy' : 'status-unhealthy';
|
|
const statusIcon = c.health === 'healthy' ? '✔' : '✘';
|
|
|
|
html += '<div class="container-card ' + healthClass + '">';
|
|
html += '<div class="container-header">';
|
|
html += '<span class="container-name">' + c.name + '</span>';
|
|
html += '<span class="container-status">' + statusIcon + ' ' + c.status + '</span>';
|
|
html += '</div>';
|
|
html += '<div class="container-stats">';
|
|
html += '<div class="stat"><span class="stat-label">CPU</span><span class="stat-value">' + c.cpu_percent + '%</span></div>';
|
|
html += '<div class="stat"><span class="stat-label">Memory</span><span class="stat-value">' + c.memory_mb + ' MB</span></div>';
|
|
html += '<div class="stat"><span class="stat-label">Uptime</span><span class="stat-value">' + c.uptime + '</span></div>';
|
|
html += '</div>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
})
|
|
.catch(err => {
|
|
container.innerHTML = '<p style="color: var(--da-danger);">Fehler: ' + err.message + '</p>';
|
|
});
|
|
}
|
|
|
|
// ===================
|
|
// SERVICE HEALTH
|
|
// ===================
|
|
function loadServices() {
|
|
const container = document.getElementById('services-content');
|
|
container.innerHTML = '<div style="color: var(--da-text-muted);">Pruefe Services...</div>';
|
|
|
|
fetch('/api/v1/security/monitoring/services')
|
|
.then(res => res.json())
|
|
.then(services => {
|
|
let html = '<div class="services-list">';
|
|
services.forEach(svc => {
|
|
const healthClass = svc.status === 'healthy' ? 'status-healthy' : 'status-unhealthy';
|
|
const statusIcon = svc.status === 'healthy' ? '✔' : '✘';
|
|
const responseClass = svc.response_time_ms < 100 ? 'response-fast' : (svc.response_time_ms < 500 ? 'response-medium' : 'response-slow');
|
|
|
|
html += '<div class="service-row ' + healthClass + '">';
|
|
html += '<div class="service-info">';
|
|
html += '<span class="service-status">' + statusIcon + '</span>';
|
|
html += '<span class="service-name">' + svc.name + '</span>';
|
|
html += '</div>';
|
|
html += '<div class="service-url">' + svc.url + '</div>';
|
|
html += '<div class="service-response ' + responseClass + '">' + svc.response_time_ms + 'ms</div>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
})
|
|
.catch(err => {
|
|
container.innerHTML = '<p style="color: var(--da-danger);">Fehler: ' + err.message + '</p>';
|
|
});
|
|
}
|
|
|
|
// Initialisierung beim Laden
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Theme aus localStorage laden
|
|
const savedTheme = localStorage.getItem('dev-admin-theme');
|
|
if (savedTheme) {
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
}
|
|
|
|
// Security Dashboard initialisieren
|
|
if (typeof SecurityDashboard !== 'undefined') {
|
|
SecurityDashboard.init();
|
|
}
|
|
});
|
|
"""
|
|
|
|
|
|
@router.get("/dev-admin", response_class=HTMLResponse)
|
|
def dev_admin_ui():
|
|
"""
|
|
Rendert das Developer Admin Frontend.
|
|
|
|
Enthaelt:
|
|
- Security Dashboard (DevSecOps)
|
|
- SBOM Viewer
|
|
- Security Scans
|
|
- Logs (geplant)
|
|
- Metrics (geplant)
|
|
- Container Status (geplant)
|
|
- Service Health (geplant)
|
|
"""
|
|
# CSS zusammenfuegen
|
|
all_css = get_dev_admin_css() + "\n" + SecurityModule.get_css()
|
|
|
|
# Security-Modul HTML (ohne eigenen Container, wird in Panel eingebettet)
|
|
security_html = SecurityModule.get_html()
|
|
|
|
# JavaScript zusammenfuegen
|
|
all_js = get_dev_admin_js() + "\n" + SecurityModule.get_js()
|
|
|
|
# HTML Template fuellen
|
|
html = get_dev_admin_html()
|
|
html = html.replace("{css}", all_css)
|
|
html = html.replace("{security_content}", security_html)
|
|
html = html.replace("{js}", all_js)
|
|
|
|
return html
|