WebPanel: Redesign Email Audit section, add stats widgets and dynamic AJAX counters
This commit is contained in:
parent
35ffe3442c
commit
ed565c54c3
4 changed files with 311 additions and 117 deletions
|
|
@ -14,6 +14,9 @@ services:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ../libs/swarmlib:/app/swarmlib:ro
|
- ../libs/swarmlib:/app/swarmlib:ro
|
||||||
- /home/matrixhasyou/domovoy_google_creds.json:/home/matrixhasyou/domovoy_google_creds.json:ro
|
- /home/matrixhasyou/domovoy_google_creds.json:/home/matrixhasyou/domovoy_google_creds.json:ro
|
||||||
|
- /home/matrixhasyou/mutt:/home/matrixhasyou/mutt:rw
|
||||||
|
- /home/matrixhasyou/domovoy_drive_sync.py:/home/matrixhasyou/domovoy_drive_sync.py:ro
|
||||||
|
- /home/matrixhasyou/.muttrc:/home/matrixhasyou/.muttrc:ro
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
|
|
@ -34,6 +37,9 @@ services:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ../libs/swarmlib:/app/swarmlib:ro
|
- ../libs/swarmlib:/app/swarmlib:ro
|
||||||
- /home/matrixhasyou/domovoy_google_creds.json:/home/matrixhasyou/domovoy_google_creds.json:ro
|
- /home/matrixhasyou/domovoy_google_creds.json:/home/matrixhasyou/domovoy_google_creds.json:ro
|
||||||
|
- /home/matrixhasyou/mutt:/home/matrixhasyou/mutt:rw
|
||||||
|
- /home/matrixhasyou/domovoy_drive_sync.py:/home/matrixhasyou/domovoy_drive_sync.py:ro
|
||||||
|
- /home/matrixhasyou/.muttrc:/home/matrixhasyou/.muttrc:ro
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,13 @@ pydub
|
||||||
psycopg2-binary>=2.9.9
|
psycopg2-binary>=2.9.9
|
||||||
asyncpg>=0.29.0
|
asyncpg>=0.29.0
|
||||||
google-generativeai
|
google-generativeai
|
||||||
|
imap-tools
|
||||||
|
weasyprint
|
||||||
|
fpdf
|
||||||
|
pymupdf
|
||||||
|
google-api-python-client
|
||||||
|
google-auth
|
||||||
|
google-auth-httplib2
|
||||||
|
google-auth-oauthlib
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
94
web/app.py
94
web/app.py
|
|
@ -691,17 +691,45 @@ async def emails_page(request: Request, username: str = Depends(get_current_admi
|
||||||
except:
|
except:
|
||||||
last_sync_time = datetime.fromtimestamp(sync_time_file.stat().st_mtime, tz=timezone.utc)
|
last_sync_time = datetime.fromtimestamp(sync_time_file.stat().st_mtime, tz=timezone.utc)
|
||||||
|
|
||||||
|
# Global stats calculation
|
||||||
|
total_inbound = sum(d.inbound_count for d in audit_data)
|
||||||
|
total_outbound = sum(d.outbound_count for d in audit_data)
|
||||||
|
total_emails = total_inbound + total_outbound
|
||||||
|
|
||||||
|
total_attachments = 0
|
||||||
|
export_log_path = Path("/home/matrixhasyou/mutt/export.log")
|
||||||
|
if export_log_path.exists():
|
||||||
|
try:
|
||||||
|
content = export_log_path.read_text(errors='ignore')
|
||||||
|
matches = re.findall(r"Has (\d+) attachment", content)
|
||||||
|
total_attachments = sum(int(m) for m in matches)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
pdf_size_mb = 0.0
|
||||||
|
if master_pdf_path.exists():
|
||||||
|
pdf_size_mb = round(master_pdf_path.stat().st_size / (1024 * 1024), 2)
|
||||||
|
|
||||||
|
last_email_date = None
|
||||||
|
if audit_data:
|
||||||
|
dates = [d.last_email_date for d in audit_data if d.last_email_date]
|
||||||
|
if dates:
|
||||||
|
last_email_date = max(dates)
|
||||||
|
|
||||||
return templates.TemplateResponse(request=request, name="emails.html", context={
|
return templates.TemplateResponse(request=request, name="emails.html", context={
|
||||||
"username": username, "audit_data": audit_data,
|
"username": username, "audit_data": audit_data,
|
||||||
"last_log": last_log, "latest_emails": latest_emails,
|
"last_log": last_log, "latest_emails": latest_emails,
|
||||||
"has_master_pdf": master_pdf_path.exists(), "last_update": last_update,
|
"has_master_pdf": master_pdf_path.exists(), "last_update": last_update,
|
||||||
"last_sync_time": last_sync_time, "fmt_local": fmt_local
|
"last_sync_time": last_sync_time, "fmt_local": fmt_local,
|
||||||
|
"total_inbound": total_inbound, "total_outbound": total_outbound,
|
||||||
|
"total_emails": total_emails, "total_attachments": total_attachments,
|
||||||
|
"pdf_size_mb": pdf_size_mb, "last_email_date": last_email_date
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.post("/api/emails/run_full_sync")
|
@app.post("/api/emails/run_full_sync")
|
||||||
async def api_emails_run_full_sync(username: str = Depends(get_current_admin)):
|
async def api_emails_run_full_sync(username: str = Depends(get_current_admin)):
|
||||||
master_script = "/home/matrixhasyou/mutt/master_ai_sync.py"
|
master_script = "/home/matrixhasyou/mutt/master_ai_sync.py"
|
||||||
python_path = "/usr/bin/python3"
|
python_path = sys.executable
|
||||||
|
|
||||||
if not os.path.exists(master_script):
|
if not os.path.exists(master_script):
|
||||||
return JSONResponse(status_code=404, content={"message": "Master sync script not found"})
|
return JSONResponse(status_code=404, content={"message": "Master sync script not found"})
|
||||||
|
|
@ -716,7 +744,7 @@ async def api_emails_run_full_sync(username: str = Depends(get_current_admin)):
|
||||||
@app.post("/api/emails/sync_ai")
|
@app.post("/api/emails/sync_ai")
|
||||||
async def api_emails_sync_ai(username: str = Depends(get_current_admin)):
|
async def api_emails_sync_ai(username: str = Depends(get_current_admin)):
|
||||||
sync_script = "/home/matrixhasyou/domovoy_drive_sync.py"
|
sync_script = "/home/matrixhasyou/domovoy_drive_sync.py"
|
||||||
python_path = "/home/matrixhasyou/domovoy_bot/venv-google/bin/python"
|
python_path = sys.executable
|
||||||
master_pdf = "/home/matrixhasyou/mutt/TOTAL_ARCHIVE_2025-2026.pdf"
|
master_pdf = "/home/matrixhasyou/mutt/TOTAL_ARCHIVE_2025-2026.pdf"
|
||||||
|
|
||||||
if not os.path.exists(sync_script):
|
if not os.path.exists(sync_script):
|
||||||
|
|
@ -733,18 +761,68 @@ async def api_emails_sync_ai(username: str = Depends(get_current_admin)):
|
||||||
async def api_emails_pipeline_status(username: str = Depends(get_current_admin)):
|
async def api_emails_pipeline_status(username: str = Depends(get_current_admin)):
|
||||||
import time
|
import time
|
||||||
status_file = Path("/home/matrixhasyou/mutt/pipeline_status.json")
|
status_file = Path("/home/matrixhasyou/mutt/pipeline_status.json")
|
||||||
if not status_file.exists():
|
master_pdf_path = Path("/home/matrixhasyou/mutt/ALL_EMAILS_CONSOLIDATED.pdf")
|
||||||
return JSONResponse(content={"status": "idle", "step": "Неактивен", "updated_at": "-", "new_emails": 0, "total_emails": 0, "pdf_size_mb": 0.0})
|
|
||||||
|
# Calculate fallback values
|
||||||
|
pdf_size_mb = 0.0
|
||||||
|
if master_pdf_path.exists():
|
||||||
|
pdf_size_mb = round(master_pdf_path.stat().st_size / (1024 * 1024), 2)
|
||||||
|
|
||||||
|
total_emails = 0
|
||||||
|
total_inbound = 0
|
||||||
|
total_outbound = 0
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
audit_data = (await session.execute(select(EmailAudit))).scalars().all()
|
||||||
|
total_inbound = sum(d.inbound_count for d in audit_data)
|
||||||
|
total_outbound = sum(d.outbound_count for d in audit_data)
|
||||||
|
total_emails = total_inbound + total_outbound
|
||||||
|
|
||||||
|
total_attachments = 0
|
||||||
|
export_log_path = Path("/home/matrixhasyou/mutt/export.log")
|
||||||
|
if export_log_path.exists():
|
||||||
|
try:
|
||||||
|
content = export_log_path.read_text(errors='ignore')
|
||||||
|
matches = re.findall(r"Has (\d+) attachment", content)
|
||||||
|
total_attachments = sum(int(m) for m in matches)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sync_time_file = Path("/home/matrixhasyou/mutt/last_ai_sync.txt")
|
||||||
|
last_sync_time = "-"
|
||||||
|
if sync_time_file.exists():
|
||||||
|
try:
|
||||||
|
last_sync_time = sync_time_file.read_text().strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"status": "idle",
|
||||||
|
"step": "Неактивен",
|
||||||
|
"updated_at": "-",
|
||||||
|
"new_emails": 0,
|
||||||
|
"total_emails": total_emails,
|
||||||
|
"total_inbound": total_inbound,
|
||||||
|
"total_outbound": total_outbound,
|
||||||
|
"total_attachments": total_attachments,
|
||||||
|
"pdf_size_mb": pdf_size_mb,
|
||||||
|
"last_sync_time": last_sync_time
|
||||||
|
}
|
||||||
|
|
||||||
|
if status_file.exists():
|
||||||
try:
|
try:
|
||||||
with open(status_file) as f:
|
with open(status_file) as f:
|
||||||
data = json.load(f)
|
file_data = json.load(f)
|
||||||
|
data.update(file_data)
|
||||||
|
|
||||||
mtime = status_file.stat().st_mtime
|
mtime = status_file.stat().st_mtime
|
||||||
if data.get("status") == "running" and (time.time() - mtime > 600):
|
if data.get("status") == "running" and (time.time() - mtime > 600):
|
||||||
data["status"] = "stuck"
|
data["status"] = "stuck"
|
||||||
data["step"] = "Процесс завис или был остановлен"
|
data["step"] = "Процесс завис или был остановлен"
|
||||||
return JSONResponse(content=data)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse(content={"status": "error", "message": str(e)})
|
data["status"] = "error"
|
||||||
|
data["message"] = str(e)
|
||||||
|
|
||||||
|
return JSONResponse(content=data)
|
||||||
|
|
||||||
@app.get("/infra", response_class=HTMLResponse)
|
@app.get("/infra", response_class=HTMLResponse)
|
||||||
async def get_infra_page(request: Request, username: str = Depends(get_current_admin)):
|
async def get_infra_page(request: Request, username: str = Depends(get_current_admin)):
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
{% block nav_emails %}active{% endblock %}
|
{% block nav_emails %}active{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<!-- ЗАГОЛОВОК -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<h2 style="color: var(--matrix-green); text-shadow: 0 0 10px var(--matrix-green);">
|
<h2 style="color: var(--matrix-green); text-shadow: 0 0 10px var(--matrix-green);">
|
||||||
|
|
@ -15,17 +16,84 @@
|
||||||
</div>
|
</div>
|
||||||
</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="row g-4">
|
||||||
<!-- СТАТУС АРХИВА -->
|
<!-- ЛЕВАЯ КОЛОНКА: УПРАВЛЕНИЕ И СТАТУС -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card h-100">
|
<!-- КАРТОЧКА УПРАВЛЕНИЯ -->
|
||||||
<div class="card-header">АРХИВ_PDF (NOTEBOOK_LM)</div>
|
<div class="card mb-4 shadow">
|
||||||
<div class="card-body text-center d-flex flex-column justify-content-center">
|
<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 %}
|
{% if has_master_pdf %}
|
||||||
<i class="bi bi-file-earmark-pdf-fill display-1 text-danger mb-3"></i>
|
<i class="bi bi-file-earmark-pdf-fill display-2 text-danger mb-3"></i>
|
||||||
<h5>ALL_EMAILS_CONSOLIDATED.pdf</h5>
|
<h6 class="text-white">ALL_EMAILS_CONSOLIDATED.pdf</h6>
|
||||||
<p class="small opacity-75"><b>Файл создан:</b> {{ fmt_local(last_update) }}</p>
|
<p class="small opacity-50 mb-4">Архив готов к использованию в NotebookLM</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">
|
<button id="fullSyncBtn" class="btn btn-warning w-100 mb-2">
|
||||||
<i class="bi bi-lightning-charge-fill"></i> ТУРБО СИНХРОНИЗАЦИЯ
|
<i class="bi bi-lightning-charge-fill"></i> ТУРБО СИНХРОНИЗАЦИЯ
|
||||||
|
|
@ -41,39 +109,82 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-file-earmark-x display-1 opacity-20 mb-3"></i>
|
<i class="bi bi-file-earmark-x display-2 opacity-20 mb-3"></i>
|
||||||
<p>Архив еще не сформирован</p>
|
<p class="text-warning">Архив еще не сформирован</p>
|
||||||
<button class="btn btn-outline-secondary w-100" disabled>НЕДОСТУПНО</button>
|
<button id="fullSyncBtn" class="btn btn-warning w-100 mb-2">
|
||||||
|
<i class="bi bi-lightning-charge-fill"></i> ЗАПУСТИТЬ ПЕРВЫЙ СБОР
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<!-- ПОСЛЕДНЯЯ АКТИВНОСТЬ -->
|
<!-- ЛОГ ПОСЛЕДНЕГО ЗАПУСКА -->
|
||||||
|
<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="col-md-8">
|
||||||
<div class="card h-100">
|
<div class="card h-100 shadow">
|
||||||
<div class="card-header">СТАТИСТИКА_ВЕДОМСТВ</div>
|
<div class="card-header bg-dark border-bottom border-secondary text-success font-monospace">
|
||||||
|
[STATS] СТАТИСТИКА_ПО_ВЕДОМСТВАМ_И_ОРГАНАМ
|
||||||
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive" style="max-height: 700px; overflow-y: auto;">
|
||||||
<table class="table table-dark table-hover mb-0">
|
<table class="table table-dark table-hover mb-0 align-middle">
|
||||||
<thead>
|
<thead class="sticky-top bg-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Ведомство / Домен</th>
|
<th>Ведомство / Домен</th>
|
||||||
<th class="text-center">Вх.</th>
|
<th class="text-center" style="width: 100px;">Вх.</th>
|
||||||
<th class="text-center">Исх.</th>
|
<th class="text-center" style="width: 100px;">Исх.</th>
|
||||||
<th>Последнее письмо</th>
|
<th style="width: 180px;">Последнее письмо</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for domain in audit_data %}
|
{% for domain in audit_data %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="text-success">{{ domain.domain }}</span>
|
<span class="text-success fw-bold">{{ domain.domain }}</span>
|
||||||
<br><small class="opacity-50">{{ domain.description }}</small>
|
<br><small class="opacity-50 text-white">{{ domain.description }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ domain.inbound_count }}</td>
|
<td class="text-center font-monospace text-success fw-bold">{{ domain.inbound_count }}</td>
|
||||||
<td class="text-center">{{ domain.outbound_count }}</td>
|
<td class="text-center font-monospace text-info fw-bold">{{ domain.outbound_count }}</td>
|
||||||
<td>{{ domain.last_email_date.strftime('%d.%m.%Y') if domain.last_email_date else '-' }}</td>
|
<td class="font-monospace">{{ domain.last_email_date.strftime('%d.%m.%Y %H:%M') if domain.last_email_date else '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -88,27 +199,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- СВЕЖИЕ ПОСТУПЛЕНИЯ В АРХИВ -->
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<!-- СВЕЖИЕ ПОСТУПЛЕНИЯ -->
|
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card">
|
<div class="card shadow">
|
||||||
<div class="card-header">СВЕЖИЕ_ПОСТУПЛЕНИЯ_В_АРХИВ (ПОСЛЕДНИЕ 10)</div>
|
<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="card-body p-0">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-dark table-hover mb-0" style="font-size: 0.85rem;">
|
<table class="table table-dark table-hover mb-0" style="font-size: 0.85rem;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Дата</th>
|
<th style="width: 180px;">Дата</th>
|
||||||
<th>Ведомство</th>
|
<th style="width: 200px;">Ведомство</th>
|
||||||
<th>Тема письма</th>
|
<th>Тема письма</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for email in latest_emails %}
|
{% for email in latest_emails %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><code class="text-info">{{ email.date }}</code></td>
|
<td><code class="text-info font-monospace">{{ email.date }}</code></td>
|
||||||
<td><span class="badge bg-secondary">{{ email.domain }}</span></td>
|
<td><span class="badge bg-secondary font-monospace">{{ email.domain }}</span></td>
|
||||||
<td class="text-truncate" style="max-width: 500px;">{{ email.subject }}</td>
|
<td class="text-truncate text-white" style="max-width: 500px;">{{ email.subject }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -123,49 +236,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<!-- SCRIPTS -->
|
||||||
<!-- КАРТОЧКА ДИНАМИЧЕСКОГО СТАТУСА ПАЙПЛАЙНА -->
|
|
||||||
<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>
|
<script>
|
||||||
async function checkPipelineStatus() {
|
async function checkPipelineStatus() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -181,12 +252,44 @@ async function checkPipelineStatus() {
|
||||||
const size = document.getElementById('pipelineSize');
|
const size = document.getElementById('pipelineSize');
|
||||||
const spinner = document.getElementById('pipelineSpinner');
|
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') {
|
if (data.status === 'running') {
|
||||||
card.style.display = 'block';
|
card.className = "card border-primary mb-4 shadow";
|
||||||
card.className = "card border-primary h-100";
|
step.className = "text-info fw-bold";
|
||||||
step.className = "text-info";
|
|
||||||
step.textContent = data.step;
|
step.textContent = data.step;
|
||||||
time.textContent = data.updated_at;
|
|
||||||
spinner.style.display = 'inline-block';
|
spinner.style.display = 'inline-block';
|
||||||
|
|
||||||
// Динамический прогресс
|
// Динамический прогресс
|
||||||
|
|
@ -197,42 +300,40 @@ async function checkPipelineStatus() {
|
||||||
progress.style.width = width;
|
progress.style.width = width;
|
||||||
progress.className = "progress-bar progress-bar-striped progress-bar-animated bg-primary";
|
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);
|
setTimeout(checkPipelineStatus, 2000);
|
||||||
} else if (data.status === 'success') {
|
} else if (data.status === 'success') {
|
||||||
card.style.display = 'block';
|
card.className = "card border-success mb-4 shadow";
|
||||||
card.className = "card border-success h-100";
|
step.className = "text-success fw-bold";
|
||||||
step.className = "text-success";
|
|
||||||
step.innerHTML = '✅ Завершен успешно';
|
step.innerHTML = '✅ Завершен успешно';
|
||||||
time.textContent = data.updated_at;
|
|
||||||
spinner.style.display = 'none';
|
spinner.style.display = 'none';
|
||||||
progress.style.width = '100%';
|
progress.style.width = '100%';
|
||||||
progress.className = "progress-bar bg-success";
|
progress.className = "progress-bar bg-success";
|
||||||
|
|
||||||
newEmails.textContent = data.new_emails;
|
// Опрашиваем реже в простое
|
||||||
totalEmails.textContent = data.total_emails;
|
setTimeout(checkPipelineStatus, 15000);
|
||||||
size.textContent = data.pdf_size_mb + ' MB';
|
|
||||||
|
|
||||||
// Скроем статус через 10 секунд
|
|
||||||
setTimeout(() => { card.style.display = 'none'; }, 10000);
|
|
||||||
} else if (data.status === 'error' || data.status === 'stuck') {
|
} else if (data.status === 'error' || data.status === 'stuck') {
|
||||||
card.style.display = 'block';
|
card.className = "card border-danger mb-4 shadow";
|
||||||
card.className = "card border-danger h-100";
|
step.className = "text-danger fw-bold";
|
||||||
step.className = "text-danger";
|
step.innerHTML = '❌ ' + (data.step || 'Ошибка выполнения');
|
||||||
step.innerHTML = '❌ ' + data.step;
|
|
||||||
time.textContent = data.updated_at;
|
|
||||||
spinner.style.display = 'none';
|
spinner.style.display = 'none';
|
||||||
progress.style.width = '100%';
|
progress.style.width = '100%';
|
||||||
progress.className = "progress-bar bg-danger";
|
progress.className = "progress-bar bg-danger";
|
||||||
|
|
||||||
|
setTimeout(checkPipelineStatus, 15000);
|
||||||
} else {
|
} else {
|
||||||
card.style.display = 'none';
|
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) {
|
} catch (e) {
|
||||||
console.error('Failed to get status:', e);
|
console.error('Failed to get status:', e);
|
||||||
|
setTimeout(checkPipelineStatus, 15000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue