release
This commit is contained in:
82
templates/base.html
Normal file
82
templates/base.html
Normal 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
182
templates/charts.html
Normal 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
190
templates/check.html
Normal 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
161
templates/index.html
Normal 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
205
templates/list.html
Normal 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
107
templates/logs.html
Normal 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
99
templates/reset.html
Normal 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
97
templates/stats.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user