UMBRA/backend/app/routers/dashboard.py
Kyle Pope bdae07fb7d Compact dashboard: single-line rows, multi-star, weather city
- UpcomingWidget: single-line rows with icon/title/date/type/priority
- CalendarWidget: whitespace-nowrap time ranges, no wrapping
- TodoWidget: compact dot + title + date + badge on one line
- Active Reminders: single-line with dot indicator
- CountdownWidget: supports array of starred events
- StatsWidget: shows city name in weather card
- Dashboard API: returns starred_events array (up to 5) instead of single

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 14:28:39 +08:00

211 lines
7.0 KiB
Python

from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from datetime import datetime, date, timedelta
from typing import Optional, List, Dict, Any
from app.database import get_db
from app.models.settings import Settings
from app.models.todo import Todo
from app.models.calendar_event import CalendarEvent
from app.models.reminder import Reminder
from app.models.project import Project
from app.routers.auth import get_current_session
router = APIRouter()
@router.get("/dashboard")
async def get_dashboard(
client_date: Optional[date] = Query(None),
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
"""Get aggregated dashboard data."""
today = client_date or date.today()
upcoming_cutoff = today + timedelta(days=current_user.upcoming_days)
# Today's events
today_start = datetime.combine(today, datetime.min.time())
today_end = datetime.combine(today, datetime.max.time())
events_query = select(CalendarEvent).where(
CalendarEvent.start_datetime >= today_start,
CalendarEvent.start_datetime <= today_end
)
events_result = await db.execute(events_query)
todays_events = events_result.scalars().all()
# Upcoming todos (not completed, with due date within upcoming_days)
todos_query = select(Todo).where(
Todo.completed == False,
Todo.due_date.isnot(None),
Todo.due_date <= upcoming_cutoff
).order_by(Todo.due_date.asc())
todos_result = await db.execute(todos_query)
upcoming_todos = todos_result.scalars().all()
# Active reminders (not dismissed, is_active = true)
reminders_query = select(Reminder).where(
Reminder.is_active == True,
Reminder.is_dismissed == False
).order_by(Reminder.remind_at.asc())
reminders_result = await db.execute(reminders_query)
active_reminders = reminders_result.scalars().all()
# Project stats
total_projects_result = await db.execute(select(func.count(Project.id)))
total_projects = total_projects_result.scalar()
projects_by_status_query = select(
Project.status,
func.count(Project.id).label("count")
).group_by(Project.status)
projects_by_status_result = await db.execute(projects_by_status_query)
projects_by_status = {row[0]: row[1] for row in projects_by_status_result}
# Total incomplete todos count
total_incomplete_result = await db.execute(
select(func.count(Todo.id)).where(Todo.completed == False)
)
total_incomplete_todos = total_incomplete_result.scalar()
# Starred events (upcoming, ordered by date)
now = datetime.now()
starred_query = select(CalendarEvent).where(
CalendarEvent.is_starred == True,
CalendarEvent.start_datetime > now
).order_by(CalendarEvent.start_datetime.asc()).limit(5)
starred_result = await db.execute(starred_query)
starred_events = starred_result.scalars().all()
starred_events_data = [
{
"id": e.id,
"title": e.title,
"start_datetime": e.start_datetime
}
for e in starred_events
]
return {
"todays_events": [
{
"id": event.id,
"title": event.title,
"start_datetime": event.start_datetime,
"end_datetime": event.end_datetime,
"all_day": event.all_day,
"color": event.color,
"is_starred": event.is_starred
}
for event in todays_events
],
"upcoming_todos": [
{
"id": todo.id,
"title": todo.title,
"due_date": todo.due_date,
"priority": todo.priority,
"category": todo.category
}
for todo in upcoming_todos
],
"active_reminders": [
{
"id": reminder.id,
"title": reminder.title,
"remind_at": reminder.remind_at
}
for reminder in active_reminders
],
"project_stats": {
"total": total_projects,
"by_status": projects_by_status
},
"total_incomplete_todos": total_incomplete_todos,
"starred_events": starred_events_data
}
@router.get("/upcoming")
async def get_upcoming(
days: int = Query(default=7, ge=1, le=90),
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
"""Get unified list of upcoming items (todos, events, reminders) sorted by date."""
today = date.today()
cutoff_date = today + timedelta(days=days)
cutoff_datetime = datetime.combine(cutoff_date, datetime.max.time())
# Get upcoming todos with due dates
todos_query = select(Todo).where(
Todo.completed == False,
Todo.due_date.isnot(None),
Todo.due_date <= cutoff_date
)
todos_result = await db.execute(todos_query)
todos = todos_result.scalars().all()
# Get upcoming events (from today onward)
today_start = datetime.combine(today, datetime.min.time())
events_query = select(CalendarEvent).where(
CalendarEvent.start_datetime >= today_start,
CalendarEvent.start_datetime <= cutoff_datetime,
)
events_result = await db.execute(events_query)
events = events_result.scalars().all()
# Get upcoming reminders
reminders_query = select(Reminder).where(
Reminder.is_active == True,
Reminder.is_dismissed == False,
Reminder.remind_at <= cutoff_datetime
)
reminders_result = await db.execute(reminders_query)
reminders = reminders_result.scalars().all()
# Combine into unified list
upcoming_items: List[Dict[str, Any]] = []
for todo in todos:
upcoming_items.append({
"type": "todo",
"id": todo.id,
"title": todo.title,
"date": todo.due_date.isoformat() if todo.due_date else None,
"datetime": None,
"priority": todo.priority,
"category": todo.category
})
for event in events:
upcoming_items.append({
"type": "event",
"id": event.id,
"title": event.title,
"date": event.start_datetime.date().isoformat(),
"datetime": event.start_datetime.isoformat(),
"all_day": event.all_day,
"color": event.color,
"is_starred": event.is_starred
})
for reminder in reminders:
upcoming_items.append({
"type": "reminder",
"id": reminder.id,
"title": reminder.title,
"date": reminder.remind_at.date().isoformat(),
"datetime": reminder.remind_at.isoformat()
})
# Sort by date/datetime
upcoming_items.sort(key=lambda x: x["datetime"] if x["datetime"] else x["date"])
return {
"items": upcoming_items,
"days": days,
"cutoff_date": cutoff_date.isoformat()
}