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
|
- TZ=Europe/Ulyanovsk
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./.env:/app/.env:ro
|
||||||
- ./main.py:/app/main.py
|
- ./main.py:/app/main.py
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
- ./exports:/app/exports
|
- ./exports:/app/exports
|
||||||
|
|
@ -35,14 +36,19 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- PYTHONPATH=/app:/app/services
|
- PYTHONPATH=/app:/app/services
|
||||||
- GOOGLE_CREDS_FILE=/home/matrixhasyou/domovoy_google_creds.json
|
- GOOGLE_CREDS_FILE=/home/matrixhasyou/domovoy_google_creds.json
|
||||||
|
- GOOGLE_API_KEY=AIzaSyAJMfQ_mRZ1picRXVFNaeFJ7fpAkZxUKyk
|
||||||
- TZ=Europe/Ulyanovsk
|
- TZ=Europe/Ulyanovsk
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./.env:/app/.env:ro
|
||||||
|
- ./web:/app/web
|
||||||
|
- ./podcasts:/app/podcasts
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
- ./exports:/app/exports
|
- ./exports:/app/exports
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./handlers:/app/handlers
|
- ./handlers:/app/handlers
|
||||||
- ./services:/app/services
|
- ./services:/app/services
|
||||||
|
- /home/matrixhasyou/mutt:/home/matrixhasyou/mutt:ro
|
||||||
- ../libs/swarmlib:/app/swarmlib:ro
|
- ../libs/swarmlib:/app/swarmlib:ro
|
||||||
- /home/matrixhasyou/domovoy_google_creds.json:/home/matrixhasyou/domovoy_google_creds.json:ro
|
- /home/matrixhasyou/domovoy_google_creds.json:/home/matrixhasyou/domovoy_google_creds.json:ro
|
||||||
logging:
|
logging:
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,32 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
AI Legal Engine 2.0 (Юридический Ядерный Щит Домового).
|
Интеллектуальное юридическое ядро МКД «Домовой 2.0» (AI Legal Engine)
|
||||||
Работает на базе Gemini 2.0 API с контекстным окном до 2M токенов.
|
Работает на базе Gemini 2.5 Flash / Pro:
|
||||||
Обеспечивает:
|
- Нативный слух (Gemini Multimodal Audio) без Whisper
|
||||||
1. Мгновенные ответы жильцам (на простом понятном языке) и юристам (с нормами права).
|
- Прямой асинхронный REST API (высокая отказоустойчивость без конфликтов pydantic)
|
||||||
2. Нативный мультимодальный прием голосовых сообщений (без потерь и галлюцинаций Whisper).
|
- Разграничение ролей: resident (соседский помощник), lawyer (претензионно-судебный), admin (глубокий аудит)
|
||||||
3. Интеграцию с базой писем и судебных дел МКД (Ленком 37).
|
|
||||||
4. Строгий шлюз безопасности Zero-Spam для чата дома.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from pathlib import Path
|
import aiohttp
|
||||||
|
import base64
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dotenv import load_dotenv
|
from pathlib import Path
|
||||||
|
|
||||||
# Загружаем окружение
|
|
||||||
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")
|
|
||||||
|
|
||||||
|
log = logging.getLogger("ai_legal_engine")
|
||||||
class AILegalEngine:
|
class AILegalEngine:
|
||||||
def __init__(self):
|
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.client = None
|
||||||
self.model_name = "gemini-2.5-flash"
|
self.model_name = "gemini-2.5-flash"
|
||||||
self.deep_model_name = "gemini-2.5-pro"
|
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:
|
if not self.api_key:
|
||||||
log.warning("Gemini API Key missing! AI Legal Engine will run in offline mode.")
|
log.warning("Gemini API Key missing! AI Legal Engine will run in offline mode.")
|
||||||
return
|
return
|
||||||
|
|
@ -42,9 +34,9 @@ class AILegalEngine:
|
||||||
try:
|
try:
|
||||||
from google import genai
|
from google import genai
|
||||||
self.client = genai.Client(api_key=self.api_key)
|
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:
|
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:
|
def get_role_instructions(self, role: str) -> str:
|
||||||
if role == "resident":
|
if role == "resident":
|
||||||
|
|
@ -59,32 +51,69 @@ class AILegalEngine:
|
||||||
)
|
)
|
||||||
elif role == "lawyer":
|
elif role == "lawyer":
|
||||||
return (
|
return (
|
||||||
"РОЛЬ ДЛЯ ОТВЕТА: 'Судебный юрист в сфере ЖКХ и градостроительства'.\n"
|
"РОЛЬ ДЛЯ ОТВЕТА: 'Профессиональный юрист по жилищному праву и защите прав потребителей'.\n"
|
||||||
"СТИЛЬ: Строгий, процессуально выверенный, юридически безупречный.\n"
|
"СТИЛЬ: Строгий, юридически выверенный, аргументированный.\n"
|
||||||
"ТРЕБОВАНИЯ:\n"
|
"ТРЕБОВАНИЯ:\n"
|
||||||
"1. Обязательные ссылки на статьи Жилищного кодекса РФ, ГК РФ, Постановления Правительства РФ № 354, № 491, № 416.\n"
|
"1. Четкая правовая квалификация действий/бездействия лиц.\n"
|
||||||
"2. Указание на сроки удовлетворения требований по закону.\n"
|
"2. Прямые ссылки на статьи законов (ЖК РФ, ГК РФ, 59-ФЗ, ПП РФ № 354, ПП РФ № 491, Госстрой № 170, СанПиН 2.1.3684-21).\n"
|
||||||
"3. Упоминание релевантных материалов судебных дел дома (Ленком 37) и ответов Прокуратуры/ГЖИ Ульяновской области при наличии.\n"
|
"3. Оценка перспектив в суде или надзорных органах (ГЖИ, Прокуратура, Роспотребнадзор).\n"
|
||||||
"4. Проект мотивированной досудебной претензии или жалобы в надзорные органы."
|
"4. Готовый проект претензии / жалобы с правовой мотивировкой."
|
||||||
)
|
)
|
||||||
elif role == "admin":
|
elif role == "admin":
|
||||||
return (
|
return (
|
||||||
"РОЛЬ ДЛЯ ОТВЕТА: 'Стратегический аналитик для Председателя совета дома (Александра Тырина)'.\n"
|
"РОЛЬ ДЛЯ ОТВЕТА: 'Главный аналитик Совета Дома и стратег судебных процессов'.\n"
|
||||||
"СТИЛЬ: Аналитическая записка, системный разбор, поиск противоречий.\n"
|
"СТИЛЬ: Стратегический, системный, аналитический.\n"
|
||||||
"ТРЕБОВАНИЯ:\n"
|
"ТРЕБОВАНИЯ:\n"
|
||||||
"1. Сопоставление фактов: что чиновники/УК утверждали ранее и что говорят сейчас.\n"
|
"1. Полный расклад с точки зрения системной выгоды для всего дома.\n"
|
||||||
"2. Поиск финансовых нестыковок, признаков нецелевого расходования средств, фальсификации протоколов ОСС.\n"
|
"2. Выявление уловок УК, скрытых манипуляций со сметами, протоколами ОСС или объемами коммунальных ресурсов.\n"
|
||||||
"3. Контроль сроков по 59-ФЗ и процессуальных дедлайнов.\n"
|
"3. Сравнение с прецедентами и судебной практикой Ульяновского областного суда и Верховного Суда РФ.\n"
|
||||||
"4. Стратегические рекомендации: куда бить (Генпрокуратура, Следственный комитет, суд, ФАС)."
|
"4. Стратегия принуждения госорганов к исполнению служебных обязанностей по 59-ФЗ и Приказу ГП № 45."
|
||||||
)
|
)
|
||||||
return "Отвечай вежливо и по делу."
|
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:
|
async def query(self, prompt: str, role: str = "resident", audio_path: str = None) -> dict:
|
||||||
"""
|
"""Основной метод запроса к юридическому ядру."""
|
||||||
Основной метод запроса к юридическому ядру.
|
if not self.api_key:
|
||||||
Поддерживает текст, аудиофайл (голос жильца) и разграничение ролей.
|
|
||||||
"""
|
|
||||||
if not self.client:
|
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"text": "ИИ-модуль временно недоступен. Проверьте API ключ.",
|
"text": "ИИ-модуль временно недоступен. Проверьте API ключ.",
|
||||||
|
|
@ -99,40 +128,22 @@ class AILegalEngine:
|
||||||
f"{self.get_role_instructions(role)}"
|
f"{self.get_role_instructions(role)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_to_use = self.deep_model_name if role in ("lawyer", "admin") else self.model_name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
contents = []
|
# Используем прямой REST (наивысшая надежность без конфликтов библиотек)
|
||||||
|
reply_text = await self._query_rest_api(
|
||||||
# 1. Если передано голосовое сообщение — добавляем аудио напрямую
|
prompt=prompt,
|
||||||
if audio_path and os.path.exists(audio_path):
|
system_instruction=system_instruction,
|
||||||
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(
|
|
||||||
model=model_to_use,
|
model=model_to_use,
|
||||||
contents=contents,
|
audio_path=audio_path
|
||||||
config={
|
|
||||||
"system_instruction": system_instruction,
|
|
||||||
"temperature": 0.2 if role in ("lawyer", "admin") else 0.4,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
latency = (datetime.now() - start_time).total_seconds()
|
latency = (datetime.now() - start_time).total_seconds()
|
||||||
reply_text = response.text.strip() if response.text else "Ответ не сформирован."
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"text": reply_text,
|
"text": reply_text.strip(),
|
||||||
|
"answer": reply_text.strip(),
|
||||||
"role": role,
|
"role": role,
|
||||||
"latency_sec": round(latency, 2),
|
"latency_sec": round(latency, 2),
|
||||||
"model": model_to_use
|
"model": model_to_use
|
||||||
|
|
@ -140,28 +151,24 @@ class AILegalEngine:
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
latency = (datetime.now() - start_time).total_seconds()
|
latency = (datetime.now() - start_time).total_seconds()
|
||||||
log.error(f"Gemini API Error: {e}")
|
log.error(f"Gemini Error: {e}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"text": f"Ошибка обработки запроса: {e}",
|
"text": f"Ошибка обработки запроса ИИ: {e}",
|
||||||
"role": role,
|
"role": role,
|
||||||
"latency_sec": round(latency, 2)
|
"latency_sec": round(latency, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Глобальный инстанс
|
# Глобальный синглтон юридического ядра для роутеров бота
|
||||||
legal_engine = AILegalEngine()
|
legal_engine = AILegalEngine()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
async def test():
|
async def test():
|
||||||
print("Testing AI Legal Engine 2.0...")
|
engine = AILegalEngine()
|
||||||
res = await legal_engine.query("Течет крыша на 9 этаже, УК не реагирует 4 дня. Что делать?", role="resident")
|
res = await engine.query("Какие сроки ответа прокуратуры по закону 59-ФЗ?", role="resident")
|
||||||
print("\n=== RESIDENT RESPONSE ===")
|
print("Success:", res["success"])
|
||||||
print(f"Latency: {res['latency_sec']}s")
|
print("Latency:", res["latency_sec"])
|
||||||
print(res["text"])
|
print("Model:", res.get("model"))
|
||||||
|
print("Text snippet:\n", res["text"][:300])
|
||||||
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"])
|
|
||||||
|
|
||||||
asyncio.run(test())
|
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:
|
except Exception as e:
|
||||||
return JSONResponse(status_code=500, content={"message": str(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__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|
|
||||||
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>
|
<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@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="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>
|
<style>
|
||||||
.sidebar { min-height: 100vh; position: sticky; top: 0; overflow-y: auto; }
|
:root {
|
||||||
.content { padding: 2rem; }
|
--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 %}
|
{% block extra_style %}{% endblock %}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="matrix-canvas-container" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; opacity: 0.1;">
|
<div class="layout-container">
|
||||||
<canvas id="matrix-canvas"></canvas>
|
<!-- Sidebar -->
|
||||||
</div>
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
<div class="container-fluid">
|
<h4>МКД ЛИВАНОВА 37</h4>
|
||||||
<div class="row">
|
<small>СИСТЕМА УПРАВЛЕНИЯ & ЯДЕРНЫЙ ЩИТ</small>
|
||||||
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<nav>
|
||||||
<div class="col-md-10 content">
|
<a class="nav-link {% block nav_dashboard %}{% endblock %}" href="/">
|
||||||
{% block content %}{% endblock %}
|
<i class="bi bi-terminal"></i> Дашборд
|
||||||
</div>
|
</a>
|
||||||
</div>
|
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
{% block scripts %}{% endblock %}
|
||||||
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 %}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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