v1.6.0: max_bot fixes — feature keys, flush→commit, test-run, categories, broadcast page, proxy error handling, deploy scripts

This commit is contained in:
2026-05-24 07:50:38 +03:00
parent bd048ea23d
commit 493e0b37a1
127 changed files with 6082 additions and 65 deletions
@@ -0,0 +1,241 @@
{% extends "page.html" %}
{% block page_content %}
<style>
.broadcast-layout { display:flex; gap:24px; margin-top:16px; flex-wrap:wrap; }
.broadcast-left { flex:1; min-width:300px; }
.broadcast-right { flex:1; min-width:300px; }
.msg-preview { background:#e3f2fd; border-radius:12px; padding:12px 16px; margin-top:8px;
white-space:pre-wrap; word-break:break-word; line-height:1.5; font-size:14px; }
.msg-preview.empty { color:#999; font-style:italic; }
.recipient-item { display:flex; align-items:center; gap:8px; padding:6px 8px;
border-bottom:1px solid #eee; cursor:pointer; }
.recipient-item:hover { background:#f8f9fa; }
.recipient-item .name { flex:1; }
.recipient-item .phone { color:#666; font-size:13px; }
.recipient-list { max-height:300px; overflow-y:auto; border:1px solid #ddd; border-radius:8px;
margin-top:8px; }
.recipient-list .select-all { padding:8px; border-bottom:1px solid #eee; background:#f8f9fa;
font-weight:600; cursor:pointer; user-select:none; }
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0;
background:rgba(0,0,0,0.5); z-index:1000;
align-items:center; justify-content:center; }
.modal-overlay.active { display:flex; }
.modal-box { background:#fff; border-radius:12px; padding:24px; max-width:600px;
width:90%; max-height:80vh; overflow-y:auto; }
.modal-box h3 { margin:0 0 16px; }
.modal-box .actions { display:flex; gap:12px; justify-content:flex-end; margin-top:20px; }
.broadcast-history { margin-top:24px; }
.broadcast-history table { width:100%; }
.broadcast-history td, .broadcast-history th { padding:8px 12px; text-align:left; border-bottom:1px solid #eee; }
.broadcast-history .text-col { max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
</style>
<div class="card">
<div class="card-header"><h2>Рассылки</h2></div>
<div class="broadcast-layout">
<div class="broadcast-left">
<div class="form-group">
<label>Текст сообщения</label>
<textarea id="broadcast-text" rows="5" style="width:100%;" oninput="updatePreview()" placeholder="Введите текст рассылки... Markdown поддерживается"></textarea>
</div>
<div>
<label>📱 Предпросмотр сообщения</label>
<div id="live-preview" class="msg-preview empty">Текст сообщения появится здесь...</div>
</div>
</div>
<div class="broadcast-right">
<div class="form-group">
<label>Получатели</label>
<input type="text" id="recipient-search" placeholder="Поиск..." style="width:100%;margin-bottom:8px;" oninput="loadRecipients()">
</div>
<div class="recipient-list" id="recipient-list">
<div class="select-all" onclick="toggleSelectAll()">
<span id="select-all-checkbox"></span> Выбрать всех согласных
<span style="float:right;color:#666;font-weight:400;" id="recipient-count"></span>
</div>
<div id="recipient-items"></div>
</div>
<div style="display:flex;gap:12px;margin-top:12px;">
<button class="btn btn-primary" onclick="previewBroadcast()">👁 Предпросмотр</button>
<button class="btn btn-primary" onclick="sendBroadcast()">📨 Отправить</button>
</div>
</div>
</div>
</div>
<div class="card broadcast-history">
<div class="card-header"><h3>История рассылок</h3></div>
<div class="table-wrap"><table>
<thead><tr><th>ID</th><th>Текст</th><th>Получателей</th><th>Дата</th></tr></thead>
<tbody id="history-list"></tbody>
</table></div>
</div>
<div class="modal-overlay" id="preview-modal">
<div class="modal-box">
<h3>👁 Предпросмотр рассылки</h3>
<div style="background:#e3f2fd;border-radius:12px;padding:16px;white-space:pre-wrap;line-height:1.5;" id="preview-text"></div>
<div style="margin-top:16px;padding:12px;background:#f8f9fa;border-radius:8px;">
<strong>Получателей:</strong> <span id="preview-count"></span>
<div style="margin-top:8px;max-height:150px;overflow-y:auto;" id="preview-recipients"></div>
</div>
<div class="actions">
<button class="btn btn-secondary" onclick="closePreview()">Отмена</button>
<button class="btn btn-primary" id="preview-send-btn" onclick="confirmSend()">📨 Отправить</button>
</div>
</div>
</div>
<script>
var selectedUserIds = new Set();
var allRecipients = [];
var currentDraftId = null;
function updatePreview(){
var text = document.getElementById('broadcast-text').value.trim();
var preview = document.getElementById('live-preview');
if(text){
preview.textContent = text;
preview.className = 'msg-preview';
} else {
preview.textContent = 'Текст сообщения появится здесь...';
preview.className = 'msg-preview empty';
}
}
function loadRecipients(){
var q = document.getElementById('recipient-search').value.trim();
var url = '/service/api/bot/broadcast/recipients?limit=200';
if(q) url += '&q=' + encodeURIComponent(q);
fetch(url).then(function(r){ return r.json() }).then(function(d){
allRecipients = d.users || [];
renderRecipients();
});
}
function renderRecipients(){
var container = document.getElementById('recipient-items');
var countEl = document.getElementById('recipient-count');
container.innerHTML = '';
countEl.textContent = allRecipients.length + ' чел.';
allRecipients.forEach(function(u){
var div = document.createElement('div');
div.className = 'recipient-item';
var checked = selectedUserIds.has(u.max_user_id) ? '☑' : '☐';
var name = (u.first_name || '') + ' ' + (u.last_name || '');
if(!name.trim()) name = 'ID ' + u.max_user_id;
div.innerHTML = '<span class="cb">' + checked + '</span>' +
'<span class="name">' + name + '</span>' +
'<span class="phone">' + (u.phone || '') + '</span>';
div.onclick = function(){ toggleRecipient(u.max_user_id); };
container.appendChild(div);
});
updateSelectAllCheckbox();
}
function toggleRecipient(id){
if(selectedUserIds.has(id)){
selectedUserIds['delete'](id);
} else {
selectedUserIds.add(id);
}
renderRecipients();
}
function toggleSelectAll(){
if(selectedUserIds.size === allRecipients.length){
selectedUserIds.clear();
} else {
selectedUserIds = new Set(allRecipients.map(function(u){ return u.max_user_id; }));
}
renderRecipients();
}
function updateSelectAllCheckbox(){
var cb = document.getElementById('select-all-checkbox');
if(allRecipients.length > 0 && selectedUserIds.size === allRecipients.length){
cb.textContent = '☑';
} else {
cb.textContent = '☐';
}
}
function previewBroadcast(){
var text = document.getElementById('broadcast-text').value.trim();
if(!text){ alert('Введите текст рассылки'); return; }
var userIds = Array.from(selectedUserIds);
fetch('/service/api/bot/broadcast', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text: text, user_ids: userIds, action: 'preview', draft_id: currentDraftId})
}).then(function(r){ return r.json() }).then(function(d){
if(d.error){ alert(d.error); return; }
currentDraftId = d.draft_id;
document.getElementById('preview-text').textContent = d.text;
document.getElementById('preview-count').textContent = d.recipient_count + ' чел.';
var recEl = document.getElementById('preview-recipients');
recEl.innerHTML = d.recipients.join(', ');
document.getElementById('preview-modal').className = 'modal-overlay active';
});
}
function closePreview(){
document.getElementById('preview-modal').className = 'modal-overlay';
}
function confirmSend(){
var text = document.getElementById('broadcast-text').value.trim();
if(!currentDraftId){ alert('Сначала сделайте предпросмотр'); return; }
if(!confirm('Отправить рассылку?')) return;
var userIds = Array.from(selectedUserIds);
document.getElementById('preview-send-btn').disabled = true;
fetch('/service/api/bot/broadcast', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text: text, user_ids: userIds, action: 'send', draft_id: currentDraftId})
}).then(function(r){ return r.json() }).then(function(d){
closePreview();
if(d.ok){
alert('Рассылка отправлена: ' + d.sent_to + ' из ' + d.total_recipients + ' получателей');
document.getElementById('broadcast-text').value = '';
document.getElementById('live-preview').textContent = 'Текст сообщения появится здесь...';
document.getElementById('live-preview').className = 'msg-preview empty';
selectedUserIds.clear();
renderRecipients();
currentDraftId = null;
loadHistory();
} else {
alert('Ошибка: ' + (d.error || 'неизвестная'));
}
document.getElementById('preview-send-btn').disabled = false;
});
}
function sendBroadcast(){
var text = document.getElementById('broadcast-text').value.trim();
if(!text){ alert('Введите текст рассылки'); return; }
var userIds = Array.from(selectedUserIds);
if(userIds.length === 0 && !confirm('Не выбрано ни одного получателя. Отправить всем согласным?')) return;
previewBroadcast();
}
function loadHistory(){
fetch('/service/api/bot/broadcast/history').then(function(r){ return r.json() }).then(function(d){
var tbody = document.getElementById('history-list');
tbody.innerHTML = '';
(d.broadcasts || []).forEach(function(b){
var tr = document.createElement('tr');
tr.innerHTML = '<td>' + b.id + '</td>' +
'<td class="text-col" title="' + b.text.replace(/"/g,'&quot;') + '">' + b.text + '</td>' +
'<td>' + b.recipient_count + '</td>' +
'<td>' + b.sent_at + '</td>';
tbody.appendChild(tr);
});
if(!d.broadcasts || d.broadcasts.length === 0){
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:#999;">Рассылок ещё не было</td></tr>';
}
});
}
loadRecipients();
loadHistory();
</script>
{% endblock %}