Compare commits

..

5 commits

13 changed files with 868 additions and 140 deletions

View file

@ -85,6 +85,11 @@ if not ADMIN_USER_ID:
# ===== БЕЗОПАСНОСТЬ ===== # ===== БЕЗОПАСНОСТЬ =====
SAFETY_MODE = os.getenv('SAFETY_MODE', 'production').lower() # 'production' или 'safe' SAFETY_MODE = os.getenv('SAFETY_MODE', 'production').lower() # 'production' или 'safe'
# ===== ДИНАМИЧЕСКИЕ ПУТИ (ПЕРЕНОСИМОСТЬ) =====
MUTT_DIR = os.getenv('MUTT_DIR', '/app/mutt' if os.path.exists('/app/mutt') else str(BASE_DIR.parent.parent / 'mutt'))
GOOGLE_CREDS_FILE = os.getenv('GOOGLE_CREDS_FILE', '/app/config/google_creds.json' if os.path.exists('/app/config/google_creds.json') else str(BASE_DIR.parent.parent / 'domovoy_google_creds.json'))
def get_proxy_url() -> str | None: def get_proxy_url() -> str | None:
"""Получить URL прокси для aiohttp""" """Получить URL прокси для aiohttp"""

View file

@ -8,12 +8,17 @@ services:
restart: always restart: always
environment: environment:
- PYTHONPATH=/app:/app/services - PYTHONPATH=/app:/app/services
- MUTT_DIR=/home/matrixhasyou/mutt
- GOOGLE_CREDS_FILE=/home/matrixhasyou/domovoy_google_creds.json
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs
- ./exports:/app/exports - ./exports:/app/exports
- ./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:
@ -28,12 +33,17 @@ services:
restart: always restart: always
environment: environment:
- PYTHONPATH=/app:/app/services - PYTHONPATH=/app:/app/services
- MUTT_DIR=/home/matrixhasyou/mutt
- GOOGLE_CREDS_FILE=/home/matrixhasyou/domovoy_google_creds.json
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs
- ./exports:/app/exports - ./exports:/app/exports
- ./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:

View file

@ -100,8 +100,15 @@ async def process_jurist_question(message: Message, state: FSMContext):
import asyncio import asyncio
import os import os
# Динамический поиск бинарника nlm
nlm_bin = "nlm"
for p in ["/app/bin/nlm", "/usr/local/bin/nlm", os.path.expanduser("~/.local/bin/nlm"), "/home/matrixhasyou/.local/bin/nlm"]:
if os.path.exists(p):
nlm_bin = p
break
cmd = [ cmd = [
"/home/matrixhasyou/.local/bin/nlm", "query", "notebook", nlm_bin, "query", "notebook",
"00ed7d92-1ab2-4159-ac72-1d0c4be21377", "00ed7d92-1ab2-4159-ac72-1d0c4be21377",
message.text message.text
] ]

View file

@ -116,6 +116,47 @@ async def cb_phones(event):
kb = get_phones_menu() kb = get_phones_menu()
await safe_edit_or_reply(event, text, reply_markup=kb, parse_mode='HTML') await safe_edit_or_reply(event, text, reply_markup=kb, parse_mode='HTML')
@router.message(Command('masters'))
@router.message(F.text.lower() == 'мастера')
async def cmd_masters(message: Message):
"""Выдача списка мастеров по команде /masters или тексту 'мастера'"""
from database.models import Service
from keyboards.inline import get_main_menu
from aiogram.types import FSInputFile
import os
user_id = message.from_user.id
async with AsyncSessionLocal() as session:
stmt = select(Service).where(Service.category == 'masters').order_by(Service.name)
services = list((await session.execute(stmt)).scalars().all())
user_stmt = select(User).where(User.user_id == user_id)
user = (await session.execute(user_stmt)).scalar_one_or_none()
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == user_id, InitiativeGroup.is_active == True)
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
is_admin = user.is_admin or user_id == config.ADMIN_USER_ID if user else False
is_superadmin = user_id == config.ADMIN_USER_ID
verified = user.verified if user else False
menu_kb = get_main_menu(verified=verified, is_ig=is_ig, is_admin=is_admin, is_superadmin=is_superadmin)
if not services:
await message.answer("👷 Данные для этой категории еще не заполнены.", reply_markup=menu_kb, parse_mode='HTML')
return
text = "📞 <b>СПИСОК ТЕЛЕФОНОВ: МАСТЕРА</b>\n\n"
for s in services:
text += f"🔹 <b>{s.name}</b>\n └ 📱 <code>{s.phone}</code>\n"
if s.description:
text += f" └ 📝 <i>{s.description}</i>\n"
text += "\n"
photo_path = services[0].image_path if services[0].image_path else None
if photo_path and os.path.exists(photo_path):
await message.answer_photo(FSInputFile(photo_path), caption=text, reply_markup=menu_kb, parse_mode='HTML')
else:
await message.answer(text, reply_markup=menu_kb, parse_mode='HTML')
@router.message(F.text & ~F.text.startswith('/')) @router.message(F.text & ~F.text.startswith('/'))
async def chat_with_ai(message: Message): async def chat_with_ai(message: Message):
"""Обработка свободного общения с ИИ (только для верифицированных)""" """Обработка свободного общения с ИИ (только для верифицированных)"""

View file

@ -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

View file

@ -0,0 +1,145 @@
import os
import sys
import logging
import json
from pathlib import Path
from pypdf import PdfWriter, PdfReader
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from PIL import Image
import subprocess
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler("/home/matrixhasyou/swarm-services/domovoy-bot/scraper_gis/logs/sudrf_compiler.log", encoding="utf-8"),
logging.StreamHandler(sys.stdout)
]
)
log = logging.getLogger("SUD-Compiler")
# Add libs path
sys.path.append("/home/matrixhasyou/swarm-services/libs")
try:
from swarmlib.gdrive import GDriveManager
except ImportError as e:
log.error(f"Cannot import swarmlib: {e}")
GDriveManager = None
EXPORT_DIR = Path("/home/matrixhasyou/swarm-services/domovoy-bot/scraper_gis/exports/sudrf")
OUTPUT_DIR = Path("/home/matrixhasyou/swarm-services/domovoy-bot/exports/compiled_sudrf")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
BOT_TOKEN = "8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M"
CHAT_ID = "197957361"
def send_tg(msg):
subprocess.run(["curl", "-s", "-X", "POST", f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
"-d", f"chat_id={CHAT_ID}&text={msg}&parse_mode=markdown"])
def tag_image_to_pdf(image_path, output_pdf_path, label):
try:
c = canvas.Canvas(output_pdf_path, pagesize=A4)
width, height = A4
c.setFillColor(colors.black)
c.rect(0, height - 50, width, 50, fill=1)
c.setFillColor(colors.white)
c.setFont("Helvetica-Bold", 10)
c.drawString(20, height - 30, f"SUD: {label}".upper())
img = Image.open(image_path)
img_width, img_height = img.size
aspect = img_height / float(img_width)
display_width = width - 40
display_height = display_width * aspect
if display_height > (height - 70):
display_height = height - 70
display_width = display_height / aspect
c.drawImage(image_path, 20, height - 60 - display_height, width=display_width, height=display_height)
c.showPage()
c.save()
return True
except Exception as e:
log.error(f"Failed to tag image {image_path}: {e}")
return False
def compile_appeals():
send_tg("🧬 **ГАС Правосудие: Запуск PDF-компилятора**\nОбрабатываю выгруженные материалы...")
if not EXPORT_DIR.exists():
log.error(f"Export dir not found: {EXPORT_DIR}")
send_tg("❌ **Компиляция прервана**: папка с выгрузками не найдена.")
return
gdrive = None
if GDriveManager:
try:
gdrive = GDriveManager()
except Exception as e:
log.error(f"Failed to initialize GDriveManager: {e}")
folders = [d for d in EXPORT_DIR.iterdir() if d.is_dir() and d.name.startswith("appeal_")]
processed = 0
for folder in folders:
appeal_id = folder.name.replace("appeal_", "")
log.info(f"Compiling appeal: {appeal_id}")
writer = PdfWriter()
# 1. Convert detail screenshot (evidence.png) to PDF first
evidence_img = folder / "evidence.png"
evidence_pdf = folder / "evidence.pdf"
if evidence_img.exists():
if tag_image_to_pdf(str(evidence_img), str(evidence_pdf), f"APPEAL {appeal_id}"):
writer.append(str(evidence_pdf))
# 2. Append all other PDF files inside the folder
files = sorted(list(folder.iterdir()))
for f in files:
if f.suffix.lower() == ".pdf" and f.name != "evidence.pdf":
try:
writer.append(str(f))
log.info(f"Appended PDF: {f.name}")
except Exception as e:
log.error(f"Failed to append PDF {f.name}: {e}")
elif f.suffix.lower() in [".png", ".jpg", ".jpeg"] and f.name != "evidence.png":
# Convert image attachments to PDF and append
img_pdf = folder / f"{f.stem}_attachment.pdf"
if tag_image_to_pdf(str(f), str(img_pdf), f"ATTACHMENT {f.name}"):
try:
writer.append(str(img_pdf))
log.info(f"Appended converted image: {f.name}")
except Exception as e:
log.error(f"Failed to append converted image {f.name}: {e}")
output_pdf = OUTPUT_DIR / f"SUD_APPEAL_{appeal_id}.pdf"
try:
with open(output_pdf, "wb") as out_f:
writer.write(out_f)
processed += 1
log.info(f"Successfully compiled: {output_pdf.name}")
# Upload to GDrive
drive_status = "не настроен"
if gdrive:
try:
drive_status = gdrive.upload_or_update(str(output_pdf), output_pdf.name)
except Exception as e:
drive_status = f"ошибка GDrive: {e}"
send_tg(f"📄 **Обращение {appeal_id} собрано в PDF**\n☁️ GDrive: `{drive_status}`")
except Exception as e:
log.error(f"Failed to write compiled PDF for {appeal_id}: {e}")
send_tg(f"❌ **Ошибка сборки обращения {appeal_id}**: {e}")
send_tg(f"🏁 **Компиляция судебных дел завершена!**\nСобрано обращений: {processed}")
if __name__ == "__main__":
compile_appeals()

230
scraper_gis/sudrf_reaper.py Normal file
View file

@ -0,0 +1,230 @@
import asyncio
import os
import sys
import logging
import random
from datetime import datetime
from pathlib import Path
from playwright.async_api import async_playwright
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler("/app/logs/sudrf.log", encoding="utf-8"),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger("SUD-Reaper")
USER_DATA_DIR = "/app/browser_context_sudrf"
EXPORT_DIR = "/app/exports/sudrf"
os.makedirs(EXPORT_DIR, exist_ok=True)
os.makedirs("/app/logs", exist_ok=True)
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M")
CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "197957361")
def send_tg(msg):
import subprocess
subprocess.run(["curl", "-s", "-X", "POST", f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
"-d", f"chat_id={CHAT_ID}&text={msg}&parse_mode=markdown"])
async def human_delay(min_sec=3, max_sec=6):
await asyncio.sleep(random.uniform(min_sec, max_sec))
async def run_mission():
logger.info("Starting Playwright in VNC display...")
send_tg("🕵️‍♂️ **ГАС Правосудие: Запуск браузера**\nОжидаю авторизацию через Госуслуги во VNC: http://192.168.10.116:6080/vnc.html")
async with async_playwright() as p:
# Launch persistent browser context
context = await p.chromium.launch_persistent_context(
user_data_dir=USER_DATA_DIR,
headless=False,
args=["--no-sandbox", "--disable-setuid-sandbox", "--start-maximized"]
)
page = context.pages[0] if context.pages else await context.new_page()
await page.set_viewport_size({"width": 1280, "height": 1024})
# Navigate to portal
logger.info("Navigating to sudrf...")
await page.goto("https://ej.sudrf.ru/")
await human_delay(4, 7)
# Phase 0: Wait for login
logger.info("Waiting for user authorization...")
logged_in = False
for i in range(120): # 10 minutes total
content = ""
try:
content = await page.content()
except Exception as e:
logger.error(f"Error reading page content: {e}")
# Check for common logout or profile indicators
if any(indicator in content for indicator in ["Выйти", "Выход", "Личный кабинет", "История обращений"]):
logged_in = True
break
if i % 6 == 0:
logger.info(f"Still waiting... i={i}")
await asyncio.sleep(5)
if not logged_in:
logger.error("Login timeout exceeded.")
send_tg("❌ **ГАС Правосудие: Превышено время ожидания входа**.")
await context.close()
return
logger.info("Login detected. Navigating to appeals history...")
send_tg("✅ **Авторизация успешна!** Начинаю сбор обращений...")
await page.goto("https://ej.sudrf.ru/#/appeals")
await asyncio.sleep(10) # wait for page to render appeals list
# Take list page screenshot
list_screenshot = os.path.join(EXPORT_DIR, "appeals_list.png")
await page.screenshot(path=list_screenshot, full_page=True)
# Dump HTML for structural analysis
list_html = os.path.join(EXPORT_DIR, "appeals_list.html")
with open(list_html, "w", encoding="utf-8") as f:
f.write(await page.content())
logger.info(f"Appeals list screenshot saved to {list_screenshot}")
# Extract appeal links
links = []
all_links = await page.query_selector_all("a")
for link in all_links:
href = await link.get_attribute("href")
text = await link.inner_text()
if href and ("appeal" in href or "card" in href or "request" in href):
links.append({"href": href, "text": text.strip()})
logger.info(f"Found {len(links)} matching links on page: {links}")
# Process each appeal
# We can extract rows from the table to get exact dates, numbers and link objects
# For sudrf, appeals table often has tr tags with status and numbers
rows = await page.query_selector_all("tr, .appeals-item")
logger.info(f"Found {len(rows)} rows/items in appeals list.")
# Dump structured report of links to a json file
import json
with open(os.path.join(EXPORT_DIR, "found_links.json"), "w", encoding="utf-8") as f:
json.dump({"links": links, "rows_count": len(rows)}, f, indent=4, ensure_ascii=False)
# Try to parse the table rows if they contain appeal details
parsed_appeals = []
for index, row in enumerate(rows[:20]): # parse top 20 appeals first for safety
cells = await row.query_selector_all("td")
if not cells: continue
row_data = []
for cell in cells:
row_data.append((await cell.inner_text()).strip())
# Look for link inside the row
row_link = await row.query_selector("a")
href = await row_link.get_attribute("href") if row_link else None
parsed_appeals.append({
"row_index": index,
"data": row_data,
"href": href
})
with open(os.path.join(EXPORT_DIR, "parsed_table.json"), "w", encoding="utf-8") as f:
json.dump(parsed_appeals, f, indent=4, ensure_ascii=False)
# Loop through found details pages and download documents
count = 0
processed_hrefs = set()
# Collect actual detail pages
detail_hrefs = []
for appeal in parsed_appeals:
href = appeal.get("href")
if href and href not in processed_hrefs:
detail_hrefs.append(href)
processed_hrefs.add(href)
# Also check all links for hrefs like #/appeals/view/ or similar
for link in links:
href = link["href"]
if href and href not in processed_hrefs:
detail_hrefs.append(href)
processed_hrefs.add(href)
logger.info(f"Unique detail pages to process: {detail_hrefs}")
for href in detail_hrefs[:10]: # Safe limit to top 10 appeals in recon mode
try:
full_url = href
if href.startswith("#"):
full_url = f"https://ej.sudrf.ru/{href}"
elif href.startswith("/"):
full_url = f"https://ej.sudrf.ru{href}"
logger.info(f"Navigating to detail page: {full_url}")
await page.goto(full_url)
await asyncio.sleep(8) # Wait for page load
safe_id = "".join([c for c in href if c.isalnum() or c in ("-", "_")]).strip()
appeal_dir = os.path.join(EXPORT_DIR, f"appeal_{safe_id}")
os.makedirs(appeal_dir, exist_ok=True)
# Take detail screenshot
detail_screenshot = os.path.join(appeal_dir, "evidence.png")
await page.screenshot(path=detail_screenshot, full_page=True)
# Dump detail HTML
detail_html = os.path.join(appeal_dir, "page.html")
with open(detail_html, "w", encoding="utf-8") as f:
f.write(await page.content())
# Look for download links
download_selectors = [
"a[href*='download']",
"a[href*='file']",
"button:has-text('Скачать')",
"a:has-text('Скачать')"
]
files_downloaded = []
for selector in download_selectors:
elements = await page.locator(selector).all()
for el in elements:
if not await el.is_visible(): continue
try:
async with page.expect_download(timeout=15000) as di:
await el.click(force=True)
download = await di.value
fname = download.suggested_filename
dest = os.path.join(appeal_dir, fname)
await download.save_as(dest)
files_downloaded.append(fname)
logger.info(f"Downloaded: {fname}")
except Exception as e:
logger.warning(f"Failed download click: {e}")
count += 1
send_tg(f"📥 **Обращение {count} собрано**\nПапка: `appeal_{safe_id}`\nФайлов скачано: {len(files_downloaded)}")
except Exception as e:
logger.error(f"Error processing appeal details {href}: {e}")
send_tg(f"🏁 **Миссия ГАС Правосудие завершена!** Собрано обращений: {count}. Запускаю сборку в PDF.")
# Trigger PDF compilation script
import subprocess
subprocess.Popen(["python3", "/app/sudrf_pdf_compiler.py"], start_new_session=True)
await context.close()
if __name__ == "__main__":
asyncio.run(run_mission())

@ -1 +1 @@
Subproject commit 1d8871f348b4df9561aec957f2a7f07790862899 Subproject commit 93a3d14d571832433bfcd3d1fe5a244d234fc020

View file

@ -5,7 +5,7 @@ from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload from googleapiclient.http import MediaFileUpload
SCOPES = ['https://www.googleapis.com/auth/drive'] SCOPES = ['https://www.googleapis.com/auth/drive']
SERVICE_ACCOUNT_FILE = '/home/matrixhasyou/domovoy_google_creds.json' SERVICE_ACCOUNT_FILE = os.getenv('GOOGLE_CREDS_FILE', '/app/config/google_creds.json' if os.path.exists('/app/config/google_creds.json') else '/home/matrixhasyou/domovoy_google_creds.json')
PARENT_FOLDER_ID = '1lnHt9Os0L_SnBa8i2dHrI0lBepuVm0Om' PARENT_FOLDER_ID = '1lnHt9Os0L_SnBa8i2dHrI0lBepuVm0Om'
GIS_FILE_NAME = "TOTAL_GIS_ARCHIVE.pdf" GIS_FILE_NAME = "TOTAL_GIS_ARCHIVE.pdf"

View file

@ -6,15 +6,18 @@ from pdf_tagger import tag_image_to_pdf
from gdrive_uploader import upload_gis_archive from gdrive_uploader import upload_gis_archive
# --- CONFIG (LEGAL PATHS ONLY) --- # --- CONFIG (LEGAL PATHS ONLY) ---
LOCAL_ARCHIVE = Path("/home/matrixhasyou/domovoy_bot/services/gis_harvester/archive") BASE_DIR = Path(__file__).resolve().parent.parent.parent
TEMP_DIR = Path("/home/matrixhasyou/domovoy_bot/services/gis_pdf_pipeline/temp") LOCAL_ARCHIVE = BASE_DIR / "services" / "gis_harvester" / "archive"
OUTPUT_FILE = Path("/home/matrixhasyou/domovoy_bot/services/gis_pdf_pipeline/TOTAL_GIS_ARCHIVE.pdf") TEMP_DIR = BASE_DIR / "services" / "gis_pdf_pipeline" / "temp"
OUTPUT_FILE = BASE_DIR / "services" / "gis_pdf_pipeline" / "TOTAL_GIS_ARCHIVE.pdf"
NAS_LEGAL_PATH = "/volume1/web/legal/gis/TOTAL_GIS_ARCHIVE.pdf" NAS_LEGAL_PATH = "/volume1/web/legal/gis/TOTAL_GIS_ARCHIVE.pdf"
NAS_IP = "192.168.10.105" NAS_IP = "192.168.10.105"
def send_tg(msg): def send_tg(msg):
subprocess.run(["curl", "-s", "-X", "POST", f"https://api.telegram.org/bot8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M/sendMessage", token = os.getenv("BOT_TOKEN", "8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M")
"-d", f"chat_id=197957361&text={msg}&parse_mode=markdown"]) chat_id = os.getenv("ADMIN_USER_ID", "197957361")
subprocess.run(["curl", "-s", "-X", "POST", f"https://api.telegram.org/bot{token}/sendMessage",
"-d", f"chat_id={chat_id}&text={msg}&parse_mode=markdown"])
def run_pipeline(): def run_pipeline():
send_tg("🧬 **PDF-КОНВЕЙЕР (LEGAL MODE)**\nНачинаю сборку из /services/gis_harvester/archive...") send_tg("🧬 **PDF-КОНВЕЙЕР (LEGAL MODE)**\nНачинаю сборку из /services/gis_harvester/archive...")

View file

@ -117,8 +117,8 @@ async def upload_to_tg(content, filename, content_type):
@app.post("/api/emails/sync_ai") @app.post("/api/emails/sync_ai")
async def api_sync_emails_to_ai(username: str = Depends(get_current_admin)): async def api_sync_emails_to_ai(username: str = Depends(get_current_admin)):
sync_script = "/home/matrixhasyou/domovoy_drive_sync.py" sync_script = "/app/domovoy_drive_sync.py" if os.path.exists("/app/domovoy_drive_sync.py") else str(Path(config.MUTT_DIR).parent / "domovoy_drive_sync.py")
python_path = "/home/matrixhasyou/domovoy_bot/venv-google/bin/python" python_path = sys.executable
try: try:
# Запуск синхронизации # Запуск синхронизации
@ -667,7 +667,7 @@ async def emails_page(request: Request, username: str = Depends(get_current_admi
audit_data = (await session.execute(select(EmailAudit).order_by(EmailAudit.last_email_date.desc()))).scalars().all() 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() last_log = (await session.execute(select(EmailExportLog).order_by(EmailExportLog.start_time.desc()).limit(1))).scalar_one_or_none()
latest_emails = [] latest_emails = []
all_folder = Path("/home/matrixhasyou/mutt/exported_emails/all") all_folder = Path(config.MUTT_DIR) / "exported_emails" / "all"
if all_folder.exists(): if all_folder.exists():
files = sorted(all_folder.glob("*.pdf"), key=lambda x: x.stat().st_mtime, reverse=True)[:10] files = sorted(all_folder.glob("*.pdf"), key=lambda x: x.stat().st_mtime, reverse=True)[:10]
for f in files: for f in files:
@ -678,11 +678,11 @@ async def emails_page(request: Request, username: str = Depends(get_current_admi
else: else:
latest_emails.append({"date": "-", "domain": "other", "subject": f.stem}) latest_emails.append({"date": "-", "domain": "other", "subject": f.stem})
master_pdf_path = Path("/home/matrixhasyou/mutt/ALL_EMAILS_CONSOLIDATED.pdf") master_pdf_path = Path(config.MUTT_DIR) / "ALL_EMAILS_CONSOLIDATED.pdf"
last_update = datetime.fromtimestamp(master_pdf_path.stat().st_mtime, tz=timezone.utc) if master_pdf_path.exists() else None last_update = datetime.fromtimestamp(master_pdf_path.stat().st_mtime, tz=timezone.utc) if master_pdf_path.exists() else None
# Drive sync status # Drive sync status
sync_time_file = Path("/home/matrixhasyou/mutt/last_ai_sync.txt") sync_time_file = Path(config.MUTT_DIR) / "last_ai_sync.txt"
last_sync_time = None last_sync_time = None
if sync_time_file.exists(): if sync_time_file.exists():
try: try:
@ -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(config.MUTT_DIR) / "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 = str(Path(config.MUTT_DIR) / "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"})
@ -715,9 +743,9 @@ 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 = "/app/domovoy_drive_sync.py" if os.path.exists("/app/domovoy_drive_sync.py") else str(Path(config.MUTT_DIR).parent / "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 = str(Path(config.MUTT_DIR) / "TOTAL_ARCHIVE_2025-2026.pdf")
if not os.path.exists(sync_script): if not os.path.exists(sync_script):
return JSONResponse(status_code=404, content={"message": "Sync script not found"}) return JSONResponse(status_code=404, content={"message": "Sync script not found"})
@ -732,19 +760,69 @@ async def api_emails_sync_ai(username: str = Depends(get_current_admin)):
@app.get("/api/emails/pipeline_status") @app.get("/api/emails/pipeline_status")
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(config.MUTT_DIR) / "pipeline_status.json"
if not status_file.exists(): master_pdf_path = Path(config.MUTT_DIR) / "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(config.MUTT_DIR) / "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(config.MUTT_DIR) / "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)):
@ -766,9 +844,9 @@ async def get_infra_page(request: Request, username: str = Depends(get_current_a
@app.post("/api/infra/run_speedtest") @app.post("/api/infra/run_speedtest")
async def api_run_speedtest(username: str = Depends(get_current_admin)): async def api_run_speedtest(username: str = Depends(get_current_admin)):
script_path = "/home/matrixhasyou/infra/speedtest_monitor.py" script_path = "/app/infra/speedtest_monitor.py" if os.path.exists("/app/infra/speedtest_monitor.py") else "/home/matrixhasyou/infra/speedtest_monitor.py"
try: try:
subprocess.Popen(["python3", script_path], start_new_session=True) subprocess.Popen([sys.executable if os.path.exists("/app/infra/speedtest_monitor.py") else "python3", script_path], start_new_session=True)
return JSONResponse(content={"message": "Тест скорости запущен в фоне. Результаты появятся через минуту."}) return JSONResponse(content={"message": "Тест скорости запущен в фоне. Результаты появятся через минуту."})
except Exception as e: except Exception as e:
return JSONResponse(status_code=500, content={"message": str(e)}) return JSONResponse(status_code=500, content={"message": str(e)})
@ -781,8 +859,8 @@ async def api_run_gis_harvest(username: str = Depends(get_current_admin)):
subprocess.Popen(["docker", "exec", "-d", "lkm37-gis-scraper", "python3", "/app/services/gis_harvester/harvester_v5.py"]) subprocess.Popen(["docker", "exec", "-d", "lkm37-gis-scraper", "python3", "/app/services/gis_harvester/harvester_v5.py"])
# 2. Запуск PDF-конвейера # 2. Запуск PDF-конвейера
pipeline_script = "/home/matrixhasyou/domovoy_bot/services/gis_pdf_pipeline/pipeline_main.py" pipeline_script = str(Path(__file__).resolve().parent.parent / "services" / "gis_pdf_pipeline" / "pipeline_main.py")
venv_python = "/home/matrixhasyou/domovoy_bot/services/gis_pdf_pipeline/venv/bin/python3" venv_python = sys.executable
subprocess.Popen([venv_python, pipeline_script], start_new_session=True) subprocess.Popen([venv_python, pipeline_script], start_new_session=True)
return JSONResponse(content={"message": "Процесс ГИС ЖКХ запущен (Жатва + PDF). Следите за Telegram."}) return JSONResponse(content={"message": "Процесс ГИС ЖКХ запущен (Жатва + PDF). Следите за Telegram."})
@ -806,6 +884,28 @@ async def api_gis_status(username: str = Depends(get_current_admin)):
except Exception as e: except Exception as e:
return JSONResponse(status_code=500, content={"message": str(e)}) return JSONResponse(status_code=500, content={"message": str(e)})
@app.post("/api/infra/run_sudrf_parser")
async def api_run_sudrf_parser(username: str = Depends(get_current_admin)):
try:
subprocess.run(["docker", "exec", "lkm37-gis-scraper", "pkill", "-f", "sudrf_reaper.py"])
subprocess.Popen(["docker", "exec", "-d", "lkm37-gis-scraper", "python3", "/app/sudrf_reaper.py"])
return JSONResponse(content={"message": "Сессия ГАС Правосудие успешно запущена. Перейдите по ссылке VNC и авторизуйтесь."})
except Exception as e:
return JSONResponse(status_code=500, content={"message": str(e)})
@app.get("/api/infra/sudrf_status")
async def api_sudrf_status(username: str = Depends(get_current_admin)):
try:
proc = subprocess.run(["docker", "exec", "lkm37-gis-scraper", "ps", "aux"], capture_output=True, text=True)
is_alive = "sudrf_reaper.py" in proc.stdout
log_proc = subprocess.run(["docker", "exec", "lkm37-gis-scraper", "tail", "-n", "30", "/app/logs/sudrf.log"], capture_output=True, text=True)
return JSONResponse(content={
"alive": is_alive,
"log": log_proc.stdout or "Log is empty or file not found."
})
except Exception as e:
return JSONResponse(status_code=500, content={"message": str(e)})
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)

View file

@ -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,20 +16,87 @@
</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> ТУРБО СИНХРОНИЗАЦИЯ
</button> </button>
<div class="dropdown w-100"> <div class="dropdown w-100">
@ -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);
} }
} }

View file

@ -78,6 +78,30 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6">
<div class="card bg-dark border-danger">
<div class="card-header text-danger">ГАС ПРАВОСУДИЕ (СУДЕБНЫЙ АРХИВ)</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<button id="runSudrfParserBtn" class="btn btn-outline-danger flex-grow-1 me-2">
<i class="bi bi-play-circle"></i> ЗАПУСТИТЬ ЖАТВУ СУДОВ
</button>
<div id="sudrfStatusBadge" class="badge bg-secondary p-2">СТАТУС: НЕИЗВЕСТНО</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<a href="http://192.168.10.116:6080/vnc.html" target="_blank" class="btn btn-outline-info w-100">
<i class="bi bi-window"></i> ОТКРЫТЬ VNC БРАУЗЕР (ДЛЯ ЕСИА)
</a>
</div>
<div class="mb-2 d-flex justify-content-between">
<span class="small text-muted">ПОСЛЕДНИЕ ЛОГИ:</span>
<button id="refreshSudrfStatusBtn" class="btn btn-sm btn-link p-0 text-danger">ОБНОВИТЬ СТАТУС</button>
</div>
<pre id="sudrfLogs" class="bg-black text-danger p-2 small border border-secondary" style="height: 200px; overflow-y: scroll; font-size: 0.7rem;"></pre>
<div class="small opacity-50 mt-2">Выполняет: Открытие сессии -> Ввод 2FA пользователем -> Парсинг ej.sudrf.ru -> Сборка PDF -> Google Drive</div>
</div>
</div>
</div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card bg-dark border-warning"> <div class="card bg-dark border-warning">
<div class="card-header text-warning">ДЕЙСТВИЯ</div> <div class="card-header text-warning">ДЕЙСТВИЯ</div>
@ -155,9 +179,62 @@ async function refreshGisStatus() {
} }
} }
document.getElementById('runSudrfParserBtn').addEventListener('click', async function() {
const btn = this;
if (!confirm('Запустить сборку судебного архива ГАС Правосудие? Потребуется войти через Госуслуги во VNC.')) return;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> СЕССИЯ СУДОВ...';
try {
const response = await fetch('/api/infra/run_sudrf_parser', { method: 'POST' });
const result = await response.json();
alert(result.message);
} catch (e) {
alert('Ошибка: ' + e);
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-play-circle"></i> ЗАПУСТИТЬ ЖАТВУ СУДОВ';
refreshSudrfStatus();
}
});
async function refreshSudrfStatus() {
const badge = document.getElementById('sudrfStatusBadge');
const logs = document.getElementById('sudrfLogs');
try {
const response = await fetch('/api/infra/sudrf_status');
const data = await response.json();
if (data.alive) {
badge.className = 'badge bg-success p-2';
badge.innerText = 'СТАТУС: РАБОТАЕТ';
} else {
badge.className = 'badge bg-danger p-2';
badge.innerText = 'СТАТУС: ОСТАНОВЛЕН';
}
logs.innerText = data.log;
logs.scrollTop = logs.scrollHeight;
} catch (e) {
badge.className = 'badge bg-warning p-2';
badge.innerText = 'ОШИБКА API';
}
}
document.getElementById('refreshGisStatusBtn').addEventListener('click', refreshGisStatus); document.getElementById('refreshGisStatusBtn').addEventListener('click', refreshGisStatus);
window.addEventListener('load', refreshGisStatus); document.getElementById('refreshSudrfStatusBtn').addEventListener('click', refreshSudrfStatus);
window.addEventListener('load', () => {
refreshGisStatus();
refreshSudrfStatus();
});
// Авто-обновление раз в 30 секунд // Авто-обновление раз в 30 секунд
setInterval(refreshGisStatus, 30000); setInterval(() => {
refreshGisStatus();
refreshSudrfStatus();
}, 30000);
</script> </script>
{% endblock %} {% endblock %}