Compare commits
5 commits
35ffe3442c
...
48b23f5980
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48b23f5980 | ||
|
|
23f44e7679 | ||
|
|
8e9e800ea8 | ||
|
|
1c342fd3c1 | ||
|
|
ed565c54c3 |
13 changed files with 868 additions and 140 deletions
|
|
@ -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"""
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
"""Обработка свободного общения с ИИ (только для верифицированных)"""
|
"""Обработка свободного общения с ИИ (только для верифицированных)"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
145
scraper_gis/sudrf_pdf_compiler.py
Normal file
145
scraper_gis/sudrf_pdf_compiler.py
Normal 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
230
scraper_gis/sudrf_reaper.py
Normal 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
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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...")
|
||||||
|
|
|
||||||
156
web/app.py
156
web/app.py
|
|
@ -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})
|
|
||||||
try:
|
# Calculate fallback values
|
||||||
with open(status_file) as f:
|
pdf_size_mb = 0.0
|
||||||
data = json.load(f)
|
if master_pdf_path.exists():
|
||||||
mtime = status_file.stat().st_mtime
|
pdf_size_mb = round(master_pdf_path.stat().st_size / (1024 * 1024), 2)
|
||||||
if data.get("status") == "running" and (time.time() - mtime > 600):
|
|
||||||
data["status"] = "stuck"
|
total_emails = 0
|
||||||
data["step"] = "Процесс завис или был остановлен"
|
total_inbound = 0
|
||||||
return JSONResponse(content=data)
|
total_outbound = 0
|
||||||
except Exception as e:
|
async with AsyncSessionLocal() as session:
|
||||||
return JSONResponse(content={"status": "error", "message": str(e)})
|
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:
|
||||||
|
with open(status_file) as f:
|
||||||
|
file_data = json.load(f)
|
||||||
|
data.update(file_data)
|
||||||
|
|
||||||
|
mtime = status_file.stat().st_mtime
|
||||||
|
if data.get("status") == "running" and (time.time() - mtime > 600):
|
||||||
|
data["status"] = "stuck"
|
||||||
|
data["step"] = "Процесс завис или был остановлен"
|
||||||
|
except Exception as 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)
|
||||||
|
|
|
||||||
|
|
@ -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 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>
|
||||||
|
|
||||||
<!-- ПОСЛЕДНЯЯ АКТИВНОСТЬ -->
|
<!-- ПРАВАЯ КОЛОНКА: ТАБЛИЦА ВЕДОМСТВ -->
|
||||||
<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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue