diff --git a/app/routers/auth.py b/app/routers/auth.py index c94e96e..3dc57b8 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -55,7 +55,7 @@ async def auth_callback(request: Request): return RedirectResponse(url="/") -@router.get("/logout") +@router.post("/logout") async def logout(request: Request): request.session.clear() end_session_endpoint = None diff --git a/app/routers/jobs.py b/app/routers/jobs.py index 4486541..e022849 100644 --- a/app/routers/jobs.py +++ b/app/routers/jobs.py @@ -99,7 +99,14 @@ async def view_log(run_id: int, user: dict = Depends(require_auth), db=Depends(g raise HTTPException(404, "no such run/log") from pathlib import Path - path = Path(run.log_path) + from app.settings import settings + + # Containment check: only ever serve files from under the logs dir. Paths + # are app-generated so this is defense in depth against a malformed row. + logs_dir = settings.logs_dir.resolve() + path = Path(run.log_path).resolve() + if logs_dir != path and logs_dir not in path.parents: + raise HTTPException(404, "no such run/log") if not path.exists(): raise HTTPException(404, "log file no longer exists") return path.read_text(errors="replace") diff --git a/app/services/pipeline_runner.py b/app/services/pipeline_runner.py index a595f46..d2b108e 100644 --- a/app/services/pipeline_runner.py +++ b/app/services/pipeline_runner.py @@ -42,6 +42,7 @@ def _subprocess_env() -> dict: env["MUSIC_DATA_DIR"] = str(settings.music_data_dir) env["PIPELINE_DIR"] = str(settings.pipeline_dir) env["BEETSDIR"] = str(settings.beets_dir) + env["MIN_ARTIST_DIRS"] = str(settings.min_artist_dirs) return env diff --git a/app/settings.py b/app/settings.py index 956e9ca..e2cbf17 100644 --- a/app/settings.py +++ b/app/settings.py @@ -48,6 +48,12 @@ class Settings(BaseSettings): validation_alias=AliasChoices("TZ", "ALEMBIC_TIMEZONE", "TIMEZONE"), ) + # Share-health gate: the Navidrome scan and Bandcamp sync abort if the + # library has fewer than this many artist folders, to catch a half-mounted + # or empty share before it does damage. The default suits a large library; + # set it lower (via env MIN_ARTIST_DIRS) if yours is small. + min_artist_dirs: int = 500 + @property def beets_dir(self) -> Path: return self.alembic_config_dir / "beets" diff --git a/app/static/style.css b/app/static/style.css index a2cb705..c16bc83 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -219,11 +219,21 @@ a:hover { color: var(--iris-pink); text-decoration: underline; } background: var(--status-success); box-shadow: 0 0 8px var(--status-success); } -.user-chip a.logout { +.user-chip a.logout, +.user-chip button.logout { color: var(--muted-2); font-size: 0.95rem; } -.user-chip a.logout:hover { color: var(--status-danger); text-decoration: none; } +.user-chip button.logout { + background: none; + border: none; + padding: 0; + cursor: pointer; + font-family: inherit; + line-height: 1; +} +.user-chip a.logout:hover, +.user-chip button.logout:hover { color: var(--status-danger); text-decoration: none; } /* ---- layout ---- */ diff --git a/app/templates/base.html b/app/templates/base.html index 7a1fd99..1f88845 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -51,7 +51,9 @@
{{ user.email or user.name }} - +
+ +
{% endif %} diff --git a/pipeline/lib/navidrome-scan.sh b/pipeline/lib/navidrome-scan.sh index 90fe4f8..d488e45 100755 --- a/pipeline/lib/navidrome-scan.sh +++ b/pipeline/lib/navidrome-scan.sh @@ -21,7 +21,7 @@ NAVIDROME_ENV="$ALEMBIC_CONFIG_DIR/pipeline/navidrome/admin.env" LIB="$MUSIC_DATA_DIR/Library" CANARY="$LIB/.navidrome-canary" -MIN_ARTIST_DIRS=500 # library has ~1500; 500 is "clearly not wiped" +MIN_ARTIST_DIRS="${MIN_ARTIST_DIRS:-500}" # override via env for a small library ND="${ND_BASE:-http://navidrome:4533}" ND_USER="${ND_USER:-}" ND_PASS="${ND_PASS:-}" @@ -46,8 +46,10 @@ if [ "$n" -lt "$MIN_ARTIST_DIRS" ]; then fi log "library healthy ($n artist dirs) — triggering full scan" -resp=$(curl -s -G "$ND/rest/startScan.view" \ - --data-urlencode "u=$ND_USER" --data-urlencode "p=$ND_PASS" \ +# Pass the password via stdin (--data-urlencode "p@-"), not on the command +# line, so it isn't visible in ps/proc. printf %s avoids a trailing newline. +resp=$(printf '%s' "$ND_PASS" | curl -s -G "$ND/rest/startScan.view" \ + --data-urlencode "u=$ND_USER" --data-urlencode "p@-" \ --data-urlencode 'v=1.16.0' --data-urlencode 'c=cron' \ --data-urlencode 'f=json' --data-urlencode 'fullScan=true' || true) # Don't log "scan triggered" on faith: a rotated password or Navidrome error diff --git a/pipeline/lib/pipeline-status.sh b/pipeline/lib/pipeline-status.sh index 22ac376..cdaa8e4 100755 --- a/pipeline/lib/pipeline-status.sh +++ b/pipeline/lib/pipeline-status.sh @@ -351,9 +351,10 @@ dedup_today_status() { # 6. Navidrome section "Navidrome" - ND_RESP=$(curl -s --connect-timeout 5 -G "$ND_BASE/rest/getScanStatus.view" \ + # Password via stdin (p@-), not argv, so it isn't exposed in ps/proc. + ND_RESP=$(printf '%s' "$ND_PASS" | curl -s --connect-timeout 5 -G "$ND_BASE/rest/getScanStatus.view" \ --data-urlencode "u=$ND_USER" \ - --data-urlencode "p=$ND_PASS" \ + --data-urlencode "p@-" \ --data-urlencode 'v=1.16.0' --data-urlencode 'c=status' --data-urlencode 'f=json' 2>/dev/null) ND_LINE=$(echo "$ND_RESP" | python3 -c " import sys, json diff --git a/pipeline/lib/sync-bandcamp.sh b/pipeline/lib/sync-bandcamp.sh index fb678e2..9e82899 100755 --- a/pipeline/lib/sync-bandcamp.sh +++ b/pipeline/lib/sync-bandcamp.sh @@ -41,7 +41,7 @@ fi # equivalent (same pattern as navidrome-scan.sh's share-health gate). LIB="${MUSIC_DATA_DIR:-/data/music}/Library" CANARY="$LIB/.navidrome-canary" -MIN_ARTIST_DIRS=500 +MIN_ARTIST_DIRS="${MIN_ARTIST_DIRS:-500}" if [[ ! -r "$CANARY" ]]; then log "ERROR: canary $CANARY missing/unreadable — share looks unhealthy — aborting sync" exit 1