283 lines
13 KiB
HTML
283 lines
13 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-4">
|
||
<!-- СТАТУС АРХИВА -->
|
||
<div class="col-md-4">
|
||
<div class="card h-100">
|
||
<div class="card-header">АРХИВ_PDF (NOTEBOOK_LM)</div>
|
||
<div class="card-body text-center d-flex flex-column justify-content-center">
|
||
{% if has_master_pdf %}
|
||
<i class="bi bi-file-earmark-pdf-fill display-1 text-danger mb-3"></i>
|
||
<h5>ALL_EMAILS_CONSOLIDATED.pdf</h5>
|
||
<p class="small opacity-75"><b>Файл создан:</b> {{ fmt_local(last_update) }}</p>
|
||
<p class="small opacity-75"><b>G-Drive Sync:</b> {{ fmt_local(last_sync_time) if last_sync_time else 'НЕИЗВЕСТНО' }}</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-1 opacity-20 mb-3"></i>
|
||
<p>Архив еще не сформирован</p>
|
||
<button class="btn btn-outline-secondary w-100" disabled>НЕДОСТУПНО</button>
|
||
{% endif %}
|
||
</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">
|
||
<table class="table table-dark table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Ведомство / Домен</th>
|
||
<th class="text-center">Вх.</th>
|
||
<th class="text-center">Исх.</th>
|
||
<th>Последнее письмо</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for domain in audit_data %}
|
||
<tr>
|
||
<td>
|
||
<span class="text-success">{{ domain.domain }}</span>
|
||
<br><small class="opacity-50">{{ domain.description }}</small>
|
||
</td>
|
||
<td class="text-center">{{ domain.inbound_count }}</td>
|
||
<td class="text-center">{{ domain.outbound_count }}</td>
|
||
<td>{{ domain.last_email_date.strftime('%d.%m.%Y') 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">
|
||
<div class="card-header">СВЕЖИЕ_ПОСТУПЛЕНИЯ_В_АРХИВ (ПОСЛЕДНИЕ 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>Дата</th>
|
||
<th>Ведомство</th>
|
||
<th>Тема письма</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for email in latest_emails %}
|
||
<tr>
|
||
<td><code class="text-info">{{ email.date }}</code></td>
|
||
<td><span class="badge bg-secondary">{{ email.domain }}</span></td>
|
||
<td class="text-truncate" 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>
|
||
|
||
<div class="row mt-4">
|
||
<!-- КАРТОЧКА ДИНАМИЧЕСКОГО СТАТУСА ПАЙПЛАЙНА -->
|
||
<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-body">
|
||
<div class="p-3 bg-black border border-success 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{% else %}text-warning{% endif %}">{{ last_log.status.upper() }}</span> <br>
|
||
[DATA] Новых писем: {{ last_log.new_emails_count }}
|
||
{% else %}
|
||
[INFO] Запуски еще не производились.
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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;
|
||
|
||
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 %}
|