domovoy_bot/web/templates/broadcast.html
Admin ff03e38ee7 v1.20 ИГ кнопка + логирование + настройка в broadcast
🎯 ИНИЦИАТИВНАЯ ГРУППА:
• Кнопка 'ИГ' в дашборде и пользователях
• Фильтр по ИГ в /users?filter=ig
• Синяя карточка для ИГ

⚙️ НАСТРОЙКИ:
• Перенесено из /settings в /broadcast
• Лимит рассылки ИГ (секунды)
• GET/POST /api/settings/ig_broadcast_limit

📊 ЛОГИРОВАНИЕ:
• Таблица activity_log
• Логирование рассылок
• API /api/activity_log

🐛 ИСПРАВЛЕНИЯ:
• Удалена /settings (Internal Server Error)
• Исправлен web/app.py (повреждённый код)

📁 ФАЙЛЫ:
• web/app.py: фильтр ИГ, логи, настройка
• web/templates/dashboard.html: кнопка ИГ
• web/templates/users.html: кнопка ИГ
• web/templates/broadcast.html: настройка лимита
• database: activity_log table

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-10 17:10:26 +00:00

252 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Рассылки - Домовой Бот</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
.sidebar { min-height: 100vh; background: linear-gradient(180deg, #2c3e50 0%, #1a252f 100%); }
.sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 1rem; }
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: white; background-color: rgba(255,255,255,0.1); }
.content { padding: 2rem; }
#preview { max-width: 400px; margin-top: 1rem; display: none; }
#preview img { max-width: 100%; border-radius: 8px; }
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-2 sidebar p-0">
<div class="p-3 text-white text-center">
<h4><i class="bi bi-house-door"></i> Домовой Бот</h4>
</div>
<nav class="nav flex-column">
<a class="nav-link" href="/"><i class="bi bi-speedometer2"></i> Дашборд</a>
<a class="nav-link" href="/users"><i class="bi bi-people"></i> Пользователи</a>
<a class="nav-link" href="/verification"><i class="bi bi-shield-check"></i> Верификация</a>
<a class="nav-link" href="/phones"><i class="bi bi-telephone"></i> Телефоны</a>
<a class="nav-link active" href="/broadcast"><i class="bi bi-broadcast"></i> Рассылки</a>
<a class="nav-link" href="/polls"><i class="bi bi-bar-chart"></i> Опросы</a>
<a class="nav-link" href="/events"><i class="bi bi-calendar-event"></i> События</a>
<a class="nav-link" href="/schedules"><i class="bi bi-clock"></i> Расписания</a>
<a class="nav-link" href="/export"><i class="bi bi-download"></i> Экспорт</a>
<a class="nav-link" href="/settings"><i class="bi bi-gear"></i> Настройки</a>
</nav>
</div>
<div class="col-md-10 content">
<h2 class="mb-4"><i class="bi bi-broadcast"></i> Рассылка объявлений</h2>
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-send"></i> Создать рассылку</h5>
</div>
<div class="card-body">
<form id="broadcastForm" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">Текст объявления</label>
<textarea class="form-control" id="broadcastText" rows="5"
placeholder="Введите текст объявления..." required></textarea>
<small class="text-muted">Поддерживается HTML: &lt;b&gt;жирный&lt;/b&gt;, &lt;i&gt;курсив&lt;/i&gt;</small>
</div>
<div class="mb-3">
<label class="form-label">Картинка (необязательно)</label>
<input type="file" class="form-control" id="broadcastPhoto" accept="image/*"
onchange="previewImage()">
<small class="text-muted">JPG, PNG до 5MB</small>
<div id="preview">
<img id="previewImg" alt="Предпросмотр">
</div>
</div>
<div class="mb-3">
<label class="form-label">Кому отправить:</label>
<select class="form-select" id="broadcastRecipients">
<option value="all_verified">✅ Всем верифицированным</option>
<option value="ig_only">🔵 Только Инициативной Группе</option>
<option value="chat_only">📢 Только в общий чат</option>
<option value="all_and_chat">✅ Всем + в чат (по умолчанию)</option>
</select>
<small class="text-muted">Выберите получателей рассылки</small>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<b>Получатели:</b> <span id="recipientsInfo">Все верифицированные пользователи + общий чат</span>
</div>
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-send"></i> Отправить рассылку
</button>
</form>
<div id="result" class="mt-4"></div>
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<h5>Как использовать:</h5>
<ol>
<li>Введите текст объявления</li>
<li>Прикрепите картинку (необязательно)</li>
<li>Выберите получателей</li>
<li>Нажмите "Отправить рассылку"</li>
<li>Бот отправит выбранным получателям</li>
</ol>
</div>
</div>
<!-- IG Broadcast Limit -->
<div class="card mt-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-people"></i> Настройка рассылки ИГ</h5>
</div>
<div class="card-body">
<form id="igLimitForm">
<div class="mb-3">
<label class="form-label">Минимальный интервал между рассылками ИГ (секунды)</label>
<input type="number" class="form-control" id="igBroadcastLimit" value="0" min="0" step="60">
<small class="text-muted">
<ul class="mb-0">
<li><code>0</code> — Без ограничений</li>
<li><code>3600</code> — 1 раз в час</li>
<li><code>86400</code> — 1 раз в сутки</li>
</ul>
</small>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Сохранить настройку
</button>
</form>
<div id="igLimitResult" class="mt-3"></div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Загрузка настройки лимита ИГ
async function loadIgLimit() {
try {
const r = await fetch('/api/settings/ig_broadcast_limit');
const result = await r.json();
if (result.success) {
document.getElementById('igBroadcastLimit').value = parseInt(result.value) || 0;
}
} catch (err) {
console.error('Ошибка загрузки настройки:', err);
}
}
// Сохранение настройки лимита ИГ
document.getElementById('igLimitForm').addEventListener('submit', async (e) => {
e.preventDefault();
const limit = parseInt(document.getElementById('igBroadcastLimit').value);
const resultDiv = document.getElementById('igLimitResult');
resultDiv.innerHTML = '<div class="alert alert-info">💾 Сохранение...</div>';
try {
const r = await fetch('/api/settings/ig_broadcast_limit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ limit: limit })
});
const result = await r.json();
if (result.success) {
resultDiv.innerHTML = '<div class="alert alert-success">✅ Настройка сохранена!</div>';
} else {
resultDiv.innerHTML = '<div class="alert alert-danger">❌ Ошибка: ' + result.error + '</div>';
}
} catch (err) {
resultDiv.innerHTML = '<div class="alert alert-danger">❌ Ошибка: ' + err.message + '</div>';
}
});
// Загружаем настройку при загрузке страницы
document.addEventListener('DOMContentLoaded', loadIgLimit);
// Обновление информации о получателях
document.getElementById('broadcastRecipients').addEventListener('change', function() {
const info = {
'all_verified': 'Все верифицированные пользователи (в личку)',
'ig_only': 'Только участники Инициативной Группы (в личку)',
'chat_only': 'Только в общий чат (одно сообщение)',
'all_and_chat': 'Все верифицированные (в личку) + общий чат'
};
document.getElementById('recipientsInfo').textContent = info[this.value] || '';
});
function previewImage() {
const file = document.getElementById('broadcastPhoto').files[0];
const preview = document.getElementById('preview');
const previewImg = document.getElementById('previewImg');
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
previewImg.src = e.target.result;
preview.style.display = 'block';
}
reader.readAsDataURL(file);
} else {
preview.style.display = 'none';
}
}
document.getElementById('broadcastForm').addEventListener('submit', async (e) => {
e.preventDefault();
const text = document.getElementById('broadcastText').value;
const photo = document.getElementById('broadcastPhoto').files[0];
const recipients = document.getElementById('broadcastRecipients').value;
const resultDiv = document.getElementById('result');
if (!text.trim()) {
alert('Введите текст объявления!');
return;
}
const recipientNames = {
'all_verified': 'всем верифицированным',
'ig_only': 'Инициативной Группе',
'chat_only': 'в общий чат',
'all_and_chat': 'всем верифицированным + в чат'
};
if (!confirm(`Отправить рассылку ${recipientNames[recipients] || 'всем'}?`)) return;
resultDiv.innerHTML = '<div class="alert alert-info">📤 Отправка рассылки...</div>';
const formData = new FormData();
formData.append('text', text);
formData.append('recipients', recipients);
if (photo) {
formData.append('photo', photo);
}
try {
const r = await fetch('/api/broadcast/send', {
method: 'POST',
body: formData
});
const result = await r.json();
if (result.success) {
resultDiv.innerHTML = `<div class="alert alert-success">✅ ${result.message}</div>`;
document.getElementById('broadcastForm').reset();
document.getElementById('preview').style.display = 'none';
} else {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error || 'Неизвестная'}</div>`;
}
} catch (err) {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
}
});
</script>
</body>
</html>