""" Life OS - Main Application FastAPI server-rendered monolith with async PostgreSQL. """ import os from pathlib import Path from contextlib import asynccontextmanager from dotenv import load_dotenv load_dotenv() from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from fastapi.responses import RedirectResponse from core.database import check_db from core.sidebar import get_sidebar_data from core.database import get_db # Import routers from routers import ( domains as domains_router, areas as areas_router, projects as projects_router, tasks as tasks_router, notes as notes_router, links as links_router, focus as focus_router, capture as capture_router, contacts as contacts_router, search as search_router, admin as admin_router, lists as lists_router, files as files_router, meetings as meetings_router, decisions as decisions_router, weblinks as weblinks_router, appointments as appointments_router, time_tracking as time_tracking_router, processes as processes_router, ) @asynccontextmanager async def lifespan(app: FastAPI): """Startup/shutdown events.""" # Verify database connection try: ok = await check_db() if ok: print("Database connection OK") else: print("WARNING: Database check returned unexpected result") except Exception as e: print(f"WARNING: Database connection failed: {e}") yield app = FastAPI( title="Life OS", version="1.0.0", lifespan=lifespan, ) # Static files app.mount("/static", StaticFiles(directory="static"), name="static") # Templates templates = Jinja2Templates(directory="templates") # ---- Template globals and filters ---- from starlette.types import ASGIApp, Receive, Scope, Send class RequestContextMiddleware: """Pure ASGI middleware - avoids BaseHTTPMiddleware's TaskGroup issues with asyncpg.""" def __init__(self, app: ASGIApp): self.app = app self.environment = os.getenv("ENVIRONMENT", "production") async def __call__(self, scope: Scope, receive: Receive, send: Send): if scope["type"] == "http": scope.setdefault("state", {})["environment"] = self.environment await self.app(scope, receive, send) app.add_middleware(RequestContextMiddleware) # ---- Dashboard ---- @app.get("/") async def dashboard(request: Request): """Main dashboard view.""" from core.database import async_session_factory async with async_session_factory() as db: sidebar = await get_sidebar_data(db) # Today's focus items from sqlalchemy import text result = await db.execute(text(""" SELECT df.*, t.title, t.priority, t.status as task_status, t.project_id, p.name as project_name, d.name as domain_name, d.color as domain_color FROM daily_focus df JOIN tasks t ON df.task_id = t.id LEFT JOIN projects p ON t.project_id = p.id LEFT JOIN domains d ON t.domain_id = d.id WHERE df.focus_date = CURRENT_DATE AND df.is_deleted = false ORDER BY df.sort_order, df.created_at """)) focus_items = [dict(r._mapping) for r in result] # Overdue tasks result = await db.execute(text(""" SELECT t.*, d.name as domain_name, d.color as domain_color, p.name as project_name FROM tasks t LEFT JOIN domains d ON t.domain_id = d.id LEFT JOIN projects p ON t.project_id = p.id WHERE t.is_deleted = false AND t.status NOT IN ('done', 'cancelled') AND t.due_date < CURRENT_DATE ORDER BY t.due_date ASC LIMIT 10 """)) overdue_tasks = [dict(r._mapping) for r in result] # Upcoming deadlines (next 7 days) result = await db.execute(text(""" SELECT t.*, d.name as domain_name, d.color as domain_color, p.name as project_name FROM tasks t LEFT JOIN domains d ON t.domain_id = d.id LEFT JOIN projects p ON t.project_id = p.id WHERE t.is_deleted = false AND t.status NOT IN ('done', 'cancelled') AND t.due_date >= CURRENT_DATE AND t.due_date <= CURRENT_DATE + INTERVAL '7 days' ORDER BY t.due_date ASC LIMIT 10 """)) upcoming_tasks = [dict(r._mapping) for r in result] # Task stats result = await db.execute(text(""" SELECT count(*) FILTER (WHERE status NOT IN ('done', 'cancelled')) as open_tasks, count(*) FILTER (WHERE status = 'done' AND completed_at >= CURRENT_DATE - INTERVAL '7 days') as done_this_week, count(*) FILTER (WHERE status = 'in_progress') as in_progress FROM tasks WHERE is_deleted = false """)) stats = dict(result.first()._mapping) return templates.TemplateResponse("dashboard.html", { "request": request, "sidebar": sidebar, "focus_items": focus_items, "overdue_tasks": overdue_tasks, "upcoming_tasks": upcoming_tasks, "stats": stats, "page_title": "Dashboard", "active_nav": "dashboard", }) # ---- Health check ---- @app.get("/health") async def health(): try: ok = await check_db() return {"status": "ok" if ok else "degraded", "database": ok} except Exception as e: return {"status": "error", "database": False, "error": str(e)} # ---- Include routers ---- app.include_router(domains_router.router) app.include_router(areas_router.router) app.include_router(projects_router.router) app.include_router(tasks_router.router) app.include_router(notes_router.router) app.include_router(links_router.router) app.include_router(focus_router.router) app.include_router(capture_router.router) app.include_router(contacts_router.router) app.include_router(search_router.router) app.include_router(admin_router.router) app.include_router(lists_router.router) app.include_router(files_router.router) app.include_router(meetings_router.router) app.include_router(decisions_router.router) app.include_router(weblinks_router.router) app.include_router(appointments_router.router) app.include_router(time_tracking_router.router) app.include_router(processes_router.router)