CheddahBot/cheddahbot/web/routes_pages.py

173 lines
5.6 KiB
Python

"""Page routes: GET / (chat), GET /dashboard, dashboard partials."""
from __future__ import annotations
import logging
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
if TYPE_CHECKING:
from ..agent_registry import AgentRegistry
from ..config import Config
from ..db import Database
from ..llm import LLMAdapter
from ..scheduler import Scheduler
log = logging.getLogger(__name__)
router = APIRouter()
_registry: AgentRegistry | None = None
_config: Config | None = None
_llm: LLMAdapter | None = None
_db: Database | None = None
_scheduler: Scheduler | None = None
_templates: Jinja2Templates | None = None
def setup(registry, config, llm, templates, db=None, scheduler=None):
global _registry, _config, _llm, _templates, _db, _scheduler
_registry = registry
_config = config
_llm = llm
_templates = templates
_db = db
_scheduler = scheduler
@router.get("/")
async def chat_page(request: Request):
agent_names = _registry.list_agents() if _registry else []
agents = []
for name in agent_names:
agent = _registry.get(name)
display = agent.agent_config.display_name if agent else name
agents.append({"name": name, "display_name": display})
default_agent = _registry.default_name if _registry else "default"
chat_model = _config.chat_model if _config else "unknown"
exec_available = _llm.is_execution_brain_available() if _llm else False
clickup_enabled = _config.clickup.enabled if _config else False
return _templates.TemplateResponse("chat.html", {
"request": request,
"agents": agents,
"default_agent": default_agent,
"chat_model": chat_model,
"exec_available": exec_available,
"clickup_enabled": clickup_enabled,
})
@router.get("/dashboard")
async def dashboard_page(request: Request):
return _templates.TemplateResponse("dashboard.html", {
"request": request,
})
@router.get("/dashboard/pipeline")
async def dashboard_pipeline():
"""Return pipeline panel HTML partial with task data."""
if not _config or not _config.clickup.enabled:
return HTMLResponse('<p class="text-muted">ClickUp not configured</p>')
try:
from ..api import get_tasks
data = await get_tasks()
all_tasks = data.get("tasks", [])
except Exception as e:
log.error("Pipeline data fetch failed: %s", e)
return HTMLResponse(f'<p class="text-err">Error: {e}</p>')
# Group by work category, then by status
pipeline_statuses = [
"to do", "automation underway", "outline review", "internal review", "error",
]
categories = {} # category -> {status -> [tasks]}
for t in all_tasks:
cat = t.get("task_type") or "Other"
status = t.get("status", "unknown")
# Only show tasks in pipeline-relevant statuses
if status not in pipeline_statuses:
continue
if cat not in categories:
categories[cat] = {}
categories[cat].setdefault(status, []).append(t)
# Build HTML
html_parts = []
# Status summary counts
total_counts = {}
for cat_data in categories.values():
for status, tasks in cat_data.items():
total_counts[status] = total_counts.get(status, 0) + len(tasks)
if total_counts:
html_parts.append('<div class="pipeline-stats">')
for status in pipeline_statuses:
count = total_counts.get(status, 0)
html_parts.append(
f'<div class="pipeline-stat">'
f'<div class="stat-count">{count}</div>'
f'<div class="stat-label">{status}</div>'
f'</div>'
)
html_parts.append('</div>')
# Per-category tables
for cat_name in sorted(categories.keys()):
cat_data = categories[cat_name]
all_cat_tasks = []
for status in pipeline_statuses:
all_cat_tasks.extend(cat_data.get(status, []))
if not all_cat_tasks:
continue
html_parts.append(f'<div class="pipeline-group"><h4>{cat_name} ({len(all_cat_tasks)})</h4>')
html_parts.append('<table class="task-table"><thead><tr>'
'<th>Task</th><th>Customer</th><th>Status</th><th>Due</th>'
'</tr></thead><tbody>')
for task in all_cat_tasks:
name = task.get("name", "")
url = task.get("url", "")
customer = (task.get("custom_fields") or {}).get("Client", "N/A")
status = task.get("status", "")
status_class = "status-" + status.replace(" ", "-")
# Format due date
due_display = "-"
due_raw = task.get("due_date")
if due_raw:
try:
due_dt = datetime.fromtimestamp(int(due_raw) / 1000, tz=UTC)
due_display = due_dt.strftime("%b %d")
except (ValueError, TypeError, OSError):
pass
name_cell = (
f'<a href="{url}" target="_blank">{name}</a>' if url else name
)
html_parts.append(
f'<tr><td>{name_cell}</td><td>{customer}</td>'
f'<td><span class="status-badge {status_class}">{status}</span></td>'
f'<td>{due_display}</td></tr>'
)
html_parts.append('</tbody></table></div>')
if not html_parts:
return HTMLResponse('<p class="text-muted">No active pipeline tasks</p>')
return HTMLResponse('\n'.join(html_parts))