397 lines
19 KiB
HTML
397 lines
19 KiB
HTML
{% 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 %}
|