384 lines
20 KiB
HTML
384 lines
20 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Email Audit - LKM37 Admin{% endblock %}
|
||
{% block nav_emails %}active{% endblock %}
|
||
|
||
{% block content %}
|
||
<!-- ЗАГОЛОВОК -->
|
||
<div class="row mb-4">
|
||
<div class="col-md-8">
|
||
<h2 style="color: var(--matrix-green); text-shadow: 0 0 10px var(--matrix-green);">
|
||
>_ АУДИТ_ВЗАИМОДЕЙСТВИЯ_С_ОРГАНАМИ
|
||
</h2>
|
||
</div>
|
||
<div class="col-md-4 text-end">
|
||
<span class="badge bg-success p-2">АВТОМАТИЗАЦИЯ_CRON: ACTIVE (04:00)</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- МИНИ-КАРТОЧКИ АНАЛИТИКИ (ВИДЖЕТЫ) -->
|
||
<div class="row g-3 mb-4">
|
||
<!-- КАРТОЧКА 1: ВСЕГО ПИСЕМ -->
|
||
<div class="col-md-3">
|
||
<div class="card bg-dark border-success h-100 shadow">
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<h6 class="text-success text-uppercase small font-monospace mb-0">>_ ВСЕГО_ПИСЕМ</h6>
|
||
<i class="bi bi-envelope-open text-success fs-4"></i>
|
||
</div>
|
||
<h2 class="display-6 font-monospace my-2 text-white" id="statTotalEmails">{{ total_emails }}</h2>
|
||
<div class="small text-muted">
|
||
Вх: <span id="statInboundEmails" class="text-success fw-bold">{{ total_inbound }}</span> /
|
||
Исх: <span id="statOutboundEmails" class="text-info fw-bold">{{ total_outbound }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- КАРТОЧКА 2: ОБЪЕМ АРХИВА -->
|
||
<div class="col-md-3">
|
||
<div class="card bg-dark border-info h-100 shadow">
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<h6 class="text-info text-uppercase small font-monospace mb-0">>_ ОБЪЕМ_АРХИВА</h6>
|
||
<i class="bi bi-file-earmark-pdf text-info fs-4"></i>
|
||
</div>
|
||
<h2 class="display-6 font-monospace my-2 text-white" id="statPdfSize">{{ pdf_size_mb }} MB</h2>
|
||
<div class="small text-muted text-truncate" id="statLastUpdate">
|
||
Создан: {{ fmt_local(last_update) if last_update else '-' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- КАРТОЧКА 3: ВСЕГО ВЛОЖЕНИЙ -->
|
||
<div class="col-md-3">
|
||
<div class="card bg-dark border-warning h-100 shadow">
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<h6 class="text-warning text-uppercase small font-monospace mb-0">>_ ВШИТЫХ_ВЛОЖЕНИЙ</h6>
|
||
<i class="bi bi-paperclip text-warning fs-4"></i>
|
||
</div>
|
||
<h2 class="display-6 font-monospace my-2 text-white" id="statAttachments">{{ total_attachments }}</h2>
|
||
<div class="small text-muted">Интегрировано в листы PDF</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- КАРТОЧКА 4: ПОСЛЕДНЯЯ СИНХРОНИЗАЦИЯ -->
|
||
<div class="col-md-3">
|
||
<div class="card bg-dark border-primary h-100 shadow">
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<h6 class="text-primary text-uppercase small font-monospace mb-0">>_ DRIVE_SYNC</h6>
|
||
<i class="bi bi-google-play text-primary fs-4"></i>
|
||
</div>
|
||
<h2 class="display-6 font-monospace my-2 text-white" id="statLastSync">
|
||
{% if last_sync_time %}{{ last_sync_time.strftime('%d.%m.%Y') }}{% else %}-{% endif %}
|
||
</h2>
|
||
<div class="small text-muted text-truncate" id="statLastSyncTime">
|
||
Время: {{ fmt_local(last_sync_time) if last_sync_time else 'Неизвестно' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row g-4">
|
||
<!-- ЛЕВАЯ КОЛОНКА: УПРАВЛЕНИЕ И СТАТУС -->
|
||
<div class="col-md-4">
|
||
<!-- КАРТОЧКА УПРАВЛЕНИЯ -->
|
||
<div class="card mb-4 shadow">
|
||
<div class="card-header bg-dark border-bottom border-secondary text-success font-monospace">
|
||
[CONTROL] УПРАВЛЕНИЕ_АРХИВОМ
|
||
</div>
|
||
<div class="card-body text-center py-4">
|
||
{% if has_master_pdf %}
|
||
<i class="bi bi-file-earmark-pdf-fill display-2 text-danger mb-3"></i>
|
||
<h6 class="text-white">ALL_EMAILS_CONSOLIDATED.pdf</h6>
|
||
<p class="small opacity-50 mb-4">Архив готов к использованию в NotebookLM</p>
|
||
|
||
<button id="fullSyncBtn" class="btn btn-warning w-100 mb-2">
|
||
<i class="bi bi-lightning-charge-fill"></i> ТУРБО СИНХРОНИЗАЦИЯ
|
||
</button>
|
||
|
||
<div class="dropdown w-100">
|
||
<button class="btn btn-outline-secondary dropdown-toggle w-100" type="button" data-bs-toggle="dropdown">
|
||
ДОПОЛНИТЕЛЬНО
|
||
</button>
|
||
<ul class="dropdown-menu dropdown-menu-dark w-100">
|
||
<li><a href="/static/email_archive/TOTAL_ARCHIVE_2025-2026.pdf" class="dropdown-item" download>СКАЧАТЬ АРХИВ</a></li>
|
||
<li><button id="syncAiBtn" class="dropdown-item">ТОЛЬКО UPLOAD В DRIVE</button></li>
|
||
</ul>
|
||
</div>
|
||
{% else %}
|
||
<i class="bi bi-file-earmark-x display-2 opacity-20 mb-3"></i>
|
||
<p class="text-warning">Архив еще не сформирован</p>
|
||
<button id="fullSyncBtn" class="btn btn-warning w-100 mb-2">
|
||
<i class="bi bi-lightning-charge-fill"></i> ЗАПУСТИТЬ ПЕРВЫЙ СБОР
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- СТАТУС ПАЙПЛАЙНА (ПОСТОЯННЫЙ) -->
|
||
<div class="card border-primary mb-4 shadow" id="pipelineStatusCard">
|
||
<div class="card-header bg-dark border-bottom border-primary d-flex justify-content-between align-items-center text-primary font-monospace">
|
||
<span>[PIPELINE] МОНИТОРИНГ_ПРОЦЕССА</span>
|
||
<span class="spinner-grow spinner-grow-sm text-primary" id="pipelineSpinner" style="display: none;" role="status"></span>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="p-3 bg-black border border-secondary font-monospace small">
|
||
[STEP] Шаг: <span id="pipelineStep" class="text-info fw-bold">Ожидание</span> <br>
|
||
[TIME] Обновлено: <span id="pipelineTime">-</span> <br>
|
||
<div class="progress my-3" style="height: 12px; 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 fw-bold">0</span> <br>
|
||
[DATA] Всего в архиве: <span id="pipelineTotalEmails" class="fw-bold">0</span> <br>
|
||
[DATA] Размер файла: <span id="pipelineSize" class="fw-bold">0.00 MB</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ЛОГ ПОСЛЕДНЕГО ЗАПУСКА -->
|
||
<div class="card shadow">
|
||
<div class="card-header bg-dark border-bottom border-secondary text-warning font-monospace">
|
||
[LOG] ЛОГ_ПОСЛЕДНЕГО_ЗАПУСКА
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="p-3 bg-black border border-warning font-monospace small">
|
||
{% if last_log %}
|
||
[TIME] Начат: {{ fmt_local(last_log.start_time) }} <br>
|
||
[TIME] Завершен: {{ fmt_local(last_log.end_time) if last_log.end_time else 'В ПРОЦЕССЕ...' }} <br>
|
||
[STATUS] Статус: <span class="{% if last_log.status == 'success' %}text-success{% elif last_log.status == 'error' %}text-danger{% else %}text-warning{% endif %} fw-bold">{{ last_log.status.upper() }}</span> <br>
|
||
[DATA] Новых писем: {{ last_log.new_emails_count }}
|
||
{% else %}
|
||
[INFO] Запуски еще не производились.
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ПРАВАЯ КОЛОНКА: ТАБЛИЦА ВЕДОМСТВ -->
|
||
<div class="col-md-8">
|
||
<div class="card h-100 shadow">
|
||
<div class="card-header bg-dark border-bottom border-secondary text-success font-monospace">
|
||
[STATS] СТАТИСТИКА_ПО_ВЕДОМСТВАМ_И_ОРГАНАМ
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<div class="table-responsive" style="max-height: 700px; overflow-y: auto;">
|
||
<table class="table table-dark table-hover mb-0 align-middle">
|
||
<thead class="sticky-top bg-dark">
|
||
<tr>
|
||
<th>Ведомство / Домен</th>
|
||
<th class="text-center" style="width: 100px;">Вх.</th>
|
||
<th class="text-center" style="width: 100px;">Исх.</th>
|
||
<th style="width: 180px;">Последнее письмо</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for domain in audit_data %}
|
||
<tr>
|
||
<td>
|
||
<span class="text-success fw-bold">{{ domain.domain }}</span>
|
||
<br><small class="opacity-50 text-white">{{ domain.description }}</small>
|
||
</td>
|
||
<td class="text-center font-monospace text-success fw-bold">{{ domain.inbound_count }}</td>
|
||
<td class="text-center font-monospace text-info fw-bold">{{ domain.outbound_count }}</td>
|
||
<td class="font-monospace">{{ domain.last_email_date.strftime('%d.%m.%Y %H:%M') if domain.last_email_date else '-' }}</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr>
|
||
<td colspan="4" class="text-center opacity-50 py-4">Данных пока нет. Запустите экспорт.</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- СВЕЖИЕ ПОСТУПЛЕНИЯ В АРХИВ -->
|
||
<div class="row mt-4">
|
||
<div class="col-md-12">
|
||
<div class="card shadow">
|
||
<div class="card-header bg-dark border-bottom border-secondary text-info font-monospace">
|
||
[FEED] СВЕЖИЕ_ПОСТУПЛЕНИЯ_В_АРХИВ (ПОСЛЕДНИЕ 10)
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<div class="table-responsive">
|
||
<table class="table table-dark table-hover mb-0" style="font-size: 0.85rem;">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 180px;">Дата</th>
|
||
<th style="width: 200px;">Ведомство</th>
|
||
<th>Тема письма</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for email in latest_emails %}
|
||
<tr>
|
||
<td><code class="text-info font-monospace">{{ email.date }}</code></td>
|
||
<td><span class="badge bg-secondary font-monospace">{{ email.domain }}</span></td>
|
||
<td class="text-truncate text-white" style="max-width: 500px;">{{ email.subject }}</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr>
|
||
<td colspan="3" class="text-center opacity-50 py-3">Новых файлов пока нет</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SCRIPTS -->
|
||
<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');
|
||
|
||
// Виджеты статистики
|
||
const widgetTotal = document.getElementById('statTotalEmails');
|
||
const widgetInbound = document.getElementById('statInboundEmails');
|
||
const widgetOutbound = document.getElementById('statOutboundEmails');
|
||
const widgetSize = document.getElementById('statPdfSize');
|
||
const widgetAttachments = document.getElementById('statAttachments');
|
||
const widgetLastSync = document.getElementById('statLastSync');
|
||
const widgetLastSyncTime = document.getElementById('statLastSyncTime');
|
||
|
||
// Обновляем виджеты
|
||
if (widgetTotal) widgetTotal.textContent = data.total_emails || '0';
|
||
if (widgetInbound) widgetInbound.textContent = data.total_inbound || '0';
|
||
if (widgetOutbound) widgetOutbound.textContent = data.total_outbound || '0';
|
||
if (widgetSize) widgetSize.textContent = (data.pdf_size_mb ? data.pdf_size_mb.toFixed(2) : '0.00') + ' MB';
|
||
if (widgetAttachments) widgetAttachments.textContent = data.total_attachments || '0';
|
||
|
||
if (data.last_sync_time && data.last_sync_time !== '-') {
|
||
if (widgetLastSync) {
|
||
// Извлекаем только дату из YYYY-MM-DD HH:MM:SS
|
||
const parts = data.last_sync_time.split(' ');
|
||
if (parts[0]) {
|
||
const dateParts = parts[0].split('-');
|
||
widgetLastSync.textContent = dateParts[2] + '.' + dateParts[1] + '.' + dateParts[0];
|
||
}
|
||
}
|
||
if (widgetLastSyncTime) widgetLastSyncTime.textContent = 'Время: ' + data.last_sync_time;
|
||
}
|
||
|
||
// Обновляем карту пайплайна
|
||
newEmails.textContent = data.new_emails || '0';
|
||
totalEmails.textContent = data.total_emails || '0';
|
||
size.textContent = (data.pdf_size_mb ? data.pdf_size_mb.toFixed(2) : '0.00') + ' MB';
|
||
time.textContent = data.updated_at || '-';
|
||
|
||
if (data.status === 'running') {
|
||
card.className = "card border-primary mb-4 shadow";
|
||
step.className = "text-info fw-bold";
|
||
step.textContent = data.step;
|
||
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";
|
||
|
||
// Опрашиваем часто при работе
|
||
setTimeout(checkPipelineStatus, 2000);
|
||
} else if (data.status === 'success') {
|
||
card.className = "card border-success mb-4 shadow";
|
||
step.className = "text-success fw-bold";
|
||
step.innerHTML = '✅ Завершен успешно';
|
||
spinner.style.display = 'none';
|
||
progress.style.width = '100%';
|
||
progress.className = "progress-bar bg-success";
|
||
|
||
// Опрашиваем реже в простое
|
||
setTimeout(checkPipelineStatus, 15000);
|
||
} else if (data.status === 'error' || data.status === 'stuck') {
|
||
card.className = "card border-danger mb-4 shadow";
|
||
step.className = "text-danger fw-bold";
|
||
step.innerHTML = '❌ ' + (data.step || 'Ошибка выполнения');
|
||
spinner.style.display = 'none';
|
||
progress.style.width = '100%';
|
||
progress.className = "progress-bar bg-danger";
|
||
|
||
setTimeout(checkPipelineStatus, 15000);
|
||
} else {
|
||
card.className = "card border-secondary mb-4 shadow";
|
||
step.className = "text-muted";
|
||
step.textContent = "Неактивен";
|
||
spinner.style.display = 'none';
|
||
progress.style.width = '0%';
|
||
progress.className = "progress-bar bg-secondary";
|
||
|
||
setTimeout(checkPipelineStatus, 15000);
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to get status:', e);
|
||
setTimeout(checkPipelineStatus, 15000);
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', checkPipelineStatus);
|
||
|
||
document.getElementById('fullSyncBtn').addEventListener('click', async function() {
|
||
const btn = this;
|
||
if (!confirm('Запустить полный цикл: Сбор почты -> PDF -> G-Drive?')) return;
|
||
|
||
btn.disabled = true;
|
||
const oldHtml = btn.innerHTML;
|
||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> ЗАПУСКАЮ...';
|
||
|
||
try {
|
||
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 {
|
||
btn.disabled = false;
|
||
btn.innerHTML = oldHtml;
|
||
}
|
||
});
|
||
|
||
document.getElementById('syncAiBtn').addEventListener('click', async function() {
|
||
const btn = this;
|
||
if (!confirm('Отправить архив в NotebookLM для анализа?')) return;
|
||
|
||
btn.disabled = true;
|
||
const oldHtml = btn.innerHTML;
|
||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> СИНХРОНИЗИРУЮ...';
|
||
|
||
try {
|
||
const response = await fetch('/api/emails/sync_ai', { method: 'POST' });
|
||
const result = await response.json();
|
||
alert(result.message);
|
||
} catch (e) {
|
||
alert('Ошибка при синхронизации: ' + e);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerHTML = oldHtml;
|
||
}
|
||
});
|
||
</script>
|
||
{% endblock %}
|