- Remove Planka: docker-compose services, apache /board/ proxy, env vars, custom CSS dir
- Add Kanban board at /tasks: 4 columns (To Do / Doing / Testing / Done),
native HTML5 drag-and-drop, card modal (TaskForm reuse), per-column "+" button
- Add 'testing' status to Task model validator and frontend union type
- Add GET /api/tasks/{id} endpoint (was missing, frontend already called it)
- Enrich DevOps clone: live-fetches description, AC, assignee, iteration,
comments and attachments from ADO; renders as Markdown in task.notes
- Add /omg page: standalone project/client/job# registry with inline editing
and create/edit/delete dialog; backed by new omg_entries table (migration 0008)
- Add omg router to main.py; add OMG + Tasks to sidebar and router
- Fix dead /planner link on Dashboard -> /tasks
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
122 lines
3.3 KiB
Python
122 lines
3.3 KiB
Python
import logging
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
import structlog
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from src.config import settings
|
|
|
|
STATIC_DIR = Path("src/static")
|
|
|
|
|
|
def _ensure_static_dir() -> None:
|
|
"""Create src/static with a stub index.html if the frontend hasn't been built yet."""
|
|
STATIC_DIR.mkdir(parents=True, exist_ok=True)
|
|
idx = STATIC_DIR / "index.html"
|
|
if not idx.exists():
|
|
idx.write_text(
|
|
"<html><body><h2>Frontend not built. Run: cd web && npm run build</h2></body></html>"
|
|
)
|
|
from src.middleware.logging import LoggingMiddleware
|
|
from src.routers import admin, auth, dashboard, events, ingest, keys, projects
|
|
from src.routers import calendar, tasks, manual_entries, budgets, tags, devops, exports, reports, omg
|
|
from src.services.scheduler import scheduler, setup_scheduler
|
|
|
|
BASE = settings.BASE_PATH
|
|
|
|
|
|
def _configure_logging() -> None:
|
|
shared_processors = [
|
|
structlog.contextvars.merge_contextvars,
|
|
structlog.stdlib.add_log_level,
|
|
structlog.processors.TimeStamper(fmt="iso"),
|
|
]
|
|
if settings.LOG_FORMAT == "json":
|
|
renderer = structlog.processors.JSONRenderer()
|
|
else:
|
|
renderer = structlog.dev.ConsoleRenderer(colors=True)
|
|
structlog.configure(
|
|
processors=shared_processors + [renderer],
|
|
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
|
|
context_class=dict,
|
|
logger_factory=structlog.PrintLoggerFactory(),
|
|
cache_logger_on_first_use=True,
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
_configure_logging()
|
|
_ensure_static_dir()
|
|
setup_scheduler()
|
|
scheduler.start()
|
|
yield
|
|
scheduler.shutdown()
|
|
|
|
|
|
app = FastAPI(
|
|
title=settings.APP_TITLE,
|
|
docs_url=f"{BASE}/docs" if settings.DEBUG else None,
|
|
redoc_url=None,
|
|
root_path=BASE,
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
app.add_middleware(LoggingMiddleware)
|
|
|
|
for router in [
|
|
auth.router,
|
|
keys.router,
|
|
admin.router,
|
|
ingest.router,
|
|
dashboard.router,
|
|
events.router,
|
|
projects.router,
|
|
calendar.router,
|
|
tasks.router,
|
|
manual_entries.router,
|
|
budgets.router,
|
|
tags.router,
|
|
devops.router,
|
|
exports.router,
|
|
reports.router,
|
|
omg.router,
|
|
]:
|
|
app.include_router(router)
|
|
|
|
|
|
@app.get("/healthz", include_in_schema=False)
|
|
async def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
app.mount("/static", StaticFiles(directory="src/static"), name="static")
|
|
|
|
|
|
_NO_CACHE = {"Cache-Control": "no-cache, no-store, must-revalidate"}
|
|
|
|
|
|
@app.get("/", include_in_schema=False)
|
|
@app.get("", include_in_schema=False)
|
|
async def spa_root():
|
|
return FileResponse("src/static/index.html", headers=_NO_CACHE)
|
|
|
|
|
|
@app.get("/{path:path}", include_in_schema=False)
|
|
async def spa_fallback(path: str, request: Request):
|
|
if path.startswith("api/") or path.startswith("static/"):
|
|
from fastapi import HTTPException
|
|
|
|
raise HTTPException(status_code=404)
|
|
return FileResponse("src/static/index.html", headers=_NO_CACHE)
|