tts-voice-creator-clone-and.../routes/admin.py
mARTin-B78 40e42590cc Release v1.6.0: a11y (WCAG AA), i18n (DE), PWA, perf, tests, Cast UX
Cast: card/list views, sort & filter, online voice picker, "Hear a line"
sample button, AI character notes, import auto-save.

Platform: WCAG 2.1 AA accessibility pass; German UI translation + language
picker; installable PWA with offline shell; GZip + content-visibility
virtualization + lazy images + Rehearser PCM memory cap (mobile stability);
Playwright suite (desktop + iPhone); opt-in minified bundle build.

Fixes: screenplay parser false characters; Fish-Speech inline-tag tones;
narrator/voice pickers list full library; clone GUI rework; fish.audio
import dedup; voice-ID rename; bulk-delete modal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:23:35 +02:00

82 lines
2.4 KiB
Python

"""Admin routes: index, favicon, browse-dirs, robots."""
from __future__ import annotations
from pathlib import Path
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse, PlainTextResponse, Response
from core.constants import STATIC_DIR, __version__
router = APIRouter()
_BROWSE_BLOCKED: frozenset[str] = frozenset({
"/proc", "/sys", "/dev", "/run", "/boot",
})
@router.get("/")
async def index():
return FileResponse(
STATIC_DIR / "index.html",
headers={"Cache-Control": "no-store, max-age=0"},
)
@router.get("/favicon.ico")
async def favicon():
return FileResponse(STATIC_DIR / "icon.svg", media_type="image/svg+xml")
# ── PWA: serve the service worker and manifest from root so the SW controls "/" ──
@router.get("/sw.js")
async def service_worker():
return FileResponse(
STATIC_DIR / "sw.js",
media_type="application/javascript",
headers={"Cache-Control": "no-cache", "Service-Worker-Allowed": "/"},
)
@router.get("/manifest.webmanifest")
async def manifest():
return FileResponse(STATIC_DIR / "manifest.webmanifest", media_type="application/manifest+json")
@router.get("/api/browse-dirs")
async def browse_dirs(path: str = "/"):
p = Path(path).resolve()
p_str = str(p)
if any(p_str == b or p_str.startswith(b + "/") for b in _BROWSE_BLOCKED):
raise HTTPException(403, "Access to this path is not permitted")
if not p.is_dir():
raise HTTPException(404, "Not a directory")
try:
entries = sorted(
[d.name for d in p.iterdir() if d.is_dir() and not d.name.startswith(".")],
key=str.lower,
)
except PermissionError:
raise HTTPException(403, "Permission denied")
parent = str(p.parent) if p.parent != p else None
return {"path": str(p), "parent": parent, "dirs": entries}
@router.get("/api/version")
async def get_version():
return {"version": __version__}
@router.get("/api/changelog", response_class=PlainTextResponse)
async def get_changelog():
root = Path(__file__).parent.parent
for cl in (root / "CHANGELOG.md", root / "static" / "CHANGELOG.md"):
if cl.is_file():
return cl.read_text(encoding="utf-8")
raise HTTPException(404, "CHANGELOG.md not found")
@router.get("/robots.txt", response_class=PlainTextResponse)
async def robots_txt():
return "User-agent: *\nDisallow: /"