206 lines
6.7 KiB
Python
206 lines
6.7 KiB
Python
"""
|
|
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,
|
|
calendar as calendar_router,
|
|
time_budgets as time_budgets_router,
|
|
eisenhower as eisenhower_router,
|
|
history as history_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)
|
|
app.include_router(calendar_router.router)
|
|
app.include_router(time_budgets_router.router)
|
|
app.include_router(eisenhower_router.router)
|
|
app.include_router(history_router.router)
|