style/feat(web): restyle digests, add users filters, fix dashboard stats and emails status UI
This commit is contained in:
parent
a1ae78e5f8
commit
35ffe3442c
6 changed files with 190 additions and 66 deletions
28
web/app.py
28
web/app.py
|
|
@ -167,11 +167,12 @@ async def dashboard(request: Request, username: str = Depends(get_current_admin)
|
|||
|
||||
return templates.TemplateResponse(request=request, name="dashboard.html", context={
|
||||
"username": username,
|
||||
"stats": {
|
||||
"users": users_count, "verified": verified_count,
|
||||
"messages": messages_count, "ads": active_ads,
|
||||
"polls": active_polls, "events": upcoming_events
|
||||
},
|
||||
"users_count": users_count,
|
||||
"verified_count": verified_count,
|
||||
"messages_count": messages_count,
|
||||
"active_ads": active_ads,
|
||||
"active_polls": active_polls,
|
||||
"upcoming_events": upcoming_events,
|
||||
"pending_count": pending_count
|
||||
})
|
||||
|
||||
|
|
@ -728,6 +729,23 @@ async def api_emails_sync_ai(username: str = Depends(get_current_admin)):
|
|||
except Exception as e:
|
||||
return JSONResponse(status_code=500, content={"message": str(e)})
|
||||
|
||||
@app.get("/api/emails/pipeline_status")
|
||||
async def api_emails_pipeline_status(username: str = Depends(get_current_admin)):
|
||||
import time
|
||||
status_file = Path("/home/matrixhasyou/mutt/pipeline_status.json")
|
||||
if not status_file.exists():
|
||||
return JSONResponse(content={"status": "idle", "step": "Неактивен", "updated_at": "-", "new_emails": 0, "total_emails": 0, "pdf_size_mb": 0.0})
|
||||
try:
|
||||
with open(status_file) as f:
|
||||
data = json.load(f)
|
||||
mtime = status_file.stat().st_mtime
|
||||
if data.get("status") == "running" and (time.time() - mtime > 600):
|
||||
data["status"] = "stuck"
|
||||
data["step"] = "Процесс завис или был остановлен"
|
||||
return JSONResponse(content=data)
|
||||
except Exception as e:
|
||||
return JSONResponse(content={"status": "error", "message": str(e)})
|
||||
|
||||
@app.get("/infra", response_class=HTMLResponse)
|
||||
async def get_infra_page(request: Request, username: str = Depends(get_current_admin)):
|
||||
async with AsyncSessionLocal() as session:
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
<div class="card-header">СИСТЕМНЫЙ_СТАТУС</div>
|
||||
<div class="card-body">
|
||||
<div class="p-3 bg-black border border-success font-monospace small">
|
||||
[OK] Database: sqlite3/domovoy.db<br>
|
||||
[OK] Database: PostgreSQL (Synology NAS)<br>
|
||||
[OK] Telegram: @lkmthreeseven_bot<br>
|
||||
[OK] Proxy: Active (127.0.0.1:10808)<br>
|
||||
[OK] Node: {{ username }}@server<br>
|
||||
|
|
|
|||
|
|
@ -7,19 +7,21 @@
|
|||
{% block extra_style %}
|
||||
<style>
|
||||
.digest-settings-container { max-width: 900px; }
|
||||
.digest-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 10px; margin-bottom: 30px; }
|
||||
.digest-header h1 { font-size: 2em; margin-bottom: 10px; }
|
||||
.digest-back-link { color: white; text-decoration: none; opacity: 0.9; }
|
||||
.digest-back-link:hover { opacity: 1; }
|
||||
.digest-card { background: white; padding: 25px; border-radius: 10px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||
.digest-card h3 { margin-bottom: 15px; color: #667eea; }
|
||||
.digest-header { background: linear-gradient(135deg, var(--color-primary) 0%, rgba(10, 10, 15, 0.7) 100%); border: 1px solid var(--panel-border); color: white; padding: 30px; border-radius: 16px; margin-bottom: 30px; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
|
||||
.digest-header h1 { font-size: 2em; margin-bottom: 10px; text-shadow: 0 0 10px var(--color-primary-glow); }
|
||||
.digest-back-link { color: var(--color-success); text-decoration: none; opacity: 0.9; transition: var(--transition-smooth); }
|
||||
.digest-back-link:hover { opacity: 1; text-shadow: 0 0 8px var(--color-success-glow); }
|
||||
.digest-card { background: var(--panel-bg); border: 1px solid var(--panel-border); padding: 25px; border-radius: 16px; margin-bottom: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); transition: var(--transition-smooth); }
|
||||
.digest-card:hover { border-color: var(--panel-border-hover); background: var(--panel-bg-hover); }
|
||||
.digest-card h3 { margin-bottom: 15px; color: var(--color-success); text-shadow: 0 0 5px var(--color-success-glow); }
|
||||
.digest-form-group { margin-bottom: 20px; }
|
||||
.digest-form-group label { display: block; margin-bottom: 5px; font-weight: 500; }
|
||||
.digest-form-group input[type="time"], .digest-form-group select { width: 100%; padding: 10px; border: 2px solid #e2e8f0; border-radius: 5px; font-size: 1em; }
|
||||
.digest-checkbox-group { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||||
.digest-checkbox-group input[type="checkbox"] { width: 20px; height: 20px; }
|
||||
.digest-btn { padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; background: #667eea; color: white; }
|
||||
.digest-btn:hover { background: #5568d3; }
|
||||
.digest-form-group label { display: block; margin-bottom: 5px; font-weight: 500; color: var(--color-text-main); }
|
||||
.digest-form-group input[type="time"], .digest-form-group select { width: 100%; padding: 10px; border: 1px solid var(--panel-border); border-radius: 8px; background: rgba(0, 0, 0, 0.3); color: var(--color-text-main); font-size: 1em; transition: var(--transition-smooth); }
|
||||
.digest-form-group input[type="time"]:focus, .digest-form-group select:focus { outline: none; border-color: var(--color-primary); box-shadow: 0 0 8px var(--color-primary-glow); }
|
||||
.digest-checkbox-group { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; color: var(--color-text-main); }
|
||||
.digest-checkbox-group input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--color-success); cursor: pointer; }
|
||||
.digest-btn { padding: 12px 24px; border: none; border-radius: 8px; cursor: pointer; font-size: 1em; background: var(--color-success); color: #000; font-weight: 600; transition: var(--transition-smooth); }
|
||||
.digest-btn:hover { background: #00cca3; box-shadow: 0 0 10px var(--color-success-glow); }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,44 +11,47 @@
|
|||
{% block content %}
|
||||
<style>
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 30px; border-radius: 10px; margin-bottom: 30px; }
|
||||
.header h1 { font-size: 2em; margin-bottom: 10px; }
|
||||
.back-link { color: white; text-decoration: none; opacity: 0.9; }
|
||||
.back-link:hover { opacity: 1; }
|
||||
.header { background: linear-gradient(135deg, var(--color-primary) 0%, rgba(10, 10, 15, 0.7) 100%); border: 1px solid var(--panel-border); color: white; padding: 30px; border-radius: 16px; margin-bottom: 30px; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
|
||||
.header h1 { font-size: 2em; margin-bottom: 10px; text-shadow: 0 0 10px var(--color-primary-glow); }
|
||||
.back-link { color: var(--color-success); text-decoration: none; opacity: 0.9; }
|
||||
.back-link:hover { opacity: 1; text-shadow: 0 0 8px var(--color-success-glow); }
|
||||
.tabs { display: flex; gap: 10px; margin-bottom: 20px; }
|
||||
.tab { padding: 12px 24px; background: white; border: none; border-radius: 8px 8px 0 0; cursor: pointer; font-size: 1em; }
|
||||
.tab.active { background: #f5576c; color: white; }
|
||||
.tab { padding: 12px 24px; background: rgba(255, 255, 255, 0.05); color: var(--color-text-muted); border: 1px solid var(--panel-border); border-radius: 8px 8px 0 0; cursor: pointer; font-size: 1em; transition: var(--transition-smooth); }
|
||||
.tab:hover { color: var(--color-text-main); background: rgba(255, 255, 255, 0.08); }
|
||||
.tab.active { background: var(--color-primary); color: white; border-color: var(--color-primary); box-shadow: 0 0 10px var(--color-primary-glow); }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
.pending-card { background: white; padding: 25px; border-radius: 10px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-left: 5px solid #f5576c; }
|
||||
.pending-card h3 { color: #f5576c; margin-bottom: 10px; }
|
||||
.pending-card .meta { color: #666; font-size: 0.9em; margin-bottom: 15px; }
|
||||
.pending-card .text { background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 15px 0; white-space: pre-wrap; max-height: 300px; overflow-y: auto; }
|
||||
.btn { padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 0.95em; margin-right: 10px; transition: all 0.2s; }
|
||||
.btn-approve { background: #48bb78; color: white; }
|
||||
.btn-approve:hover { background: #38a169; }
|
||||
.btn-reject { background: #f56565; color: white; }
|
||||
.btn-reject:hover { background: #e53e3e; }
|
||||
.btn-edit { background: #ed8936; color: white; }
|
||||
.btn-edit:hover { background: #dd6b20; }
|
||||
.btn-send { background: #4299e1; color: white; }
|
||||
.btn-send:hover { background: #3182ce; }
|
||||
table { width: 100%; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
th { background: #f5576c; color: white; padding: 15px; text-align: left; }
|
||||
td { padding: 15px; border-bottom: 1px solid #eee; }
|
||||
tr:hover { background: #f8f9fa; }
|
||||
.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
|
||||
.badge-pending { background: #feebc8; color: #7c2d12; }
|
||||
.badge-approved { background: #c6f6d5; color: #22543d; }
|
||||
.badge-rejected { background: #fed7d7; color: #742a2a; }
|
||||
.badge-sent { background: #bee3f8; color: #2c5282; }
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; }
|
||||
.pending-card { background: var(--panel-bg); padding: 25px; border-radius: 16px; margin-bottom: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); border: 1px solid var(--panel-border); border-left: 5px solid var(--color-primary); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); transition: var(--transition-smooth); }
|
||||
.pending-card:hover { border-color: var(--panel-border-hover); background: var(--panel-bg-hover); }
|
||||
.pending-card h3 { color: var(--color-success); margin-bottom: 10px; text-shadow: 0 0 5px var(--color-success-glow); }
|
||||
.pending-card .meta { color: var(--color-text-muted); font-size: 0.9em; margin-bottom: 15px; }
|
||||
.pending-card .text { background: rgba(0, 0, 0, 0.3); color: var(--color-text-main); border: 1px solid var(--panel-border); padding: 15px; border-radius: 8px; margin: 15px 0; white-space: pre-wrap; max-height: 300px; overflow-y: auto; font-family: var(--font-mono); }
|
||||
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 0.95em; margin-right: 10px; transition: var(--transition-smooth); }
|
||||
.btn-approve { background: var(--color-success); color: #000; font-weight: 600; }
|
||||
.btn-approve:hover { background: #00cca3; box-shadow: 0 0 10px var(--color-success-glow); }
|
||||
.btn-reject { background: var(--color-danger); color: white; }
|
||||
.btn-reject:hover { background: #e0244a; box-shadow: 0 0 10px var(--color-danger-glow); }
|
||||
.btn-edit { background: var(--color-warning); color: #000; font-weight: 600; }
|
||||
.btn-edit:hover { background: #d99800; box-shadow: 0 0 10px var(--color-warning-glow); }
|
||||
.btn-send { background: var(--color-info); color: #000; font-weight: 600; }
|
||||
.btn-send:hover { background: #0099cc; box-shadow: 0 0 10px var(--color-info-glow); }
|
||||
table { width: 100%; background: var(--panel-bg); border-radius: 16px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.2); border: 1px solid var(--panel-border); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }
|
||||
th { background: rgba(138, 43, 226, 0.2); color: var(--color-success); padding: 15px; text-align: left; border-bottom: 2px solid var(--panel-border); font-weight: 600; text-transform: uppercase; font-size: 0.85em; letter-spacing: 0.5px; }
|
||||
td { padding: 15px; border-bottom: 1px solid var(--panel-border); color: var(--color-text-main); }
|
||||
tr:hover { background: rgba(255, 255, 255, 0.02); }
|
||||
.badge { display: inline-block; padding: 4px 8px; border-radius: 6px; font-size: 0.85em; font-weight: 500; border: 1px solid transparent; }
|
||||
.badge-pending { background: rgba(255, 179, 0, 0.15); color: var(--color-warning); border-color: var(--color-warning); }
|
||||
.badge-approved { background: rgba(0, 255, 204, 0.15); color: var(--color-success); border-color: var(--color-success); }
|
||||
.badge-rejected { background: rgba(255, 45, 85, 0.15); color: var(--color-danger); border-color: var(--color-danger); }
|
||||
.badge-sent { background: rgba(0, 191, 255, 0.15); color: var(--color-info); border-color: var(--color-info); }
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; backdrop-filter: blur(8px); }
|
||||
.modal.active { display: flex; align-items: center; justify-content: center; }
|
||||
.modal-content { background: white; padding: 30px; border-radius: 10px; max-width: 900px; width: 90%; max-height: 80vh; overflow-y: auto; }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.close-btn { background: none; border: none; font-size: 1.5em; cursor: pointer; }
|
||||
textarea { width: 100%; min-height: 300px; padding: 15px; border: 2px solid #e2e8f0; border-radius: 5px; font-family: inherit; font-size: 0.95em; }
|
||||
textarea:focus { outline: none; border-color: #f5576c; }
|
||||
.modal-content { background: var(--bg-main); border: 1px solid var(--panel-border); color: var(--color-text-main); padding: 30px; border-radius: 16px; max-width: 900px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 0 15px var(--color-primary-glow); }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid var(--panel-border); padding-bottom: 15px; }
|
||||
.close-btn { background: none; border: none; font-size: 1.5em; cursor: pointer; color: var(--color-text-muted); }
|
||||
.close-btn:hover { color: var(--color-text-main); }
|
||||
textarea { width: 100%; min-height: 300px; padding: 15px; border: 1px solid var(--panel-border); border-radius: 8px; background: rgba(0, 0, 0, 0.3); color: var(--color-text-main); font-family: var(--font-mono); font-size: 0.95em; transition: var(--transition-smooth); }
|
||||
textarea:focus { outline: none; border-color: var(--color-primary); box-shadow: 0 0 8px var(--color-primary-glow); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -124,9 +124,32 @@
|
|||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<!-- КАРТОЧКА ДИНАМИЧЕСКОГО СТАТУСА ПАЙПЛАЙНА -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border-primary" id="pipelineStatusCard" style="display: none;">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>>_ ТЕКУЩИЙ_СТАТУС_ПАЙПЛАЙНА</span>
|
||||
<span class="spinner-grow spinner-grow-sm text-primary" id="pipelineSpinner" role="status"></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="p-3 bg-black border border-primary font-monospace small">
|
||||
[STEP] Шаг: <span id="pipelineStep" class="text-info">Ожидание</span> <br>
|
||||
[TIME] Обновлено: <span id="pipelineTime">-</span> <br>
|
||||
<div class="progress my-3" style="height: 10px; background-color: #111;">
|
||||
<div id="pipelineProgress" class="progress-bar progress-bar-striped progress-bar-animated bg-primary" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
[DATA] Новых писем: <span id="pipelineNewEmails" class="text-success">0</span> <br>
|
||||
[DATA] Всего в архиве: <span id="pipelineTotalEmails">0</span> <br>
|
||||
[DATA] Размер архива: <span id="pipelineSize">0.00 MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ЛОГ ПОСЛЕДНЕГО ЗАПУСКА -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">ЛОГ_ПОСЛЕДНЕГО_ЗАПУСКА</div>
|
||||
<div class="card-header">ЛОГ_ПОСЛЕДНЕГО_ЗАПУСКА (БАЗА ДАННЫХ)</div>
|
||||
<div class="card-body">
|
||||
<div class="p-3 bg-black border border-success font-monospace small">
|
||||
{% if last_log %}
|
||||
|
|
@ -144,6 +167,77 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
async function checkPipelineStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/emails/pipeline_status');
|
||||
const data = await response.json();
|
||||
|
||||
const card = document.getElementById('pipelineStatusCard');
|
||||
const step = document.getElementById('pipelineStep');
|
||||
const time = document.getElementById('pipelineTime');
|
||||
const progress = document.getElementById('pipelineProgress');
|
||||
const newEmails = document.getElementById('pipelineNewEmails');
|
||||
const totalEmails = document.getElementById('pipelineTotalEmails');
|
||||
const size = document.getElementById('pipelineSize');
|
||||
const spinner = document.getElementById('pipelineSpinner');
|
||||
|
||||
if (data.status === 'running') {
|
||||
card.style.display = 'block';
|
||||
card.className = "card border-primary h-100";
|
||||
step.className = "text-info";
|
||||
step.textContent = data.step;
|
||||
time.textContent = data.updated_at;
|
||||
spinner.style.display = 'inline-block';
|
||||
|
||||
// Динамический прогресс
|
||||
let width = '10%';
|
||||
if (data.step.includes('Сбор')) width = '35%';
|
||||
else if (data.step.includes('Консолидация')) width = '70%';
|
||||
else if (data.step.includes('Google Drive')) width = '90%';
|
||||
progress.style.width = width;
|
||||
progress.className = "progress-bar progress-bar-striped progress-bar-animated bg-primary";
|
||||
|
||||
newEmails.textContent = data.new_emails;
|
||||
totalEmails.textContent = data.total_emails;
|
||||
size.textContent = data.pdf_size_mb + ' MB';
|
||||
|
||||
// Опрашиваем часто
|
||||
setTimeout(checkPipelineStatus, 2000);
|
||||
} else if (data.status === 'success') {
|
||||
card.style.display = 'block';
|
||||
card.className = "card border-success h-100";
|
||||
step.className = "text-success";
|
||||
step.innerHTML = '✅ Завершен успешно';
|
||||
time.textContent = data.updated_at;
|
||||
spinner.style.display = 'none';
|
||||
progress.style.width = '100%';
|
||||
progress.className = "progress-bar bg-success";
|
||||
|
||||
newEmails.textContent = data.new_emails;
|
||||
totalEmails.textContent = data.total_emails;
|
||||
size.textContent = data.pdf_size_mb + ' MB';
|
||||
|
||||
// Скроем статус через 10 секунд
|
||||
setTimeout(() => { card.style.display = 'none'; }, 10000);
|
||||
} else if (data.status === 'error' || data.status === 'stuck') {
|
||||
card.style.display = 'block';
|
||||
card.className = "card border-danger h-100";
|
||||
step.className = "text-danger";
|
||||
step.innerHTML = '❌ ' + data.step;
|
||||
time.textContent = data.updated_at;
|
||||
spinner.style.display = 'none';
|
||||
progress.style.width = '100%';
|
||||
progress.className = "progress-bar bg-danger";
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get status:', e);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', checkPipelineStatus);
|
||||
|
||||
document.getElementById('fullSyncBtn').addEventListener('click', async function() {
|
||||
const btn = this;
|
||||
if (!confirm('Запустить полный цикл: Сбор почты -> PDF -> G-Drive?')) return;
|
||||
|
|
@ -156,6 +250,8 @@ document.getElementById('fullSyncBtn').addEventListener('click', async function(
|
|||
const response = await fetch('/api/emails/run_full_sync', { method: 'POST' });
|
||||
const result = await response.json();
|
||||
alert(result.message);
|
||||
// Запускаем мониторинг статуса
|
||||
setTimeout(checkPipelineStatus, 1000);
|
||||
} catch (e) {
|
||||
alert('Ошибка: ' + e);
|
||||
} finally {
|
||||
|
|
@ -185,4 +281,3 @@ document.getElementById('syncAiBtn').addEventListener('click', async function()
|
|||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,36 +19,42 @@
|
|||
</h2>
|
||||
|
||||
<!-- Статистика (ПЛОСКИЕ ПЕРЕМЕННЫЕ) -->
|
||||
<style>
|
||||
.filter-card { cursor: pointer; transition: var(--transition-smooth) !important; }
|
||||
.filter-card:hover { transform: translateY(-4px) !important; }
|
||||
.filter-card.active-filter { box-shadow: 0 0 15px currentColor !important; border-width: 2px !important; }
|
||||
</style>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-black border-success text-success">
|
||||
<div class="card bg-black border-success text-success filter-card {% if not current_filter %}active-filter{% endif %}" onclick="location.href='/users'">
|
||||
<div class="card-body text-center">
|
||||
<h3>{{ total_count }}</h3>
|
||||
<small>ВСЕГО</small>
|
||||
<small>⚡️ ВСЕГО</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-black border-success text-success">
|
||||
<div class="card bg-black border-success text-success filter-card {% if current_filter == 'verified' %}active-filter{% endif %}" onclick="location.href='/users?filter=verified'">
|
||||
<div class="card-body text-center">
|
||||
<h3>{{ verified_count }}</h3>
|
||||
<small>ВЕРИФИЦИРОВАНЫ</small>
|
||||
<small>✅ ВЕРИФИЦИРОВАНЫ</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-black border-info text-info">
|
||||
<div class="card bg-black border-info text-info filter-card {% if current_filter == 'ig' %}active-filter{% endif %}" onclick="location.href='/users?filter=ig'">
|
||||
<div class="card-body text-center">
|
||||
<h3>{{ ig_count }}</h3>
|
||||
<small>ИНИЦИАТИВНАЯ ГРУППА</small>
|
||||
<small>🔵 ИНИЦИАТИВНАЯ ГРУППА</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-black border-warning text-warning">
|
||||
<div class="card bg-black border-warning text-warning filter-card {% if current_filter == 'active_unverified' %}active-filter{% endif %}" onclick="location.href='/users?filter=active_unverified'">
|
||||
<div class="card-body text-center">
|
||||
<h3>{{ active_unverified }}</h3>
|
||||
<small>АКТИВНЫЕ ГОСТИ</small>
|
||||
<small>🔥 АКТИВНЫЕ ГОСТИ</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue