feat(web): стабилизация UI, Gemini-скроллинг, роутеры контекста/снапшотов и актуализация роадмапа
This commit is contained in:
+46
-75
@@ -3,99 +3,70 @@
|
||||
FILE: services/tasks/exporter.py
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: services / tasks
|
||||
ROLE: Экспорт бэклога задач в форматированный Markdown файл (ROADMAP).
|
||||
|
||||
AI-CONTEXT-ANCHORS:
|
||||
- ANCHOR[TASK_EXPORT_MARKDOWN]: Построение структуры Markdown с чекбоксами.
|
||||
ROLE: Экспорт задач в чистый Markdown и генерация прямой ссылки на скачивание.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, List
|
||||
from config import OUTPUT_DIR
|
||||
from typing import Dict, Any, Optional
|
||||
from config import BASE_DIR
|
||||
from .repository import repo_get_tasks
|
||||
|
||||
logger = logging.getLogger("TASK_EXPORTER")
|
||||
WEB_OUTPUT_DIR = os.path.join(BASE_DIR, "output", "web", "db_export_tasks_markdown")
|
||||
|
||||
|
||||
# ANCHOR[TASK_EXPORT_MARKDOWN]
|
||||
def export_tasks_to_markdown(user_id: int, filename: Optional[str] = None, status_filter: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Генерирует Markdown-отчет задач с сохранением в output/web/tasks_export/{uuid}/."""
|
||||
tasks = repo_get_tasks(user_id)
|
||||
if not tasks:
|
||||
return {"status": "error", "message": "Список задач пуст, экспорт отменен"}
|
||||
def export_tasks_to_markdown(
|
||||
user_id: int,
|
||||
filename: Optional[str] = "ROADMAP.md",
|
||||
status_filter: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
tasks = repo_get_tasks(user_id=user_id, status=status_filter)
|
||||
safe_filename = os.path.basename(filename or "ROADMAP.md")
|
||||
if not safe_filename.endswith(".md"):
|
||||
safe_filename += ".md"
|
||||
|
||||
# 1. Фильтрация задач по статусу
|
||||
if status_filter and status_filter.upper() != "ALL":
|
||||
tgt = status_filter.upper()
|
||||
if tgt in ["COMPLETED", "DONE", "ВЫПОЛНЕННЫЕ"]:
|
||||
tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["COMPLETED", "DONE"]]
|
||||
elif tgt in ["IN_PROGRESS", "PROGRESS", "В РАБОТЕ"]:
|
||||
tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["IN_PROGRESS", "PROGRESS"]]
|
||||
elif tgt in ["BACKLOG", "PLANNED", "В ПЛАНАХ"]:
|
||||
tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["BACKLOG", "PLANNED"]]
|
||||
session_uuid = str(uuid.uuid4())[:8]
|
||||
target_dir = os.path.join(WEB_OUTPUT_DIR, session_uuid)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
target_filepath = os.path.join(target_dir, safe_filename)
|
||||
|
||||
if not tasks:
|
||||
return {"status": "error", "message": f"Нет задач с фильтром '{status_filter}' для экспорта"}
|
||||
|
||||
target_filename = filename.strip() if (filename and filename.strip()) else "ROADMAP.md"
|
||||
if not target_filename.endswith(".md"):
|
||||
target_filename = f"{target_filename}.md"
|
||||
|
||||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# 2. Группировка по модулям
|
||||
modules: Dict[str, List[Dict[str, Any]]] = {}
|
||||
for t in tasks:
|
||||
mod = t.get("module") or "general"
|
||||
modules.setdefault(mod, []).append(t)
|
||||
|
||||
md_lines = [
|
||||
"# 🗺️ Дорожная карта задач проекта (ROADMAP)\n",
|
||||
f"> **Сформировано:** {now_str} | **Всего задач:** {len(tasks)}\n",
|
||||
"---\n"
|
||||
lines = [
|
||||
f"# 📋 Реестр задач проекта ({safe_filename})",
|
||||
f"**Всего задач:** {len(tasks)} ",
|
||||
f"**Пользователь ID:** {user_id} ",
|
||||
"",
|
||||
"| ID | Статус | Приоритет | Модуль | Срок | Задача |",
|
||||
"| :--- | :--- | :--- | :--- | :--- | :--- |"
|
||||
]
|
||||
|
||||
for mod_name, mod_tasks in sorted(modules.items()):
|
||||
md_lines.append(f"## Модуль `{mod_name}`\n")
|
||||
for t in sorted(mod_tasks, key=lambda x: x.get("id", 0)):
|
||||
status = str(t.get("status", "BACKLOG")).upper()
|
||||
is_done = status in ["COMPLETED", "DONE"]
|
||||
is_progress = status in ["IN_PROGRESS", "PROGRESS"]
|
||||
|
||||
check_box = "[x]" if is_done else "[ ]"
|
||||
t_id = t.get("id")
|
||||
title = t.get("title", "Без названия")
|
||||
prio = t.get("priority", "MEDIUM")
|
||||
due = f" *(срок: {t['due_date']})*" if t.get("due_date") else ""
|
||||
status_tag = " `[В РАБОТЕ]`" if is_progress else (" `[ЗАВЕРШЕНО]`" if is_done else "")
|
||||
status_icons = {
|
||||
"IN_PROGRESS": "⚙️ В работе",
|
||||
"COMPLETED": "✓ Завершено",
|
||||
"BACKLOG": "📋 Бэклог"
|
||||
}
|
||||
|
||||
md_lines.append(f"- {check_box} **#{t_id}** [{prio}]{status_tag} {title}{due}")
|
||||
|
||||
md_lines.append("\n---\n")
|
||||
for t in tasks:
|
||||
t_id = t.get("task_id") or f"#{t.get('id')}"
|
||||
t_status = status_icons.get(t.get("status"), t.get("status", "BACKLOG"))
|
||||
t_prio = t.get("priority", "MEDIUM")
|
||||
t_mod = t.get("module", "general")
|
||||
t_due = t.get("due_date") or "—"
|
||||
t_title = str(t.get("title", "")).replace("|", "\\|").strip()
|
||||
lines.append(f"| `{t_id}` | {t_status} | {t_prio} | `{t_mod}` | {t_due} | {t_title} |")
|
||||
|
||||
content = "\n".join(md_lines)
|
||||
lines.append("")
|
||||
content = "\n".join(lines)
|
||||
|
||||
# 3. Сохранение файла в изолированную сессионную папку
|
||||
tool_dir = os.path.join(OUTPUT_DIR, "web", "tasks_export")
|
||||
os.makedirs(tool_dir, exist_ok=True)
|
||||
|
||||
session_token = uuid.uuid4().hex[:8]
|
||||
session_dir = os.path.join(tool_dir, session_token)
|
||||
os.makedirs(session_dir, exist_ok=True)
|
||||
|
||||
filepath = os.path.join(session_dir, target_filename)
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
with open(target_filepath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
download_url = f"/api/v1/files/download/db_export_tasks_markdown/{session_uuid}/{safe_filename}"
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"filename": target_filename,
|
||||
"filepath": filepath,
|
||||
"download_url": f"/api/v1/files/download/tasks_export/{session_token}/{target_filename}",
|
||||
"tasks_count": len(tasks),
|
||||
"message": f"Отчет успешно сформирован в файл `{target_filename}` (всего задач: {len(tasks)})."
|
||||
"message": f"Отчет успешно сформирован в файл `{safe_filename}` (всего задач: {len(tasks)}).",
|
||||
"filename": safe_filename,
|
||||
"download_url": download_url,
|
||||
"tasks_count": len(tasks)
|
||||
}
|
||||
Reference in New Issue
Block a user