481 lines
25 KiB
Python
481 lines
25 KiB
Python
"""
|
||
Web Admin Interface v4.5 REBORN - MEGA ADMIN PANEL
|
||
Полноценное управление ВСЕМ функционалом бота:
|
||
- Dashboard, Users, Verification, Phones, Ads, Polls, Events, Schedules
|
||
- Smart Broadcasts (с отслеживанием прочтения)
|
||
- Scheduled Posts (с фото/документами)
|
||
- Weekly Digests (v3.1)
|
||
- Chat Exporter (JSON + Gitea Push)
|
||
- Email Auditor (аудит переписки)
|
||
"""
|
||
import logging
|
||
import json
|
||
import aiohttp
|
||
import re
|
||
import os
|
||
import sys
|
||
import secrets
|
||
import subprocess
|
||
from pathlib import Path
|
||
from datetime import datetime, timedelta, timezone
|
||
|
||
# ГАРАНТИРУЕМ, что корень проекта в пути поиска модулей
|
||
BASE_DIR = Path(__file__).parent.parent
|
||
if str(BASE_DIR) not in sys.path:
|
||
sys.path.insert(0, str(BASE_DIR))
|
||
|
||
from fastapi import FastAPI, Request, Depends, HTTPException, status, Form, File, UploadFile, Query
|
||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||
from fastapi.staticfiles import StaticFiles
|
||
from fastapi.templating import Jinja2Templates
|
||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, FileResponse
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, func, update, delete, desc, text
|
||
|
||
import config
|
||
from database.db import AsyncSessionLocal
|
||
from database.models import (
|
||
User, Message as DBMessage, Poll, Ad, PaymentReminder, Schedule,
|
||
VerificationRequest, Service, Announcement, Event, ScheduledPost,
|
||
Broadcast, BroadcastRead, Digest, InitiativeGroup, EmailAudit, EmailExportLog,
|
||
ProfileHistory, Achievement, Thank, SpyLog
|
||
)
|
||
|
||
# ===== ЧАСОВОЙ ПОЯС УЛЬЯНОВСК (UTC+4) =====
|
||
ULY_TZ = timezone(timedelta(hours=4))
|
||
|
||
def to_local(dt) -> datetime:
|
||
if dt is None: return None
|
||
if isinstance(dt, str):
|
||
try:
|
||
dt = datetime.fromisoformat(dt.replace(' ', 'T'))
|
||
except: return None
|
||
if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc)
|
||
return dt.astimezone(ULY_TZ)
|
||
|
||
def fmt_local(dt, fmt: str = '%d.%m.%Y %H:%M') -> str:
|
||
local_dt = to_local(dt)
|
||
return local_dt.strftime(fmt) if local_dt else '-'
|
||
|
||
logger = logging.getLogger(__name__)
|
||
logger.info(f"!!! [STARTUP] Unified Web App loaded. PID: {os.getpid()}")
|
||
|
||
app = FastAPI(title="LKM37 MEGA Admin Panel", version="4.5.0")
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
templates = Jinja2Templates(directory=str(BASE_DIR / "web" / "templates"))
|
||
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "web" / "static")), name="static")
|
||
app.mount("/static/service_images", StaticFiles(directory=str(BASE_DIR / "data" / "service_images")), name="service_images")
|
||
app.mount("/static/email_archive", StaticFiles(directory=str(BASE_DIR / "services" / "email_auditor" / "exported_emails")), name="email_archive")
|
||
|
||
security = HTTPBasic()
|
||
|
||
def get_current_admin(credentials: HTTPBasicCredentials = Depends(security)):
|
||
correct_username = secrets.compare_digest(credentials.username, config.WEB_ADMIN_LOGIN)
|
||
correct_password = secrets.compare_digest(credentials.password, config.WEB_ADMIN_PASSWORD)
|
||
if not (correct_username and correct_password):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Incorrect login or password",
|
||
headers={"WWW-Authenticate": "Basic"},
|
||
)
|
||
return credentials.username
|
||
|
||
# ============================================================================
|
||
# ГЛАВНАЯ (DASHBOARD)
|
||
# ============================================================================
|
||
|
||
@app.get("/", response_class=HTMLResponse)
|
||
async def dashboard(request: Request, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
users_count = (await session.execute(select(func.count(User.user_id)))).scalar()
|
||
verified_count = (await session.execute(select(func.count(User.user_id)).where(User.verified == True))).scalar()
|
||
messages_count = (await session.execute(select(func.count(DBMessage.id)))).scalar()
|
||
active_ads = (await session.execute(select(func.count(Ad.ad_id)).where(Ad.is_active == True))).scalar()
|
||
active_polls = (await session.execute(select(func.count(Poll.poll_id)).where(Poll.is_active == True))).scalar()
|
||
upcoming_events = (await session.execute(select(func.count(Event.id)).where(Event.is_active == True))).scalar()
|
||
pending_count = (await session.execute(select(func.count(VerificationRequest.id)).where(VerificationRequest.status == 'pending'))).scalar()
|
||
|
||
return templates.TemplateResponse("dashboard.html", {
|
||
"request": request, "username": username,
|
||
"stats": {
|
||
"users": users_count, "verified": verified_count,
|
||
"messages": messages_count, "ads": active_ads,
|
||
"polls": active_polls, "events": upcoming_events
|
||
},
|
||
"pending_count": pending_count
|
||
})
|
||
|
||
# ============================================================================
|
||
# ПОЛЬЗОВАТЕЛИ
|
||
# ============================================================================
|
||
|
||
@app.get("/users", response_class=HTMLResponse)
|
||
async def users_page(request: Request, search: str = "", filter: str = "", username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(User)
|
||
if filter == 'verified': stmt = stmt.where(User.verified == True)
|
||
elif filter == 'unverified': stmt = stmt.where(User.verified == False)
|
||
elif filter == 'active_unverified': stmt = stmt.where(User.verified == False, User.message_count >= 50)
|
||
elif filter == 'ig':
|
||
ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True)
|
||
ig_user_ids = [row[0] for row in (await session.execute(ig_stmt)).all()]
|
||
stmt = stmt.where(User.user_id.in_(ig_user_ids))
|
||
|
||
if search:
|
||
stmt = stmt.where(
|
||
(User.first_name.ilike(f"%{search}%")) |
|
||
(User.last_name.ilike(f"%{search}%")) |
|
||
(User.username.ilike(f"%{search}%")) |
|
||
(User.apartment.ilike(f"%{search}%"))
|
||
)
|
||
|
||
stmt = stmt.order_by(User.user_id.desc()).limit(100)
|
||
users = list((await session.execute(stmt)).scalars().all())
|
||
|
||
ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True)
|
||
ig_user_ids = set(row[0] for row in (await session.execute(ig_stmt)).all())
|
||
|
||
pending_stmt = select(VerificationRequest).where(VerificationRequest.status == 'pending').order_by(VerificationRequest.created_at.desc())
|
||
pending_requests = (await session.execute(pending_stmt)).scalars().all()
|
||
|
||
total_count = (await session.execute(select(func.count(User.user_id)))).scalar()
|
||
verified_count = (await session.execute(select(func.count(User.user_id)).where(User.verified == True))).scalar()
|
||
active_unverified = (await session.execute(select(func.count(User.user_id)).where(User.verified == False, User.message_count >= 50))).scalar()
|
||
|
||
return templates.TemplateResponse("users.html", {
|
||
"request": request, "username": username, "users": users, "search": search, "current_filter": filter,
|
||
"stats": {"total": total_count, "verified": verified_count, "unverified": total_count - verified_count, "active_unverified": active_unverified, "ig": len(ig_user_ids)},
|
||
"ig_user_ids": ig_user_ids, "pending_count": len(list(pending_requests))
|
||
})
|
||
|
||
@app.get("/api/user/{user_id}")
|
||
async def api_get_user(user_id: int, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
user = await session.get(User, user_id)
|
||
if not user: return JSONResponse({"error": "Not found"}, status_code=404)
|
||
return JSONResponse({"user": {"id": user.user_id, "username": user.username, "first_name": user.first_name, "last_name": user.last_name, "apartment": user.apartment, "phone": user.phone, "verified": user.verified, "rating": user.rating, "message_count": user.message_count, "is_banned": user.is_banned}})
|
||
|
||
@app.post("/api/user/{user_id}/update")
|
||
async def api_update_user(user_id: int, request: Request, username: str = Depends(get_current_admin)):
|
||
data = await request.json()
|
||
async with AsyncSessionLocal() as session:
|
||
update_data = {}
|
||
if 'apartment' in data: update_data['apartment'] = data['apartment'].upper()
|
||
if 'phone' in data: update_data['phone'] = data['phone']
|
||
if 'verified' in data:
|
||
update_data['verified'] = bool(data['verified'])
|
||
if data['verified']: update_data['verification_date'] = datetime.utcnow()
|
||
if 'rating' in data: update_data['rating'] = int(data['rating'])
|
||
if update_data:
|
||
await session.execute(update(User).where(User.user_id == user_id).values(**update_data))
|
||
await session.commit()
|
||
return JSONResponse({"success": True})
|
||
|
||
@app.post("/api/user/{user_id}/verify")
|
||
async def api_verify_user(user_id: int, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
await session.execute(update(User).where(User.user_id == user_id).values(verified=True, verification_date=datetime.utcnow()))
|
||
await session.commit()
|
||
return JSONResponse({"success": True})
|
||
|
||
@app.post("/api/user/{user_id}/unverify")
|
||
async def api_unverify_user(user_id: int, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
await session.execute(update(User).where(User.user_id == user_id).values(verified=False))
|
||
await session.commit()
|
||
return JSONResponse({"success": True})
|
||
|
||
# ============================================================================
|
||
# ВЕРИФИКАЦИЯ
|
||
# ============================================================================
|
||
|
||
@app.get("/verification", response_class=HTMLResponse)
|
||
async def verification_page(request: Request, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
pending = (await session.execute(select(VerificationRequest).where(VerificationRequest.status == 'pending').order_by(VerificationRequest.created_at.desc()))).scalars().all()
|
||
unverified = (await session.execute(select(User).where(User.verified == False).order_by(User.join_date.desc()).limit(50))).scalars().all()
|
||
return templates.TemplateResponse("verification.html", {"request": request, "username": username, "pending_requests": pending, "unverified_users": unverified})
|
||
|
||
@app.post("/api/verification/{req_id}/approve")
|
||
async def api_approve_verification(req_id: int, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
req = (await session.execute(select(VerificationRequest).where(VerificationRequest.id == req_id))).scalar_one_or_none()
|
||
if req:
|
||
req.status = 'approved'
|
||
req.reviewed_at = datetime.utcnow()
|
||
await session.execute(update(User).where(User.user_id == req.user_id).values(verified=True, apartment=req.apartment, verification_date=datetime.utcnow()))
|
||
await session.commit()
|
||
return JSONResponse({"success": True})
|
||
|
||
@app.post("/api/verification/{req_id}/reject")
|
||
async def api_reject_verification(req_id: int, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
req = (await session.execute(select(VerificationRequest).where(VerificationRequest.id == req_id))).scalar_one_or_none()
|
||
if req:
|
||
req.status = 'rejected'
|
||
req.reviewed_at = datetime.utcnow()
|
||
await session.commit()
|
||
return JSONResponse({"success": True})
|
||
|
||
# ============================================================================
|
||
# ТЕЛЕФОНЫ / ОБЪЯВЛЕНИЯ / ОПРОСЫ / СОБЫТИЯ / ГРАФИКИ
|
||
# ============================================================================
|
||
|
||
@app.get("/phones", response_class=HTMLResponse)
|
||
async def phones_page(request: Request, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
services = (await session.execute(select(Service).order_by(Service.category, Service.name))).scalars().all()
|
||
return templates.TemplateResponse("phones.html", {"request": request, "username": username, "services": services})
|
||
|
||
@app.get("/ads", response_class=HTMLResponse)
|
||
async def ads_page(request: Request, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
ads = (await session.execute(select(Ad).order_by(Ad.created_at.desc()).limit(100))).scalars().all()
|
||
return templates.TemplateResponse("ads.html", {"request": request, "username": username, "ads": ads})
|
||
|
||
@app.get("/polls", response_class=HTMLResponse)
|
||
async def polls_page(request: Request, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
polls = (await session.execute(select(Poll).order_by(Poll.created_at.desc()).limit(50))).scalars().all()
|
||
return templates.TemplateResponse("polls.html", {"request": request, "username": username, "polls": polls})
|
||
|
||
@app.get("/events", response_class=HTMLResponse)
|
||
async def events_page(request: Request, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
events = (await session.execute(select(Event).order_by(Event.event_date.desc()).limit(50))).scalars().all()
|
||
return templates.TemplateResponse("events.html", {"request": request, "username": username, "events": events})
|
||
|
||
@app.get("/schedules", response_class=HTMLResponse)
|
||
async def schedules_page(request: Request, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
schedules = (await session.execute(select(Schedule).order_by(Schedule.start_time.desc()))).scalars().all()
|
||
return templates.TemplateResponse("schedules.html", {"request": request, "username": username, "schedules": schedules})
|
||
|
||
# ============================================================================
|
||
# РАССЫЛКИ (SMART BROADCAST)
|
||
# ============================================================================
|
||
|
||
@app.get("/broadcast", response_class=HTMLResponse)
|
||
async def broadcast_page(request: Request, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
broadcasts = (await session.execute(select(Broadcast).order_by(Broadcast.created_at.desc()).limit(50))).scalars().all()
|
||
broadcasts_data = []
|
||
for b in broadcasts:
|
||
read_count = (await session.execute(select(func.count(BroadcastRead.id)).where(BroadcastRead.broadcast_id == b.id))).scalar() or 0
|
||
broadcasts_data.append({
|
||
'id': b.id, 'text': b.text[:100] + '...' if len(b.text) > 100 else b.text,
|
||
'sent_at': fmt_local(b.sent_at) if b.sent_at else '-',
|
||
'total_sent': b.total_sent, 'read_count': read_count,
|
||
'read_percent': round((read_count / b.total_sent * 100), 1) if b.total_sent > 0 else 0,
|
||
'broadcast_type': b.broadcast_type or 'regular',
|
||
})
|
||
return templates.TemplateResponse("broadcast.html", {"request": request, "username": username, "broadcasts": broadcasts_data})
|
||
|
||
# ============================================================================
|
||
# ЗАПЛАНИРОВАННЫЕ ПОСТЫ
|
||
# ============================================================================
|
||
|
||
@app.get("/scheduled_posts", response_class=HTMLResponse)
|
||
async def scheduled_posts_page(request: Request, username: str = Depends(get_current_admin)):
|
||
return templates.TemplateResponse("scheduled_posts.html", {"request": request, "username": username})
|
||
|
||
@app.get("/api/scheduled_posts/list")
|
||
async def api_list_scheduled_posts(username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
posts = (await session.execute(select(ScheduledPost).order_by(ScheduledPost.scheduled_time.desc()))).scalars().all()
|
||
posts_data = []
|
||
for post in posts:
|
||
posts_data.append({
|
||
"id": post.id, "text": post.text, "topic_name": post.topic_name,
|
||
"topic_emoji": post.get_topic_emoji(), "recipients": post.recipients,
|
||
"scheduled_time": fmt_local(post.scheduled_time), "status": post.status,
|
||
"status_emoji": post.get_status_emoji(), "has_photo": bool(post.photo_file_id)
|
||
})
|
||
return JSONResponse({"success": True, "posts": posts_data})
|
||
|
||
# ============================================================================
|
||
# ДАЙДЖЕСТЫ (V3.1)
|
||
# ============================================================================
|
||
|
||
@app.get("/digests", response_class=HTMLResponse)
|
||
async def digests_page(request: Request, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
# Получаем список дайджестов из БД
|
||
stmt_pending = select(Digest).where(Digest.status.in_(['draft', 'pending_approval'])).order_by(Digest.created_at.desc())
|
||
pending = (await session.execute(stmt_pending)).scalars().all()
|
||
|
||
stmt_archive = select(Digest).where(Digest.status.in_(['approved', 'sent', 'rejected'])).order_by(Digest.created_at.desc()).limit(20)
|
||
archive = (await session.execute(stmt_archive)).scalars().all()
|
||
|
||
return templates.TemplateResponse("digests.html", {
|
||
"request": request,
|
||
"username": username,
|
||
"pending_digests": pending,
|
||
"archive_digests": archive
|
||
})
|
||
|
||
@app.post("/api/digest/generate")
|
||
async def api_generate_digest(username: str = Depends(get_current_admin)):
|
||
from services.digest_service import DigestService
|
||
try:
|
||
stats = await DigestService.collect_weekly_stats()
|
||
digest_text = DigestService.format_digest_text(stats)
|
||
|
||
now = datetime.now()
|
||
week_num = now.isocalendar()[1]
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
new_digest = Digest(
|
||
year=now.year,
|
||
week_number=week_num,
|
||
period_start=now - timedelta(days=7),
|
||
period_end=now,
|
||
digest_text=digest_text,
|
||
status='draft'
|
||
)
|
||
session.add(new_digest)
|
||
await session.commit()
|
||
|
||
return JSONResponse({"success": True, "message": f"Дайджест за {week_num} неделю успешно сгенерирован (черновик)"})
|
||
except Exception as e:
|
||
logger.error(f"Error generating digest: {e}", exc_info=True)
|
||
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
|
||
|
||
@app.get("/api/digest/{digest_id}")
|
||
async def api_get_digest(digest_id: int, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
digest = await session.get(Digest, digest_id)
|
||
if not digest: return JSONResponse({"error": "Not found"}, status_code=404)
|
||
return JSONResponse({
|
||
"digest": {
|
||
"id": digest.id,
|
||
"week": digest.week_number,
|
||
"year": digest.year,
|
||
"text": digest.digest_text,
|
||
"status": digest.status
|
||
}
|
||
})
|
||
|
||
# ============================================================================
|
||
# ФАЙЛЫ (FILES)
|
||
# ============================================================================
|
||
|
||
@app.get("/files", response_class=HTMLResponse)
|
||
async def files_page(request: Request, username: str = Depends(get_current_admin)):
|
||
return templates.TemplateResponse("files.html", {"request": request, "username": username})
|
||
|
||
@app.get("/api/files/list")
|
||
async def api_list_files(username: str = Depends(get_current_admin)):
|
||
files_data = []
|
||
|
||
# 1. Экспорты
|
||
export_path = BASE_DIR / "exports"
|
||
if export_path.exists():
|
||
for f in export_path.glob("*.json"):
|
||
files_data.append({
|
||
"filename": f.name,
|
||
"file_type": "EXPORT",
|
||
"size": f"{round(f.stat().st_size / 1024, 1)} KB",
|
||
"path": f"/exports/{f.name}"
|
||
})
|
||
|
||
# 2. Логи
|
||
log_path = BASE_DIR / "logs"
|
||
if log_path.exists():
|
||
for f in log_path.glob("*.log"):
|
||
files_data.append({
|
||
"filename": f.name,
|
||
"file_type": "LOG",
|
||
"size": f"{round(f.stat().st_size / 1024, 1)} KB",
|
||
"path": f"/api/files/download/logs/{f.name}"
|
||
})
|
||
|
||
return JSONResponse({"success": True, "files": files_data})
|
||
|
||
@app.get("/api/files/download/logs/{filename}")
|
||
async def download_log(filename: str, username: str = Depends(get_current_admin)):
|
||
filepath = BASE_DIR / "logs" / filename
|
||
if not filepath.exists(): raise HTTPException(status_code=404, detail="File not found")
|
||
return FileResponse(filepath, filename=filename)
|
||
|
||
# ============================================================================
|
||
# ЭКСПОРТ (JSON + GITEA)
|
||
# ============================================================================
|
||
|
||
@app.get("/export", response_class=HTMLResponse)
|
||
async def export_page(request: Request, username: str = Depends(get_current_admin)):
|
||
from services.chat_exporter import ChatExporter
|
||
async with AsyncSessionLocal() as session:
|
||
exporter = ChatExporter(session)
|
||
stats = await exporter.get_stats()
|
||
return templates.TemplateResponse("export.html", {"request": request, "username": username, "export_stats": stats, "fmt_local": fmt_local})
|
||
|
||
@app.get("/api/export/json")
|
||
async def api_export_json(username: str = Depends(get_current_admin)):
|
||
from services.chat_exporter import ChatExporter
|
||
try:
|
||
async with AsyncSessionLocal() as session:
|
||
exporter = ChatExporter(session)
|
||
result = await exporter.run_full_export()
|
||
if not result.get('success'): return JSONResponse(status_code=500, content=result)
|
||
return JSONResponse(result)
|
||
except Exception as e:
|
||
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
|
||
|
||
@app.get("/exports/{filename}")
|
||
async def download_export(filename: str, username: str = Depends(get_current_admin)):
|
||
filepath = BASE_DIR / "exports" / filename
|
||
if not filepath.exists(): raise HTTPException(status_code=404, detail="File not found")
|
||
return FileResponse(filepath, filename=filename, media_type='application/json')
|
||
|
||
# ============================================================================
|
||
# ПОЧТА (EMAIL AUDITOR)
|
||
# ============================================================================
|
||
|
||
@app.get("/emails", response_class=HTMLResponse)
|
||
async def emails_page(request: Request, username: str = Depends(get_current_admin)):
|
||
async with AsyncSessionLocal() as session:
|
||
audit_data = (await session.execute(select(EmailAudit).order_by(EmailAudit.last_email_date.desc()))).scalars().all()
|
||
last_log = (await session.execute(select(EmailExportLog).order_by(EmailExportLog.start_time.desc()).limit(1))).scalar_one_or_none()
|
||
|
||
latest_emails = []
|
||
all_folder = BASE_DIR / "services" / "email_auditor" / "exported_emails" / "all"
|
||
if all_folder.exists():
|
||
files = sorted(all_folder.glob("*.pdf"), key=lambda x: x.stat().st_mtime, reverse=True)[:10]
|
||
for f in files:
|
||
match = re.match(r'(\d{4}-\d{2}-\d{2})_([^_]+)_(.*)', f.stem)
|
||
if match: latest_emails.append({"date": match.group(1), "domain": match.group(2), "subject": match.group(3)})
|
||
|
||
master_pdf_path = BASE_DIR / "services" / "email_auditor" / "exported_emails" / "TOTAL_ARCHIVE_2025-2026.pdf"
|
||
last_update = datetime.fromtimestamp(master_pdf_path.stat().st_mtime, tz=timezone.utc) if master_pdf_path.exists() else None
|
||
|
||
return templates.TemplateResponse("emails.html", {
|
||
"request": request, "username": username, "audit_data": audit_data, "last_log": last_log,
|
||
"latest_emails": latest_emails, "has_master_pdf": master_pdf_path.exists(), "last_update": last_update, "fmt_local": fmt_local
|
||
})
|
||
|
||
@app.post("/api/emails/run")
|
||
async def api_run_email_audit(username: str = Depends(get_current_admin)):
|
||
script_path = BASE_DIR / "services" / "email_auditor" / "export_emails_v2.py"
|
||
venv_python = BASE_DIR / "venv" / "bin" / "python3"
|
||
if not script_path.exists(): return JSONResponse(status_code=404, content={"message": "Script not found"})
|
||
try:
|
||
env = os.environ.copy()
|
||
env["PYTHONPATH"] = str(BASE_DIR)
|
||
process = subprocess.Popen([str(venv_python), str(script_path)], cwd=str(BASE_DIR / "services" / "email_auditor"), env=env, start_new_session=True)
|
||
return JSONResponse(content={"message": f"Audit started (PID: {process.pid})"})
|
||
except Exception as e:
|
||
return JSONResponse(status_code=500, content={"message": str(e)})
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
uvicorn.run(app, host="0.0.0.0", port=8000)
|