Compare commits
No commits in common. "48b23f598066dadad929ab9b594d9fb90e7533d1" and "35ffe3442c273c94de8b5590611a45455befc59d" have entirely different histories.
48b23f5980
...
35ffe3442c
13 changed files with 140 additions and 868 deletions
|
|
@ -85,11 +85,6 @@ if not ADMIN_USER_ID:
|
|||
# ===== БЕЗОПАСНОСТЬ =====
|
||||
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:
|
||||
"""Получить URL прокси для aiohttp"""
|
||||
|
|
|
|||
|
|
@ -8,17 +8,12 @@ services:
|
|||
restart: always
|
||||
environment:
|
||||
- PYTHONPATH=/app:/app/services
|
||||
- MUTT_DIR=/home/matrixhasyou/mutt
|
||||
- GOOGLE_CREDS_FILE=/home/matrixhasyou/domovoy_google_creds.json
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./exports:/app/exports
|
||||
- ./data:/app/data
|
||||
- ../libs/swarmlib:/app/swarmlib: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:
|
||||
driver: "json-file"
|
||||
options:
|
||||
|
|
@ -33,17 +28,12 @@ services:
|
|||
restart: always
|
||||
environment:
|
||||
- PYTHONPATH=/app:/app/services
|
||||
- MUTT_DIR=/home/matrixhasyou/mutt
|
||||
- GOOGLE_CREDS_FILE=/home/matrixhasyou/domovoy_google_creds.json
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./exports:/app/exports
|
||||
- ./data:/app/data
|
||||
- ../libs/swarmlib:/app/swarmlib: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:
|
||||
driver: "json-file"
|
||||
options:
|
||||
|
|
|
|||
|
|
@ -100,15 +100,8 @@ async def process_jurist_question(message: Message, state: FSMContext):
|
|||
import asyncio
|
||||
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 = [
|
||||
nlm_bin, "query", "notebook",
|
||||
"/home/matrixhasyou/.local/bin/nlm", "query", "notebook",
|
||||
"00ed7d92-1ab2-4159-ac72-1d0c4be21377",
|
||||
message.text
|
||||
]
|
||||
|
|
|
|||
|
|
@ -116,47 +116,6 @@ async def cb_phones(event):
|
|||
kb = get_phones_menu()
|
||||
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('/'))
|
||||
async def chat_with_ai(message: Message):
|
||||
"""Обработка свободного общения с ИИ (только для верифицированных)"""
|
||||
|
|
|
|||
|
|
@ -15,13 +15,4 @@ pydub
|
|||
psycopg2-binary>=2.9.9
|
||||
asyncpg>=0.29.0
|
||||
google-generativeai
|
||||
imap-tools
|
||||
weasyprint
|
||||
fpdf
|
||||
pymupdf
|
||||
google-api-python-client
|
||||
google-auth
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
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()
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
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 93a3d14d571832433bfcd3d1fe5a244d234fc020
|
||||
Subproject commit 1d8871f348b4df9561aec957f2a7f07790862899
|
||||
|
|
@ -5,7 +5,7 @@ from googleapiclient.discovery import build
|
|||
from googleapiclient.http import MediaFileUpload
|
||||
|
||||
SCOPES = ['https://www.googleapis.com/auth/drive']
|
||||
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')
|
||||
SERVICE_ACCOUNT_FILE = '/home/matrixhasyou/domovoy_google_creds.json'
|
||||
PARENT_FOLDER_ID = '1lnHt9Os0L_SnBa8i2dHrI0lBepuVm0Om'
|
||||
GIS_FILE_NAME = "TOTAL_GIS_ARCHIVE.pdf"
|
||||
|
||||
|
|
|
|||
|
|
@ -6,18 +6,15 @@ from pdf_tagger import tag_image_to_pdf
|
|||
from gdrive_uploader import upload_gis_archive
|
||||
|
||||
# --- CONFIG (LEGAL PATHS ONLY) ---
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
LOCAL_ARCHIVE = BASE_DIR / "services" / "gis_harvester" / "archive"
|
||||
TEMP_DIR = BASE_DIR / "services" / "gis_pdf_pipeline" / "temp"
|
||||
OUTPUT_FILE = BASE_DIR / "services" / "gis_pdf_pipeline" / "TOTAL_GIS_ARCHIVE.pdf"
|
||||
LOCAL_ARCHIVE = Path("/home/matrixhasyou/domovoy_bot/services/gis_harvester/archive")
|
||||
TEMP_DIR = Path("/home/matrixhasyou/domovoy_bot/services/gis_pdf_pipeline/temp")
|
||||
OUTPUT_FILE = Path("/home/matrixhasyou/domovoy_bot/services/gis_pdf_pipeline/TOTAL_GIS_ARCHIVE.pdf")
|
||||
NAS_LEGAL_PATH = "/volume1/web/legal/gis/TOTAL_GIS_ARCHIVE.pdf"
|
||||
NAS_IP = "192.168.10.105"
|
||||
|
||||
def send_tg(msg):
|
||||
token = os.getenv("BOT_TOKEN", "8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M")
|
||||
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"])
|
||||
subprocess.run(["curl", "-s", "-X", "POST", f"https://api.telegram.org/bot8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M/sendMessage",
|
||||
"-d", f"chat_id=197957361&text={msg}&parse_mode=markdown"])
|
||||
|
||||
def run_pipeline():
|
||||
send_tg("🧬 **PDF-КОНВЕЙЕР (LEGAL MODE)**\nНачинаю сборку из /services/gis_harvester/archive...")
|
||||
|
|
|
|||
142
web/app.py
142
web/app.py
|
|
@ -117,8 +117,8 @@ async def upload_to_tg(content, filename, content_type):
|
|||
|
||||
@app.post("/api/emails/sync_ai")
|
||||
async def api_sync_emails_to_ai(username: str = Depends(get_current_admin)):
|
||||
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 = sys.executable
|
||||
sync_script = "/home/matrixhasyou/domovoy_drive_sync.py"
|
||||
python_path = "/home/matrixhasyou/domovoy_bot/venv-google/bin/python"
|
||||
|
||||
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()
|
||||
last_log = (await session.execute(select(EmailExportLog).order_by(EmailExportLog.start_time.desc()).limit(1))).scalar_one_or_none()
|
||||
latest_emails = []
|
||||
all_folder = Path(config.MUTT_DIR) / "exported_emails" / "all"
|
||||
all_folder = Path("/home/matrixhasyou/mutt/exported_emails/all")
|
||||
if all_folder.exists():
|
||||
files = sorted(all_folder.glob("*.pdf"), key=lambda x: x.stat().st_mtime, reverse=True)[:10]
|
||||
for f in files:
|
||||
|
|
@ -678,11 +678,11 @@ async def emails_page(request: Request, username: str = Depends(get_current_admi
|
|||
else:
|
||||
latest_emails.append({"date": "-", "domain": "other", "subject": f.stem})
|
||||
|
||||
master_pdf_path = Path(config.MUTT_DIR) / "ALL_EMAILS_CONSOLIDATED.pdf"
|
||||
master_pdf_path = Path("/home/matrixhasyou/mutt/ALL_EMAILS_CONSOLIDATED.pdf")
|
||||
last_update = datetime.fromtimestamp(master_pdf_path.stat().st_mtime, tz=timezone.utc) if master_pdf_path.exists() else None
|
||||
|
||||
# Drive sync status
|
||||
sync_time_file = Path(config.MUTT_DIR) / "last_ai_sync.txt"
|
||||
sync_time_file = Path("/home/matrixhasyou/mutt/last_ai_sync.txt")
|
||||
last_sync_time = None
|
||||
if sync_time_file.exists():
|
||||
try:
|
||||
|
|
@ -691,45 +691,17 @@ async def emails_page(request: Request, username: str = Depends(get_current_admi
|
|||
except:
|
||||
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={
|
||||
"username": username, "audit_data": audit_data,
|
||||
"last_log": last_log, "latest_emails": latest_emails,
|
||||
"has_master_pdf": master_pdf_path.exists(), "last_update": last_update,
|
||||
"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
|
||||
"last_sync_time": last_sync_time, "fmt_local": fmt_local
|
||||
})
|
||||
|
||||
@app.post("/api/emails/run_full_sync")
|
||||
async def api_emails_run_full_sync(username: str = Depends(get_current_admin)):
|
||||
master_script = str(Path(config.MUTT_DIR) / "master_ai_sync.py")
|
||||
python_path = sys.executable
|
||||
master_script = "/home/matrixhasyou/mutt/master_ai_sync.py"
|
||||
python_path = "/usr/bin/python3"
|
||||
|
||||
if not os.path.exists(master_script):
|
||||
return JSONResponse(status_code=404, content={"message": "Master sync script not found"})
|
||||
|
|
@ -743,9 +715,9 @@ async def api_emails_run_full_sync(username: str = Depends(get_current_admin)):
|
|||
|
||||
@app.post("/api/emails/sync_ai")
|
||||
async def api_emails_sync_ai(username: str = Depends(get_current_admin)):
|
||||
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 = sys.executable
|
||||
master_pdf = str(Path(config.MUTT_DIR) / "TOTAL_ARCHIVE_2025-2026.pdf")
|
||||
sync_script = "/home/matrixhasyou/domovoy_drive_sync.py"
|
||||
python_path = "/home/matrixhasyou/domovoy_bot/venv-google/bin/python"
|
||||
master_pdf = "/home/matrixhasyou/mutt/TOTAL_ARCHIVE_2025-2026.pdf"
|
||||
|
||||
if not os.path.exists(sync_script):
|
||||
return JSONResponse(status_code=404, content={"message": "Sync script not found"})
|
||||
|
|
@ -760,69 +732,19 @@ async def api_emails_sync_ai(username: str = Depends(get_current_admin)):
|
|||
@app.get("/api/emails/pipeline_status")
|
||||
async def api_emails_pipeline_status(username: str = Depends(get_current_admin)):
|
||||
import time
|
||||
status_file = Path(config.MUTT_DIR) / "pipeline_status.json"
|
||||
master_pdf_path = Path(config.MUTT_DIR) / "ALL_EMAILS_CONSOLIDATED.pdf"
|
||||
|
||||
# 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():
|
||||
status_file = Path("/home/matrixhasyou/mutt/pipeline_status.json")
|
||||
if not status_file.exists():
|
||||
return JSONResponse(content={"status": "idle", "step": "Неактивен", "updated_at": "-", "new_emails": 0, "total_emails": 0, "pdf_size_mb": 0.0})
|
||||
try:
|
||||
with open(status_file) as f:
|
||||
file_data = json.load(f)
|
||||
data.update(file_data)
|
||||
|
||||
data = json.load(f)
|
||||
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)
|
||||
except Exception as e:
|
||||
return JSONResponse(content={"status": "error", "message": str(e)})
|
||||
|
||||
@app.get("/infra", response_class=HTMLResponse)
|
||||
async def get_infra_page(request: Request, username: str = Depends(get_current_admin)):
|
||||
|
|
@ -844,9 +766,9 @@ async def get_infra_page(request: Request, username: str = Depends(get_current_a
|
|||
|
||||
@app.post("/api/infra/run_speedtest")
|
||||
async def api_run_speedtest(username: str = Depends(get_current_admin)):
|
||||
script_path = "/app/infra/speedtest_monitor.py" if os.path.exists("/app/infra/speedtest_monitor.py") else "/home/matrixhasyou/infra/speedtest_monitor.py"
|
||||
script_path = "/home/matrixhasyou/infra/speedtest_monitor.py"
|
||||
try:
|
||||
subprocess.Popen([sys.executable if os.path.exists("/app/infra/speedtest_monitor.py") else "python3", script_path], start_new_session=True)
|
||||
subprocess.Popen(["python3", script_path], start_new_session=True)
|
||||
return JSONResponse(content={"message": "Тест скорости запущен в фоне. Результаты появятся через минуту."})
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=500, content={"message": str(e)})
|
||||
|
|
@ -859,8 +781,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"])
|
||||
|
||||
# 2. Запуск PDF-конвейера
|
||||
pipeline_script = str(Path(__file__).resolve().parent.parent / "services" / "gis_pdf_pipeline" / "pipeline_main.py")
|
||||
venv_python = sys.executable
|
||||
pipeline_script = "/home/matrixhasyou/domovoy_bot/services/gis_pdf_pipeline/pipeline_main.py"
|
||||
venv_python = "/home/matrixhasyou/domovoy_bot/services/gis_pdf_pipeline/venv/bin/python3"
|
||||
subprocess.Popen([venv_python, pipeline_script], start_new_session=True)
|
||||
|
||||
return JSONResponse(content={"message": "Процесс ГИС ЖКХ запущен (Жатва + PDF). Следите за Telegram."})
|
||||
|
|
@ -884,28 +806,6 @@ async def api_gis_status(username: str = Depends(get_current_admin)):
|
|||
except Exception as 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__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
{% block nav_emails %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- ЗАГОЛОВОК -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2 style="color: var(--matrix-green); text-shadow: 0 0 10px var(--matrix-green);">
|
||||
|
|
@ -16,84 +15,17 @@
|
|||
</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="col-md-4">
|
||||
<!-- КАРТОЧКА УПРАВЛЕНИЯ -->
|
||||
<div class="card mb-4 shadow">
|
||||
<div class="card-header bg-dark border-bottom border-secondary text-success font-monospace">
|
||||
[CONTROL] УПРАВЛЕНИЕ_АРХИВОМ
|
||||
</div>
|
||||
<div class="card-body text-center py-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">АРХИВ_PDF (NOTEBOOK_LM)</div>
|
||||
<div class="card-body text-center d-flex flex-column justify-content-center">
|
||||
{% if has_master_pdf %}
|
||||
<i class="bi bi-file-earmark-pdf-fill display-2 text-danger mb-3"></i>
|
||||
<h6 class="text-white">ALL_EMAILS_CONSOLIDATED.pdf</h6>
|
||||
<p class="small opacity-50 mb-4">Архив готов к использованию в NotebookLM</p>
|
||||
<i class="bi bi-file-earmark-pdf-fill display-1 text-danger mb-3"></i>
|
||||
<h5>ALL_EMAILS_CONSOLIDATED.pdf</h5>
|
||||
<p class="small opacity-75"><b>Файл создан:</b> {{ fmt_local(last_update) }}</p>
|
||||
<p class="small opacity-75"><b>G-Drive Sync:</b> {{ fmt_local(last_sync_time) if last_sync_time else 'НЕИЗВЕСТНО' }}</p>
|
||||
|
||||
<button id="fullSyncBtn" class="btn btn-warning w-100 mb-2">
|
||||
<i class="bi bi- lightning-charge-fill"></i> ТУРБО СИНХРОНИЗАЦИЯ
|
||||
|
|
@ -109,82 +41,39 @@
|
|||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<i class="bi bi-file-earmark-x display-2 opacity-20 mb-3"></i>
|
||||
<p class="text-warning">Архив еще не сформирован</p>
|
||||
<button id="fullSyncBtn" class="btn btn-warning w-100 mb-2">
|
||||
<i class="bi bi-lightning-charge-fill"></i> ЗАПУСТИТЬ ПЕРВЫЙ СБОР
|
||||
</button>
|
||||
{% endif %}
|
||||
</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] Запуски еще не производились.
|
||||
<i class="bi bi-file-earmark-x display-1 opacity-20 mb-3"></i>
|
||||
<p>Архив еще не сформирован</p>
|
||||
<button class="btn btn-outline-secondary w-100" disabled>НЕДОСТУПНО</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ПРАВАЯ КОЛОНКА: ТАБЛИЦА ВЕДОМСТВ -->
|
||||
<!-- ПОСЛЕДНЯЯ АКТИВНОСТЬ -->
|
||||
<div class="col-md-8">
|
||||
<div class="card h-100 shadow">
|
||||
<div class="card-header bg-dark border-bottom border-secondary text-success font-monospace">
|
||||
[STATS] СТАТИСТИКА_ПО_ВЕДОМСТВАМ_И_ОРГАНАМ
|
||||
</div>
|
||||
<div class="card h-100">
|
||||
<div class="card-header">СТАТИСТИКА_ВЕДОМСТВ</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 700px; overflow-y: auto;">
|
||||
<table class="table table-dark table-hover mb-0 align-middle">
|
||||
<thead class="sticky-top bg-dark">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ведомство / Домен</th>
|
||||
<th class="text-center" style="width: 100px;">Вх.</th>
|
||||
<th class="text-center" style="width: 100px;">Исх.</th>
|
||||
<th style="width: 180px;">Последнее письмо</th>
|
||||
<th class="text-center">Вх.</th>
|
||||
<th class="text-center">Исх.</th>
|
||||
<th>Последнее письмо</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for domain in audit_data %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="text-success fw-bold">{{ domain.domain }}</span>
|
||||
<br><small class="opacity-50 text-white">{{ domain.description }}</small>
|
||||
<span class="text-success">{{ domain.domain }}</span>
|
||||
<br><small class="opacity-50">{{ domain.description }}</small>
|
||||
</td>
|
||||
<td class="text-center font-monospace text-success fw-bold">{{ domain.inbound_count }}</td>
|
||||
<td class="text-center font-monospace text-info fw-bold">{{ domain.outbound_count }}</td>
|
||||
<td class="font-monospace">{{ domain.last_email_date.strftime('%d.%m.%Y %H:%M') if domain.last_email_date else '-' }}</td>
|
||||
<td class="text-center">{{ domain.inbound_count }}</td>
|
||||
<td class="text-center">{{ domain.outbound_count }}</td>
|
||||
<td>{{ domain.last_email_date.strftime('%d.%m.%Y') if domain.last_email_date else '-' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
|
|
@ -199,29 +88,27 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- СВЕЖИЕ ПОСТУПЛЕНИЯ В АРХИВ -->
|
||||
<div class="row mt-4">
|
||||
<!-- СВЕЖИЕ ПОСТУПЛЕНИЯ -->
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-dark border-bottom border-secondary text-info font-monospace">
|
||||
[FEED] СВЕЖИЕ_ПОСТУПЛЕНИЯ_В_АРХИВ (ПОСЛЕДНИЕ 10)
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">СВЕЖИЕ_ПОСТУПЛЕНИЯ_В_АРХИВ (ПОСЛЕДНИЕ 10)</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0" style="font-size: 0.85rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 180px;">Дата</th>
|
||||
<th style="width: 200px;">Ведомство</th>
|
||||
<th>Дата</th>
|
||||
<th>Ведомство</th>
|
||||
<th>Тема письма</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for email in latest_emails %}
|
||||
<tr>
|
||||
<td><code class="text-info font-monospace">{{ email.date }}</code></td>
|
||||
<td><span class="badge bg-secondary font-monospace">{{ email.domain }}</span></td>
|
||||
<td class="text-truncate text-white" style="max-width: 500px;">{{ email.subject }}</td>
|
||||
<td><code class="text-info">{{ email.date }}</code></td>
|
||||
<td><span class="badge bg-secondary">{{ email.domain }}</span></td>
|
||||
<td class="text-truncate" style="max-width: 500px;">{{ email.subject }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
|
|
@ -236,7 +123,49 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SCRIPTS -->
|
||||
<div class="row mt-4">
|
||||
<!-- КАРТОЧКА ДИНАМИЧЕСКОГО СТАТУСА ПАЙПЛАЙНА -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border-primary" id="pipelineStatusCard" style="display: none;">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>>_ ТЕКУЩИЙ_СТАТУС_ПАЙПЛАЙНА</span>
|
||||
<span class="spinner-grow spinner-grow-sm text-primary" id="pipelineSpinner" role="status"></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="p-3 bg-black border border-primary font-monospace small">
|
||||
[STEP] Шаг: <span id="pipelineStep" class="text-info">Ожидание</span> <br>
|
||||
[TIME] Обновлено: <span id="pipelineTime">-</span> <br>
|
||||
<div class="progress my-3" style="height: 10px; background-color: #111;">
|
||||
<div id="pipelineProgress" class="progress-bar progress-bar-striped progress-bar-animated bg-primary" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
[DATA] Новых писем: <span id="pipelineNewEmails" class="text-success">0</span> <br>
|
||||
[DATA] Всего в архиве: <span id="pipelineTotalEmails">0</span> <br>
|
||||
[DATA] Размер архива: <span id="pipelineSize">0.00 MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ЛОГ ПОСЛЕДНЕГО ЗАПУСКА -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">ЛОГ_ПОСЛЕДНЕГО_ЗАПУСКА (БАЗА ДАННЫХ)</div>
|
||||
<div class="card-body">
|
||||
<div class="p-3 bg-black border border-success font-monospace small">
|
||||
{% if last_log %}
|
||||
[TIME] Начат: {{ fmt_local(last_log.start_time) }} <br>
|
||||
[TIME] Завершен: {{ fmt_local(last_log.end_time) if last_log.end_time else 'В ПРОЦЕССЕ...' }} <br>
|
||||
[STATUS] Статус: <span class="{% if last_log.status == 'success' %}text-success{% else %}text-warning{% endif %}">{{ last_log.status.upper() }}</span> <br>
|
||||
[DATA] Новых писем: {{ last_log.new_emails_count }}
|
||||
{% else %}
|
||||
[INFO] Запуски еще не производились.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function checkPipelineStatus() {
|
||||
try {
|
||||
|
|
@ -252,44 +181,12 @@ async function checkPipelineStatus() {
|
|||
const size = document.getElementById('pipelineSize');
|
||||
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') {
|
||||
card.className = "card border-primary mb-4 shadow";
|
||||
step.className = "text-info fw-bold";
|
||||
card.style.display = 'block';
|
||||
card.className = "card border-primary h-100";
|
||||
step.className = "text-info";
|
||||
step.textContent = data.step;
|
||||
time.textContent = data.updated_at;
|
||||
spinner.style.display = 'inline-block';
|
||||
|
||||
// Динамический прогресс
|
||||
|
|
@ -300,40 +197,42 @@ async function checkPipelineStatus() {
|
|||
progress.style.width = width;
|
||||
progress.className = "progress-bar progress-bar-striped progress-bar-animated bg-primary";
|
||||
|
||||
// Опрашиваем часто при работе
|
||||
newEmails.textContent = data.new_emails;
|
||||
totalEmails.textContent = data.total_emails;
|
||||
size.textContent = data.pdf_size_mb + ' MB';
|
||||
|
||||
// Опрашиваем часто
|
||||
setTimeout(checkPipelineStatus, 2000);
|
||||
} else if (data.status === 'success') {
|
||||
card.className = "card border-success mb-4 shadow";
|
||||
step.className = "text-success fw-bold";
|
||||
card.style.display = 'block';
|
||||
card.className = "card border-success h-100";
|
||||
step.className = "text-success";
|
||||
step.innerHTML = '✅ Завершен успешно';
|
||||
time.textContent = data.updated_at;
|
||||
spinner.style.display = 'none';
|
||||
progress.style.width = '100%';
|
||||
progress.className = "progress-bar bg-success";
|
||||
|
||||
// Опрашиваем реже в простое
|
||||
setTimeout(checkPipelineStatus, 15000);
|
||||
newEmails.textContent = data.new_emails;
|
||||
totalEmails.textContent = data.total_emails;
|
||||
size.textContent = data.pdf_size_mb + ' MB';
|
||||
|
||||
// Скроем статус через 10 секунд
|
||||
setTimeout(() => { card.style.display = 'none'; }, 10000);
|
||||
} else if (data.status === 'error' || data.status === 'stuck') {
|
||||
card.className = "card border-danger mb-4 shadow";
|
||||
step.className = "text-danger fw-bold";
|
||||
step.innerHTML = '❌ ' + (data.step || 'Ошибка выполнения');
|
||||
card.style.display = 'block';
|
||||
card.className = "card border-danger h-100";
|
||||
step.className = "text-danger";
|
||||
step.innerHTML = '❌ ' + data.step;
|
||||
time.textContent = data.updated_at;
|
||||
spinner.style.display = 'none';
|
||||
progress.style.width = '100%';
|
||||
progress.className = "progress-bar bg-danger";
|
||||
|
||||
setTimeout(checkPipelineStatus, 15000);
|
||||
} else {
|
||||
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);
|
||||
card.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get status:', e);
|
||||
setTimeout(checkPipelineStatus, 15000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,30 +78,6 @@
|
|||
</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="card bg-dark border-warning">
|
||||
<div class="card-header text-warning">ДЕЙСТВИЯ</div>
|
||||
|
|
@ -179,62 +155,9 @@ 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('refreshSudrfStatusBtn').addEventListener('click', refreshSudrfStatus);
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
refreshGisStatus();
|
||||
refreshSudrfStatus();
|
||||
});
|
||||
|
||||
window.addEventListener('load', refreshGisStatus);
|
||||
// Авто-обновление раз в 30 секунд
|
||||
setInterval(() => {
|
||||
refreshGisStatus();
|
||||
refreshSudrfStatus();
|
||||
}, 30000);
|
||||
setInterval(refreshGisStatus, 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Reference in a new issue