siftlode/backend/app/main.py
npeter83 b9a3a9012d feat(notifications): durable per-user inbox (P1) + maintenance schema
Add a server-backed notification center that coexists with the client-side
transient bell: a per-user `notifications` table (type/title/body/data JSON/
read/dismissed), a `/api/me/notifications` CRUD API (list, unread_count, read,
read_all, dismiss, clear), and a left-nav inbox module with a live unread badge
polled via useLiveQuery. Known types render trilingual text from type+data
(English stored text is the fallback); read rows are trimmed past a soft cap.

Also adds the schema the maintenance job builds on: videos.list?part=status
columns (embeddable/privacy_status/upload_status) and the validation lifecycle
columns (last_checked_at, unavailable_since, unavailable_reason).

Page-id validation is centralized in one PAGES source of truth (isPage) so the
new page survives reload without a second allowlist to keep in sync.
2026-06-18 03:20:17 +02:00

108 lines
3.1 KiB
Python

import logging
import sys
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException
# When started via uvicorn --log-config the "subfeed" logger is already configured
# (see log_config.json). This block is a timestamped fallback for other entrypoints
# (tests, scripts) so our logs are never silently dropped.
_subfeed_logger = logging.getLogger("subfeed")
if not _subfeed_logger.handlers:
_handler = logging.StreamHandler(sys.stdout)
_handler.setFormatter(
logging.Formatter("%(asctime)s %(levelname)-5s [%(name)s] %(message)s")
)
_subfeed_logger.addHandler(_handler)
_subfeed_logger.setLevel(logging.INFO)
_subfeed_logger.propagate = False
log = logging.getLogger("subfeed.app")
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from app import auth
from app.config import settings
from app.routes import (
admin,
channels,
feed,
health,
me,
notifications,
playlists,
quota,
scheduler as scheduler_routes,
sync,
tags,
version,
)
from app.scheduler import shutdown_scheduler, start_scheduler
@asynccontextmanager
async def lifespan(app: FastAPI):
log.info("Siftlode starting up")
start_scheduler()
try:
yield
finally:
log.info("Siftlode shutting down")
shutdown_scheduler()
app = FastAPI(title=settings.app_name, lifespan=lifespan)
app.add_middleware(
SessionMiddleware,
secret_key=settings.secret_key,
same_site="lax", # required so the cookie rides the OAuth redirect back from Google
https_only=settings.session_https_only, # Secure flag when served over HTTPS (prod)
)
if settings.frontend_origin:
app.add_middleware(
CORSMiddleware,
allow_origins=[settings.frontend_origin],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(health.router)
app.include_router(auth.router)
app.include_router(sync.router)
app.include_router(tags.router)
app.include_router(feed.router)
app.include_router(me.router)
app.include_router(notifications.router)
app.include_router(channels.router)
app.include_router(playlists.router)
app.include_router(admin.router)
app.include_router(scheduler_routes.router)
app.include_router(quota.router)
app.include_router(version.router)
# The built SPA (populated by the Docker frontend build stage).
STATIC_DIR = Path(__file__).parent / "static_spa"
app.mount(
"/assets",
StaticFiles(directory=STATIC_DIR / "assets", check_dir=False),
name="assets",
)
@app.get("/")
async def index() -> FileResponse:
return FileResponse(STATIC_DIR / "index.html")
@app.get("/{full_path:path}")
async def spa_fallback(full_path: str) -> FileResponse:
# Client-side routes fall back to index.html; real API/asset paths are matched above.
if full_path.startswith(("api/", "auth/", "healthz", "assets/")):
raise HTTPException(status_code=404)
return FileResponse(STATIC_DIR / "index.html")