Files
lifeos-dev/main.py

188 lines
6.0 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,
)
@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 ----
@app.middleware("http")
async def add_request_context(request: Request, call_next):
"""Make environment available to all templates."""
request.state.environment = os.getenv("ENVIRONMENT", "production")
response = await call_next(request)
return response
# ---- 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)