This commit is contained in:
root
2026-01-01 02:13:34 +01:00
commit b05793228a
29 changed files with 4608 additions and 0 deletions

82
templates/base.html Normal file
View File

@@ -0,0 +1,82 @@
</html>
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>{% block title %}Panel{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
{% block head %}{% endblock %}
</head>
<body class="d-flex flex-column min-vh-100">
<header class="border-bottom border-secondary-subtle">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" aria-label="Główna nawigacja">
<div class="container-xxl">
<a class="navbar-brand fw-semibold text-primary" href="{{ url_for('index') }}">autoban</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav"
aria-controls="mainNav" aria-label="Menu">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link {% if request.endpoint=='index' %}active{% endif %}"
href="{{ url_for('index') }}">Dashboard</a></li>
<li class="nav-item"><a class="nav-link {% if request.endpoint=='ban_management' %}active{% endif %}"
href="{{ url_for('ban_management') }}">Zarządzanie Banami</a></li>
<li class="nav-item"><a class="nav-link {% if request.endpoint=='stats_page' %}active{% endif %}"
href="{{ url_for('stats_page') }}">Statystyki</a></li>
<li class="nav-item"><a class="nav-link {% if request.endpoint=='charts_page' %}active{% endif %}"
href="{{ url_for('charts_page') }}">Wykresy</a></li>
<li class="nav-item"><a class="nav-link {% if request.endpoint=='view_logs' %}active{% endif %}"
href="{{ url_for('view_logs') }}">Logi</a></li>
<li class="nav-item"><a class="nav-link {% if request.endpoint=='check_ip_info' %}active{% endif %}"
href="{{ url_for('check_ip_info') }}">Sprawdź IP</a></li>
<li class="nav-item"><a class="nav-link {% if request.endpoint=='reset_counters' %}active{% endif %}"
href="{{ url_for('reset_counters') }}">Reset błędów</a></li>
</ul>
<form class="d-flex" action="{{ url_for('check_ip_info') }}" method="post" role="search">
<input class="form-control form-control-sm bg-dark text-white border-secondary" name="ip"
placeholder="Szybkie IP" inputmode="numeric" pattern="\d{1,3}(\.\d{1,3}){3}">
</form>
</div>
</div>
</nav>
</header>
<main id="main" class="container-xxl flex-grow-1 py-3">
{% block content %}{% endblock %}
</main>
<footer class="border-top border-secondary-subtle mt-auto bg-dark">
<div class="container-xxl py-3 text-secondary small d-flex flex-wrap gap-2 justify-content-between">
<span>linuxiarz.pl</span>
<a class="link-secondary" href="{{ url_for('healthcheck') }}">Status</a>
</div>
</footer>
{# GLOBALNY TOAST (w całej aplikacji) #}
<div class="position-fixed end-0 bottom-0 p-3" style="z-index:1080;">
<div id="appToast" class="toast text-bg-dark border border-secondary" role="status" aria-live="polite"
aria-atomic="true">
<div class="toast-header bg-dark text-white border-bottom border-secondary">
<strong class="me-auto">{{ app_name or "Ban Manager" }}</strong>
<small>Teraz</small>
<button class="btn-close btn-close-white ms-2 mb-1" data-bs-dismiss="toast" aria-label="Zamknij"></button>
</div>
<div class="toast-body" id="appToastBody">Gotowe.</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/toast.js') }}" defer></script>
{% block scripts %}{% endblock %}
</body>
</html>

182
templates/charts.html Normal file
View File

@@ -0,0 +1,182 @@
{% extends "base.html" %}
{% block title %}Wykresy{% endblock %}
{% block content %}
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<h1 class="h3 text-white mb-1">Wykresy</h1>
<p class="text-secondary mb-0">Podgląd trendów, najczęstszych powodów i źródeł banów.</p>
</div>
<form id="options-form" method="get" class="d-flex align-items-center gap-2">
<label for="top_n" class="text-secondary small">TOP:</label>
<select id="top_n" name="top_n" class="form-select form-select-sm bg-dark text-white border-secondary"
style="width:auto">
{% for n in [5,10,25,50,100] %}
<option value="{{ n }}" {% if n==top_n %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
<label for="period" class="text-secondary small ms-2">Okres:</label>
<select id="period" name="period" class="form-select form-select-sm bg-dark text-white border-secondary"
style="width:auto">
{% for p in [("week","Tydzień"),("month","Miesiąc"),("year","Rok")] %}
<option value="{{ p[0] }}" {% if p[0]==period %}selected{% endif %}>{{ p[1] }}</option>
{% endfor %}
</select>
<div class="btn-group btn-group-sm ms-1" role="group" aria-label="Szybkie TOP">
{% for n in [5,10,25,50,100] %}
<button type="button" class="btn btn-outline-secondary quick-top {% if n == top_n %}active{% endif %}"
data-value="{{ n }}">{{ n }}</button>
{% endfor %}
</div>
<div class="btn-group btn-group-sm ms-1" role="group" aria-label="Szybki okres">
{% for p in [("week","Tydzień"),("month","Miesiąc"),("year","Rok")] %}
<button type="button" class="btn btn-outline-secondary quick-period {% if p[0] == period %}active{% endif %}"
data-value="{{ p[0] }}">{{ p[1] }}</button>
{% endfor %}
</div>
<!-- <button type="submit" class="btn btn-primary btn-sm ms-1">Zastosuj</button> -->
</form>
</div>
<!-- skeleton podczas ładowania -->
<div id="charts-skeleton" class="placeholder-glow mb-3">
<span class="placeholder col-12"></span>
<span class="placeholder col-12"></span>
<span class="placeholder col-12"></span>
</div>
<div id="charts-root" class="d-none">
<div class="card bg-dark border-secondary mb-4">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Top {{ top_n }} powody banów</span>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary chart-download" data-target="reasonsChart" data-type="png">PNG</button>
<button class="btn btn-outline-secondary chart-download" data-target="reasonsChart" data-type="csv">CSV</button>
<button class="btn btn-outline-secondary chart-fullscreen" data-target="reasonsChart">Fullscreen</button>
</div>
</div>
<div class="card-body">
<canvas id="reasonsChart" height="120"></canvas>
<div class="text-center text-secondary small d-none" id="reasonsEmpty">Brak danych do wyświetlenia.</div>
</div>
</div>
<div class="card bg-dark border-secondary mb-4">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Top {{ top_n }} URL (bany)</span>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary chart-download" data-target="urlsChart" data-type="png">PNG</button>
<button class="btn btn-outline-secondary chart-download" data-target="urlsChart" data-type="csv">CSV</button>
<button class="btn btn-outline-secondary chart-fullscreen" data-target="urlsChart">Fullscreen</button>
</div>
</div>
<div class="card-body">
<canvas id="urlsChart" height="120"></canvas>
<div class="text-center text-secondary small d-none" id="urlsEmpty">Brak danych do wyświetlenia.</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="card bg-dark border-secondary h-100">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Kraje pochodzenia (bany)</span>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary chart-download" data-target="countriesChart"
data-type="png">PNG</button>
<button class="btn btn-outline-secondary chart-download" data-target="countriesChart"
data-type="csv">CSV</button>
<button class="btn btn-outline-secondary chart-fullscreen" data-target="countriesChart">Fullscreen</button>
</div>
</div>
<div class="card-body d-flex justify-content-center">
<canvas id="countriesChart" style="max-width: 340px; max-height: 340px;"></canvas>
</div>
<div class="text-center text-secondary small d-none" id="countriesEmpty">Brak danych do wyświetlenia.</div>
</div>
</div>
<div class="col-md-6">
<div class="card bg-dark border-secondary h-100">
<div class="card-header">Legenda krajów</div>
<div class="card-body">
<ul class="list-group list-group-flush">
{% for item in stats.top_countries %}
<li
class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center border-secondary">
{{ item.country }}
<span class="badge bg-primary rounded-pill">{{ item.count }}</span>
</li>
{% endfor %}
{% if stats.top_countries|length == 0 %}
<li class="list-group-item bg-dark text-secondary border-secondary text-center">Brak danych</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
<div class="card bg-dark border-secondary my-4">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Bany w czasie (ostatnie 6 tygodni)</span>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary chart-download" data-target="bansOverTimeChart"
data-type="png">PNG</button>
<button class="btn btn-outline-secondary chart-download" data-target="bansOverTimeChart"
data-type="csv">CSV</button>
<button class="btn btn-outline-secondary chart-fullscreen" data-target="bansOverTimeChart">Fullscreen</button>
</div>
</div>
<div class="card-body">
<canvas id="bansOverTimeChart" height="120"></canvas>
<div class="text-center text-secondary small d-none" id="timeEmpty">Brak danych do wyświetlenia.</div>
</div>
</div>
</div>
<!-- Modal fullscreen dla dowolnego canvas -->
<div class="modal fade" id="chartModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title">Podgląd wykresu</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body d-flex align-items-center justify-content-center">
<canvas id="chartModalCanvas" style="max-width:95vw; max-height:80vh;"></canvas>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6"></script>
<script>
const statsData = {{ stats | tojson }};
document.getElementById('top_n')?.addEventListener('change', () => {
document.getElementById('options-form').requestSubmit();
});
window.addEventListener('DOMContentLoaded', () => {
const sk = document.getElementById('charts-skeleton');
const root = document.getElementById('charts-root');
try {
renderCharts(statsData);
sk?.classList.add('d-none');
root?.classList.remove('d-none');
} catch (e) {
window.showToast?.({ text: 'Błąd renderowania wykresów', variant: 'danger' });
console.error(e);
}
});
</script>
<script src="{{ url_for('static', filename='js/charts.js') }}"></script>
{% endblock %}

190
templates/check.html Normal file
View File

@@ -0,0 +1,190 @@
{% extends "base.html" %}
{% block title %}Sprawdź IP{% endblock %}
{% block content %}
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<h1 class="h3 text-white mb-1">Sprawdź IP</h1>
<p class="text-secondary mb-0">Szybka diagnostyka adresu — logi, endpointy i metryki powiązane z IP.</p>
</div>
</div>
<!-- Formularz -->
<div class="card bg-dark border-secondary mb-4">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Wprowadź IP do sprawdzenia</span>
</div>
<div class="card-body">
<form method="post" id="reset-ip-form" novalidate>
<div class="mb-3 col-sm-6 col-md-5 col-lg-4">
<label for="ip" class="form-label text-primary">Adres IPv4</label>
<input id="ip" name="ip" class="form-control bg-dark text-white border-secondary" inputmode="numeric"
autocomplete="off" required placeholder="np. 185.12.34.56" value="{{ ip }}" pattern="\d{1,3}(\.\d{1,3}){3}">
<div class="form-text text-primary"><code>Format: xxx.xxx.xxx.xxx</code></div>
<div class="invalid-feedback">Podaj poprawny adres IPv4.</div>
</div>
<div class="col-12">
<button id="btn-confirm-ip" type="submit" class="btn btn-outline-primary">Sprawdź</button>
</div>
</form>
</div>
</div>
<!-- Ostatnie błędy -->
{% if recent_errors %}
<div class="card bg-dark border-secondary mb-4">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Ostatnie błędy</span>
<div class="input-group input-group-sm" style="width: 260px;">
<span class="input-group-text bg-dark border-secondary text-secondary">🔎</span>
<input id="errors-search" type="search" class="form-control bg-dark border-secondary text-white"
placeholder="Filtruj błędy…">
</div>
</div>
<div class="card-body">
<div class="accordion" id="errorsAccordion">
{% for error_entry in recent_errors %}
<div class="accordion-item bg-transparent border-secondary">
<h2 class="accordion-header" id="err-h-{{ loop.index }}">
<button class="accordion-button collapsed bg-dark text-white border-secondary" type="button"
data-bs-toggle="collapse" data-bs-target="#err-c-{{ loop.index }}" aria-expanded="false"
aria-controls="err-c-{{ loop.index }}">
<span class="me-2 badge bg-danger">#{{ loop.index }}</span>
<span class="error-title text-truncate" style="max-width: 70%;">{{ error_entry[:120] }}{% if
error_entry|length > 120 %}…{% endif %}</span>
</button>
</h2>
<div id="err-c-{{ loop.index }}" class="accordion-collapse collapse" aria-labelledby="err-h-{{ loop.index }}"
data-bs-parent="#errorsAccordion">
<div class="accordion-body">
<pre class="mb-2 text-white small" style="white-space: pre-wrap;">{{ error_entry }}</pre>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- Endpointy -->
{% if endpoints %}
<div class="card bg-dark border-secondary mb-4">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Zalogowane endpointy <span class="text-secondary">( {{ endpoints|length }} )</span></span>
<div class="d-flex align-items-center gap-2">
<div class="input-group input-group-sm" style="width: 320px;">
<span class="input-group-text bg-dark border-secondary text-secondary">🔎</span>
<input id="endpoints-search" type="search" class="form-control bg-dark border-secondary text-white"
placeholder="Filtruj endpointy…">
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-dark table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:64px">#</th>
<th>Endpoint</th>
</tr>
</thead>
<tbody id="endpoints-tbody">
{% for endpoint in endpoints %}
<tr>
<td class="text-secondary">{{ loop.index }}</td>
<td>
<span class="d-inline-block text-truncate" style="max-width: 70%;" title="{{ endpoint }}">{{ endpoint
}}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div id="endpoints-empty" class="text-center text-secondary small d-none py-4">Brak wyników dla podanego filtra
</div>
</div>
</div>
</div>
{% endif %}
<!-- Metryki -->
{% if metrics %}
<div class="card bg-dark border-secondary mb-4">
<div class="card-header">Metryki</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-dark align-middle mb-0">
<thead>
<tr>
<th>Klucz</th>
<th class="text-end">Wartość</th>
</tr>
</thead>
<tbody>
{% for key, value in metrics.items() %}
<tr>
<td class="text-secondary">{{ key }}</td>
<td class="text-end">
{% if value is number %}
<span class="badge bg-primary">{{ value }}</span>
{% else %}
<span class="badge bg-secondary">{{ value }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<!-- Status bana -->
{% if is_banned %}
<div class="card bg-dark border-danger mb-4">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="text-danger">Status: IP jest ZBANOWANE</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-dark align-middle mb-0">
<tbody>
<tr>
<td class="text-secondary" style="width:220px">Powód</td>
<td>{{ ban_info.reason or ban_info.source or "brak danych" }}</td>
</tr>
<tr>
<td class="text-secondary">Źródło</td>
<td>{{ ban_info.source or "-" }}</td>
</tr>
<tr>
<td class="text-secondary">Utworzono</td>
<td>{{ ban_info.created_at or ban_info.timestamp or "-" }}</td>
</tr>
<tr>
<td class="text-secondary">Wygasa</td>
<td>{{ ban_info.expires or "-" }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/check.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/ip_validate.js') }}" defer></script>
{% if message %}
<script>window.addEventListener('DOMContentLoaded', () => window.showToast?.({ text: {{ message| tojson }}, variant: 'success' }));</script>
{% endif %}
{% if error %}
<script>window.addEventListener('DOMContentLoaded', () => window.showToast?.({ text: {{ error| tojson }}, variant: 'danger' }));</script>
{% endif %}
{% endblock %}

161
templates/index.html Normal file
View File

@@ -0,0 +1,161 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<h1 class="h3 text-white mb-1">Dashboard</h1>
<p class="text-secondary mb-0">Szybki podgląd stanu systemu i dostępnych endpointów.</p>
</div>
</div>
<!-- KPI tiles -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card bg-dark border-secondary h-100">
<div class="card-body">
<div class="text-secondary small">Aktywne bany</div>
<div class="display-6 fw-bold text-white">{{ stats.system.active_bans|int }}</div>
<div class="text-secondary small">ogółem</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card bg-dark border-secondary h-100">
<div class="card-body">
<div class="text-secondary small">Drupal attacks</div>
<div class="display-6 fw-bold text-white">{{ stats.system.drupal_attacks|int }}</div>
<div class="text-secondary small">wykryte łącznie</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card bg-dark border-secondary h-100">
<div class="card-body">
<div class="text-secondary small">Uptime</div>
<div class="h3 fw-bold text-white mb-0">{{ stats.system.uptime }}</div>
<div class="text-secondary small">czas działania</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card bg-dark border-secondary h-100">
<div class="card-body">
<div class="text-secondary small">Zużycie pamięci</div>
<div class="h3 fw-bold text-white mb-0">{{ stats.system.memory_usage }}</div>
<div class="text-secondary small">aktualnie</div>
</div>
</div>
</div>
</div>
<!-- System -->
<div class="card bg-dark border-secondary mb-4">
<div class="card-header">Informacje o systemie</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-dark table-hover align-middle mb-0" id="sys-table">
<tbody>
{% for key, value in sys_info.system.items() %}
<tr>
<th style="width:30%;" class="text-secondary">{{ key | capitalize }}</th>
<td class="text-white">{{ value }}</td>
</tr>
{% endfor %}
{% if sys_info.system|length == 0 %}
<tr>
<td colspan="2" class="text-center text-secondary">Brak danych</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Aplikacja -->
<div class="card bg-dark border-secondary mb-4">
<div class="card-header">Informacje o aplikacji</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-dark table-hover align-middle mb-0" id="app-table">
<tbody>
{% for key, value in sys_info.application.items() %}
<tr>
<th style="width:30%;" class="text-secondary">{{ key | capitalize }}</th>
<td class="text-white">{{ value }}</td>
</tr>
{% endfor %}
{% if sys_info.application|length == 0 %}
<tr>
<td colspan="2" class="text-center text-secondary">Brak danych</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Endpointy — domyślnie zwinięte -->
<div class="card bg-dark border-secondary">
<div class="card-header d-flex flex-wrap align-items-center justify-content-between gap-2">
<span>Dostępne endpointy API <span class="text-secondary">({{ routes|length }})</span></span>
<button class="btn btn-outline-secondary btn-sm" data-bs-toggle="collapse" data-bs-target="#ep-collapse"
aria-expanded="false" aria-controls="ep-collapse" id="btn-toggle-endpoints">
Pokaż endpoint
</button>
</div>
<div id="ep-collapse" class="collapse">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-dark table-hover align-middle mb-0" id="ep-table">
<thead>
<tr>
<th style="width:20px">#</th>
<th>URL</th>
<th>Metody</th>
<th class="text-end">Akcje</th>
</tr>
</thead>
<tbody>
{% for route in routes %}
<tr>
<td class="text-secondary">{{ loop.index }}</td>
<td>
<a href="{{ route.url }}" class="text-white text-decoration-none" target="_blank" rel="noopener">{{
route.url }}</a>
</td>
<td>
{% for m in route.methods.split(',') if route.methods %}
<span class="badge bg-secondary me-1">{{ m.strip() }}</span>
{% endfor %}
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-light copy-url" data-url="{{ route.url }}">Kopiuj URL</button>
<button class="btn btn-outline-light copy-curl" data-url="{{ route.url }}"
data-method="{{ (route.methods.split(',')[0] if route.methods else 'GET')|trim }}">
Kopiuj cURL
</button>
</div>
</td>
</tr>
{% endfor %}
{% if routes|length == 0 %}
<tr>
<td colspan="4" class="text-center text-secondary">Brak endpointów</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/dashboard.js') }}" defer></script>
{% endblock %}

205
templates/list.html Normal file
View File

@@ -0,0 +1,205 @@
{% extends "base.html" %}
{% block title %}Zarządzanie banami{% endblock %}
{% block content %}
<div class="container-fluid px-0 px-md-2">
<!-- Header z wyszukiwarką i przyciskiem Nowy ban -->
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<h1 class="h3 text-white mb-1">Zarządzanie banami</h1>
<p class="text-secondary mb-0">Dodawaj, przeglądaj i usuwaj blokady IP. Szybkie wyszukiwanie, selekcja zbiorcza i
podgląd szczegółów.</p>
</div>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-outline-light" data-bs-toggle="offcanvas" data-bs-target="#newBanOffcanvas"
aria-controls="newBanOffcanvas">
+ Dodaj
</button>
<div class="vr d-none d-md-block"></div>
<div class="input-group input-group-sm" style="min-width: 260px;">
<span class="input-group-text bg-dark border-secondary text-secondary">🔎</span>
<input id="search-input" type="search" class="form-control bg-dark border-secondary text-white"
placeholder="Szukaj po IP, hostname, powodzie…" aria-label="Szukaj">
</div>
</div>
</div>
<!-- Pasek narzędzi + jeden wspólny formularz dla selekcji zbiorczej -->
<div class="card bg-dark border-secondary mb-3">
<div class="card-body d-flex flex-wrap align-items-center gap-2">
<div class="me-auto d-flex align-items-center gap-3">
<span id="selection-counter" class="text-secondary small">0 zaznaczonych</span>
<span class="text-secondary small">Aktualne bany: <span class="text-white fw-semibold">{{ banned_ips|length
}}</span></span>
</div>
<div class="btn-group" role="group" aria-label="Akcje zbiorcze">
<!-- Przycisk zbiorczego usuwania zwiążemy z formularzem poniżej -->
<button type="submit" name="delete" id="delete-selected" class="btn btn-outline-danger" form="bulk-form" disabled>
Usuń zaznaczone
</button>
<button type="button" id="delete-all-btn" class="btn btn-outline-danger">Usuń wszystkie</button>
</div>
</div>
</div>
<!-- Jeden wspólny formularz obejmujący tabelę z checkboxami -->
<form id="bulk-form" method="post" class="mb-0">
<div class="card bg-dark border-secondary">
<div class="table-responsive">
<table class="table table-dark table-hover align-middle mb-0">
<thead class="position-sticky top-0" style="z-index: 1;">
<tr class="border-secondary">
<th style="width:42px">
<input type="checkbox" id="select-all" aria-label="Zaznacz/odznacz wszystkie">
</th>
<th class="text-uppercase text-secondary small">IP</th>
<th class="text-uppercase text-secondary small">Hostname</th>
<th class="text-uppercase text-secondary small">Powód</th>
<th class="text-uppercase text-secondary small">Wygasa</th>
<th class="text-uppercase text-secondary small text-end" style="width:80px">Akcje</th>
</tr>
</thead>
<tbody id="bans-tbody">
{% if banned_ips|length == 0 %}
<tr>
<td colspan="6" class="text-center py-5 text-secondary">
Brak aktywnych banów. Użyj przycisku <span class="text-white">“Nowy ban”</span>, aby dodać pierwszy
wpis.
</td>
</tr>
{% else %}
{% for ban in banned_ips %}
<tr class="ban-row">
<td>
<input type="checkbox" class="row-check" name="selected_ips" value="{{ ban.ip }}"
aria-label="Zaznacz {{ ban.ip }}">
</td>
<td>
<button class="btn btn-link p-0 text-decoration-none ban-ip text-white" data-ip="{{ ban.ip }}"
type="button">
{{ ban.ip }}
</button>
</td>
<td>
{% if ban.hostname and ban.hostname != 'Manual ban' %}
<span class="badge bg-primary-subtle text-primary-emphasis border border-primary-subtle">{{ ban.hostname
}}</span>
{% else %}
<span class="text-primary">Brak</span>
{% endif %}
</td>
<td>
{% if ban.reason %}{{ ban.reason }}{% else %}<span class="text-secondary">Manual ban</span>{% endif %}
</td>
<td><span class="expires-text">{{ ban.expires }}</span></td>
<td class="text-end">
<!-- Pojedyncze usunięcie: oddzielny mini-form w wierszu -->
<form method="post" class="d-inline">
<input type="hidden" name="selected_ips" value="{{ ban.ip }}">
<button type="submit" name="delete" value="1" class="btn btn-sm btn-outline-danger"
title="Usuń ten ban">Usuń</button>
</form>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
</div>
</form>
</div>
<!-- Offcanvas: Nowy ban -->
<div class="offcanvas offcanvas-end text-bg-dark" tabindex="-1" id="newBanOffcanvas" aria-labelledby="newBanLabel">
<div class="offcanvas-header border-bottom border-secondary">
<h5 class="offcanvas-title" id="newBanLabel">Dodaj nowy ban</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Zamknij"></button>
</div>
<div class="offcanvas-body">
<form method="post" id="add-ban-form" novalidate>
<input type="hidden" name="add_ban">
<div class="mb-3">
<label for="ip" class="form-label">Adres IP</label>
<input id="ip" name="ip" inputmode="numeric" autocomplete="off"
class="form-control bg-dark text-white border-secondary" required placeholder="np. 192.168.0.10"
pattern="\d{1,3}(\.\d{1,3}){3}">
<code>Format: xxx.xxx.xxx.xxx</code>
<div class="invalid-feedback">Podaj poprawny adres IPv4.</div>
</div>
<div class="mb-3">
<label for="reason" class="form-label">Powód <span class="text-secondary">(opcjonalnie)</span></label>
<input id="reason" name="reason" class="form-control bg-dark text-white border-secondary"
placeholder="np. brute force / abuse">
</div>
<div class="mb-3">
<label for="duration" class="form-label">Czas trwania</label>
<select id="duration" name="duration" class="form-select bg-dark text-white border-secondary">
<option value="3600">1 godzina</option>
<option value="86400">1 dzień</option>
<option value="604800">1 tydzień</option>
<option value="2592000">1 miesiąc</option>
<option value="5184000">2 miesiące</option>
<option value="7776000">3 miesiące</option>
<option value="10368000">4 miesiące</option>
<option value="12960000">5 miesięcy</option>
<option value="15552000">6 miesięcy</option>
<option value="18144000">7 miesięcy</option>
<option value="20736000">8 miesięcy</option>
<option value="23328000">9 miesięcy</option>
<option value="25920000">10 miesięcy</option>
<option value="28512000">11 miesięcy</option>
<option value="31536000">1 rok</option>
<option value="63072000">2 lata</option>
</select>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-outline-primary">Dodaj ban</button>
</div>
</form>
</div>
</div>
<!-- Modal: Szczegóły bana -->
<div class="modal fade" id="banModal" tabindex="-1" aria-labelledby="banModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title" id="banModalLabel">Szczegóły bana</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body" id="banModalBody">
<div class="placeholder-glow">
<span class="placeholder col-12"></span>
<span class="placeholder col-10"></span>
<span class="placeholder col-8"></span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Zamknij</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/bans.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/delete_bans.js') }}" defer></script>
{% if message %}
<script>
window.addEventListener('DOMContentLoaded', () => {
window.showToast({ text: {{ message| tojson }}, variant: 'success' });
});
</script>
{% endif %}
{% if error %}
<script>
window.addEventListener('DOMContentLoaded', () => {
window.showToast({ text: {{ error| tojson }}, variant: 'danger' });
});
</script>
{% endif %}
{% endblock %}

107
templates/logs.html Normal file
View File

@@ -0,0 +1,107 @@
{% extends "base.html" %}
{% block title %}Podgląd logów{% endblock %}
{% block content %}
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-3">
<div>
<h1 class="h3 text-white mb-1">Podgląd logów</h1>
<p class="text-secondary mb-0">Podgląd pliku <code>app.log</code> z podświetlaniem i live tail.</p>
</div>
</div>
<!-- Zmień poziom logowania aplikacji -->
<div class="card bg-dark border-warning mb-2">
<div class="card-body d-flex align-items-center gap-2">
<form id="setLogLevelForm" method="post" action="{{ url_for('set_log_level') }}" class="d-flex align-items-center gap-2">
<label for="appLogLevel" class="text-warning small me-2">Poziom logowania aplikacji:</label>
<select name="level" id="appLogLevel" class="form-select form-select-sm bg-dark text-warning border-warning me-2" style="width:120px;">
<option value="DEBUG">DEBUG</option>
<option value="INFO">INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
<option value="CRITICAL">CRITICAL</option>
</select>
<button type="submit" class="btn btn-outline-warning btn-sm">Ustaw</button>
</form>
<span id="setLogLevelStatus" class="text-warning ms-2 small"></span>
</div>
</div>
<!-- Toolbar -->
<div class="card bg-dark border-secondary mb-3">
<div class="card-body d-flex flex-wrap align-items-center gap-2">
<!-- Poziom wyświetlania -->
<form method="get" id="displayLevelForm" action="{{ url_for('view_logs') }}"
class="d-flex align-items-center gap-2 me-auto">
<label class="text-secondary small me-2">Poziom:</label>
<div class="btn-group btn-group-sm" role="group" aria-label="Szybki poziom">
{% for lvl in ['DEBUG','INFO','WARNING','ERROR','CRITICAL'] %}
<button type="submit" class="btn btn-outline-secondary{% if lvl==selected_level %} active{% endif %}"
name="level" value="{{ lvl }}">
{{ lvl }}
</button>
{% endfor %}
</div>
</form>
<!-- Wyszukiwarka -->
<form method="get" id="searchForm" action="{{ url_for('view_logs') }}" class="d-flex align-items-center gap-1">
<input type="hidden" name="level" value="{{ selected_level }}">
<div class="input-group input-group-sm" style="min-width:280px;">
<span class="input-group-text bg-dark border-secondary text-secondary">🔎</span>
<input type="search" name="query" id="query" class="form-control bg-dark text-white border-secondary"
placeholder="Szukaj (regexp/grep)..." value="{{ request.args.get('query','') }}">
<button type="submit" class="btn btn-outline-secondary">Szukaj</button>
</div>
</form>
<!-- Przełączniki -->
<div class="btn-group btn-group-sm ms-1" role="group" aria-label="Widok">
<button id="btn-live" class="btn btn-outline-light" data-on-text="Live ON" data-off-text="Live OFF">Live
ON</button>
<button id="btn-autoscroll" class="btn btn-outline-secondary">Auto-scroll</button>
<button id="btn-wrap" class="btn btn-outline-secondary">Zawijaj</button>
</div>
<!-- Czcionka -->
<div class="input-group input-group-sm ms-1" style="width:140px;">
<span class="input-group-text bg-dark border-secondary text-secondary">A↕</span>
<select id="font-size" class="form-select bg-dark text-white border-secondary">
{% for size in ['12px','13px','14px','15px','16px'] %}
<option value="{{ size }}" {% if size=='14px' %}selected{% endif %}>{{ size }}</option>
{% endfor %}
</select>
</div>
<!-- Akcje -->
<label class="text-secondary small me-2">Akcje:</label>
<div class="btn-group btn-group-sm ms-1" role="group" aria-label="Akcje">
<button id="btn-copy" class="btn btn-outline-secondary">Kopiuj</button>
<button id="btn-download" class="btn btn-outline-secondary">Pobierz</button>
</div>
</div>
</div>
<!-- Log viewer -->
<div class="card bg-dark border-secondary">
<div class="card-body p-0" style="max-height: 70vh; overflow: auto;" id="logScroll">
<pre id="logContainer" class="bg-dark text-white hljs mb-0"
style="font-size:14px; line-height:1.35; padding: 1rem; border-radius: 0; white-space: pre; tab-size: 2;"></pre>
<div id="logEmpty" class="text-center text-secondary small py-4 d-none">Brak danych do wyświetlenia.</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<!-- Highlight.js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/monokai.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
<script>
SET_LOG_LEVEL_URL = "{{ url_for('set_log_level') }}";
</script>
<script defer src="{{ url_for('static', filename='js/logs.js') }}"></script>
<script defer src="{{ url_for('static', filename='js/set_log_level.js') }}"></script>
{% endblock %}

99
templates/reset.html Normal file
View File

@@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block title %}Reset liczników{% endblock %}
{% block content %}
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<h1 class="h3 text-white mb-1">Reset liczników</h1>
<p class="text-secondary mb-0">Wyzeruj liczniki błędów globalnie lub dla konkretnego adresu IP.</p>
</div>
</div>
<!-- Reset dla konkretnego IP -->
<div class="card bg-dark border-secondary mb-4">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Reset liczników dla IP</span>
</div>
<div class="card-body">
<form method="post" id="reset-ip-form" novalidate>
<input type="hidden" name="reset_ip">
<div class="mb-3 col-sm-6 col-md-5 col-lg-4">
<label for="ip" class="form-label text-primary">Adres IPv4</label>
<input id="ip" name="ip" class="form-control bg-dark text-white border-secondary" inputmode="numeric"
autocomplete="off" required placeholder="np. 185.12.34.56" pattern="\\d{1,3}(\\.\\d{1,3}){3}">
<div class="form-text text-primary"><code>Format: xxx.xxx.xxx.xxx</code></div>
<div class="invalid-feedback">Podaj poprawny adres IPv4.</div>
</div>
<div class="col-12">
<button type="button" class="btn btn-primary-outline" data-bs-toggle="modal" data-bs-target="#confirmResetIpModal">
Resetuj liczniki dla IP
</button>
</div>
</form>
</div>
</div>
<!-- Reset globalny -->
<div class="card bg-dark border-danger mb-4">
<div class="card-header">Reset wszystkich liczników błędów</div>
<div class="card-body">
<form method="post" id="reset-all-form">
<input type="hidden" name="reset_all_errors">
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#confirmResetAllModal">
Resetuj wszystkie liczniki błędów
</button>
</form>
</div>
</div>
<!-- Modale potwierdzeń -->
<div class="modal fade" id="confirmResetIpModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title">Potwierdź reset dla IP</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
Czy na pewno zresetować liczniki błędów dla adresu <span class="fw-bold" id="confirm-ip-value"></span>?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Anuluj</button>
<button type="submit" form="reset-ip-form" class="btn btn-outline-warning" id="btn-confirm-ip">Tak, resetuj</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="confirmResetAllModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title">Potwierdź reset globalny</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
Ta operacja wyzeruje <span class="fw-bold">wszystkie</span> liczniki błędów. Kontynuować?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Anuluj</button>
<button type="submit" form="reset-all-form" class="btn btn-outline-danger" id="btn-confirm-all">Tak, resetuj
wszystko</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/ip_validate.js') }}" defer></script>
{% if message %}
<script>window.addEventListener('DOMContentLoaded', () => window.showToast?.({ text: {{ message| tojson }}, variant: 'success' }));</script>
{% endif %}
{% if error %}
<script>window.addEventListener('DOMContentLoaded', () => window.showToast?.({ text: {{ error| tojson }}, variant: 'danger' }));</script>
{% endif %}
{% endblock %}

97
templates/stats.html Normal file
View File

@@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block title %}Statystyki{% endblock %}
{% block content %}
<div class="card bg-dark border-secondary mb-4">
<div class="card-header">Rozkład geograficzny</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-dark align-middle mb-0" id="geo-table">
<thead>
<tr>
<th style="width:48%">Kraj</th>
<th style="width:40%">Udział</th>
<th class="text-end" style="width:12%">Liczba</th>
</tr>
</thead>
<tbody>
{% set ns = namespace(total_geo=0) %}
{% for _, c in stats.geo_distribution.items() %}{% set ns.total_geo = ns.total_geo + (c|int) %}{%
endfor %}
{% for country, count in stats.geo_distribution.items() %}
{% set pct = ((count|int) / (ns.total_geo if ns.total_geo>0 else 1) * 100) | round(1) %}
{% set pct = 0 if pct < 0 else (100 if pct> 100 else pct) %}
<tr>
<td><span class="d-inline-block text-truncate" style="max-width:95%"
title="{{ country }}">{{ country }}</span></td>
<td>
<div class="progress bg-black border border-secondary" style="height:10px;">
<div class="progress-bar bg-primary" role="progressbar" style="width: {{ pct }}%;"
aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="text-secondary small mt-1">{{ pct }}%</div>
</td>
<td class="text-end"><span class="badge bg-secondary">{{ count|int }}</span></td>
</tr>
{% endfor %}
{% if stats.geo_distribution|length == 0 %}
<tr>
<td colspan="3" class="text-center text-secondary">Brak danych</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Reasons -->
<div class="card bg-dark border-secondary mb-4">
<div class="card-header">Przyczyny banów</div>
<div class="card-body">
{% set nsr = namespace(total=0) %}
{% for _, v in stats.ban_reasons.items() %}{% set nsr.total = nsr.total + (v|int) %}{% endfor %}
{% if stats.ban_reasons|length == 0 %}
<div class="text-center text-secondary small">Brak danych</div>
{% else %}
<div class="list-group list-group-flush">
{% for reason, count in (stats.ban_reasons|dictsort(by='value', reverse=true)) %}
{% set label = reason if reason else 'Manual ban' %}
{% set pct = ((count|int) / (nsr.total if nsr.total>0 else 1) * 100) | round(1) %}
{% set pct = 0 if pct < 0 else (100 if pct> 100 else pct) %}
<div class="list-group-item bg-dark text-white border-secondary">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-truncate" title="{{ label }}">{{ label }}</span>
<span class="small text-secondary">{{ count|int }} ({{ pct }}%)</span>
</div>
<div class="progress bg-black border border-secondary" style="height:10px;">
<div class="progress-bar bg-primary" role="progressbar" style="width: {{ pct }}%;"
aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6"></script>
<script>
// dane dla wykresu z Jinja -> JS
const reasonsData = (() => {
const entries = Object.entries({{ stats.ban_reasons | tojson }});
// posortuj malejąco
entries.sort((a, b) => (b[1] || 0) - (a[1] || 0));
const labels = entries.map(([k]) => k && k.length ? k : "Manual ban");
const data = entries.map(([, v]) => v);
return { labels, data };
}) ();
</script>
<script src="{{ url_for('static', filename='js/stats.js') }}" defer></script>
{% endblock %}