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:
@@ -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,'"') + '">' + 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 %}
|
||||
Reference in New Issue
Block a user