feat: анти-спам блокировка повторной отправки на главной (cooldown по spam_interval_min)

- index.html/index.js: экран подтверждения отправки с обратным отсчётом
- локальный запрет повторной отправки через localStorage (по настройке spam_interval_min)
- server.js: отдаём spam_interval_min в public-settings (default 30)
- Dockerfile: timeout для apk add, чтобы сборка не зависала на недоступном зеркале
- docker-compose.yml: отключаем cloudflared сервис (закомментирован)
This commit is contained in:
dev
2026-09-13 12:27:35 +03:00
parent 0b763e5738
commit 2c8beedc4b
5 changed files with 93 additions and 23 deletions
+60 -4
View File
@@ -15,6 +15,61 @@ const filesInput = document.getElementById('filesInput');
const filesList = document.getElementById('filesList');
const filesClearBtn = document.getElementById('filesClearBtn');
let selectedFiles = [];
const sentPanel = document.getElementById('sentPanel');
const sentTitle = document.getElementById('sentTitle');
const sentText = document.getElementById('sentText');
const sentTimer = document.getElementById('sentTimer');
const SENT_KEY = 'whatido_entry_sent_at';
let entryCooldownMin = 30;
let sentTimerId = null;
function unlockForm() {
clearInterval(sentTimerId);
sentTimerId = null;
localStorage.removeItem(SENT_KEY);
sentPanel.style.display = 'none';
form.style.display = 'grid';
toast.textContent = 'Можно отправить новую запись';
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2500);
}
function tickSentTimer(unlockTime) {
const remain = Math.max(0, unlockTime - Date.now());
if (remain <= 0) {
sentTimer.textContent = '';
unlockForm();
return;
}
const m = Math.floor(remain / 60000);
const s = Math.floor((remain % 60000) / 1000);
sentTimer.textContent = 'Новую запись можно отправить через ' + m + ' мин ' + s + ' сек';
}
function showSent(title, text, unlockTime) {
form.style.display = 'none';
sentPanel.style.display = 'flex';
sentTitle.textContent = title;
sentText.textContent = text;
tickSentTimer(unlockTime);
clearInterval(sentTimerId);
sentTimerId = setInterval(() => tickSentTimer(unlockTime), 1000);
}
function cooldownMs() { return entryCooldownMin * 60 * 1000; }
function checkPreviousSend() {
const raw = localStorage.getItem(SENT_KEY);
if (!raw) return;
const sentAt = parseInt(raw, 10);
if (!Number.isFinite(sentAt)) { localStorage.removeItem(SENT_KEY); return; }
const unlockTime = sentAt + cooldownMs();
if (Date.now() < unlockTime) {
showSent('Запись уже отправлена', 'Ты уже отправил(а) запись сегодня. Дождись окончания отсчёта, чтобы отправить новую.', unlockTime);
} else {
localStorage.removeItem(SENT_KEY);
}
}
function humanSize(b) {
if (b < 1024) return b + ' Б';
@@ -99,7 +154,10 @@ if (params.get('nameInput')) {
const f = await fRes.json();
if (f.footer_left) document.getElementById('footerLeft').textContent = f.footer_left;
if (f.footer_right) document.getElementById('footerRight').textContent = f.footer_right;
const c = parseInt(f.spam_interval_min, 10);
if (Number.isFinite(c) && c >= 1) entryCooldownMin = c;
} catch (e) { /* ignore */ }
checkPreviousSend();
})();
function showPreview(src) {
@@ -177,10 +235,8 @@ form.addEventListener('submit', async (e) => {
form.reset();
removePhoto();
clearFiles();
toast.textContent = 'Готово!';
toast.classList.remove('error');
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2500);
localStorage.setItem(SENT_KEY, String(Date.now()));
showSent('Запись отправлена!', 'Спасибо! Твоя запись уже передана преподавателю.', Date.now() + cooldownMs());
} catch (err) {
alert('Ошибка: ' + err.message);
} finally {