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

71
static/css/base.css Normal file
View File

@@ -0,0 +1,71 @@
/* === Ogólne === */
body {
background-color: #121212;
color: #ffffff;
}
/* === Karty === */
.card {
background-color: #1e1e1e;
border: 1px solid #333333;
}
.card-header {
background-color: #212529;
color: #ffffff;
}
/* === Tabele === */
.table {
background-color: #1e1e1e;
color: #ffffff;
}
.table thead th {
background-color: #343a40;
}
/* === Listy === */
.list-group-item {
background-color: #1e1e1e;
color: #ffffff;
border: 1px solid #333333;
}
/* === Formularze === */
.form-control,
.form-select {
background-color: #212529;
color: #ffffff;
border: 1px solid #343a40;
}
/* === Linki === */
a.text-decoration-none:hover {
text-decoration: underline;
}
/* === Lista banów === */
tr.ban-row {
cursor: pointer;
}
td.ban-ip {
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
#selection-counter {
opacity: 0.8;
}
th:first-child,
td:first-child {
width: 42px;
}
.navbar .form-control::placeholder {
color: #adb5bd;
opacity: 1;
}

146
static/js/bans.js Normal file
View File

@@ -0,0 +1,146 @@
(function () {
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
// --- SEARCH / FILTER (client-side) ---
const searchInput = $('#search-input');
const rows = () => $$('#bans-tbody tr.ban-row');
const noDataRow = () => $('#bans-tbody tr[data-empty]');
function applySearch() {
const q = (searchInput?.value || '').trim().toLowerCase();
let visible = 0;
rows().forEach(r => {
const text = r.textContent.toLowerCase();
const show = !q || text.includes(q);
r.classList.toggle('d-none', !show);
if (show) visible++;
});
if (!visible && !noDataRow()) {
const tr = document.createElement('tr');
tr.setAttribute('data-empty', '');
tr.innerHTML = '<td colspan="6" class="text-center py-5 text-secondary">Brak wyników dla podanego filtra</td>';
$('#bans-tbody').appendChild(tr);
} else if (visible && noDataRow()) {
noDataRow().remove();
}
}
let t;
searchInput?.addEventListener('input', () => { clearTimeout(t); t = setTimeout(applySearch, 120); });
// --- BULK SELECT ---
const selectAll = $('#select-all');
const counter = $('#selection-counter');
const delBtn = $('#delete-selected');
const bulkForm = $('#bulk-form');
function currentChecks() { return $$('.row-check'); }
function selectedCount() { return currentChecks().filter(c => c.checked).length; }
function updateState() {
const total = currentChecks().length;
const sel = selectedCount();
if (counter) counter.textContent = sel + ' zaznaczonych';
if (delBtn) delBtn.disabled = sel === 0;
if (selectAll) {
selectAll.checked = sel > 0 && sel === total;
selectAll.indeterminate = sel > 0 && sel < total;
}
}
selectAll?.addEventListener('change', () => {
currentChecks().forEach(c => (c.checked = selectAll.checked));
updateState();
});
document.addEventListener('change', (e) => {
if (e.target.classList?.contains('row-check')) updateState();
});
// Kliknięcie w wiersz przełącza zaznaczenie (poza elementami interaktywnymi)
document.addEventListener('click', (e) => {
const row = e.target.closest('tr.ban-row');
if (!row) return;
if (e.target.closest('input,button,a,label,select,textarea')) return;
const cb = row.querySelector('.row-check');
if (cb) { cb.checked = !cb.checked; updateState(); }
});
updateState();
// Potwierdzenia akcji
bulkForm?.addEventListener('submit', (e) => {
const submitter = e.submitter;
if (!submitter) return;
const sel = selectedCount();
const isDeleteAll = submitter.name === 'delete_all';
const msg = isDeleteAll ? 'Usunąć WSZYSTKIE bany?' : `Usunąć ${sel} wybrane bany?`;
if (!confirm(msg)) e.preventDefault();
});
// --- ADD BAN: walidacja klientowa ---
const addForm = $('#add-ban-form');
addForm?.addEventListener('submit', (e) => {
if (!addForm.checkValidity()) {
e.preventDefault();
addForm.classList.add('was-validated');
}
});
// --- DETAILS MODAL ---
function formatBanDetails(data) {
let html = '<div class="table-responsive"><table class="table table-bordered table-dark">';
html += '<thead><tr><th>Klucz</th><th>Wartość</th></tr></thead><tbody>';
for (let key in data) {
let value = data[key];
if (key === 'attack_details' || key === 'geo') {
try {
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
let nested = '<table class="table table-sm table-bordered table-dark mb-0">';
for (let subKey in parsed) nested += `<tr><td>${subKey}</td><td>${parsed[subKey]}</td></tr>`;
nested += '</table>';
value = nested;
} catch (_) { }
}
html += `<tr><td>${key}</td><td>${value}</td></tr>`;
}
html += '</tbody></table></div>';
return html;
}
// Otwórz modal po kliknięciu IP
document.addEventListener('click', (e) => {
const btn = e.target.closest('.ban-ip');
if (!btn) return;
const ip = btn.dataset.ip;
const modalEl = document.getElementById('banModal');
const bodyEl = document.getElementById('banModalBody');
const titleEl = document.getElementById('banModalLabel');
if (titleEl) titleEl.textContent = 'Szczegóły bana dla ' + ip;
if (bodyEl) {
bodyEl.innerHTML = '<div class="placeholder-glow"><span class="placeholder col-12"></span><span class="placeholder col-10"></span></div>';
}
fetch('/api/banned/' + encodeURIComponent(ip) + '?full_info=1')
.then(r => r.ok ? r.json() : Promise.reject(r.status))
.then(data => { bodyEl.innerHTML = formatBanDetails(data); })
.catch(() => { bodyEl.innerHTML = '<div class="text-danger">Nie udało się pobrać szczegółów.</div>'; })
.finally(() => {
const m = new bootstrap.Modal(modalEl);
m.show();
});
});
document.getElementById('delete-all-btn')?.addEventListener('click', async () => {
if (!confirm('Usunąć WSZYSTKIE bany?')) return;
try {
const res = await fetch('/api/banned/all', { method: 'DELETE' });
if (!res.ok) throw new Error('HTTP ' + res.status);
window.showToast?.({ text: 'Wszystkie bany usunięte', variant: 'success' });
location.reload();
} catch (e) {
window.showToast?.({ text: 'Nie udało się usunąć wszystkich banów', variant: 'danger' });
}
});
const url = new URL(location.href);
if (url.searchParams.get('created') === '1') {
window.showToast?.({ text: 'Ban został dodany.', variant: 'success' });
}
})();

279
static/js/charts.js Normal file
View File

@@ -0,0 +1,279 @@
/* eslint-disable no-undef */
// charts.js — UX v2 (fixed quick-top)
(function () {
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
// Kolory dla dark mode
const palette = {
grid: 'rgba(255,255,255,0.1)',
tick: 'rgba(255,255,255,0.7)',
bar: 'rgba(54, 162, 235, 0.8)',
bar2: 'rgba(255, 159, 64, 0.85)',
line: 'rgba(255, 99, 132, 1)',
lineFill: 'rgba(255, 99, 132, 0.2)',
pie: [
'rgba(255, 99, 132, 0.85)',
'rgba(54, 162, 235, 0.85)',
'rgba(255, 206, 86, 0.85)',
'rgba(75, 192, 192, 0.85)',
'rgba(153, 102, 255, 0.85)',
'rgba(255, 159, 64, 0.85)',
'rgba(199, 199, 199, 0.85)'
]
};
function emptyState(canvasId, emptyId, labels, dataArr) {
const hasData = Array.isArray(labels) && labels.length > 0 && Array.isArray(dataArr) && dataArr.some(v => v > 0);
const empty = document.getElementById(emptyId);
const canvas = document.getElementById(canvasId);
if (!hasData) {
canvas?.classList.add('d-none');
empty?.classList.remove('d-none');
return true;
}
canvas?.classList.remove('d-none');
empty?.classList.add('d-none');
return false;
}
function csvFromSeries(labels, data, headerX = 'Label', headerY = 'Value') {
const rows = [[headerX, headerY], ...labels.map((l, i) => [l, data[i] ?? 0])];
return rows.map(r => r.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n');
}
function download(filename, content, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
}
function bindDownloads(id, labels, data) {
$$('.chart-download').forEach(btn => {
if (btn.dataset.target !== id) return;
btn.addEventListener('click', () => {
const type = btn.dataset.type;
if (type === 'csv') {
download(`${id}.csv`, csvFromSeries(labels, data), 'text/csv;charset=utf-8');
} else if (type === 'png') {
const c = document.getElementById(id);
if (!c) return;
const url = c.toDataURL('image/png');
const a = document.createElement('a');
a.href = url; a.download = `${id}.png`; a.click();
}
});
});
}
function bindFullscreen(id) {
$$('.chart-fullscreen').forEach(btn => {
if (btn.dataset.target !== id) return;
btn.addEventListener('click', () => {
const src = document.getElementById(id);
const dst = document.getElementById('chartModalCanvas');
const chart = Chart.getChart(src);
if (!chart) return;
const modal = new bootstrap.Modal(document.getElementById('chartModal'));
modal.show();
setTimeout(() => {
const old = Chart.getChart(dst);
old && old.destroy();
new Chart(dst.getContext('2d'), {
type: chart.config.type,
data: JSON.parse(JSON.stringify(chart.config.data)),
options: Object.assign({}, chart.config.options, {
maintainAspectRatio: false
})
});
}, 150);
});
});
}
// Bind przycisków szybkiego wyboru: nie wysyłamy duplikatu name,
// tylko ustawiamy <select name="top_n"> i submitujemy formularz.
function bindQuickTop() {
const form = document.getElementById('options-form');
const select = document.getElementById('top_n');
if (!form || !select) return;
$$('.quick-top').forEach(btn => {
btn.addEventListener('click', () => {
const val = btn.dataset.value;
if (val && select) select.value = String(val);
// odśwież aktywny stan w UI (opcjonalnie, dla natychmiastowego feedbacku)
$$('.quick-top').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
form.requestSubmit();
});
});
// Zmiana w <select> nadal auto-submituje:
select.addEventListener('change', () => form.requestSubmit());
}
// Auto-inicjalizacja niezależnie od kolejności ładowania
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', bindQuickTop);
} else {
bindQuickTop();
}
function bindQuickPeriod() {
const form = document.getElementById('options-form');
const select = document.getElementById('period');
if (!form || !select) return;
// Szybkie przyciski okresu (Tydzień/Miesiąc/Rok)
$$('.quick-period').forEach(btn => {
btn.addEventListener('click', () => {
const val = btn.dataset.value;
if (val) select.value = String(val);
// natychmiastowy feedback w UI
$$('.quick-period').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
form.requestSubmit();
});
});
// Zmiana w <select id="period"> też auto-submituje
select.addEventListener('change', () => form.requestSubmit());
}
// Auto-inicjalizacja (analogicznie do bindQuickTop)
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', bindQuickPeriod);
} else {
bindQuickPeriod();
}
// Główne API — rysowanie wykresów
window.renderCharts = function renderCharts(stats) {
// ====== Powody (bar - poziomo) ======
{
const labels = (stats.top_reasons || []).map(r => r.reason);
const data = (stats.top_reasons || []).map(r => r.count);
if (!emptyState('reasonsChart', 'reasonsEmpty', labels, data)) {
const ctx = document.getElementById('reasonsChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [{ data, backgroundColor: palette.bar2, borderRadius: 6, borderSkipped: false }]
},
options: {
indexAxis: 'y',
responsive: true,
plugins: { legend: { display: false }, tooltip: { intersect: false, mode: 'nearest' } },
scales: {
x: { grid: { color: palette.grid }, ticks: { color: palette.tick, precision: 0 } },
y: { grid: { display: false }, ticks: { color: palette.tick } }
}
}
});
}
bindDownloads('reasonsChart', labels, data);
bindFullscreen('reasonsChart');
}
// ====== URL (bar - poziomo) ======
{
const labels = (stats.top_urls || []).map(u => u.url);
const data = (stats.top_urls || []).map(u => u.count);
if (!emptyState('urlsChart', 'urlsEmpty', labels, data)) {
const ctx = document.getElementById('urlsChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [{ data, backgroundColor: palette.bar, borderRadius: 6, borderSkipped: false }]
},
options: {
indexAxis: 'y',
responsive: true,
plugins: { legend: { display: false }, tooltip: { intersect: false, mode: 'nearest' } },
scales: {
x: { grid: { color: palette.grid }, ticks: { color: palette.tick, precision: 0 } },
y: { grid: { display: false }, ticks: { color: palette.tick } }
}
}
});
}
bindDownloads('urlsChart', labels, data);
bindFullscreen('urlsChart');
}
// ====== Kraje (pie) ======
{
const labels = (stats.top_countries || []).map(c => c.country);
const data = (stats.top_countries || []).map(c => c.count);
if (!emptyState('countriesChart', 'countriesEmpty', labels, data)) {
const ctx = document.getElementById('countriesChart').getContext('2d');
new Chart(ctx, {
type: 'pie',
data: { labels, datasets: [{ data, backgroundColor: palette.pie, borderWidth: 1, borderColor: '#222' }] },
options: {
responsive: true,
plugins: {
legend: { position: 'bottom', labels: { color: palette.tick } },
tooltip: { callbacks: { label: (item) => `${item.label}: ${item.formattedValue}` } }
}
}
});
}
bindDownloads('countriesChart', labels, data);
bindFullscreen('countriesChart');
}
// ====== Bany w czasie (line) ======
{
const labels = stats.weeks || [];
const data = stats.bans_per_week || [];
if (!emptyState('bansOverTimeChart', 'timeEmpty', labels, data)) {
const ctx = document.getElementById('bansOverTimeChart').getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 200);
gradient.addColorStop(0, palette.lineFill);
gradient.addColorStop(1, 'rgba(0,0,0,0)');
new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: 'Bany / tydzień',
data,
borderColor: palette.line,
backgroundColor: gradient,
fill: true,
cubicInterpolationMode: 'monotone',
tension: 0.25,
pointRadius: 2,
pointHoverRadius: 4
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: palette.grid }, ticks: { color: palette.tick } },
y: { grid: { color: palette.grid }, ticks: { color: palette.tick, precision: 0 } }
}
}
});
}
bindDownloads('bansOverTimeChart', labels, data);
bindFullscreen('bansOverTimeChart');
}
};
})();

59
static/js/check.js Normal file
View File

@@ -0,0 +1,59 @@
(function () {
'use strict';
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
function bindErrorsFilter() {
const search = $('#errors-search');
if (!search) return;
const items = $$('#errorsAccordion .accordion-item');
const apply = () => {
const q = search.value.toLowerCase().trim();
items.forEach(it => {
const text = it.textContent.toLowerCase();
it.style.display = q && !text.includes(q) ? 'none' : '';
});
};
search.addEventListener('input', apply);
apply();
}
function bindEndpointsFilter() {
const tbody = $('#endpoints-tbody');
if (!tbody) return;
const input = $('#endpoints-search');
const empty = $('#endpoints-empty');
const applyFilter = () => {
const q = (input?.value || '').toLowerCase().trim();
let visible = 0;
$$('#endpoints-tbody tr').forEach(tr => {
const txt = tr.textContent.toLowerCase();
const show = !q || txt.includes(q);
tr.style.display = show ? '' : 'none';
if (show) visible++;
});
empty?.classList.toggle('d-none', visible !== 0);
};
input?.addEventListener('input', applyFilter);
applyFilter();
}
function init() {
bindErrorsFilter();
bindEndpointsFilter();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

93
static/js/dashboard.js Normal file
View File

@@ -0,0 +1,93 @@
(function () {
const $ = (s, r = document) => r.querySelector(s);
// --- helpers ---
function toAbsoluteUrl(u) {
try { return new URL(u, window.location.origin).href; } catch { return u; }
}
async function copyToClipboard(text) {
try {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(text);
return true;
}
} catch (_) { /* fallback below */ }
// Fallback dla nieszyfrowanego HTTP / braku uprawnień
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.top = '-1000px';
ta.style.left = '-1000px';
document.body.appendChild(ta);
ta.select();
ta.setSelectionRange(0, ta.value.length);
let ok = false;
try { ok = document.execCommand('copy'); } catch (_) { ok = false; }
document.body.removeChild(ta);
return ok;
}
function flashButton(btn, okText = 'Skopiowano!', ms = 1200) {
if (!btn) return;
const origText = btn.textContent;
const hadSuccess = btn.classList.contains('btn-outline-success');
btn.textContent = okText;
btn.classList.add('btn-outline-success');
btn.classList.remove('btn-outline-light', 'btn-outline-secondary');
setTimeout(() => {
btn.textContent = origText;
btn.classList.toggle('btn-success', hadSuccess);
if (btn.classList.contains('copy-url')) btn.classList.add('btn-outline-light');
if (btn.classList.contains('copy-curl')) btn.classList.add('btn-outline-light');
}, ms);
}
// --- main: endpoint buttons ---
document.addEventListener('click', async (e) => {
const btnUrl = e.target.closest('.copy-url');
const btnCurl = e.target.closest('.copy-curl');
if (btnUrl) {
e.preventDefault();
e.stopPropagation();
const raw = btnUrl.dataset.url || '';
const href = toAbsoluteUrl(raw);
const ok = await copyToClipboard(href);
if (ok) {
flashButton(btnUrl);
window.showToast?.({ text: 'Skopiowano URL.', variant: 'success' });
} else {
window.showToast?.({ text: 'Nie udało się skopiować URL.', variant: 'danger' });
}
return;
}
if (btnCurl) {
e.preventDefault();
e.stopPropagation();
const raw = btnCurl.dataset.url || '';
const href = toAbsoluteUrl(raw);
const method = (btnCurl.dataset.method || 'GET').toUpperCase();
const cmd = `curl -X ${method} "${href}" -H "Accept: application/json" -sS`;
const ok = await copyToClipboard(cmd);
if (ok) {
flashButton(btnCurl);
window.showToast?.({ text: 'Skopiowano cURL.', variant: 'success' });
} else {
window.showToast?.({ text: 'Nie udało się skopiować cURL.', variant: 'danger' });
}
return;
}
});
// --- collapse label sync ---
const epCollapseEl = $('#ep-collapse');
const epToggleBtn = $('#btn-toggle-endpoints');
if (epCollapseEl && epToggleBtn && window.bootstrap) {
epCollapseEl.addEventListener('shown.bs.collapse', () => { epToggleBtn.textContent = 'Ukryj endpointy'; });
epCollapseEl.addEventListener('hidden.bs.collapse', () => { epToggleBtn.textContent = 'Pokaż endpointy'; });
}
})();

39
static/js/delete_bans.js Normal file
View File

@@ -0,0 +1,39 @@
window.addEventListener('DOMContentLoaded', () => {
const selectAll = document.getElementById('select-all');
const checks = () => Array.from(document.querySelectorAll('.row-check'));
const counter = document.getElementById('selection-counter');
const bulkBtn = document.getElementById('delete-selected');
function refreshState() {
const picked = checks().filter(c => c.checked);
counter.textContent = `${picked.length} zaznaczonych`;
bulkBtn.disabled = picked.length === 0;
}
if (selectAll) {
selectAll.addEventListener('change', () => {
checks().forEach(c => { c.checked = selectAll.checked; });
refreshState();
});
}
checks().forEach(c => c.addEventListener('change', refreshState));
refreshState();
const delAll = document.getElementById('delete-all-btn');
if (delAll) {
delAll.addEventListener('click', async () => {
if (!confirm('Na pewno usunąć WSZYSTKIE bany?')) return;
try {
const res = await fetch(window.DELETE_ALL_BANS_URL, { method: 'DELETE' });
const data = await res.json();
if (data.status === 'all_bans_removed') {
window.location.reload();
} else {
window.showToast?.({ text: 'Błąd podczas usuwania', variant: 'danger' });
}
} catch (e) {
window.showToast?.({ text: 'Błąd sieci', variant: 'danger' });
}
});
}
});

64
static/js/ip_validate.js Normal file
View File

@@ -0,0 +1,64 @@
(function () {
const $ = (sel, root = document) => root.querySelector(sel);
const ipInput = $('#ip');
const formIp = $('#reset-ip-form');
const formAll = $('#reset-all-form');
const btnConfirmIp = $('#btn-confirm-ip');
const btnConfirmAll = $('#btn-confirm-all');
const confirmIpVal = $('#confirm-ip-value');
(function initMyIp() {
const card = document.querySelector('.card[data-myip]');
const my = card?.dataset.myip?.trim();
const btn = $('#btn-myip');
if (my && btn) {
btn.disabled = false;
btn.addEventListener('click', () => {
ipInput.value = my;
ipInput.dispatchEvent(new Event('input'));
window.showToast?.({ text: `Użyto adresu: ${my}`, variant: 'dark' });
});
}
})();
// Walidacja IPv4 na żywo
const ipv4 = /^\d{1,3}(\.\d{1,3}){3}$/;
ipInput?.addEventListener('input', () => {
const ok = ipv4.test(ipInput.value.trim());
ipInput.classList.toggle('is-valid', ok);
ipInput.classList.toggle('is-invalid', ipInput.value.length > 0 && !ok);
});
// Uzupełnij wartość w modalu przed submit
document.getElementById('confirmResetIpModal')?.addEventListener('show.bs.modal', () => {
const ip = ipInput.value.trim() || '—';
confirmIpVal && (confirmIpVal.textContent = ip);
});
// Blokada przycisków na czas submitu + lekki feedback
function withSubmitting(btn, fn) {
if (!btn) return fn();
const original = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Przetwarzanie...';
const done = () => { btn.disabled = false; btn.innerHTML = original; };
const ret = fn();
// jeśli to klasyczny submit (nawigacja), UI i tak się przeładuje
if (ret && ret.finally) ret.finally(done); else setTimeout(done, 1200);
}
// Submity (po potwierdzeniach w modalach)
formIp?.addEventListener('submit', (e) => {
if (!ipv4.test(ipInput.value.trim())) {
e.preventDefault();
ipInput.classList.add('is-invalid');
window.showToast?.({ text: 'Nieprawidłowy adres IP.', variant: 'danger' });
return;
}
withSubmitting(btnConfirmIp, () => null);
});
formAll?.addEventListener('submit', () => withSubmitting(btnConfirmAll, () => null));
})();

286
static/js/logs.js Normal file
View File

@@ -0,0 +1,286 @@
(function () {
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const logContainer = $("#logContainer");
if (!logContainer) return;
const displayLevelForm = $("#displayLevelForm");
const searchForm = $("#searchForm");
const queryInput = $("#query");
const levelSelect = $("#level");
const logScroll = $("#logScroll");
// toolbar
const btnLive = $("#btn-live");
const btnAuto = $("#btn-autoscroll");
const btnWrap = $("#btn-wrap");
const fontSelect = $("#font-size");
const btnCopy = $("#btn-copy");
const btnDownload = $("#btn-download");
// konfiguracja
const POLL_MS = 1000;
const ORDER_NEWEST_FIRST = true; // najnowsze na GÓRZE (backend już odwraca)
const TOL = 40; // tolerancja (px) od krawędzi, aby uznać, że jesteśmy „przy krawędzi”
// state
let selectedLevel = levelSelect ? levelSelect.value : "INFO";
let pollTimer = null;
let live = true;
// autoscroll: ON domyślnie
let autoScroll = true;
// gdy użytkownik ręcznie wyłączy — blokujemy automatyczne włączanie
let autoLockByUser = false;
// gdy autoscroll wyłączył się przez przewinięcie — możemy go automatycznie przywracać
let autoDisabledBy = null; // null | 'scroll' | 'user'
let wrapped = false;
let lastPayload = "";
let debounceTimer;
function setUrlParam(key, val) {
const url = new URL(window.location.href);
if (val === "" || val == null) url.searchParams.delete(key);
else url.searchParams.set(key, val);
window.history.replaceState(null, "", url.toString());
}
function highlight() {
if (window.hljs && typeof window.hljs.highlightElement === "function") {
window.hljs.highlightElement(logContainer);
}
}
function setEmptyState(on) {
$("#logEmpty")?.classList.toggle("d-none", !on);
logContainer.classList.toggle("d-none", on);
}
function refreshBtnLive() {
if (!btnLive) return;
btnLive.classList.toggle("btn-outline-light", live);
btnLive.classList.toggle("btn-danger", !live);
btnLive.textContent = live
? (btnLive.dataset.onText || "Live ON")
: (btnLive.dataset.offText || "Live OFF");
}
function refreshBtnAuto() {
if (!btnAuto) return;
btnAuto.classList.toggle("btn-outline-secondary", !autoScroll);
btnAuto.classList.toggle("btn-light", autoScroll);
btnAuto.textContent = autoScroll ? "Auto-scroll" : "Auto-scroll (OFF)";
}
function scrollToEdgeIfNeeded() {
if (!autoScroll) return;
if (ORDER_NEWEST_FIRST) {
// najnowsze na górze -> krawędź to top
logScroll.scrollTop = 0;
} else {
// klasyczny tail -> krawędź to dół
logScroll.scrollTop = logScroll.scrollHeight;
}
}
function applyText(content) {
lastPayload = content || "";
setEmptyState(!lastPayload.length);
logContainer.textContent = lastPayload;
highlight();
// tylko gdy autoscroll jest aktywny, przeskakujemy do krawędzi
scrollToEdgeIfNeeded();
}
async function copyToClipboard(text) {
try {
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
await navigator.clipboard.writeText(text);
return true;
}
} catch (_) { /* fallback below */ }
// Fallback dla HTTP/nieobsługiwanych środowisk
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.top = "-1000px";
ta.style.left = "-1000px";
ta.setAttribute("readonly", "");
document.body.appendChild(ta);
ta.select();
ta.setSelectionRange(0, ta.value.length);
let ok = false;
try {
ok = document.execCommand("copy");
} catch (_) {
ok = false;
}
document.body.removeChild(ta);
return ok;
}
function loadLogs({ silent = false } = {}) {
const query = queryInput ? queryInput.value : "";
const url = `/logs-data?level=${encodeURIComponent(selectedLevel)}&query=${encodeURIComponent(query)}`;
return fetch(url)
.then(res => res.ok ? res.json() : Promise.reject(res.status))
.then(data => {
// backend już zwraca newest-first; nie odwracaj kolejności
const lines = Array.isArray(data.logs) ? data.logs.slice() : [];
const text = lines.join("\n");
applyText(text);
})
.catch(() => {
if (!silent) window.showToast?.({ text: "Błąd ładowania logów", variant: "danger" });
});
}
function startPolling() {
stopPolling();
if (!live) return;
pollTimer = setInterval(() => loadLogs({ silent: true }), POLL_MS);
}
function stopPolling() {
if (pollTimer) clearInterval(pollTimer);
pollTimer = null;
}
// wyszukiwarka — enter lub debounce 400ms
searchForm?.addEventListener("submit", (e) => {
e.preventDefault();
setUrlParam("query", queryInput.value || "");
loadLogs();
});
queryInput?.addEventListener("input", () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
setUrlParam("query", queryInput.value || "");
loadLogs({ silent: true });
}, 400);
});
// --- TOOLBAR ---
btnLive?.addEventListener("click", () => {
live = !live;
refreshBtnLive();
window.showToast?.({
text: live ? "Live tail: włączony" : "Live tail: wyłączony",
variant: live ? "dark" : "warning"
});
live ? startPolling() : stopPolling();
});
refreshBtnLive();
btnAuto?.addEventListener("click", () => {
// ręczne przełączenie blokuje/odblokowuje auto włączanie
autoScroll = !autoScroll;
autoLockByUser = !autoScroll; // jeśli wyłączasz przyciskiem — zablokuj auto re-enable
autoDisabledBy = autoScroll ? null : "user";
refreshBtnAuto();
window.showToast?.({
text: autoScroll ? "Auto-scroll: ON" : "Auto-scroll: OFF (ręcznie)",
variant: "info"
});
if (autoScroll) {
// po włączeniu — przeskocz do krawędzi
scrollToEdgeIfNeeded();
}
});
refreshBtnAuto();
btnWrap?.addEventListener("click", () => {
wrapped = !wrapped;
btnWrap.classList.toggle("btn-outline-secondary", !wrapped);
btnWrap.classList.toggle("btn-light", wrapped);
logContainer.style.whiteSpace = wrapped ? "pre-wrap" : "pre";
});
fontSelect?.addEventListener("change", () => {
logContainer.style.fontSize = fontSelect.value;
});
btnCopy?.addEventListener("click", async () => {
const ok = await copyToClipboard(lastPayload || "");
window.showToast?.({
text: ok ? "Skopiowano log do schowka." : "Nie udało się skopiować.",
variant: ok ? "success" : "danger"
});
});
btnDownload?.addEventListener("click", () => {
const blob = new Blob([lastPayload || ""], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = `app.log.view.txt`; a.click();
URL.revokeObjectURL(url);
});
// --- SMART AUTOSCROLL GUARD ---
// 1) auto-wyłączenie gdy oddalimy się od krawędzi
// 2) auto-włączenie gdy wrócimy do krawędzi, ale tylko jeśli nie zablokowano przyciskiem
let scrollTicking = false; // micro-raf guard
logScroll.addEventListener("scroll", () => {
if (scrollTicking) return;
scrollTicking = true;
requestAnimationFrame(() => {
scrollTicking = false;
if (ORDER_NEWEST_FIRST) {
// krawędź to góra (top)
const atTop = logScroll.scrollTop <= TOL;
const farFromTop = logScroll.scrollTop > TOL;
// 1) gdy uciekasz od góry — wyłącz auto, jeśli było włączone
if (farFromTop && autoScroll) {
autoScroll = false;
autoDisabledBy = "scroll";
refreshBtnAuto();
window.showToast?.({ text: "Auto-scroll: OFF (przewijanie)", variant: "info" });
}
// 2) gdy wracasz na samą górę — automatycznie włącz, jeśli nie ma blokady usera
if (atTop && !autoScroll && !autoLockByUser && autoDisabledBy === "scroll") {
autoScroll = true;
autoDisabledBy = null;
refreshBtnAuto();
// upewnij się, że trzymamy top
logScroll.scrollTop = 0;
window.showToast?.({ text: "Auto-scroll: ON (powrót na górę)", variant: "info" });
}
} else {
// klasyczny tail — krawędź to dół
const distanceFromBottom = logScroll.scrollHeight - logScroll.clientHeight - logScroll.scrollTop;
const atBottom = distanceFromBottom <= TOL;
const farFromBottom = distanceFromBottom > TOL;
if (farFromBottom && autoScroll) {
autoScroll = false;
autoDisabledBy = "scroll";
refreshBtnAuto();
window.showToast?.({ text: "Auto-scroll: OFF (przewijanie)", variant: "info" });
}
if (atBottom && !autoScroll && !autoLockByUser && autoDisabledBy === "scroll") {
autoScroll = true;
autoDisabledBy = null;
refreshBtnAuto();
logScroll.scrollTop = logScroll.scrollHeight;
window.showToast?.({ text: "Auto-scroll: ON (powrót na dół)", variant: "info" });
}
}
});
}, { passive: true });
// Start
loadLogs().then(() => startPolling());
// sprzątanie
window.addEventListener("beforeunload", () => {
stopPolling();
});
})();

View File

@@ -0,0 +1,16 @@
document.getElementById('setLogLevelForm').addEventListener('submit', function(e) {
e.preventDefault();
const level = document.getElementById('appLogLevel').value;
fetch(SET_LOG_LEVEL_URL, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'level=' + encodeURIComponent(level)
})
.then(res => res.json())
.then(data => {
window.showToast({ text: data.message, variant: 'success' });
})
.catch(() => {
window.showToast({ text: "Błąd podczas zmiany poziomu", variant: "error" });
});
});

72
static/js/stats.js Normal file
View File

@@ -0,0 +1,72 @@
(function () {
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
// Kopiuj: cały widok
$('#btn-copy-overview')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(document.body.innerText);
window.showToast?.({ text: 'Skopiowano podsumowanie.', variant: 'success' });
} catch {
window.showToast?.({ text: 'Nie udało się skopiować.', variant: 'danger' });
}
});
// Eksport CSV z DOM (GEO + REASONS)
$('#btn-export-overview')?.addEventListener('click', () => {
const lines = [];
lines.push('section,key,count,percent');
// GEO: z tabeli
$$('#geo-table tbody tr').forEach(tr => {
const tds = tr.querySelectorAll('td');
if (tds.length !== 3) return;
const country = tds[0]?.innerText.trim();
const percent = (tds[1]?.querySelector('.text-secondary')?.innerText.trim() || '').replace('%', '');
const count = tds[2]?.innerText.trim();
if (country) lines.push(`geo,"${country.replace(/"/g, '""')}",${count},${percent}`);
});
// REASONS: z listy
$$('.card:has(.card-header:contains("Przyczyny banów")) .list-group-item').forEach(li => {
const label = li.querySelector('.text-truncate')?.getAttribute('title') || li.querySelector('.text-truncate')?.innerText || '';
const meta = li.querySelector('.small.text-secondary')?.innerText || ''; // "123 (45.6%)"
const m = meta.match(/(\d+)\s*\(([\d.,]+)%\)/);
const count = m ? m[1] : '';
const percent = m ? m[2].replace(',', '.') : '';
if (label) lines.push(`reason,"${label.replace(/"/g, '""')}",${count},${percent}`);
});
const csv = lines.join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'stats_overview.csv'; a.click();
URL.revokeObjectURL(url);
});
// Polyfill :contains dla selektora użytego wyżej (prosty, lokalny)
(function addContainsPseudo() {
const _matches = Element.prototype.matches;
if (!document.querySelector(':contains(dummy)')) {
const oldQuerySelectorAll = Document.prototype.querySelectorAll;
Document.prototype.querySelectorAll = function (sel) {
if (!sel.includes(':contains(')) return oldQuerySelectorAll.call(this, sel);
const m = sel.match(/^(.*):has\(\.card-header:contains\("([^"]+)"\)\)\s*(.*)$/);
if (m) {
const [, pre, text, post] = m;
return $$(pre + ' .card').filter(card => {
return card.querySelector('.card-header')?.textContent.includes(text);
}).flatMap(card => card.querySelectorAll(post || ''));
}
return oldQuerySelectorAll.call(this, sel);
};
Element.prototype.matches = function (sel) {
if (!sel.includes(':contains(')) return _matches.call(this, sel);
const m = sel.match(/^:contains\("([^"]+)"\)$/);
if (m) return this.textContent.includes(m[1]);
return _matches.call(this, sel);
};
}
})();
})();

35
static/js/toast.js Normal file
View File

@@ -0,0 +1,35 @@
(function () {
const toastEl = document.getElementById('appToast');
const bodyEl = document.getElementById('appToastBody');
const variants = {
success: 'text-bg-success',
error: 'text-bg-danger',
danger: 'text-bg-danger',
warning: 'text-bg-warning',
info: 'text-bg-info',
dark: 'text-bg-dark'
};
function setVariant(el, variant) {
Object.values(variants).forEach(v => el.classList.remove(v));
el.classList.add(variants[variant] || variants.dark);
}
window.showToast = function ({ text = 'Gotowe.', variant = 'dark' } = {}) {
if (bodyEl) bodyEl.textContent = text;
setVariant(toastEl, variant);
const t = new bootstrap.Toast(toastEl);
t.show();
};
const flashNode = document.getElementById('flash-data');
if (flashNode) {
try {
const flashes = JSON.parse(flashNode.dataset.flashes || '[]');
flashes.forEach(([category, message]) => {
window.showToast({ text: message, variant: category });
});
} catch (_) { }
}
})();