Robustness: crash-safe migrations, stale-run sweep, optional prep tools; branded 404

- init_db applies each schema file with executescript (semicolons in triggers
  and string literals no longer break it) and records schema_version itself
  after each file, so a forgotten in-file bump can't re-run an ALTER and crash
  startup. Re-running is a no-op.
- On startup, sweep any job_runs left in 'running' (killed by a restart) to
  'interrupted' so the UI/history don't show a run stuck running forever.
- prep-audio.sh skips loudgain and cue_file cleanly when they aren't installed
  (they aren't in the base image) instead of failing on a missing binary and
  logging errors; pipeline-status.sh routes syslog through a guard so a missing
  `logger` doesn't spew into the job log.
- Add a branded 404 page (rendered to static HTML, no external runtime) via a
  custom exception handler that leaves all other responses, including the login
  redirect, untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
andrew
2026-07-09 17:32:26 -06:00
parent 8f1fc458f6
commit fa86e992ad
5 changed files with 188 additions and 34 deletions
+18 -2
View File
@@ -1,17 +1,30 @@
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.exception_handlers import http_exception_handler
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.sessions import SessionMiddleware
from app.db import enable_beets_db_wal, init_db
from app.db import enable_beets_db_wal, init_db, mark_interrupted_runs
from app.routers import artist_casing, auth, credentials, dashboard, dedup, genres, health, import_, jobs, library, playlists
from app.security.session import resolve_session_secret
from app.services import scheduler_service
from app.settings import settings
log = logging.getLogger("alembic")
templates = Jinja2Templates(directory="app/templates")
async def not_found_or_default(request: Request, exc: StarletteHTTPException):
"""Render the branded 404 page for missing pages; leave every other
HTTP exception (including the 303 login redirect raised by require_auth)
to FastAPI's default handler so their status and headers are preserved."""
if exc.status_code == 404:
return templates.TemplateResponse(request, "404.html", {}, status_code=404)
return await http_exception_handler(request, exc)
def _warn_if_key_colocated_with_config() -> None:
@@ -39,6 +52,7 @@ def _warn_if_key_colocated_with_config() -> None:
async def lifespan(_app: FastAPI):
_warn_if_key_colocated_with_config()
init_db()
mark_interrupted_runs()
enable_beets_db_wal()
scheduler = scheduler_service.create_scheduler()
@@ -53,6 +67,8 @@ async def lifespan(_app: FastAPI):
def create_app() -> FastAPI:
app = FastAPI(title="alembic", lifespan=lifespan)
app.add_exception_handler(StarletteHTTPException, not_found_or_default)
app.add_middleware(SessionMiddleware, secret_key=resolve_session_secret())
app.mount("/static", StaticFiles(directory="app/static"), name="static")