domovoy_bot/web/templates/emails.html

384 lines
20 KiB
HTML
Raw Permalink 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.

{% 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 %}