domovoy_bot/web/templates/emails.html

397 lines
19 KiB
HTML
Raw 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 %}Реестр писем и аудит - МКД ЛенКом 37{% endblock %}
{% block nav_emails %}active{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h4 class="mb-1" style="font-size: 16px; font-weight: bold;">Реестр официальной переписки и судебный аудит</h4>
<div class="small text-muted">Архив писем, обращений и ответов надзорных органов дома № 37</div>
</div>
<div>
<span class="badge-status badge-success">CRON: ACTIVE (04:00)</span>
</div>
</div>
<!-- МИНИ-КАРТОЧКИ АНАЛИТИКИ -->
<div class="row g-3 mb-3">
<div class="col-md-3">
<div class="card h-100">
<div class="card-header d-flex justify-content-between">
<span>ВСЕГО ПИСЕМ</span>
<i class="bi bi-envelope-open"></i>
</div>
<div class="card-body">
<div style="font-size: 22px; font-weight: bold;" id="statTotalEmails">{{ total_emails }}</div>
<div class="small text-muted mt-1">
Входящие: <b id="statInboundEmails">{{ total_inbound }}</b> / Исходящие: <b id="statOutboundEmails">{{ total_outbound }}</b>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-header d-flex justify-content-between">
<span>ОБЪЕМ АРХИВА</span>
<i class="bi bi-file-earmark-pdf"></i>
</div>
<div class="card-body">
<div style="font-size: 22px; font-weight: bold;" id="statPdfSize">{{ pdf_size_mb }} MB</div>
<div class="small text-muted mt-1 text-truncate" id="statLastUpdate">
Создан: {{ fmt_local(last_update) if last_update else '-' }}
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-header d-flex justify-content-between">
<span>ВЛОЖЕНИЯ</span>
<i class="bi bi-paperclip"></i>
</div>
<div class="card-body">
<div style="font-size: 22px; font-weight: bold;" id="statAttachments">{{ total_attachments }}</div>
<div class="small text-muted mt-1">Интегрировано в PDF</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-header d-flex justify-content-between">
<span>DRIVE СИНХРОНИЗАЦИЯ</span>
<i class="bi bi-cloud-check"></i>
</div>
<div class="card-body">
<div style="font-size: 22px; font-weight: bold;" id="statLastSync">
{% if last_sync_time %}{{ last_sync_time.strftime('%d.%m.%Y') }}{% else %}-{% endif %}
</div>
<div class="small text-muted mt-1 text-truncate" id="statLastSyncTime">
Время: {{ fmt_local(last_sync_time) if last_sync_time else 'Неизвестно' }}
</div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<!-- ЛЕВАЯ КОЛОНКА -->
<div class="col-md-4">
<!-- КАРТОЧКА УПРАВЛЕНИЯ -->
<div class="card mb-3">
<div class="card-header">УПРАВЛЕНИЕ АРХИВОМ</div>
<div class="card-body text-center py-3">
{% if has_master_pdf %}
<div class="mb-2">
<i class="bi bi-file-earmark-pdf text-danger" style="font-size: 36px;"></i>
</div>
<div style="font-weight: bold;">ALL_EMAILS_CONSOLIDATED.pdf</div>
<div class="small text-muted mb-3">Архив готов к работе в NotebookLM</div>
<button id="fullSyncBtn" class="btn-unix btn-unix-primary w-100 mb-2">
<i class="bi bi-arrow-repeat"></i> Запустить синхронизацию
</button>
<div class="d-flex gap-2">
<a href="/static/email_archive/TOTAL_ARCHIVE_2025-2026.pdf" class="btn-unix w-50 text-center" download>Скачать PDF</a>
<button id="syncAiBtn" class="btn-unix w-50">В Google Drive</button>
</div>
{% else %}
<div class="mb-2">
<i class="bi bi-file-earmark-x text-muted" style="font-size: 36px;"></i>
</div>
<div class="small text-muted mb-3">Архив еще не сформирован</div>
<button id="fullSyncBtn" class="btn-unix btn-unix-primary w-100">
<i class="bi bi-arrow-repeat"></i> Запустить первый сбор
</button>
{% endif %}
</div>
</div>
<!-- СТАТУС ПАЙПЛАЙНА -->
<div class="card mb-3" id="pipelineStatusCard">
<div class="card-header d-flex justify-content-between align-items-center">
<span>МОНИТОРИНГ ПРОЦЕССА</span>
<span class="spinner-border spinner-border-sm" id="pipelineSpinner" style="display: none;" role="status"></span>
</div>
<div class="card-body">
<table class="table mb-0">
<tbody>
<tr>
<td style="width: 120px; font-weight: bold;">Шаг:</td>
<td><span id="pipelineStep">Ожидание</span></td>
</tr>
<tr>
<td style="font-weight: bold;">Обновлено:</td>
<td><span id="pipelineTime">-</span></td>
</tr>
<tr>
<td style="font-weight: bold;">Прогресс:</td>
<td>
<div class="progress" style="height: 10px; background-color: #e8e8e8; border-radius: 0;">
<div id="pipelineProgress" class="progress-bar bg-primary" role="progressbar" style="width: 0%; border-radius: 0;"></div>
</div>
</td>
</tr>
<tr>
<td style="font-weight: bold;">Новых писем:</td>
<td><span id="pipelineNewEmails">0</span></td>
</tr>
<tr>
<td style="font-weight: bold;">В архиве:</td>
<td><span id="pipelineTotalEmails">0</span></td>
</tr>
<tr>
<td style="font-weight: bold;">Размер:</td>
<td><span id="pipelineSize">0.00 MB</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ЛОГ ПОСЛЕДНЕГО ЗАПУСКА -->
<div class="card">
<div class="card-header">ПОСЛЕДНИЙ ЗАПУСК</div>
<div class="card-body p-2">
<table class="table mb-0">
<tbody>
{% if last_log %}
<tr>
<td style="font-weight: bold;">Начат:</td>
<td>{{ fmt_local(last_log.start_time) }}</td>
</tr>
<tr>
<td style="font-weight: bold;">Завершен:</td>
<td>{{ fmt_local(last_log.end_time) if last_log.end_time else 'В процессе...' }}</td>
</tr>
<tr>
<td style="font-weight: bold;">Статус:</td>
<td>
<span class="badge-status {% if last_log.status == 'success' %}badge-success{% elif last_log.status == 'error' %}badge-danger{% else %}badge-warning{% endif %}">
{{ last_log.status.upper() }}
</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">Новых писем:</td>
<td>{{ last_log.new_emails_count }}</td>
</tr>
{% else %}
<tr>
<td colspan="2" class="text-muted text-center py-2">Запуски еще не производились</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<!-- ПРАВАЯ КОЛОНКА: ТАБЛИЦА ВЕДОМСТВ -->
<div class="col-md-8">
<div class="card h-100">
<div class="card-header">СТАТИСТИКА ПО ОРГАНАМ И ВЕДОМСТВАМ</div>
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 650px; overflow-y: auto;">
<table class="table mb-0">
<thead style="position: sticky; top: 0; z-index: 1;">
<tr>
<th>Ведомство / Домен</th>
<th class="text-center" style="width: 80px;">Вх.</th>
<th class="text-center" style="width: 80px;">Исх.</th>
<th style="width: 170px;">Последнее письмо</th>
</tr>
</thead>
<tbody>
{% for domain in audit_data %}
<tr>
<td>
<b>{{ domain.domain }}</b>
<div class="small text-muted">{{ domain.description }}</div>
</td>
<td class="text-center"><b>{{ domain.inbound_count }}</b></td>
<td class="text-center"><b>{{ domain.outbound_count }}</b></td>
<td>{{ 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 text-muted py-3">Данных пока нет. Запустите экспорт.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- СВЕЖИЕ ПОСТУПЛЕНИЯ В АРХИВ -->
<div class="row mt-3">
<div class="col-md-12">
<div class="card">
<div class="card-header">СВЕЖИЕ ПОСТУПЛЕНИЯ В АРХИВ (ПОСЛЕДНИЕ 10)</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table mb-0">
<thead>
<tr>
<th style="width: 170px;">Дата</th>
<th style="width: 220px;">Ведомство</th>
<th>Тема письма</th>
</tr>
</thead>
<tbody>
{% for email in latest_emails %}
<tr>
<td><code>{{ email.date }}</code></td>
<td><span class="badge-status">{{ email.domain }}</span></td>
<td class="text-truncate" style="max-width: 550px;">{{ email.subject }}</td>
</tr>
{% else %}
<tr>
<td colspan="3" class="text-center text-muted 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) {
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') {
step.innerHTML = '<b>' + data.step + '</b>';
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') {
step.innerHTML = '<span class="badge-status badge-success">Завершен успешно</span>';
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') {
step.innerHTML = '<span class="badge-status badge-danger">' + (data.step || 'Ошибка') + '</span>';
spinner.style.display = 'none';
progress.style.width = '100%';
progress.className = "progress-bar bg-danger";
setTimeout(checkPipelineStatus, 15000);
} else {
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 %}