feat(ui): Lissyara UNIX-Minimalism redesign and NotebookLM podcast integration
This commit is contained in:
parent
6a4610748e
commit
c49003bf39
8 changed files with 2975 additions and 257 deletions
|
|
@ -12,6 +12,7 @@ services:
|
|||
- TZ=Europe/Ulyanovsk
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./.env:/app/.env:ro
|
||||
- ./main.py:/app/main.py
|
||||
- ./logs:/app/logs
|
||||
- ./exports:/app/exports
|
||||
|
|
@ -35,14 +36,19 @@ services:
|
|||
environment:
|
||||
- PYTHONPATH=/app:/app/services
|
||||
- GOOGLE_CREDS_FILE=/home/matrixhasyou/domovoy_google_creds.json
|
||||
- GOOGLE_API_KEY=AIzaSyAJMfQ_mRZ1picRXVFNaeFJ7fpAkZxUKyk
|
||||
- TZ=Europe/Ulyanovsk
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./.env:/app/.env:ro
|
||||
- ./web:/app/web
|
||||
- ./podcasts:/app/podcasts
|
||||
- ./logs:/app/logs
|
||||
- ./exports:/app/exports
|
||||
- ./data:/app/data
|
||||
- ./handlers:/app/handlers
|
||||
- ./services:/app/services
|
||||
- /home/matrixhasyou/mutt:/home/matrixhasyou/mutt:ro
|
||||
- ../libs/swarmlib:/app/swarmlib:ro
|
||||
- /home/matrixhasyou/domovoy_google_creds.json:/home/matrixhasyou/domovoy_google_creds.json:ro
|
||||
logging:
|
||||
|
|
|
|||
|
|
@ -1,40 +1,32 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI Legal Engine 2.0 (Юридический Ядерный Щит Домового).
|
||||
Работает на базе Gemini 2.0 API с контекстным окном до 2M токенов.
|
||||
Обеспечивает:
|
||||
1. Мгновенные ответы жильцам (на простом понятном языке) и юристам (с нормами права).
|
||||
2. Нативный мультимодальный прием голосовых сообщений (без потерь и галлюцинаций Whisper).
|
||||
3. Интеграцию с базой писем и судебных дел МКД (Ленком 37).
|
||||
4. Строгий шлюз безопасности Zero-Spam для чата дома.
|
||||
Интеллектуальное юридическое ядро МКД «Домовой 2.0» (AI Legal Engine)
|
||||
Работает на базе Gemini 2.5 Flash / Pro:
|
||||
- Нативный слух (Gemini Multimodal Audio) без Whisper
|
||||
- Прямой асинхронный REST API (высокая отказоустойчивость без конфликтов pydantic)
|
||||
- Разграничение ролей: resident (соседский помощник), lawyer (претензионно-судебный), admin (глубокий аудит)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import aiohttp
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Загружаем окружение
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
load_dotenv(PROJECT_ROOT / ".env")
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
log = logging.getLogger("AILegalEngine")
|
||||
|
||||
API_KEY = os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
||||
from pathlib import Path
|
||||
|
||||
log = logging.getLogger("ai_legal_engine")
|
||||
class AILegalEngine:
|
||||
def __init__(self):
|
||||
self.api_key = API_KEY
|
||||
raw_key = os.getenv("GOOGLE_API_KEY", "AIzaSyAJMfQ_mRZ1picRXVFNaeFJ7fpAkZxUKyk")
|
||||
self.api_key = raw_key.strip("\"'") if raw_key else ""
|
||||
self.client = None
|
||||
self.model_name = "gemini-2.5-flash"
|
||||
self.deep_model_name = "gemini-2.5-pro"
|
||||
self._init_client()
|
||||
self._init_sdk_client()
|
||||
|
||||
def _init_client(self):
|
||||
def _init_sdk_client(self):
|
||||
if not self.api_key:
|
||||
log.warning("Gemini API Key missing! AI Legal Engine will run in offline mode.")
|
||||
return
|
||||
|
|
@ -42,9 +34,9 @@ class AILegalEngine:
|
|||
try:
|
||||
from google import genai
|
||||
self.client = genai.Client(api_key=self.api_key)
|
||||
log.info("Gemini 2.0 Client initialized successfully.")
|
||||
log.info("Gemini SDK Client initialized.")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to initialize google.genai: {e}")
|
||||
log.info(f"Using high-performance direct REST engine (SDK not active: {e})")
|
||||
|
||||
def get_role_instructions(self, role: str) -> str:
|
||||
if role == "resident":
|
||||
|
|
@ -59,32 +51,69 @@ class AILegalEngine:
|
|||
)
|
||||
elif role == "lawyer":
|
||||
return (
|
||||
"РОЛЬ ДЛЯ ОТВЕТА: 'Судебный юрист в сфере ЖКХ и градостроительства'.\n"
|
||||
"СТИЛЬ: Строгий, процессуально выверенный, юридически безупречный.\n"
|
||||
"РОЛЬ ДЛЯ ОТВЕТА: 'Профессиональный юрист по жилищному праву и защите прав потребителей'.\n"
|
||||
"СТИЛЬ: Строгий, юридически выверенный, аргументированный.\n"
|
||||
"ТРЕБОВАНИЯ:\n"
|
||||
"1. Обязательные ссылки на статьи Жилищного кодекса РФ, ГК РФ, Постановления Правительства РФ № 354, № 491, № 416.\n"
|
||||
"2. Указание на сроки удовлетворения требований по закону.\n"
|
||||
"3. Упоминание релевантных материалов судебных дел дома (Ленком 37) и ответов Прокуратуры/ГЖИ Ульяновской области при наличии.\n"
|
||||
"4. Проект мотивированной досудебной претензии или жалобы в надзорные органы."
|
||||
"1. Четкая правовая квалификация действий/бездействия лиц.\n"
|
||||
"2. Прямые ссылки на статьи законов (ЖК РФ, ГК РФ, 59-ФЗ, ПП РФ № 354, ПП РФ № 491, Госстрой № 170, СанПиН 2.1.3684-21).\n"
|
||||
"3. Оценка перспектив в суде или надзорных органах (ГЖИ, Прокуратура, Роспотребнадзор).\n"
|
||||
"4. Готовый проект претензии / жалобы с правовой мотивировкой."
|
||||
)
|
||||
elif role == "admin":
|
||||
return (
|
||||
"РОЛЬ ДЛЯ ОТВЕТА: 'Стратегический аналитик для Председателя совета дома (Александра Тырина)'.\n"
|
||||
"СТИЛЬ: Аналитическая записка, системный разбор, поиск противоречий.\n"
|
||||
"РОЛЬ ДЛЯ ОТВЕТА: 'Главный аналитик Совета Дома и стратег судебных процессов'.\n"
|
||||
"СТИЛЬ: Стратегический, системный, аналитический.\n"
|
||||
"ТРЕБОВАНИЯ:\n"
|
||||
"1. Сопоставление фактов: что чиновники/УК утверждали ранее и что говорят сейчас.\n"
|
||||
"2. Поиск финансовых нестыковок, признаков нецелевого расходования средств, фальсификации протоколов ОСС.\n"
|
||||
"3. Контроль сроков по 59-ФЗ и процессуальных дедлайнов.\n"
|
||||
"4. Стратегические рекомендации: куда бить (Генпрокуратура, Следственный комитет, суд, ФАС)."
|
||||
"1. Полный расклад с точки зрения системной выгоды для всего дома.\n"
|
||||
"2. Выявление уловок УК, скрытых манипуляций со сметами, протоколами ОСС или объемами коммунальных ресурсов.\n"
|
||||
"3. Сравнение с прецедентами и судебной практикой Ульяновского областного суда и Верховного Суда РФ.\n"
|
||||
"4. Стратегия принуждения госорганов к исполнению служебных обязанностей по 59-ФЗ и Приказу ГП № 45."
|
||||
)
|
||||
return "Отвечай вежливо и по делу."
|
||||
|
||||
async def _query_rest_api(self, prompt: str, system_instruction: str, model: str, audio_path: str = None) -> str:
|
||||
"""Прямой асинхронный REST запрос к Google Gemini API."""
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={self.api_key}"
|
||||
parts = []
|
||||
|
||||
# Аудио вложение
|
||||
if audio_path and os.path.exists(audio_path):
|
||||
with open(audio_path, "rb") as af:
|
||||
audio_b64 = base64.b64encode(af.read()).decode("utf-8")
|
||||
parts.append({
|
||||
"inlineData": {
|
||||
"mimeType": "audio/ogg",
|
||||
"data": audio_b64
|
||||
}
|
||||
})
|
||||
if not prompt:
|
||||
prompt = "Внимательно прослушай аудиосообщение жильца дома. Точно выдели юридическую суть и дай обоснованный ответ."
|
||||
|
||||
parts.append({"text": prompt})
|
||||
|
||||
payload = {
|
||||
"system_instruction": {
|
||||
"parts": [{"text": system_instruction}]
|
||||
},
|
||||
"contents": [
|
||||
{"parts": parts}
|
||||
],
|
||||
"generationConfig": {
|
||||
"temperature": 0.2 if model == self.deep_model_name else 0.4
|
||||
}
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=45)) as resp:
|
||||
if resp.status != 200:
|
||||
err_text = await resp.text()
|
||||
raise RuntimeError(f"Gemini API HTTP {resp.status}: {err_text[:300]}")
|
||||
data = await resp.json()
|
||||
return data["candidates"][0]["content"]["parts"][0]["text"]
|
||||
|
||||
async def query(self, prompt: str, role: str = "resident", audio_path: str = None) -> dict:
|
||||
"""
|
||||
Основной метод запроса к юридическому ядру.
|
||||
Поддерживает текст, аудиофайл (голос жильца) и разграничение ролей.
|
||||
"""
|
||||
if not self.client:
|
||||
"""Основной метод запроса к юридическому ядру."""
|
||||
if not self.api_key:
|
||||
return {
|
||||
"success": False,
|
||||
"text": "ИИ-модуль временно недоступен. Проверьте API ключ.",
|
||||
|
|
@ -99,40 +128,22 @@ class AILegalEngine:
|
|||
f"{self.get_role_instructions(role)}"
|
||||
)
|
||||
|
||||
model_to_use = self.deep_model_name if role in ("lawyer", "admin") else self.model_name
|
||||
|
||||
try:
|
||||
contents = []
|
||||
|
||||
# 1. Если передано голосовое сообщение — добавляем аудио напрямую
|
||||
if audio_path and os.path.exists(audio_path):
|
||||
log.info(f"Processing native audio input: {audio_path}")
|
||||
# Загружаем аудио через Gemini Files API
|
||||
uploaded_audio = self.client.files.upload(file=audio_path)
|
||||
contents.append(uploaded_audio)
|
||||
if not prompt:
|
||||
prompt = "Внимательно прослушай голосовое сообщение жильца. Точно расшифруй юридическую суть и дай ответ согласно твоей роли."
|
||||
|
||||
# 2. Добавляем текстовый промпт
|
||||
contents.append(prompt)
|
||||
|
||||
# Выбор модели
|
||||
model_to_use = self.deep_model_name if role == "admin" else self.model_name
|
||||
|
||||
# Запрос к Gemini
|
||||
response = self.client.models.generate_content(
|
||||
# Используем прямой REST (наивысшая надежность без конфликтов библиотек)
|
||||
reply_text = await self._query_rest_api(
|
||||
prompt=prompt,
|
||||
system_instruction=system_instruction,
|
||||
model=model_to_use,
|
||||
contents=contents,
|
||||
config={
|
||||
"system_instruction": system_instruction,
|
||||
"temperature": 0.2 if role in ("lawyer", "admin") else 0.4,
|
||||
}
|
||||
audio_path=audio_path
|
||||
)
|
||||
|
||||
latency = (datetime.now() - start_time).total_seconds()
|
||||
reply_text = response.text.strip() if response.text else "Ответ не сформирован."
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"text": reply_text,
|
||||
"text": reply_text.strip(),
|
||||
"answer": reply_text.strip(),
|
||||
"role": role,
|
||||
"latency_sec": round(latency, 2),
|
||||
"model": model_to_use
|
||||
|
|
@ -140,28 +151,24 @@ class AILegalEngine:
|
|||
|
||||
except Exception as e:
|
||||
latency = (datetime.now() - start_time).total_seconds()
|
||||
log.error(f"Gemini API Error: {e}")
|
||||
log.error(f"Gemini Error: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"text": f"Ошибка обработки запроса: {e}",
|
||||
"text": f"Ошибка обработки запроса ИИ: {e}",
|
||||
"role": role,
|
||||
"latency_sec": round(latency, 2)
|
||||
}
|
||||
|
||||
# Глобальный инстанс
|
||||
# Глобальный синглтон юридического ядра для роутеров бота
|
||||
legal_engine = AILegalEngine()
|
||||
|
||||
if __name__ == "__main__":
|
||||
async def test():
|
||||
print("Testing AI Legal Engine 2.0...")
|
||||
res = await legal_engine.query("Течет крыша на 9 этаже, УК не реагирует 4 дня. Что делать?", role="resident")
|
||||
print("\n=== RESIDENT RESPONSE ===")
|
||||
print(f"Latency: {res['latency_sec']}s")
|
||||
print(res["text"])
|
||||
|
||||
res_law = await legal_engine.query("Течет крыша на 9 этаже, УК не реагирует 4 дня. Нормативная база?", role="lawyer")
|
||||
print("\n=== LAWYER RESPONSE ===")
|
||||
print(f"Latency: {res_law['latency_sec']}s")
|
||||
print(res_law["text"])
|
||||
engine = AILegalEngine()
|
||||
res = await engine.query("Какие сроки ответа прокуратуры по закону 59-ФЗ?", role="resident")
|
||||
print("Success:", res["success"])
|
||||
print("Latency:", res["latency_sec"])
|
||||
print("Model:", res.get("model"))
|
||||
print("Text snippet:\n", res["text"][:300])
|
||||
|
||||
asyncio.run(test())
|
||||
|
|
|
|||
206
services/notebooklm_podcast_generator.py
Normal file
206
services/notebooklm_podcast_generator.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Сервис интеграции с Google NotebookLM и генерации аудио-подкастов (Audio Overviews)
|
||||
для проекта «Домовой 2.0: Юридический Ядерный Щит МКД».
|
||||
|
||||
Возможности:
|
||||
1. Автоматическая синхронизация официальных источников (полные законы РФ + тома почты с ЭЦП).
|
||||
2. Вызов генерации двухголосых аудио-обзоров NotebookLM (Audio Overview).
|
||||
3. Скачивание аудио-подкастов (.m4a/.mp3) в локальное хранилище.
|
||||
4. Fallback: Генерация детального сценария аналитического подкаста через Gemini 2.5 Pro.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
logger = logging.getLogger("notebooklm_podcast")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
NOTEBOOKLM_BIN = "/home/matrixhasyou/swarm-services/domovoy-bot/venv-ai/bin/notebooklm"
|
||||
PODCASTS_DIR = Path("/home/matrixhasyou/swarm-services/domovoy-bot/podcasts")
|
||||
LEGAL_SOURCES_DIR = Path("/home/matrixhasyou/mutt/legal_sources")
|
||||
MEGA_VOLUMES_DIR = Path("/home/matrixhasyou/mutt/mega_volumes")
|
||||
DEFAULT_NOTEBOOK_NAME = "МКД Ливанова 37 (Суды, Прокуратура, ЖКХ)"
|
||||
|
||||
class NotebookLMPodcastGenerator:
|
||||
def __init__(self, notebook_name: str = DEFAULT_NOTEBOOK_NAME):
|
||||
self.notebook_name = notebook_name
|
||||
self.cli_bin = NOTEBOOKLM_BIN
|
||||
PODCASTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def is_authenticated(self) -> bool:
|
||||
"""Проверяет, авторизован ли CLI NotebookLM."""
|
||||
storage_file = Path(os.path.expanduser("~/.notebooklm/profiles/default/storage_state.json"))
|
||||
if not storage_file.exists() and not os.getenv("NOTEBOOKLM_AUTH_JSON"):
|
||||
return False
|
||||
try:
|
||||
res = subprocess.run([self.cli_bin, "list"], capture_output=True, text=True, timeout=10)
|
||||
return res.returncode == 0 and "Not logged in" not in res.stderr and "Not logged in" not in res.stdout
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def list_notebooks(self) -> List[Dict[str, str]]:
|
||||
"""Возвращает список блокнотов из NotebookLM."""
|
||||
if not self.is_authenticated():
|
||||
return []
|
||||
try:
|
||||
res = subprocess.run([self.cli_bin, "list", "--json"], capture_output=True, text=True, timeout=15)
|
||||
if res.returncode == 0:
|
||||
data = json.loads(res.stdout)
|
||||
return data if isinstance(data, list) else []
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения списка блокнотов: {e}")
|
||||
return []
|
||||
|
||||
def get_or_create_notebook(self) -> Optional[str]:
|
||||
"""Находит существующий блокнот дома или создает новый."""
|
||||
notebooks = self.list_notebooks()
|
||||
for nb in notebooks:
|
||||
if self.notebook_name.lower() in nb.get("title", "").lower():
|
||||
return nb.get("id")
|
||||
|
||||
# Создаем новый
|
||||
try:
|
||||
res = subprocess.run([self.cli_bin, "create", self.notebook_name], capture_output=True, text=True, timeout=20)
|
||||
if res.returncode == 0:
|
||||
# Находим созданный
|
||||
for nb in self.list_notebooks():
|
||||
if self.notebook_name.lower() in nb.get("title", "").lower():
|
||||
return nb.get("id")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания блокнота: {e}")
|
||||
return None
|
||||
|
||||
def upload_source_file(self, file_path: str, notebook_id: Optional[str] = None) -> bool:
|
||||
"""Загружает PDF-файл (закон или том почты) в NotebookLM."""
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"Файл не найден: {file_path}")
|
||||
return False
|
||||
|
||||
args = [self.cli_bin, "source", "add", file_path]
|
||||
if notebook_id:
|
||||
args.extend(["--notebook", notebook_id])
|
||||
|
||||
try:
|
||||
res = subprocess.run(args, capture_output=True, text=True, timeout=180)
|
||||
return res.returncode == 0
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка загрузки источника {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def sync_all_sources(self) -> Dict[str, Any]:
|
||||
"""Синхронизирует все полные законы и тома мега-PDF с NotebookLM."""
|
||||
if not self.is_authenticated():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "NotebookLM CLI не авторизован. Требуется импорт профиля в ~/.notebooklm/profiles/default/"
|
||||
}
|
||||
|
||||
nb_id = self.get_or_create_notebook()
|
||||
if not nb_id:
|
||||
return {"success": False, "error": "Не удалось создать или получить блокнот дома"}
|
||||
|
||||
uploaded = []
|
||||
# 1. Законы РФ
|
||||
if LEGAL_SOURCES_DIR.exists():
|
||||
for pdf in sorted(LEGAL_SOURCES_DIR.glob("*Полный*.pdf")):
|
||||
if self.upload_source_file(str(pdf), nb_id):
|
||||
uploaded.append(pdf.name)
|
||||
|
||||
# 2. Мега-тома писем
|
||||
if MEGA_VOLUMES_DIR.exists():
|
||||
for pdf in sorted(MEGA_VOLUMES_DIR.glob("*.pdf")):
|
||||
if self.upload_source_file(str(pdf), nb_id):
|
||||
uploaded.append(pdf.name)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"notebook_id": nb_id,
|
||||
"uploaded_sources": uploaded
|
||||
}
|
||||
|
||||
def generate_audio_overview(self, notebook_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Запускает генерацию двухголосного подкаста в NotebookLM."""
|
||||
if not self.is_authenticated():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "NotebookLM не авторизован. Доступен fallback-режим генерации через Gemini 2.5."
|
||||
}
|
||||
|
||||
args = [self.cli_bin, "generate", "audio"]
|
||||
if notebook_id:
|
||||
args.extend(["--notebook", notebook_id])
|
||||
|
||||
try:
|
||||
res = subprocess.run(args, capture_output=True, text=True, timeout=300)
|
||||
if res.returncode == 0:
|
||||
return {"success": True, "output": res.stdout.strip()}
|
||||
return {"success": False, "error": res.stderr.strip() or res.stdout.strip()}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def download_latest_audio(self, output_filename: str = "latest_podcast.m4a") -> Optional[str]:
|
||||
"""Скачивает готовый сгенерированный подкаст."""
|
||||
dest = PODCASTS_DIR / output_filename
|
||||
args = [self.cli_bin, "download", "audio", "--output", str(dest)]
|
||||
try:
|
||||
res = subprocess.run(args, capture_output=True, text=True, timeout=120)
|
||||
if res.returncode == 0 and dest.exists():
|
||||
return str(dest)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка скачивания аудио: {e}")
|
||||
return None
|
||||
|
||||
def generate_fallback_dialogue_script(self, context_summary: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Fallback генератор: при отсутствии сессии NotebookLM формирует
|
||||
полноценный драматический сценарий двухголосного подкаста через Gemini 2.5 Pro.
|
||||
"""
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
api_key = os.getenv("GOOGLE_API_KEY", "AIzaSyAJMfQ_mRZ1picRXVFNaeFJ7fpAkZxUKyk")
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
prompt = f"""Ты — ведущий сценарист и главный аналитик юридического аудио-подкаста дома «Ливанова 37: Голос Закона».
|
||||
Твоя задача — составить живой, увлекательный, юридически безупречный сценарий подкаста на 7-10 минут (в формате диалога двух экспертов):
|
||||
|
||||
Ведущие:
|
||||
1. Александр (Ведущий / Председатель Совета Дома) — задает острые вопросы жителей, приводит факты из писем госорганов, вскрывает уловки УК и прокуратуры.
|
||||
2. Майя (Юрист-аналитик по жилищному праву) — профессионально, с юмором и железобетонными ссылками на законы (ЖК РФ, 59-ФЗ, ПП 354, Госстрой 170) объясняет, в чем неправы чиновники и как жителям защитить свои права.
|
||||
|
||||
Контекст переписки и правовой базы дома:
|
||||
{context_summary}
|
||||
|
||||
Формат ответа:
|
||||
Заголовок подкаста.
|
||||
Текст реплик с пометками [Александр] и [Майя].
|
||||
В конце — четкие выводы и 3 практических шага для жителей дома.
|
||||
"""
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.5-pro",
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
temperature=0.4
|
||||
)
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"script": response.text,
|
||||
"engine": "gemini-2.5-pro-fallback"
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
generator = NotebookLMPodcastGenerator()
|
||||
print("NotebookLM Auth:", generator.is_authenticated())
|
||||
if not generator.is_authenticated():
|
||||
print("Running fallback test...")
|
||||
res = generator.generate_fallback_dialogue_script(
|
||||
"Судебные тяжбы с УК по перерасчету отопления, игнорирование прокуратурой Заволжского района сроков 59-ФЗ."
|
||||
)
|
||||
print("Generated script snippet:\n", res["script"][:400])
|
||||
125
web/app.py
125
web/app.py
|
|
@ -910,6 +910,131 @@ async def api_sudrf_status(username: str = Depends(get_current_admin)):
|
|||
except Exception as e:
|
||||
return JSONResponse(status_code=500, content={"message": str(e)})
|
||||
|
||||
@app.get("/legal", response_class=HTMLResponse)
|
||||
async def legal_page(request: Request, username: str = Depends(get_current_admin)):
|
||||
import fitz
|
||||
legal_dir = Path("/home/matrixhasyou/mutt/legal_sources")
|
||||
volumes_dir = Path("/home/matrixhasyou/mutt/mega_volumes")
|
||||
podcasts_dir = Path("/home/matrixhasyou/swarm-services/domovoy-bot/podcasts")
|
||||
|
||||
laws = []
|
||||
if legal_dir.exists():
|
||||
for f in sorted(legal_dir.glob("*.pdf")):
|
||||
if f.is_file() and not f.name.startswith("."):
|
||||
size_mb = f.stat().st_size / (1024 * 1024)
|
||||
size_formatted = f"{size_mb:.2f} МБ" if size_mb >= 1 else f"{f.stat().st_size / 1024:.1f} КБ"
|
||||
try:
|
||||
doc = fitz.open(f)
|
||||
pages = len(doc)
|
||||
except Exception:
|
||||
pages = 0
|
||||
title = f.stem.replace("_", " ").replace("Полный", "").strip()
|
||||
laws.append({
|
||||
"filename": f.name,
|
||||
"title": title,
|
||||
"pages": pages,
|
||||
"size_formatted": size_formatted
|
||||
})
|
||||
|
||||
mega_volumes = []
|
||||
if volumes_dir.exists():
|
||||
for f in sorted(volumes_dir.glob("*.pdf")):
|
||||
if f.is_file() and not f.name.startswith("."):
|
||||
size_mb = f.stat().st_size / (1024 * 1024)
|
||||
mega_volumes.append({
|
||||
"name": f.name,
|
||||
"size_formatted": f"{size_mb:.1f} МБ"
|
||||
})
|
||||
|
||||
latest_podcast = None
|
||||
if podcasts_dir.exists():
|
||||
podcast_files = sorted(podcasts_dir.glob("*.*"), key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
if podcast_files:
|
||||
p = podcast_files[0]
|
||||
latest_podcast = {
|
||||
"name": p.name,
|
||||
"size": f"{p.stat().st_size / (1024 * 1024):.1f} МБ",
|
||||
"date": datetime.fromtimestamp(p.stat().st_mtime).strftime("%d.%m.%Y %H:%M")
|
||||
}
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="legal.html",
|
||||
context={
|
||||
"username": username,
|
||||
"laws": laws,
|
||||
"mega_volumes": mega_volumes,
|
||||
"latest_podcast": latest_podcast
|
||||
}
|
||||
)
|
||||
|
||||
@app.get("/api/legal/download/{filename}")
|
||||
async def api_download_law(filename: str, username: str = Depends(get_current_admin)):
|
||||
fpath = Path("/home/matrixhasyou/mutt/legal_sources") / filename
|
||||
if not fpath.exists() or not fpath.is_file():
|
||||
raise HTTPException(status_code=404, detail="Файл закона не найден")
|
||||
return FileResponse(fpath, filename=filename, media_type="application/pdf")
|
||||
|
||||
@app.get("/api/legal/download_volume/{filename}")
|
||||
async def api_download_volume(filename: str, username: str = Depends(get_current_admin)):
|
||||
fpath = Path("/home/matrixhasyou/mutt/mega_volumes") / filename
|
||||
if not fpath.exists() or not fpath.is_file():
|
||||
raise HTTPException(status_code=404, detail="Файл тома переписки не найден")
|
||||
return FileResponse(fpath, filename=filename, media_type="application/pdf")
|
||||
|
||||
@app.get("/api/legal/podcasts/{filename}")
|
||||
async def api_download_podcast(filename: str, username: str = Depends(get_current_admin)):
|
||||
fpath = Path("/home/matrixhasyou/swarm-services/domovoy-bot/podcasts") / filename
|
||||
if not fpath.exists() or not fpath.is_file():
|
||||
raise HTTPException(status_code=404, detail="Подкаст не найден")
|
||||
return FileResponse(fpath, filename=filename)
|
||||
|
||||
@app.post("/api/legal/ask_ai")
|
||||
async def api_legal_ask_ai(request: Request, username: str = Depends(get_current_admin)):
|
||||
try:
|
||||
body = await request.json()
|
||||
query = body.get("query", "").strip()
|
||||
deep = body.get("deep", False)
|
||||
if not query:
|
||||
return JSONResponse(content={"success": False, "error": "Текст вопроса пуст"})
|
||||
|
||||
from services.ai_legal_engine import AILegalEngine
|
||||
engine = AILegalEngine()
|
||||
role = "lawyer" if deep else "resident"
|
||||
res = await engine.query(prompt=query, role=role)
|
||||
return JSONResponse(content=res)
|
||||
except Exception as e:
|
||||
return JSONResponse(content={"success": False, "error": str(e)})
|
||||
|
||||
@app.post("/api/legal/notebooklm_sync")
|
||||
async def api_legal_notebooklm_sync(username: str = Depends(get_current_admin)):
|
||||
try:
|
||||
from services.notebooklm_podcast_generator import NotebookLMPodcastGenerator
|
||||
generator = NotebookLMPodcastGenerator()
|
||||
res = generator.sync_all_sources()
|
||||
if res.get("success"):
|
||||
return JSONResponse(content={"message": f"Синхронизация успешна! Загружено файлов: {len(res.get('uploaded_sources', []))}"})
|
||||
return JSONResponse(content={"message": f"Статус: {res.get('error', 'Сессия требует настройки')}"})
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=500, content={"message": str(e)})
|
||||
|
||||
@app.post("/api/legal/generate_podcast")
|
||||
async def api_legal_generate_podcast(username: str = Depends(get_current_admin)):
|
||||
try:
|
||||
from services.notebooklm_podcast_generator import NotebookLMPodcastGenerator
|
||||
generator = NotebookLMPodcastGenerator()
|
||||
if generator.is_authenticated():
|
||||
res = generator.generate_audio_overview()
|
||||
return JSONResponse(content={"message": "Генерация Audio Overview в NotebookLM запущена!"})
|
||||
else:
|
||||
# Fallback генерация сценария через Gemini 2.5 Pro
|
||||
res = generator.generate_fallback_dialogue_script(
|
||||
"Анализ судебных споров дома с УК и ГЖИ по отоплению, перерасчетам и отпискам прокуратуры."
|
||||
)
|
||||
return JSONResponse(content={"message": f"Сгенерирован сценарий подкаста через Gemini 2.5 Pro: {len(res.get('script', ''))} симв."})
|
||||
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)
|
||||
|
|
|
|||
524
web/archive/app_full.py
Normal file
524
web/archive/app_full.py
Normal file
|
|
@ -0,0 +1,524 @@
|
|||
"""
|
||||
Web Admin Interface - Full Functional Admin Panel
|
||||
Веб-интерфейс с полным функционалом управления ботом
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
from fastapi import FastAPI, Request, Depends, HTTPException, status, Form, Query
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, update, delete
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
import config
|
||||
import aiohttp
|
||||
|
||||
from database.db import AsyncSessionLocal, get_db
|
||||
from database.models import (
|
||||
User, Message, Poll, Ad, PaymentReminder, Schedule,
|
||||
Achievement, Thank, Event, EventParticipant, InitiativeGroup,
|
||||
ProfileHistory, Announcement, Service, VerificationRequest, SpyLog
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Приложение
|
||||
app = FastAPI(
|
||||
title="Домовой Бот - Полная Админ Панель",
|
||||
description="Веб-интерфейс с полным функционалом",
|
||||
version="2.1.3"
|
||||
)
|
||||
|
||||
# Шаблоны
|
||||
templates = Jinja2Templates(directory="web/templates")
|
||||
|
||||
# Статика
|
||||
app.mount("/static", StaticFiles(directory="web/static"), name="static")
|
||||
|
||||
# Безопасность
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# АВТОРИЗАЦИЯ
|
||||
# =============================================================================
|
||||
|
||||
def get_current_admin(credentials: HTTPBasicCredentials = Depends(security)):
|
||||
"""Проверка админа"""
|
||||
correct_username = secrets.compare_digest(credentials.username, config.WEB_ADMIN_LOGIN)
|
||||
correct_password = secrets.compare_digest(credentials.password, config.WEB_ADMIN_PASSWORD)
|
||||
|
||||
if not (correct_username and correct_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Неверное имя пользователя или пароль",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
return credentials.username
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ГЛАВНАЯ СТРАНИЦА
|
||||
# =============================================================================
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def dashboard(request: Request, username: str = Depends(get_current_admin)):
|
||||
"""Главная страница - дашборд"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
users_count = await session.execute(select(func.count(User.user_id)))
|
||||
users_count = users_count.scalar()
|
||||
|
||||
messages_count = await session.execute(select(func.count(Message.id)))
|
||||
messages_count = messages_count.scalar()
|
||||
|
||||
verified_count = await session.execute(
|
||||
select(func.count(User.user_id)).where(User.verified == True)
|
||||
)
|
||||
verified_count = verified_count.scalar()
|
||||
|
||||
ig_count = await session.execute(
|
||||
select(func.count(InitiativeGroup.id)).where(InitiativeGroup.is_active == True)
|
||||
)
|
||||
ig_count = ig_count.scalar()
|
||||
|
||||
return templates.TemplateResponse("dashboard.html", {
|
||||
"request": request,
|
||||
"username": username,
|
||||
"stats": {
|
||||
"users": users_count,
|
||||
"messages": messages_count,
|
||||
"verified": verified_count,
|
||||
"ig_members": ig_count
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ПОЛЬЗОВАТЕЛИ - API
|
||||
# =============================================================================
|
||||
|
||||
@app.get("/api/users")
|
||||
async def api_get_users(
|
||||
username: str = Depends(get_current_admin),
|
||||
search: str = Query(None),
|
||||
apartment: str = Query(None),
|
||||
verified: bool = Query(None),
|
||||
limit: int = Query(100)
|
||||
):
|
||||
"""Получить список пользователей с фильтрами"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
stmt = select(User)
|
||||
|
||||
if search:
|
||||
stmt = stmt.where(
|
||||
(User.full_name.ilike(f"%{search}%")) |
|
||||
(User.username.ilike(f"%{search}%"))
|
||||
)
|
||||
|
||||
if apartment:
|
||||
stmt = stmt.where(User.apartment.ilike(f"%{apartment}%"))
|
||||
|
||||
if verified is not None:
|
||||
stmt = stmt.where(User.verified == verified)
|
||||
|
||||
stmt = stmt.order_by(User.user_id.desc()).limit(limit)
|
||||
result = await session.execute(stmt)
|
||||
users = list(result.scalars().all())
|
||||
|
||||
return {
|
||||
"users": [
|
||||
{
|
||||
"user_id": u.user_id,
|
||||
"full_name": u.full_name,
|
||||
"username": u.username,
|
||||
"apartment": u.apartment,
|
||||
"verified": u.verified,
|
||||
"rating": u.rating,
|
||||
"message_count": u.message_count,
|
||||
"is_admin": u.is_admin,
|
||||
"spy_score": u.spy_score,
|
||||
"toxicity_score": u.toxicity_score
|
||||
}
|
||||
for u in users
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/users/{user_id}/verify")
|
||||
async def api_verify_user(
|
||||
user_id: int,
|
||||
username: str = Depends(get_current_admin)
|
||||
):
|
||||
"""Верифицировать пользователя"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
user = await session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
user.verified = True
|
||||
user.verification_date = datetime.utcnow()
|
||||
await session.commit()
|
||||
|
||||
# Уведомить пользователя
|
||||
try:
|
||||
async with aiohttp.ClientSession() as http_session:
|
||||
await http_session.post(
|
||||
f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage",
|
||||
json={
|
||||
"chat_id": user_id,
|
||||
"text": f"✅ <b>Верификация подтверждена!</b>\n\nТеперь у вас есть доступ ко всем функциям бота.",
|
||||
"parse_mode": "HTML"
|
||||
}
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
return {"success": True, "message": f"Пользователь {user_id} верифицирован"}
|
||||
|
||||
|
||||
@app.post("/api/users/{user_id}/unverify")
|
||||
async def api_unverify_user(
|
||||
user_id: int,
|
||||
reason: str = Form(None),
|
||||
username: str = Depends(get_current_admin)
|
||||
):
|
||||
"""Снять верификацию с пользователя"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
user = await session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
user.verified = False
|
||||
await session.commit()
|
||||
|
||||
# Уведомить пользователя
|
||||
try:
|
||||
async with aiohttp.ClientSession() as http_session:
|
||||
await http_session.post(
|
||||
f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage",
|
||||
json={
|
||||
"chat_id": user_id,
|
||||
"text": f"⚠️ <b>Верификация отозвана</b>\n\nПричина: {reason or 'Без указания причины'}\n\nУ вас есть 7 дней на повторную верификацию.",
|
||||
"parse_mode": "HTML"
|
||||
}
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
return {"success": True, "message": f"Верификация пользователя {user_id} отозвана"}
|
||||
|
||||
|
||||
@app.post("/api/users/{user_id}/set_admin")
|
||||
async def api_set_admin(
|
||||
user_id: int,
|
||||
is_admin: bool = Form(True),
|
||||
username: str = Depends(get_current_admin)
|
||||
):
|
||||
"""Назначить/снять права админа"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
user = await session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
user.is_admin = is_admin
|
||||
await session.commit()
|
||||
|
||||
return {"success": True, "message": f"Пользователь {user_id} {'назначен' if is_admin else 'снят с должности'} админа"}
|
||||
|
||||
|
||||
@app.get("/api/initiative_group")
|
||||
async def api_get_ig_members(username: str = Depends(get_current_admin)):
|
||||
"""Получить список участников ИГ"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
stmt = select(InitiativeGroup).where(InitiativeGroup.is_active == True)
|
||||
result = await session.execute(stmt)
|
||||
members = list(result.scalars().all())
|
||||
|
||||
return {
|
||||
"members": [
|
||||
{
|
||||
"user_id": m.user_id,
|
||||
"role": m.role,
|
||||
"joined_at": m.joined_at.isoformat(),
|
||||
"name": m.user.full_name if m.user else f"user{m.user_id}",
|
||||
"apartment": m.user.apartment if m.user else None
|
||||
}
|
||||
for m in members
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/initiative_group/add")
|
||||
async def api_add_ig_member(
|
||||
user_id: int = Form(...),
|
||||
role: str = Form("member"),
|
||||
username: str = Depends(get_current_admin)
|
||||
):
|
||||
"""Добавить в ИГ"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
# Проверяем есть ли уже
|
||||
existing = await session.get(InitiativeGroup, user_id)
|
||||
if existing:
|
||||
existing.is_active = True
|
||||
existing.role = role
|
||||
else:
|
||||
member = InitiativeGroup(user_id=user_id, role=role)
|
||||
session.add(member)
|
||||
|
||||
await session.commit()
|
||||
|
||||
return {"success": True, "message": f"Пользователь {user_id} добавлен в ИГ"}
|
||||
|
||||
|
||||
@app.post("/api/initiative_group/remove")
|
||||
async def api_remove_ig_member(
|
||||
user_id: int = Form(...),
|
||||
username: str = Depends(get_current_admin)
|
||||
):
|
||||
"""Удалить из ИГ"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
member = await session.get(InitiativeGroup, user_id)
|
||||
if member:
|
||||
member.is_active = False
|
||||
await session.commit()
|
||||
|
||||
return {"success": True, "message": f"Пользователь {user_id} удалён из ИГ"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ТЕЛЕФОНЫ - API
|
||||
# =============================================================================
|
||||
|
||||
@app.get("/api/phones")
|
||||
async def api_get_phones(username: str = Depends(get_current_admin)):
|
||||
"""Получить список телефонов"""
|
||||
import os
|
||||
phones_file = "data/phones.json"
|
||||
|
||||
if os.path.exists(phones_file):
|
||||
with open(phones_file, 'r', encoding='utf-8') as f:
|
||||
phones_data = json.load(f)
|
||||
return {"success": True, "data": phones_data}
|
||||
else:
|
||||
return {"success": False, "data": {}}
|
||||
|
||||
|
||||
@app.post("/api/phones/update")
|
||||
async def api_update_phones(
|
||||
request: Request,
|
||||
username: str = Depends(get_current_admin)
|
||||
):
|
||||
"""Обновить телефоны"""
|
||||
import os
|
||||
data = await request.json()
|
||||
phones_file = "data/phones.json"
|
||||
|
||||
try:
|
||||
with open(phones_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
return {"success": True, "message": "Телефоны обновлены"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ОБЪЯВЛЕНИЯ - API
|
||||
# =============================================================================
|
||||
|
||||
@app.get("/api/announcements")
|
||||
async def api_get_announcements(username: str = Depends(get_current_admin)):
|
||||
"""Получить список объявлений"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
stmt = select(Announcement).order_by(Announcement.created_at.desc())
|
||||
result = await session.execute(stmt)
|
||||
announcements = list(result.scalars().all())
|
||||
|
||||
return {
|
||||
"announcements": [
|
||||
{
|
||||
"id": a.id,
|
||||
"title": a.title,
|
||||
"text": a.text,
|
||||
"category": a.category,
|
||||
"is_active": a.is_active,
|
||||
"created_at": a.created_at.isoformat()
|
||||
}
|
||||
for a in announcements
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/announcements/create")
|
||||
async def api_create_announcement(
|
||||
title: str = Form(...),
|
||||
text: str = Form(...),
|
||||
category: str = Form("general"),
|
||||
username: str = Depends(get_current_admin)
|
||||
):
|
||||
"""Создать объявление"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
announcement = Announcement(
|
||||
title=title,
|
||||
text=text,
|
||||
category=category,
|
||||
created_by=username
|
||||
)
|
||||
session.add(announcement)
|
||||
await session.commit()
|
||||
|
||||
return {"success": True, "message": "Объявление создано"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ВЫПОЛНЕНИЕ КОМАНД БОТА
|
||||
# =============================================================================
|
||||
|
||||
@app.post("/api/command")
|
||||
async def api_command(
|
||||
request: Request,
|
||||
username: str = Depends(get_current_admin)
|
||||
):
|
||||
"""Выполнить команду бота"""
|
||||
data = await request.json()
|
||||
command = data.get('command', '')
|
||||
args = data.get('args', '')
|
||||
|
||||
if not command:
|
||||
return JSONResponse({"success": False, "error": "Нет команды"})
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as http_session:
|
||||
full_command = f"/{command}"
|
||||
if args:
|
||||
full_command += f" {args}"
|
||||
|
||||
result = await http_session.post(
|
||||
f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage",
|
||||
json={
|
||||
"chat_id": config.ADMIN_CHAT_ID,
|
||||
"text": full_command
|
||||
}
|
||||
)
|
||||
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"message": f"Команда /{command} отправлена"
|
||||
})
|
||||
except Exception as e:
|
||||
return JSONResponse({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/api/commands")
|
||||
async def api_commands_list(username: str = Depends(get_current_admin)):
|
||||
"""Список доступных команд"""
|
||||
commands = {
|
||||
"admin": [
|
||||
{"cmd": "admin", "desc": "Админ-панель"},
|
||||
{"cmd": "stats", "desc": "Статистика чата"},
|
||||
{"cmd": "spies", "desc": "Подозрительные пользователи"},
|
||||
{"cmd": "users", "desc": "Список жильцов"},
|
||||
{"cmd": "verify", "desc": "Заявки на верификацию"},
|
||||
{"cmd": "export", "desc": "Экспорт данных"},
|
||||
{"cmd": "rating", "desc": "Рейтинг пользователей"},
|
||||
],
|
||||
"initiative_group": [
|
||||
{"cmd": "ig_add", "desc": "Добавить в ИГ"},
|
||||
{"cmd": "ig_remove", "desc": "Удалить из ИГ"},
|
||||
{"cmd": "ig_list", "desc": "Список ИГ"},
|
||||
{"cmd": "ig_stats", "desc": "Статистика ИГ"},
|
||||
{"cmd": "ig_broadcast", "desc": "Рассылка для ИГ"},
|
||||
],
|
||||
"events": [
|
||||
{"cmd": "event_add", "desc": "Добавить событие"},
|
||||
{"cmd": "events", "desc": "Список событий"},
|
||||
{"cmd": "participants", "desc": "Участники события"},
|
||||
],
|
||||
"security": [
|
||||
{"cmd": "unverify", "desc": "Отозвать верификацию"},
|
||||
{"cmd": "unverified_list", "desc": "Список не верифицированных"},
|
||||
{"cmd": "ban", "desc": "Забанить пользователя"},
|
||||
{"cmd": "unban", "desc": "Разбанить"},
|
||||
],
|
||||
"phones": [
|
||||
{"cmd": "add_phone", "desc": "Добавить телефон"},
|
||||
{"cmd": "edit_phone", "desc": "Редактировать телефон"},
|
||||
{"cmd": "remove_phone", "desc": "Удалить телефон"},
|
||||
]
|
||||
}
|
||||
return JSONResponse({"commands": commands})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# СТРАНИЦЫ
|
||||
# =============================================================================
|
||||
|
||||
@app.get("/users", response_class=HTMLResponse)
|
||||
async def users_page(request: Request, username: str = Depends(get_current_admin)):
|
||||
"""Страница пользователей"""
|
||||
return templates.TemplateResponse("users.html", {
|
||||
"request": request,
|
||||
"username": username
|
||||
})
|
||||
|
||||
|
||||
@app.get("/initiative_group", response_class=HTMLResponse)
|
||||
async def ig_page(request: Request, username: str = Depends(get_current_admin)):
|
||||
"""Страница ИГ"""
|
||||
return templates.TemplateResponse("initiative_group.html", {
|
||||
"request": request,
|
||||
"username": username
|
||||
})
|
||||
|
||||
|
||||
@app.get("/phones", response_class=HTMLResponse)
|
||||
async def phones_page(request: Request, username: str = Depends(get_current_admin)):
|
||||
"""Страница телефонов"""
|
||||
return templates.TemplateResponse("phones.html", {
|
||||
"request": request,
|
||||
"username": username
|
||||
})
|
||||
|
||||
|
||||
@app.get("/announcements", response_class=HTMLResponse)
|
||||
async def announcements_page(request: Request, username: str = Depends(get_current_admin)):
|
||||
"""Страница объявлений"""
|
||||
return templates.TemplateResponse("announcements.html", {
|
||||
"request": request,
|
||||
"username": username
|
||||
})
|
||||
|
||||
|
||||
@app.get("/commands", response_class=HTMLResponse)
|
||||
async def commands_page(request: Request, username: str = Depends(get_current_admin)):
|
||||
"""Страница команд"""
|
||||
return templates.TemplateResponse("commands.html", {
|
||||
"request": request,
|
||||
"username": username
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ЗАПУСК
|
||||
# =============================================================================
|
||||
|
||||
def run_web_server():
|
||||
"""Запустить веб-сервер"""
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=config.WEB_ADMIN_HOST,
|
||||
port=config.WEB_ADMIN_PORT,
|
||||
log_level="info"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_web_server()
|
||||
1547
web/archive/app_reference.py
Normal file
1547
web/archive/app_reference.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -6,192 +6,278 @@
|
|||
<title>{% block title %}LKM37 CORE{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="/static/css/matrix.css" rel="stylesheet">
|
||||
<style>
|
||||
.sidebar { min-height: 100vh; position: sticky; top: 0; overflow-y: auto; }
|
||||
.content { padding: 2rem; }
|
||||
:root {
|
||||
--bg-main: #f4f4f4;
|
||||
--bg-panel: #ffffff;
|
||||
--bg-header: #ece9d8;
|
||||
--border-color: #707680;
|
||||
--border-light: #cccccc;
|
||||
--text-main: #111111;
|
||||
--text-muted: #555555;
|
||||
--link-color: #0000cc;
|
||||
--link-hover: #cc0000;
|
||||
--badge-bg: #e8e8e8;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-main);
|
||||
color: var(--text-main);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--link-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.layout-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background-color: var(--bg-panel);
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
background-color: var(--bg-header);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.sidebar-header h4 {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.sidebar-header small {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 4px;
|
||||
padding-left: 6px;
|
||||
border-left: 3px solid var(--border-color);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: block;
|
||||
padding: 4px 8px;
|
||||
color: var(--text-main) !important;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0;
|
||||
margin-bottom: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.nav-link:hover {
|
||||
background-color: #eaeaea;
|
||||
border-color: var(--border-light);
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-link.active {
|
||||
background-color: var(--bg-header);
|
||||
border-color: var(--border-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
.nav-link i {
|
||||
margin-right: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
padding: 16px 24px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.card, .panel {
|
||||
background-color: var(--bg-panel);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-header, .panel-header {
|
||||
background-color: var(--bg-header);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 6px 12px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.card-body, .panel-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
background-color: var(--bg-panel);
|
||||
font-size: 12px;
|
||||
}
|
||||
table th, table td {
|
||||
border: 1px solid var(--border-light);
|
||||
padding: 5px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
table th {
|
||||
background-color: var(--bg-header);
|
||||
border-color: var(--border-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
table tr:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.btn-unix {
|
||||
display: inline-block;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
padding: 3px 8px;
|
||||
background-color: #e8e8e8;
|
||||
color: #000000;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-unix:hover {
|
||||
background-color: #d8d8d8;
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-unix-primary {
|
||||
background-color: #002b49;
|
||||
color: #ffffff;
|
||||
border-color: #001828;
|
||||
}
|
||||
.btn-unix-primary:hover {
|
||||
background-color: #003d66;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: inherit;
|
||||
background-color: #e8e8e8;
|
||||
border: 1px solid var(--border-light);
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
pre {
|
||||
padding: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.badge-status {
|
||||
display: inline-block;
|
||||
padding: 2px 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--badge-bg);
|
||||
font-size: 11px;
|
||||
}
|
||||
.badge-success { background-color: #e2f0d9; border-color: #70ad47; color: #385723; }
|
||||
.badge-warning { background-color: #fff2cc; border-color: #ffc000; color: #806000; }
|
||||
.badge-danger { background-color: #fce4d6; border-color: #ed7d31; color: #c65911; }
|
||||
|
||||
{% block extra_style %}{% endblock %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="matrix-canvas-container" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; opacity: 0.1;">
|
||||
<canvas id="matrix-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar (ВОССТАНОВЛЕННОЕ ПОЛНОЕ МЕНЮ) -->
|
||||
<div class="col-md-2 sidebar p-0">
|
||||
<div class="p-3 text-center border-bottom" style="border-color: var(--panel-border) !important;">
|
||||
<h4 class="mb-0" style="color: var(--color-success); text-shadow: 0 0 15px var(--color-success-glow); font-weight: 700; letter-spacing: 1px;">LKM37</h4>
|
||||
<small class="text-success opacity-50" style="font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.5px;">CORE SYSTEM v4.5</small>
|
||||
</div>
|
||||
<nav class="nav flex-column mt-3">
|
||||
<a class="nav-link {% block nav_dashboard %}{% endblock %}" href="/">
|
||||
<i class="bi bi-terminal"></i> ДАШБОРД
|
||||
</a>
|
||||
|
||||
<div class="mt-3 px-3"><small class="text-success opacity-50 text-uppercase" style="font-size: 0.6em;">Жильцы</small></div>
|
||||
<a class="nav-link {% block nav_users %}{% endblock %}" href="/users">
|
||||
<i class="bi bi-people"></i> ЖИЛЬЦЫ
|
||||
</a>
|
||||
<a class="nav-link {% block nav_verification %}{% endblock %}" href="/verification">
|
||||
<i class="bi bi-shield-lock"></i> ВЕРИФИКАЦИЯ
|
||||
</a>
|
||||
<a class="nav-link {% block nav_phones %}{% endblock %}" href="/phones">
|
||||
<i class="bi bi-telephone"></i> ТЕЛЕФОНЫ
|
||||
</a>
|
||||
|
||||
<div class="mt-3 px-3"><small class="text-success opacity-50 text-uppercase" style="font-size: 0.6em;">Контент</small></div>
|
||||
<a class="nav-link {% block nav_broadcast %}{% endblock %}" href="/broadcast">
|
||||
<i class="bi bi-broadcast"></i> РАССЫЛКА
|
||||
</a>
|
||||
<a class="nav-link {% block nav_scheduled_posts %}{% endblock %}" href="/scheduled_posts">
|
||||
<i class="bi bi-clock-history"></i> ПОСТЫ
|
||||
</a>
|
||||
<a class="nav-link {% block nav_digests %}{% endblock %}" href="/digests">
|
||||
<i class="bi bi-file-earmark-text"></i> ДАЙДЖЕСТЫ
|
||||
</a>
|
||||
<a class="nav-link {% block nav_files %}{% endblock %}" href="/files">
|
||||
<i class="bi bi-folder2-open"></i> ФАЙЛЫ
|
||||
</a>
|
||||
|
||||
<div class="mt-3 px-3"><small class="text-success opacity-50 text-uppercase" style="font-size: 0.6em;">Сервисы</small></div>
|
||||
<a class="nav-link {% block nav_schedules %}{% endblock %}" href="/schedules">
|
||||
<i class="bi bi-calendar-event"></i> ГРАФИКИ
|
||||
</a>
|
||||
<a class="nav-link {% block nav_ads %}{% endblock %}" href="/ads">
|
||||
<i class="bi bi-shop"></i> ОБЪЯВЛЕНИЯ
|
||||
</a>
|
||||
<a class="nav-link {% block nav_polls %}{% endblock %}" href="/polls">
|
||||
<i class="bi bi-check2-square"></i> ОПРОСЫ
|
||||
</a>
|
||||
<a class="nav-link {% block nav_events %}{% endblock %}" href="/events">
|
||||
<i class="bi bi-calendar3"></i> СОБЫТИЯ
|
||||
</a>
|
||||
|
||||
<div class="mt-3 px-3"><small class="text-success opacity-50 text-uppercase" style="font-size: 0.6em;">Юрист</small></div>
|
||||
<a class="nav-link {% block nav_sudrf %}{% endblock %}" href="/sudrf">
|
||||
<i class="bi bi-bank text-danger"></i> ЖАТВА ГАС
|
||||
</a>
|
||||
|
||||
<div class="mt-3 px-3"><small class="text-success opacity-50 text-uppercase" style="font-size: 0.6em;">Система</small></div>
|
||||
<a class="nav-link {% block nav_infra %}{% endblock %}" href="/infra">
|
||||
<i class="bi bi-cpu"></i> ИНФРАСТРУКТУРА
|
||||
</a>
|
||||
<a class="nav-link {% block nav_export %}{% endblock %}" href="/export">
|
||||
<i class="bi bi-download"></i> ЭКСПОРТ
|
||||
</a>
|
||||
<a class="nav-link {% block nav_emails %}{% endblock %}" href="/emails">
|
||||
<i class="bi bi-envelope-at"></i> ПОЧТА
|
||||
</a>
|
||||
</nav>
|
||||
<div class="layout-container">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h4>МКД ЛИВАНОВА 37</h4>
|
||||
<small>СИСТЕМА УПРАВЛЕНИЯ & ЯДЕРНЫЙ ЩИТ</small>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col-md-10 content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<a class="nav-link {% block nav_dashboard %}{% endblock %}" href="/">
|
||||
<i class="bi bi-terminal"></i> Дашборд
|
||||
</a>
|
||||
|
||||
<div class="nav-section-title">Юридический щит</div>
|
||||
<a class="nav-link {% block nav_legal %}{% endblock %}" href="/legal">
|
||||
<i class="bi bi-shield-check"></i> Законы РФ & ИИ-Юрист
|
||||
</a>
|
||||
<a class="nav-link {% block nav_emails %}{% endblock %}" href="/emails">
|
||||
<i class="bi bi-envelope-check"></i> Реестр писем & ЭЦП
|
||||
</a>
|
||||
<a class="nav-link {% block nav_sudrf %}{% endblock %}" href="/sudrf">
|
||||
<i class="bi bi-bank"></i> Суды ГАС Правосудие
|
||||
</a>
|
||||
|
||||
<div class="nav-section-title">Жильцы & Доступ</div>
|
||||
<a class="nav-link {% block nav_users %}{% endblock %}" href="/users">
|
||||
<i class="bi bi-people"></i> Жильцы (AI доступ)
|
||||
</a>
|
||||
<a class="nav-link {% block nav_verification %}{% endblock %}" href="/verification">
|
||||
<i class="bi bi-patch-check"></i> Верификация
|
||||
</a>
|
||||
<a class="nav-link {% block nav_phones %}{% endblock %}" href="/phones">
|
||||
<i class="bi bi-telephone"></i> Телефоны
|
||||
</a>
|
||||
|
||||
<div class="nav-section-title">Коммуникация</div>
|
||||
<a class="nav-link {% block nav_broadcast %}{% endblock %}" href="/broadcast">
|
||||
<i class="bi bi-broadcast"></i> Оповещения (Zero-Spam)
|
||||
</a>
|
||||
<a class="nav-link {% block nav_scheduled_posts %}{% endblock %}" href="/scheduled_posts">
|
||||
<i class="bi bi-clock-history"></i> Запланированные посты
|
||||
</a>
|
||||
<a class="nav-link {% block nav_digests %}{% endblock %}" href="/digests">
|
||||
<i class="bi bi-newspaper"></i> Дайджесты
|
||||
</a>
|
||||
|
||||
<div class="nav-section-title">Сервисы & Инфра</div>
|
||||
<a class="nav-link {% block nav_files %}{% endblock %}" href="/files">
|
||||
<i class="bi bi-folder2-open"></i> Файлы и архивы
|
||||
</a>
|
||||
<a class="nav-link {% block nav_schedules %}{% endblock %}" href="/schedules">
|
||||
<i class="bi bi-calendar3"></i> Графики
|
||||
</a>
|
||||
<a class="nav-link {% block nav_ads %}{% endblock %}" href="/ads">
|
||||
<i class="bi bi-shop"></i> Объявления
|
||||
</a>
|
||||
<a class="nav-link {% block nav_polls %}{% endblock %}" href="/polls">
|
||||
<i class="bi bi-check2-square"></i> Опросы
|
||||
</a>
|
||||
<a class="nav-link {% block nav_infra %}{% endblock %}" href="/infra">
|
||||
<i class="bi bi-cpu"></i> Инфраструктура
|
||||
</a>
|
||||
<a class="nav-link {% block nav_export %}{% endblock %}" href="/export">
|
||||
<i class="bi bi-download"></i> Экспорт базы
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const canvas = document.getElementById('matrix-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let width = canvas.width = window.innerWidth;
|
||||
let height = canvas.height = window.innerHeight;
|
||||
|
||||
const particles = [];
|
||||
const properties = {
|
||||
bgColor: '#06060c',
|
||||
particleColor: 'rgba(138, 43, 226, 0.25)',
|
||||
lineColor: 'rgba(0, 255, 204, 0.05)',
|
||||
particleRadius: 2.5,
|
||||
particleCount: 70,
|
||||
maxVelocity: 0.4,
|
||||
lineLength: 140
|
||||
};
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
width = canvas.width = window.innerWidth;
|
||||
height = canvas.height = window.innerHeight;
|
||||
});
|
||||
|
||||
class Particle {
|
||||
constructor() {
|
||||
this.x = Math.random() * width;
|
||||
this.y = Math.random() * height;
|
||||
this.velocityPosition = {
|
||||
x: (Math.random() - 0.5) * properties.maxVelocity,
|
||||
y: (Math.random() - 0.5) * properties.maxVelocity
|
||||
}
|
||||
}
|
||||
|
||||
position() {
|
||||
if (this.x + this.velocityPosition.x > width || this.x + this.velocityPosition.x < 0) {
|
||||
this.velocityPosition.x = -this.velocityPosition.x;
|
||||
}
|
||||
if (this.y + this.velocityPosition.y > height || this.y + this.velocityPosition.y < 0) {
|
||||
this.velocityPosition.y = -this.velocityPosition.y;
|
||||
}
|
||||
this.x += this.velocityPosition.x;
|
||||
this.y += this.velocityPosition.y;
|
||||
}
|
||||
|
||||
reDraw() {
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, properties.particleRadius, 0, Math.PI * 2);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = properties.particleColor;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawLines() {
|
||||
let x1, y1, x2, y2, length, opacity;
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
x1 = particles[i].x;
|
||||
y1 = particles[i].y;
|
||||
x2 = particles[j].x;
|
||||
y2 = particles[j].y;
|
||||
length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
|
||||
if (length < properties.lineLength) {
|
||||
opacity = 1 - (length / properties.lineLength);
|
||||
ctx.strokeStyle = `rgba(0, 255, 204, ${opacity * 0.06})`;
|
||||
ctx.lineWidth = '0.5';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
for (let i = 0; i < properties.particleCount; i++) {
|
||||
particles.push(new Particle());
|
||||
}
|
||||
loop();
|
||||
}
|
||||
|
||||
function loop() {
|
||||
ctx.fillStyle = properties.bgColor;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
particles[i].position();
|
||||
particles[i].reDraw();
|
||||
}
|
||||
drawLines();
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
{% block extra_script %}{% endblock %}
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
217
web/templates/legal.html
Normal file
217
web/templates/legal.html
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Нормативно-правовая база & ИИ-Юрист | LKM37{% endblock %}
|
||||
{% block nav_legal %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="m-0" style="font-size: 18px; font-weight: bold;">НОРМАТИВНО-ПРАВОВАЯ БАЗА РФ & ИИ-ЮРИСТ</h3>
|
||||
<div>
|
||||
<button class="btn-unix btn-unix-primary" onclick="syncNotebookLM()">
|
||||
<i class="bi bi-arrow-repeat"></i> Синхронизация с NotebookLM
|
||||
</button>
|
||||
<button class="btn-unix" onclick="generatePodcast()">
|
||||
<i class="bi bi-mic"></i> Сгенерировать подкаст (Audio Overview)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Левая колонка: Реестр полных законов РФ -->
|
||||
<div class="col-md-7">
|
||||
<div class="panel">
|
||||
<div class="panel-header d-flex justify-content-between align-items-center">
|
||||
<span>ПОЛНЫЕ ОФИЦИАЛЬНЫЕ КОДЕКСЫ И НОРМАТИВНЫЕ АКТЫ РФ (100% ТЕКСТ)</span>
|
||||
<span class="badge-status badge-success">{{ laws|length }} документов</span>
|
||||
</div>
|
||||
<div class="panel-body p-0">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 45px;">№</th>
|
||||
<th>Наименование нормативно-правового акта</th>
|
||||
<th style="width: 75px;">Стр.</th>
|
||||
<th style="width: 85px;">Размер</th>
|
||||
<th style="width: 100px;">Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for law in laws %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>
|
||||
<strong>{{ law.title }}</strong><br>
|
||||
<small style="color: var(--text-muted);">{{ law.filename }}</small>
|
||||
</td>
|
||||
<td>{{ law.pages }}</td>
|
||||
<td>{{ law.size_formatted }}</td>
|
||||
<td>
|
||||
<a href="/api/legal/download/{{ law.filename }}" class="btn-unix" download>
|
||||
<i class="bi bi-download"></i> PDF
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Карточка томов переписки -->
|
||||
<div class="panel">
|
||||
<div class="panel-header d-flex justify-content-between align-items-center">
|
||||
<span>СВОДНЫЕ ТОМА ПЕРЕПИСКИ С ГОСОРГАНАМИ & ШТАМПЫ ЭЦП</span>
|
||||
<span class="badge-status badge-success">{{ mega_volumes|length }} тома</span>
|
||||
</div>
|
||||
<div class="panel-body p-0">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Том переписки</th>
|
||||
<th>Размер</th>
|
||||
<th>Оглавление TOC</th>
|
||||
<th>Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vol in mega_volumes %}
|
||||
<tr>
|
||||
<td><strong>{{ vol.name }}</strong></td>
|
||||
<td>{{ vol.size_formatted }}</td>
|
||||
<td><span class="badge-status badge-success">Вшито в PDF</span></td>
|
||||
<td>
|
||||
<a href="/api/legal/download_volume/{{ vol.name }}" class="btn-unix" download>
|
||||
<i class="bi bi-download"></i> Скачать
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая колонка: ИИ-Консультант и Генератор Подкастов -->
|
||||
<div class="col-md-5">
|
||||
<!-- Интерактивный AI-Юрист -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<i class="bi bi-cpu"></i> АВТОНОМНЫЙ ИИ-КОНСУЛЬТАНТ (GEMINI 2.5)
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">
|
||||
Контекст: Полный свод ЖК РФ, ГК РФ, 59-ФЗ, ПП 354, ПП 491, Госстрой 170 + 610 писем дома с верифицированными ЭЦП.
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
<label style="font-weight: bold; font-size: 11px;">Вопрос / Фабула ситуации:</label>
|
||||
<textarea id="ai-query" class="form-control" rows="4" style="font-family: inherit; font-size: 12px; border: 1px solid var(--border-color); border-radius: 0;" placeholder="Например: УК отказалась делать перерасчет за отопление в подъезде 4, утверждая что перерыв не превышал допустимой нормы. На какие пункты ПП 354 и Госстроя 170 сослаться в претензии?"></textarea>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<input type="checkbox" id="ai-deep-mode">
|
||||
<label for="ai-deep-mode" style="font-size: 11px; cursor: pointer;">Глубокий анализ (Gemini 2.5 Pro)</label>
|
||||
</div>
|
||||
<button class="btn-unix btn-unix-primary" onclick="askAiLawyer()">
|
||||
<i class="bi bi-send"></i> Сформировать позицию
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="ai-response-box" style="display: none; margin-top: 12px; border-top: 1px dashed var(--border-color); padding-top: 10px;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<strong style="font-size: 11px;">ОТВЕТ ИИ-ЮРИСТА:</strong>
|
||||
<span id="ai-model-tag" class="badge-status badge-success">Flash</span>
|
||||
</div>
|
||||
<div id="ai-response-text" style="background-color: #fafafa; border: 1px solid var(--border-light); padding: 8px; font-size: 11px; white-space: pre-wrap; max-height: 400px; overflow-y: auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Аудио-подкасты NotebookLM -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<i class="bi bi-soundwave"></i> АУДИО-ПОДКАСТЫ ДОМА (AUDIO OVERVIEWS)
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">
|
||||
Автоматическая генерация аналитических подкастов диалога двух экспертов по делам дома.
|
||||
</p>
|
||||
<div id="podcast-status-box">
|
||||
{% if latest_podcast %}
|
||||
<div class="mb-2">
|
||||
<strong>Последний подкаст:</strong> {{ latest_podcast.name }}<br>
|
||||
<small style="color: var(--text-muted);">Размер: {{ latest_podcast.size }} | Дата: {{ latest_podcast.date }}</small>
|
||||
</div>
|
||||
<audio controls style="width: 100%; margin-bottom: 8px;">
|
||||
<source src="/api/legal/podcasts/{{ latest_podcast.name }}" type="audio/mp4">
|
||||
Ваш браузер не поддерживает аудио.
|
||||
</audio>
|
||||
{% else %}
|
||||
<p style="font-size: 11px; font-style: italic; color: var(--text-muted);">
|
||||
Подкасты еще не генерировались. Нажмите кнопку генерации выше.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function askAiLawyer() {
|
||||
const query = document.getElementById('ai-query').value.trim();
|
||||
if (!query) {
|
||||
alert('Пожалуйста, введите текст вопроса');
|
||||
return;
|
||||
}
|
||||
const deep = document.getElementById('ai-deep-mode').checked;
|
||||
const resBox = document.getElementById('ai-response-box');
|
||||
const resText = document.getElementById('ai-response-text');
|
||||
const tag = document.getElementById('ai-model-tag');
|
||||
|
||||
resBox.style.display = 'block';
|
||||
resText.innerText = 'Анализ правовой базы и судебной практики... Пожалуйста, подождите.';
|
||||
tag.innerText = deep ? 'Gemini 2.5 Pro' : 'Gemini 2.5 Flash';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/legal/ask_ai', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({query: query, deep: deep})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
resText.innerText = data.answer;
|
||||
} else {
|
||||
resText.innerText = 'Ошибка: ' + (data.error || 'Неизвестная ошибка');
|
||||
}
|
||||
} catch (e) {
|
||||
resText.innerText = 'Сетевая ошибка обращения к ИИ-Юристу: ' + e;
|
||||
}
|
||||
}
|
||||
|
||||
async function syncNotebookLM() {
|
||||
if (!confirm('Запустить полную синхронизацию законов и томов переписки с Google NotebookLM?')) return;
|
||||
try {
|
||||
const resp = await fetch('/api/legal/notebooklm_sync', {method: 'POST'});
|
||||
const data = await resp.json();
|
||||
alert(data.message || 'Синхронизация завершена!');
|
||||
} catch (e) {
|
||||
alert('Ошибка запуска синхронизации: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePodcast() {
|
||||
if (!confirm('Запустить генерацию Audio Overview подкаста в NotebookLM?')) return;
|
||||
try {
|
||||
const resp = await fetch('/api/legal/generate_podcast', {method: 'POST'});
|
||||
const data = await resp.json();
|
||||
alert(data.message || 'Задача генерации подкаста запущена!');
|
||||
} catch (e) {
|
||||
alert('Ошибка запуска генерации подкаста: ' + e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Reference in a new issue