diff --git a/public/admin.js b/public/admin.js
index 42efe5f..56bbad2 100644
--- a/public/admin.js
+++ b/public/admin.js
@@ -60,6 +60,7 @@ function buildSidebar(active) {
{ page: 'users', label: 'Пользователи', icon: 'user-cog' },
{ page: 'worker', label: 'Воркер ИИ', icon: 'bot' },
{ page: 'audit', label: 'Аудит', icon: 'scroll-text' },
+ { page: 'bans', label: 'Блокировки', icon: 'shield-off' },
{ page: 'settings', label: 'Настройки', icon: 'settings' },
];
const nav = [...base, ...(isAdmin() ? adminNav : [])];
diff --git a/public/bans.html b/public/bans.html
new file mode 100644
index 0000000..d91f0a6
--- /dev/null
+++ b/public/bans.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+Блокировки IP — Админ-панель
+
+
+
+
+
+
+
+
Блокировки IP
+
IP автоматически блокируется на 24 часа: за заполнение скрытого антиспам-поля, за 10 неудачных попыток входа или 10 неверных паролей на защищённой ссылке
+
+
+
+ Обновить
+
+
+
+
+ IP
+ Причина
+ Заблокирован
+ Разблокируется
+
+
+
+ Загрузка…
+
+
+
+
+
+
+
+
+
+
diff --git a/public/js/bans.js b/public/js/bans.js
new file mode 100644
index 0000000..95e661a
--- /dev/null
+++ b/public/js/bans.js
@@ -0,0 +1,57 @@
+const REASONS = {
+ 'honeypot': 'Антиспам-поле заполнено',
+ 'login-bruteforce': 'Подбор пароля (10 неудачных входов)',
+ 'share-password-bruteforce': 'Подбор пароля ссылки',
+ 'manual': 'Вручную',
+};
+
+function reasonLabel(r) {
+ return REASONS[r] || r || '—';
+}
+
+async function loadBans() {
+ const tbody = document.getElementById('bansBody');
+ const info = document.getElementById('bansInfo');
+ const res = await fetch(`${API}/api/bans`, { headers: hdr() });
+ if (!res.ok) {
+ tbody.innerHTML = `Ошибка загрузки (${res.status}) `;
+ info.textContent = '';
+ return;
+ }
+ const bans = await res.json();
+ info.textContent = `Активных блокировок: ${bans.length}`;
+ if (!bans.length) {
+ tbody.innerHTML = 'Активных блокировок нет ';
+ return;
+ }
+ tbody.innerHTML = bans.map(b => `
+
+ ${esc(b.ip)}
+ ${esc(reasonLabel(b.reason))}
+ ${new Date(b.created_at).toLocaleString('ru')}
+ ${new Date(b.banned_until).toLocaleString('ru')}
+ Разблокировать
+ `).join('');
+ renderIcons();
+}
+
+document.getElementById('bansBody').addEventListener('click', async e => {
+ const ip = e.target.closest('[data-unban]')?.dataset.unban;
+ if (!ip) return;
+ const btn = e.target.closest('[data-unban]');
+ btn.disabled = true;
+ const res = await fetch(`${API}/api/bans/${encodeURIComponent(ip)}`, { method: 'DELETE', headers: hdr() });
+ if (res.ok) showToast('IP разблокирован: ' + ip);
+ else showToast('Ошибка разблокировки');
+ btn.disabled = false;
+ loadBans();
+});
+
+document.getElementById('refreshBtn').addEventListener('click', loadBans);
+
+(async () => {
+ if (await requireAdminPage()) {
+ buildSidebar(document.body.dataset.page);
+ loadBans();
+ }
+})();