This commit is contained in:
timka123 committed 2026-10-18 18:02:05 +03:00
commit 17a0a3c4c9
121 files changed
+17641

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
"""The ASGI application.
On start it creates the schema, seeds the settings from the environment, starts
the background jobs and the Telegram bot, and — when a built frontend is present
in `static/` — serves the web UI from the same origin, which is what the Docker
image does.
"""
from __future__ import annotations
import logging
import os
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from app import __version__
from app.api import api_router
from app.api.deps import require_token
from app.config import env_settings
from app.db import init_db, session_scope
from app.services import scheduler, settings_service, telegram_bot
logging.basicConfig(
level=os.environ.get("LOG_LEVEL", "INFO").upper(),
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
)
log = logging.getLogger("kadence")
STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
with session_scope() as session:
settings = settings_service.load_settings(session)
log.info("settings loaded (timezone=%s)", settings.timezone)
scheduler.start(settings)
if settings.notifications.telegram.enabled:
telegram_bot.start()
# The bot has to come up later too, if Telegram is switched on in the UI.
settings_service.on_change(
lambda new: telegram_bot.start() if new.notifications.telegram.enabled else None
)
try:
yield
finally:
scheduler.shutdown()
telegram_bot.shutdown()
app = FastAPI(
title="Kadence",
version=__version__,
summary="Pomodoro timer, todo aggregation, blocking policy and daily reports.",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=env_settings().cors_origin_list,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router, dependencies=[Depends(require_token)])
@app.get("/api/health", tags=["meta"])
def health() -> dict:
return {"status": "ok", "version": __version__}
@app.get("/api/info", tags=["meta"])
def info() -> dict:
"""What a client needs to know before it has a token: who we are, what is on."""
env = env_settings()
return {
"version": __version__,
"auth_required": bool(env.api_token),
"scheduler": env.scheduler_enabled,
"jobs": scheduler.describe(),
}
# ---- the web UI, when it has been built into ./static ----------------------
if STATIC_DIR.is_dir():
app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")
@app.get("/{path:path}", include_in_schema=False)
def spa(path: str):
"""Serve the single-page app, letting the client router own the URL."""
candidate = STATIC_DIR / path
if path and candidate.is_file():
return FileResponse(candidate)
index = STATIC_DIR / "index.html"
if index.is_file():
return FileResponse(index)
return JSONResponse({"detail": "Not found"}, status_code=404)