nordabiz/templates/admin/status_dashboard.html
Maciej Pienczyn 094379d95e
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
fix(templates): Add blueprint prefix to url_for calls across admin templates
After refactoring to blueprints, templates still used bare endpoint names
(e.g., url_for('admin_zopk')) instead of prefixed names (e.g.,
url_for('admin.admin_zopk')). While most worked via backward-compat aliases,
api_zopk_search_news was missing from the alias list causing 500 on /admin/zopk.

Fixed 19 template files and added missing alias for api_zopk_search_news.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 13:44:50 +01:00

1058 lines
45 KiB
HTML

{% extends "base.html" %}
{% block title %}Status systemu - Admin{% endblock %}
{% block extra_css %}
<style>
.status-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-xl);
}
.status-header h1 {
font-size: var(--font-size-2xl);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.status-header p {
color: var(--text-secondary);
}
.refresh-info {
text-align: right;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--spacing-md) var(--spacing-lg);
}
.refresh-info .timestamp {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
font-family: monospace;
}
.refresh-info .label {
font-size: var(--font-size-xs);
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.refresh-info .next-refresh {
font-size: var(--font-size-xs);
color: var(--primary);
margin-top: var(--spacing-xs);
}
.refresh-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
margin-right: var(--spacing-xs);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
}
.metric-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
padding: var(--spacing-xl);
position: relative;
overflow: hidden;
}
.metric-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
}
.metric-card.system::before { background: linear-gradient(90deg, #3b82f6, #8b5cf6); }
.metric-card.database::before { background: linear-gradient(90deg, #10b981, #14b8a6); }
.metric-card.app::before { background: linear-gradient(90deg, #f59e0b, #f97316); }
.metric-card.process::before { background: linear-gradient(90deg, #ec4899, #f43f5e); }
.metric-card-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.metric-card-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.metric-card.system .metric-card-icon { background: linear-gradient(135deg, #dbeafe, #ede9fe); }
.metric-card.database .metric-card-icon { background: linear-gradient(135deg, #d1fae5, #ccfbf1); }
.metric-card.app .metric-card-icon { background: linear-gradient(135deg, #fef3c7, #ffedd5); }
.metric-card.process .metric-card-icon { background: linear-gradient(135deg, #fce7f3, #fee2e2); }
.metric-card-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
}
.metric-card-subtitle {
font-size: var(--font-size-xs);
color: var(--text-secondary);
}
.gauge-container {
display: flex;
justify-content: center;
margin-bottom: var(--spacing-lg);
}
.gauge {
position: relative;
width: 140px;
height: 70px;
overflow: hidden;
}
.gauge-bg {
position: absolute;
width: 140px;
height: 140px;
border-radius: 50%;
border: 12px solid var(--border);
border-bottom-color: transparent;
border-left-color: transparent;
transform: rotate(225deg);
}
.gauge-fill {
position: absolute;
width: 140px;
height: 140px;
border-radius: 50%;
border: 12px solid;
border-bottom-color: transparent;
border-left-color: transparent;
transform: rotate(225deg);
transition: transform 0.8s ease-out;
}
.gauge-fill.low { border-top-color: #22c55e; border-right-color: #22c55e; }
.gauge-fill.medium { border-top-color: #f59e0b; border-right-color: #f59e0b; }
.gauge-fill.high { border-top-color: #ef4444; border-right-color: #ef4444; }
.gauge-value {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--text-primary);
}
.gauge-label {
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
font-size: var(--font-size-xs);
color: var(--text-secondary);
white-space: nowrap;
}
.metric-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-md);
}
.metric-stat {
background: var(--background);
padding: var(--spacing-md);
border-radius: var(--radius);
text-align: center;
}
.metric-stat-value {
font-size: var(--font-size-xl);
font-weight: 700;
color: var(--text-primary);
}
.metric-stat-label {
font-size: var(--font-size-xs);
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.progress-bar-container {
margin-bottom: var(--spacing-md);
}
.progress-bar-label {
display: flex;
justify-content: space-between;
margin-bottom: var(--spacing-xs);
font-size: var(--font-size-sm);
}
.progress-bar {
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
}
.progress-bar-fill.low { background: linear-gradient(90deg, #22c55e, #34d399); }
.progress-bar-fill.medium { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.progress-bar-fill.high { background: linear-gradient(90deg, #ef4444, #f87171); }
.status-badge {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-md);
border-radius: var(--radius);
font-size: var(--font-size-sm);
font-weight: 500;
}
.status-badge.ok {
background: #dcfce7;
color: #166534;
}
.status-badge.warning {
background: #fef3c7;
color: #92400e;
}
.status-badge.error {
background: #fee2e2;
color: #991b1b;
}
.info-row {
display: flex;
justify-content: space-between;
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--border);
font-size: var(--font-size-sm);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: var(--text-secondary);
}
.info-value {
color: var(--text-primary);
font-weight: 500;
font-family: monospace;
}
.quick-actions {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-lg);
}
.quick-action-btn {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--background);
color: var(--text-primary);
font-size: var(--font-size-sm);
cursor: pointer;
text-decoration: none;
text-align: center;
transition: all 0.2s;
}
.quick-action-btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.back-link {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
color: var(--text-secondary);
text-decoration: none;
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-lg);
}
.back-link:hover {
color: var(--primary);
}
/* Health indicators grid */
.health-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: var(--spacing-md);
margin-top: var(--spacing-lg);
}
.health-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
}
.health-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.health-dot.ok { background: #22c55e; }
.health-dot.warning { background: #f59e0b; }
.health-dot.error { background: #ef4444; }
.health-name {
font-size: var(--font-size-sm);
color: var(--text-primary);
}
</style>
{% endblock %}
{% block content %}
<a href="{{ url_for('admin.admin_security') }}" class="back-link">← Powrót do panelu bezpieczeństwa</a>
<div class="status-header">
<div>
<h1>📊 Status systemu</h1>
<p>Monitoring wydajności i zdrowia portalu NordaBiznes</p>
</div>
<div class="refresh-info">
<div class="label">Ostatnia aktualizacja</div>
<div class="timestamp" id="last-update">{{ generated_at.strftime('%H:%M:%S') }}</div>
<div class="label" style="margin-top: var(--spacing-xs);">{{ generated_at.strftime('%d.%m.%Y') }}</div>
<div class="next-refresh">
<span class="refresh-indicator"></span>
Auto-refresh: <span id="countdown">5:00</span>
</div>
</div>
</div>
<div class="metrics-grid">
<!-- System Metrics Card -->
<div class="metric-card system">
<div class="metric-card-header">
<div class="metric-card-icon">🖥️</div>
<div>
<div class="metric-card-title">System</div>
<div class="metric-card-subtitle">{{ system_metrics.hostname }}</div>
</div>
</div>
<!-- CPU Gauge -->
{% if system_metrics.cpu_percent is not none %}
<div class="gauge-container">
<div class="gauge">
<div class="gauge-bg"></div>
<div class="gauge-fill {{ 'low' if system_metrics.cpu_percent < 50 else ('medium' if system_metrics.cpu_percent < 80 else 'high') }}"
style="transform: rotate({{ 225 + (system_metrics.cpu_percent * 1.8) }}deg)"></div>
<div class="gauge-value" id="cpu-value">{{ system_metrics.cpu_percent }}%</div>
<div class="gauge-label">CPU</div>
</div>
</div>
{% endif %}
<!-- RAM Progress Bar -->
{% if system_metrics.ram_percent is not none %}
<div class="progress-bar-container">
<div class="progress-bar-label">
<span>RAM</span>
<span id="ram-text">{{ system_metrics.ram_used_gb }} GB / {{ system_metrics.ram_total_gb }} GB ({{ system_metrics.ram_percent }}%)</span>
</div>
<div class="progress-bar">
<div class="progress-bar-fill {{ 'low' if system_metrics.ram_percent < 50 else ('medium' if system_metrics.ram_percent < 80 else 'high') }}"
id="ram-bar" style="width: {{ system_metrics.ram_percent }}%"></div>
</div>
</div>
{% endif %}
<!-- Disk Progress Bar -->
{% if system_metrics.disk_percent is not none %}
<div class="progress-bar-container">
<div class="progress-bar-label">
<span>Dysk</span>
<span id="disk-text">{{ system_metrics.disk_used }} / {{ system_metrics.disk_total }} ({{ system_metrics.disk_percent }}%)</span>
</div>
<div class="progress-bar">
<div class="progress-bar-fill {{ 'low' if system_metrics.disk_percent < 50 else ('medium' if system_metrics.disk_percent < 80 else 'high') }}"
id="disk-bar" style="width: {{ system_metrics.disk_percent }}%"></div>
</div>
</div>
{% endif %}
<div class="info-row">
<span class="info-label">System operacyjny</span>
<span class="info-value">{{ system_metrics.os }}</span>
</div>
<div class="info-row">
<span class="info-label">Python</span>
<span class="info-value">{{ system_metrics.python }}</span>
</div>
{% if system_metrics.uptime %}
<div class="info-row">
<span class="info-label">Uptime</span>
<span class="info-value">{{ system_metrics.uptime }}</span>
</div>
{% endif %}
{% if system_metrics.load_1 is defined %}
<div class="info-row">
<span class="info-label">Load Average</span>
<span class="info-value">{{ system_metrics.load_1 }} / {{ system_metrics.load_5 }} / {{ system_metrics.load_15 }}</span>
</div>
{% endif %}
</div>
<!-- Database Metrics Card -->
<div class="metric-card database">
<div class="metric-card-header">
<div class="metric-card-icon">🗄️</div>
<div>
<div class="metric-card-title">Baza danych</div>
<div class="metric-card-subtitle">PostgreSQL</div>
</div>
<span class="status-badge {{ 'ok' if db_metrics.status == 'ok' else 'error' }}">
{{ '✓ OK' if db_metrics.status == 'ok' else '✗ Błąd' }}
</span>
</div>
{% if db_metrics.status == 'ok' %}
<div class="metric-stats">
<div class="metric-stat">
<div class="metric-stat-value">{{ db_metrics.size_mb }} MB</div>
<div class="metric-stat-label">Rozmiar bazy</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value" id="db-connections">{{ db_metrics.active_connections }}/{{ db_metrics.total_connections }}</div>
<div class="metric-stat-label">Połączenia aktywne</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value">{{ db_metrics.companies }}</div>
<div class="metric-stat-label">Firm</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value">{{ db_metrics.users }}</div>
<div class="metric-stat-label">Użytkowników</div>
</div>
</div>
{% if db_metrics.chat_messages is defined %}
<div style="margin-top: var(--spacing-lg);">
<div class="info-row">
<span class="info-label">Wiadomości chat</span>
<span class="info-value">{{ db_metrics.chat_messages }}</span>
</div>
<div class="info-row">
<span class="info-label">Sesje chat</span>
<span class="info-value">{{ db_metrics.chat_sessions }}</span>
</div>
<div class="info-row">
<span class="info-label">Profile Social Media</span>
<span class="info-value">{{ db_metrics.social_media }}</span>
</div>
<div class="info-row">
<span class="info-label">Audyty SEO</span>
<span class="info-value">{{ db_metrics.seo_audits }}</span>
</div>
</div>
{% endif %}
{% if db_metrics.cache_hit_ratio is defined %}
<div style="margin-top: var(--spacing-md); padding-top: var(--spacing-md); border-top: 1px solid var(--border);">
<div class="info-row">
<span class="info-label">Cache Hit Ratio</span>
<span class="info-value" style="color: {{ '#22c55e' if db_metrics.cache_hit_ratio > 95 else '#f59e0b' if db_metrics.cache_hit_ratio > 80 else '#ef4444' }};">{{ db_metrics.cache_hit_ratio }}%</span>
</div>
{% if db_metrics.slow_queries is defined %}
<div class="info-row">
<span class="info-label">Slow Queries (>1s)</span>
<span class="info-value">{{ db_metrics.slow_queries }}</span>
</div>
{% endif %}
{% if db_metrics.deadlocks is defined %}
<div class="info-row">
<span class="info-label">Deadlocks</span>
<span class="info-value" style="color: {{ '#22c55e' if db_metrics.deadlocks == 0 else '#ef4444' }};">{{ db_metrics.deadlocks }}</span>
</div>
{% endif %}
</div>
{% endif %}
{% else %}
<div style="padding: var(--spacing-xl); text-align: center; color: #991b1b;">
<p>⚠️ Błąd połączenia z bazą danych</p>
<p style="font-size: var(--font-size-xs);">{{ db_metrics.error }}</p>
</div>
{% endif %}
</div>
<!-- Application Metrics Card -->
<div class="metric-card app">
<div class="metric-card-header">
<div class="metric-card-icon">🌐</div>
<div>
<div class="metric-card-title">Aplikacja</div>
<div class="metric-card-subtitle">NordaBiznes Portal</div>
</div>
</div>
{% if app_metrics.endpoints_ok is not none %}
<div class="gauge-container">
<div class="gauge">
<div class="gauge-bg"></div>
<div class="gauge-fill {{ 'low' if app_metrics.endpoints_percent == 100 else ('medium' if app_metrics.endpoints_percent >= 80 else 'high') }}"
style="transform: rotate({{ 225 + (app_metrics.endpoints_percent * 1.8) }}deg)"></div>
<div class="gauge-value">{{ app_metrics.endpoints_percent|int }}%</div>
<div class="gauge-label">Endpointy OK</div>
</div>
</div>
{% endif %}
<div class="metric-stats">
<div class="metric-stat">
<div class="metric-stat-value">{{ app_metrics.logins_24h }}</div>
<div class="metric-stat-label">Logowań (24h)</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value" id="alerts-24h">{{ app_metrics.alerts_24h }}</div>
<div class="metric-stat-label">Alertów (24h)</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value">{{ app_metrics.admins }}</div>
<div class="metric-stat-label">Administratorów</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value">{{ app_metrics.users_with_2fa }}</div>
<div class="metric-stat-label">Z 2FA</div>
</div>
</div>
<div class="health-grid">
<div class="health-item">
<div class="health-dot ok"></div>
<span class="health-name">Frontend</span>
</div>
<div class="health-item">
<div class="health-dot {{ 'ok' if db_metrics.status == 'ok' else 'error' }}"></div>
<span class="health-name">Database</span>
</div>
<div class="health-item">
<div class="health-dot ok"></div>
<span class="health-name">API</span>
</div>
<div class="health-item">
<div class="health-dot ok"></div>
<span class="health-name">Auth</span>
</div>
</div>
</div>
<!-- Process Metrics Card -->
<div class="metric-card process">
<div class="metric-card-header">
<div class="metric-card-icon">⚙️</div>
<div>
<div class="metric-card-title">Procesy</div>
<div class="metric-card-subtitle">Serwer aplikacji</div>
</div>
<span class="status-badge {{ 'ok' if process_metrics.gunicorn_status == 'running' else 'warning' }}">
{{ 'Gunicorn ✓' if process_metrics.gunicorn_status == 'running' else 'Dev mode' }}
</span>
</div>
{% if process_metrics.gunicorn_status == 'running' %}
<div class="metric-stats" style="grid-template-columns: 1fr;">
<div class="metric-stat">
<div class="metric-stat-value">{{ process_metrics.gunicorn_workers }}</div>
<div class="metric-stat-label">Gunicorn Workers</div>
</div>
</div>
{% else %}
<div style="padding: var(--spacing-lg); text-align: center;">
<p style="color: var(--text-secondary); font-size: var(--font-size-sm);">
{% if process_metrics.gunicorn_status == 'not found' %}
Gunicorn nie wykryty - tryb deweloperski
{% else %}
Status Gunicorn nieznany
{% endif %}
</p>
</div>
{% endif %}
<div class="quick-actions">
<a href="{{ url_for('health_full') }}" class="quick-action-btn" target="_blank">🔍 Pełny health check</a>
<a href="{{ url_for('admin.admin_security') }}" class="quick-action-btn">🛡️ Bezpieczeństwo</a>
</div>
<div class="quick-actions">
<a href="{{ url_for('admin.debug_panel') }}" class="quick-action-btn">🐛 Debug panel</a>
<a href="{{ url_for('admin.admin_analytics') }}" class="quick-action-btn">📈 Analityka</a>
</div>
</div>
</div>
<!-- New Metrics Row -->
<div class="metrics-grid" style="margin-bottom: var(--spacing-xl);">
<!-- SSL Certificate Card -->
{% if ssl_metrics is defined %}
<div class="metric-card" style="border-top: 4px solid {{ '#22c55e' if ssl_metrics.status == 'ok' else '#f59e0b' if ssl_metrics.status == 'warning' else '#ef4444' }};">
<div class="metric-card-header">
<div class="metric-card-icon" style="background: linear-gradient(135deg, #dcfce7, #d1fae5);">🔒</div>
<div>
<div class="metric-card-title">Certyfikat SSL</div>
<div class="metric-card-subtitle">{{ ssl_metrics.domain if ssl_metrics.domain else 'nordabiznes.pl' }}</div>
</div>
<span class="status-badge {{ ssl_metrics.status }}">
{% if ssl_metrics.status == 'ok' %}✓ Ważny
{% elif ssl_metrics.status == 'warning' %}⚠️ Wkrótce wygasa
{% else %}✗ Problem{% endif %}
</span>
</div>
{% if ssl_metrics.days_left is defined %}
<div class="metric-stats" style="grid-template-columns: 1fr 1fr;">
<div class="metric-stat">
<div class="metric-stat-value" style="color: {{ '#22c55e' if ssl_metrics.days_left > 30 else '#f59e0b' if ssl_metrics.days_left > 7 else '#ef4444' }};">{{ ssl_metrics.days_left }}</div>
<div class="metric-stat-label">Dni do wygaśnięcia</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value" style="font-size: var(--font-size-sm);">{{ ssl_metrics.expires }}</div>
<div class="metric-stat-label">Data wygaśnięcia</div>
</div>
</div>
<div class="metric-stat" style="margin-top: var(--spacing-md); text-align: center; padding: var(--spacing-sm); background: var(--bg-secondary); border-radius: var(--radius-md);">
<div class="metric-stat-value" style="font-size: var(--font-size-sm);">{{ ssl_metrics.issuer }}</div>
<div class="metric-stat-label">Wystawca</div>
</div>
{% else %}
<div style="padding: var(--spacing-lg); text-align: center; color: #991b1b;">
<p>⚠️ Nie można sprawdzić certyfikatu</p>
{% if ssl_metrics.error %}<p style="font-size: var(--font-size-xs);">{{ ssl_metrics.error }}</p>{% endif %}
</div>
{% endif %}
</div>
{% endif %}
<!-- Deploy Info Card -->
{% if deploy_metrics is defined %}
<div class="metric-card" style="border-top: 4px solid #8b5cf6;">
<div class="metric-card-header">
<div class="metric-card-icon" style="background: linear-gradient(135deg, #ede9fe, #ddd6fe);">🚀</div>
<div>
<div class="metric-card-title">Deploy</div>
<div class="metric-card-subtitle">Git / Wersja</div>
</div>
</div>
{% if deploy_metrics.commit %}
<div class="metric-stats" style="grid-template-columns: 1fr;">
<div class="metric-stat">
<div class="metric-stat-value" style="font-family: monospace; font-size: var(--font-size-lg);">{{ deploy_metrics.commit }}</div>
<div class="metric-stat-label">Aktualny commit</div>
</div>
</div>
<div style="margin-top: var(--spacing-md);">
<div class="info-row">
<span class="info-label">Branch</span>
<span class="info-value">{{ deploy_metrics.branch }}</span>
</div>
<div class="info-row">
<span class="info-label">Data commitu</span>
<span class="info-value">{{ deploy_metrics.commit_date }}</span>
</div>
<div class="info-row">
<span class="info-label">Opis</span>
<span class="info-value" style="font-size: var(--font-size-xs); max-width: 200px; overflow: hidden; text-overflow: ellipsis;">{{ deploy_metrics.commit_message }}</span>
</div>
</div>
{% else %}
<div style="padding: var(--spacing-lg); text-align: center; color: var(--text-secondary);">
<p>Brak danych Git</p>
</div>
{% endif %}
</div>
{% endif %}
<!-- Security Metrics Card -->
{% if security_metrics is defined %}
<div class="metric-card" style="border-top: 4px solid #ef4444;">
<div class="metric-card-header">
<div class="metric-card-icon" style="background: linear-gradient(135deg, #fee2e2, #fecaca);">🛡️</div>
<div>
<div class="metric-card-title">Bezpieczeństwo</div>
<div class="metric-card-subtitle">Ostatnie 24h</div>
</div>
</div>
<div class="metric-stats">
<div class="metric-stat">
<div class="metric-stat-value" style="color: {{ '#22c55e' if security_metrics.failed_logins_24h == 0 else '#f59e0b' if security_metrics.failed_logins_24h < 10 else '#ef4444' }};">{{ security_metrics.failed_logins_24h }}</div>
<div class="metric-stat-label">Nieudane logowania</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value">{{ security_metrics.geoip_blocked_24h }}</div>
<div class="metric-stat-label">Blokady GeoIP</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value">{{ security_metrics.rate_limit_24h }}</div>
<div class="metric-stat-label">Rate limit hits</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value" style="color: {{ '#22c55e' if security_metrics.locked_accounts == 0 else '#ef4444' }};">{{ security_metrics.locked_accounts }}</div>
<div class="metric-stat-label">Zablokowane konta</div>
</div>
</div>
<div class="info-row" style="margin-top: var(--spacing-md);">
<span class="info-label">Unikalne IP zablokowane</span>
<span class="info-value">{{ security_metrics.unique_ips_blocked }}</span>
</div>
</div>
{% endif %}
<!-- External APIs Status Card -->
{% if external_apis is defined %}
<div class="metric-card" style="border-top: 4px solid #06b6d4;">
<div class="metric-card-header">
<div class="metric-card-icon" style="background: linear-gradient(135deg, #cffafe, #a5f3fc);">🔌</div>
<div>
<div class="metric-card-title">External APIs</div>
<div class="metric-card-subtitle">Status połączeń</div>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for api in external_apis %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--background); border-radius: var(--radius);">
<div class="health-dot {{ api.status }}"></div>
<span style="flex: 1; font-weight: 500;">{{ api.name }}</span>
{% if api.latency %}
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ api.latency }}ms</span>
{% else %}
<span style="font-size: var(--font-size-xs); color: #ef4444;">offline</span>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Servers Status Row -->
{% if servers_status is defined %}
<div class="metric-card" style="margin-bottom: var(--spacing-xl); border-top: 4px solid #6366f1;">
<div class="metric-card-header">
<div class="metric-card-icon" style="background: linear-gradient(135deg, #e0e7ff, #c7d2fe);">🖥️</div>
<div>
<div class="metric-card-title">Status serwerów</div>
<div class="metric-card-subtitle">Ping w sieci INPI</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--spacing-md);">
{% for server in servers_status %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-md); background: var(--background); border-radius: var(--radius); border-left: 3px solid {{ '#22c55e' if server.status == 'online' else '#ef4444' }};">
<div>
<div class="health-dot {{ 'ok' if server.status == 'online' else 'error' }}" style="width: 12px; height: 12px;"></div>
</div>
<div style="flex: 1;">
<div style="font-weight: 600;">{{ server.name }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary); font-family: monospace;">{{ server.ip }}</div>
</div>
<div style="text-align: right;">
{% if server.status == 'online' %}
<div style="font-size: var(--font-size-sm); color: #22c55e;">Online</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ server.latency }}ms</div>
{% else %}
<div style="font-size: var(--font-size-sm); color: #ef4444;">Offline</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Technology Stack Section -->
<div class="tech-stack-section" style="margin-top: var(--spacing-2xl);">
<h2 style="font-size: var(--font-size-xl); color: var(--text-primary); margin-bottom: var(--spacing-lg);">
🛠️ Stos technologiczny
</h2>
<div class="tech-categories" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: var(--spacing-lg);">
<!-- Programming -->
<div class="tech-category" style="background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--spacing-lg);">
<h3 style="display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-base); color: var(--text-primary); margin-bottom: var(--spacing-md); padding-bottom: var(--spacing-sm); border-bottom: 2px solid #3b82f6;">
💻 Programowanie
</h3>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for tech in technology_stack.programming %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--background); border-radius: var(--radius);">
<span style="font-size: 1.3em;">{{ tech.icon }}</span>
<span style="flex: 1; font-weight: 500;">{{ tech.name }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); background: var(--surface); padding: 2px 8px; border-radius: var(--radius);">{{ tech.version }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- Databases -->
<div class="tech-category" style="background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--spacing-lg);">
<h3 style="display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-base); color: var(--text-primary); margin-bottom: var(--spacing-md); padding-bottom: var(--spacing-sm); border-bottom: 2px solid #10b981;">
🗄️ Bazy danych
</h3>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for tech in technology_stack.databases %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--background); border-radius: var(--radius);">
<span style="font-size: 1.3em;">{{ tech.icon }}</span>
<span style="flex: 1; font-weight: 500;">{{ tech.name }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ tech.category }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); background: var(--surface); padding: 2px 8px; border-radius: var(--radius);">{{ tech.version }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- AI -->
<div class="tech-category" style="background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--spacing-lg);">
<h3 style="display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-base); color: var(--text-primary); margin-bottom: var(--spacing-md); padding-bottom: var(--spacing-sm); border-bottom: 2px solid #8b5cf6;">
🤖 Sztuczna inteligencja
</h3>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for tech in technology_stack.ai %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--background); border-radius: var(--radius);">
<span style="font-size: 1.3em;">{{ tech.icon }}</span>
<span style="flex: 1; font-weight: 500;">{{ tech.name }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ tech.category }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); background: var(--surface); padding: 2px 8px; border-radius: var(--radius);">{{ tech.version }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- Infrastructure -->
<div class="tech-category" style="background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--spacing-lg);">
<h3 style="display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-base); color: var(--text-primary); margin-bottom: var(--spacing-md); padding-bottom: var(--spacing-sm); border-bottom: 2px solid #f59e0b;">
🖥️ Infrastruktura
</h3>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for tech in technology_stack.infrastructure %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--background); border-radius: var(--radius);">
<span style="font-size: 1.3em;">{{ tech.icon }}</span>
<span style="flex: 1; font-weight: 500;">{{ tech.name }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ tech.category }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); background: var(--surface); padding: 2px 8px; border-radius: var(--radius);">{{ tech.version }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- Network -->
<div class="tech-category" style="background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--spacing-lg);">
<h3 style="display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-base); color: var(--text-primary); margin-bottom: var(--spacing-md); padding-bottom: var(--spacing-sm); border-bottom: 2px solid #ef4444;">
🌐 Sieć
</h3>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for tech in technology_stack.network %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--background); border-radius: var(--radius);">
<span style="font-size: 1.3em;">{{ tech.icon }}</span>
<span style="flex: 1; font-weight: 500;">{{ tech.name }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ tech.category }}</span>
{% if tech.version %}
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); background: var(--surface); padding: 2px 8px; border-radius: var(--radius);">{{ tech.version }}</span>
{% else %}
<span style="font-size: var(--font-size-xs); color: var(--warning); background: #fef3c7; padding: 2px 8px; border-radius: var(--radius);">nieznana</span>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<!-- Security -->
<div class="tech-category" style="background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--spacing-lg);">
<h3 style="display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-base); color: var(--text-primary); margin-bottom: var(--spacing-md); padding-bottom: var(--spacing-sm); border-bottom: 2px solid #14b8a6;">
🛡️ Bezpieczeństwo
</h3>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for tech in technology_stack.security %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--background); border-radius: var(--radius);">
<span style="font-size: 1.3em;">{{ tech.icon }}</span>
<span style="flex: 1; font-weight: 500;">{{ tech.name }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ tech.category }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); background: var(--surface); padding: 2px 8px; border-radius: var(--radius);">{{ tech.version }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- DevOps -->
<div class="tech-category" style="background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--spacing-lg);">
<h3 style="display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-base); color: var(--text-primary); margin-bottom: var(--spacing-md); padding-bottom: var(--spacing-sm); border-bottom: 2px solid #ec4899;">
⚙️ DevOps
</h3>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for tech in technology_stack.devops %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--background); border-radius: var(--radius);">
<span style="font-size: 1.3em;">{{ tech.icon }}</span>
<span style="flex: 1; font-weight: 500;">{{ tech.name }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ tech.category }}</span>
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); background: var(--surface); padding: 2px 8px; border-radius: var(--radius);">{{ tech.version }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- Servers -->
<div class="tech-category" style="background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--spacing-lg);">
<h3 style="display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-base); color: var(--text-primary); margin-bottom: var(--spacing-md); padding-bottom: var(--spacing-sm); border-bottom: 2px solid #6366f1;">
🖥️ Serwery
</h3>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for server in technology_stack.servers %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--background); border-radius: var(--radius);">
<span style="font-size: 1.3em;">{{ server.icon }}</span>
<span style="flex: 1; font-weight: 500;">{{ server.name }}</span>
<span style="font-family: monospace; font-size: var(--font-size-xs); color: var(--text-secondary); background: var(--surface); padding: 2px 8px; border-radius: var(--radius);">{{ server.ip }}</span>
</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary); padding-left: calc(1.3em + var(--spacing-md)); margin-top: -4px;">
{{ server.role }}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
// Auto-refresh every 5 minutes
const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes in ms
let countdownSeconds = 300;
function updateCountdown() {
const minutes = Math.floor(countdownSeconds / 60);
const seconds = countdownSeconds % 60;
document.getElementById('countdown').textContent =
`${minutes}:${seconds.toString().padStart(2, '0')}`;
if (countdownSeconds <= 0) {
refreshData();
countdownSeconds = 300;
} else {
countdownSeconds--;
}
}
async function refreshData() {
try {
const response = await fetch('/api/admin/status');
if (response.ok) {
const data = await response.json();
// Update timestamp
const timestamp = new Date(data.timestamp);
document.getElementById('last-update').textContent =
timestamp.toLocaleTimeString('pl-PL');
// Update CPU if available
if (data.system && data.system.cpu_percent !== null) {
const cpuEl = document.getElementById('cpu-value');
if (cpuEl) cpuEl.textContent = data.system.cpu_percent + '%';
}
// Update RAM if available
if (data.system && data.system.ram_percent !== null) {
const ramBar = document.getElementById('ram-bar');
if (ramBar) {
ramBar.style.width = data.system.ram_percent + '%';
ramBar.className = 'progress-bar-fill ' +
(data.system.ram_percent < 50 ? 'low' :
data.system.ram_percent < 80 ? 'medium' : 'high');
}
}
// Update disk if available
if (data.system && data.system.disk_percent !== null) {
const diskBar = document.getElementById('disk-bar');
if (diskBar) {
diskBar.style.width = data.system.disk_percent + '%';
diskBar.className = 'progress-bar-fill ' +
(data.system.disk_percent < 50 ? 'low' :
data.system.disk_percent < 80 ? 'medium' : 'high');
}
}
// Update DB connections
if (data.database && data.database.active_connections !== undefined) {
const connEl = document.getElementById('db-connections');
if (connEl) connEl.textContent = data.database.active_connections + '/...';
}
// Update alerts count
if (data.app && data.app.alerts_24h !== undefined) {
const alertsEl = document.getElementById('alerts-24h');
if (alertsEl) alertsEl.textContent = data.app.alerts_24h;
}
}
} catch (error) {
console.error('Failed to refresh status data:', error);
}
}
// Start countdown
setInterval(updateCountdown, 1000);
// Also refresh when page becomes visible again
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
refreshData();
countdownSeconds = 300;
}
});
{% endblock %}