c87ddc2649
job descriptions Dashboard: format breakdown (FLAC/MP3/etc. counts), approximate library size (bitrate*length/8, same approximation `beet stats` itself uses -- verified to match its "100.5 GiB" output exactly against the real library), and a credential auth-state summary per scope (configured y/n plus, for Bandcamp, cookie expiry parsed locally from the stored cookie jar -- deliberately not a live network probe like pipeline-status.sh's Qobuz check, since this renders on every dashboard load). Library: was one unfiltered page dumping all 4072 tracks. Added search (artist/title/album), playlist and format filter dropdowns, and real SQL-level pagination (LIMIT/OFFSET, not fetch-everything-then-slice). beets_service gained count_items()/distinct_formats() to support this. Playlists: cron_expr is still the stored/scheduled representation, but the UI now shows and edits a plain daily time picker instead of raw cron syntax -- every playlist schedule today is a simple daily HH:MM anyway. playlist_service.cron_to_time()/time_to_cron() convert at the router boundary; verified round-trip against all real playlist cron values. Jobs: each maintenance job now shows a short one-line description of what it actually does (MAINTENANCE_JOB_DESCRIPTIONS), and "next run" is formatted consistently with playlists' time style (HH:MM, with a day qualifier for non-today runs -- maintenance jobs can be weekly/monthly, unlike playlists' plain daily schedule). Verified end-to-end against the real production data (4072 tracks, 100.5 GiB, real credentials): stats/format-breakdown/search/pagination all correct, all 7 credential scopes report configured with Bandcamp's real cookie expiry (325 days), and a full authenticated page-render sweep (dashboard, library with every filter combination, playlist detail, jobs) all returned 200 with no template errors.
43 lines
1.4 KiB
Python
43 lines
1.4 KiB
Python
from fastapi import APIRouter, Depends, Request
|
|
from fastapi.templating import Jinja2Templates
|
|
from sqlalchemy import select
|
|
|
|
from app.db import get_db
|
|
from app.models import JobRun
|
|
from app.security.deps import require_auth
|
|
from app.services import beets_service, credential_service, playlist_service
|
|
|
|
router = APIRouter(tags=["dashboard"])
|
|
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
|
|
def _human_bytes(n: int) -> str:
|
|
size = float(n)
|
|
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
if size < 1024 or unit == "TB":
|
|
return f"{size:.1f} {unit}"
|
|
size /= 1024
|
|
return f"{size:.1f} TB"
|
|
|
|
|
|
@router.get("/")
|
|
async def dashboard(request: Request, user: dict = Depends(require_auth), db=Depends(get_db)):
|
|
stats = beets_service.stats()
|
|
playlists = playlist_service.list_all(db)
|
|
recent_runs = list(
|
|
db.execute(select(JobRun).order_by(JobRun.started_at.desc()).limit(10)).scalars()
|
|
)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"dashboard.html",
|
|
{
|
|
"user": user,
|
|
"stats": stats,
|
|
"total_size_human": _human_bytes(stats.get("total_bytes", 0)),
|
|
"playlist_count": len(playlists),
|
|
"active_playlist_count": len([p for p in playlists if p.active]),
|
|
"recent_runs": recent_runs,
|
|
"auth_states": credential_service.auth_states(db),
|
|
},
|
|
)
|