Initial commit
This commit is contained in:
241
main.py
Normal file
241
main.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
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)
|
||||
|
||||
# Overdue projects (target_date in the past)
|
||||
result = await db.execute(text("""
|
||||
SELECT p.id, p.name, p.priority, p.target_date, p.status,
|
||||
d.name as domain_name, d.color as domain_color,
|
||||
count(t.id) FILTER (WHERE t.is_deleted = false) as task_count,
|
||||
count(t.id) FILTER (WHERE t.is_deleted = false AND t.status = 'done') as done_count
|
||||
FROM projects p
|
||||
LEFT JOIN domains d ON p.domain_id = d.id
|
||||
LEFT JOIN tasks t ON t.project_id = p.id
|
||||
WHERE p.is_deleted = false AND p.status IN ('active', 'on_hold')
|
||||
AND p.target_date < CURRENT_DATE
|
||||
GROUP BY p.id, d.name, d.color
|
||||
ORDER BY p.target_date ASC
|
||||
LIMIT 10
|
||||
"""))
|
||||
overdue_projects = [dict(r._mapping) for r in result]
|
||||
|
||||
# Upcoming project deadlines (next 30 days)
|
||||
result = await db.execute(text("""
|
||||
SELECT p.id, p.name, p.priority, p.target_date, p.status,
|
||||
d.name as domain_name, d.color as domain_color,
|
||||
count(t.id) FILTER (WHERE t.is_deleted = false) as task_count,
|
||||
count(t.id) FILTER (WHERE t.is_deleted = false AND t.status = 'done') as done_count
|
||||
FROM projects p
|
||||
LEFT JOIN domains d ON p.domain_id = d.id
|
||||
LEFT JOIN tasks t ON t.project_id = p.id
|
||||
WHERE p.is_deleted = false AND p.status IN ('active', 'on_hold')
|
||||
AND p.target_date >= CURRENT_DATE AND p.target_date <= CURRENT_DATE + INTERVAL '30 days'
|
||||
GROUP BY p.id, d.name, d.color
|
||||
ORDER BY p.target_date ASC
|
||||
LIMIT 10
|
||||
"""))
|
||||
upcoming_projects = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("dashboard.html", {
|
||||
"request": request,
|
||||
"sidebar": sidebar,
|
||||
"focus_items": focus_items,
|
||||
"overdue_tasks": overdue_tasks,
|
||||
"upcoming_tasks": upcoming_tasks,
|
||||
"overdue_projects": overdue_projects,
|
||||
"upcoming_projects": upcoming_projects,
|
||||
"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)
|
||||
Reference in New Issue
Block a user