diff --git a/app.py b/app.py index 5e92401..492f5e3 100644 --- a/app.py +++ b/app.py @@ -16,6 +16,8 @@ import json from pathlib import Path import io import asyncio +import contextlib +import traceback import time from urllib.parse import quote @@ -26,6 +28,9 @@ from repositories.match_formation_repository import ( ) from parsers.parser_game import run_parser_game +from parsers.parser_players import run_parser_players +from parsers.parser_schedule import run_parser_schedule +from parsers.parser_standings import run_parser_standings from repositories.match_session_repository import ( create_match_session, get_match_session_by_token, @@ -725,17 +730,68 @@ def close_session(session_token: str): return RedirectResponse(url="/admin/matches", status_code=303) -@app.get("/admin/db", response_class=HTMLResponse) -def admin_db_index(request: Request): +def render_admin_db_index(request: Request, parser_output: str | None = None, parser_name: str | None = None, parser_success: bool | None = None): return templates.TemplateResponse( name="admin_db_index.html", request=request, context={ "current_user": getattr(request.state, "current_user", None), + "parser_output": parser_output, + "parser_name": parser_name, + "parser_success": parser_success, }, ) +@app.get("/admin/db", response_class=HTMLResponse) +def admin_db_index(request: Request): + return render_admin_db_index(request) + + +@app.post("/admin/db/run-parser", response_class=HTMLResponse) +def admin_db_run_parser(request: Request, parser_name: str = Form(...)): + current_user = getattr(request.state, "current_user", None) + if not current_user or current_user.get("role") != "admin": + return RedirectResponse(url="/login", status_code=303) + + parser_map = { + "players": ("Игроки", run_parser_players), + "schedule": ("Расписание", run_parser_schedule), + "standings": ("Турнирка", run_parser_standings), + } + + parser_meta = parser_map.get(parser_name) + if not parser_meta: + return render_admin_db_index( + request, + parser_output="Неизвестный парсер.", + parser_name=parser_name, + parser_success=False, + ) + + title, parser_func = parser_meta + buffer = io.StringIO() + success = True + + with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer): + print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Запуск парсера: {title}") + try: + parser_func() + print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Парсер завершён успешно") + except Exception: + success = False + print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Ошибка при выполнении парсера") + print(traceback.format_exc()) + + output = buffer.getvalue().strip() or "Парсер не вернул сообщений." + return render_admin_db_index( + request, + parser_output=output, + parser_name=title, + parser_success=success, + ) + + @app.get("/admin/db/players", response_class=HTMLResponse) def admin_db_players(request: Request, q: str = Query(default="")): # denied = require_role(request, {"admin"}) diff --git a/templates/admin_db_index.html b/templates/admin_db_index.html index 5e6777c..f6eacf7 100644 --- a/templates/admin_db_index.html +++ b/templates/admin_db_index.html @@ -13,6 +13,10 @@ --muted: #9aa4b2; --accent: #4f8cff; --accent-hover: #3e78e6; + --success-bg: rgba(46, 204, 113, 0.12); + --success-border: rgba(46, 204, 113, 0.35); + --danger-bg: rgba(255, 99, 99, 0.12); + --danger-border: rgba(255, 99, 99, 0.35); --shadow: 0 10px 30px rgba(0, 0, 0, 0.35); --radius: 16px; } @@ -46,15 +50,24 @@ margin-bottom: 18px; } - .tabs { + .tabs, + .parser-actions { display: flex; gap: 12px; flex-wrap: wrap; } - .tab-link { + .parser-actions { + margin-top: 18px; + padding-top: 18px; + border-top: 1px solid var(--border); + } + + .tab-link, + .action-btn { display: inline-flex; align-items: center; + justify-content: center; min-height: 42px; padding: 0 16px; border-radius: 999px; @@ -63,13 +76,58 @@ color: var(--text); text-decoration: none; font-weight: 600; + cursor: pointer; + font-size: 14px; } - .tab-link:hover { + .tab-link:hover, + .action-btn:hover { background: var(--accent); border-color: var(--accent); color: white; } + + .action-form { + margin: 0; + } + + .log-block { + margin-top: 20px; + border: 1px solid var(--border); + border-radius: 14px; + padding: 16px; + background: var(--panel-2); + } + + .log-block.success { + background: var(--success-bg); + border-color: var(--success-border); + } + + .log-block.error { + background: var(--danger-bg); + border-color: var(--danger-border); + } + + .log-title { + font-size: 18px; + font-weight: 700; + margin-bottom: 10px; + } + + .log-status { + color: var(--muted); + margin-bottom: 10px; + } + + .log-output { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-family: Consolas, Monaco, monospace; + font-size: 13px; + line-height: 1.5; + }
@@ -79,14 +137,45 @@ + +{{ parser_output }}
+