Add hybrid app version labels

This commit is contained in:
2026-05-07 15:17:31 +02:00
parent 1113fac144
commit 1d1f052d18
15 changed files with 158 additions and 11 deletions
+6 -1
View File
@@ -1,7 +1,12 @@
FROM python:3.11-slim FROM python:3.11-slim
ARG APP_VERSION=0.1.0
ARG APP_BUILD_INFO=dev
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1 \
APP_VERSION=${APP_VERSION} \
APP_BUILD_INFO=${APP_BUILD_INFO}
WORKDIR /app/backend WORKDIR /app/backend
+8 -1
View File
@@ -71,7 +71,13 @@ SMTP_TIMEOUT=30
# Jellyfin, Jellyseerr, and SSH targets are now configured per machine in the app's Settings tab. # Jellyfin, Jellyseerr, and SSH targets are now configured per machine in the app's Settings tab.
# The backend seeds a local machine automatically, so no global Jellyfin or SSH env vars are required. # The backend seeds a local machine automatically, so no global Jellyfin or SSH env vars are required.
#
# Versioning
# The backend tries to auto-detect its version from installed package metadata.
# If needed, you can override the displayed version/build markers with APP_VERSION and APP_BUILD_INFO.
APP_VERSION=0.1.0
APP_BUILD_INFO=dev
# Authentik / OIDC # Authentik / OIDC
AUTH_ENABLED=true AUTH_ENABLED=true
OIDC_ISSUER_URL=https://authentik.example/application/o/media-library-viewer/ OIDC_ISSUER_URL=https://authentik.example/application/o/media-library-viewer/
@@ -150,3 +156,4 @@ For local development, `docker compose -f docker-compose.dev.yml up --build` doe
- `GET /api/jobs/templates` — Available jobs - `GET /api/jobs/templates` — Available jobs
- `POST /api/jobs/run` — Execute a job - `POST /api/jobs/run` — Execute a job
- `GET /api/users` — Jellyfin users with optional Jellyseerr enrichment - `GET /api/users` — Jellyfin users with optional Jellyseerr enrichment
- `GET /api/version` — Backend version/build metadata for the UI
+11 -1
View File
@@ -14,6 +14,7 @@ from media_library_viewer_api.auth import require_jwt_auth, validate_auth_settin
from media_library_viewer_api.config import get_settings from media_library_viewer_api.config import get_settings
from media_library_viewer_api.logging_utils import configure_logging, describe_settings from media_library_viewer_api.logging_utils import configure_logging, describe_settings
from media_library_viewer_api.routers import dashboard, monitoring, media, files, jobs, users, tasks from media_library_viewer_api.routers import dashboard, monitoring, media, files, jobs, users, tasks
from .version import get_backend_version, get_version_info
from media_library_viewer_api.routers.settings import router as settings_router from media_library_viewer_api.routers.settings import router as settings_router
from media_library_viewer_api.dependencies import get_mail_queue, get_monitoring_poller, get_settings_store from media_library_viewer_api.dependencies import get_mail_queue, get_monitoring_poller, get_settings_store
from media_library_viewer_api.services.known_hosts import ensure_known_hosts_for_machines from media_library_viewer_api.services.known_hosts import ensure_known_hosts_for_machines
@@ -46,7 +47,7 @@ async def lifespan(app: FastAPI):
app = FastAPI( app = FastAPI(
title="Manage API", title="Manage API",
version="0.1.0", version=get_backend_version(),
description="Manage API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access.", description="Manage API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access.",
lifespan=lifespan, lifespan=lifespan,
) )
@@ -67,6 +68,8 @@ app.add_middleware(
@app.middleware("http") @app.middleware("http")
async def enforce_jwt_auth(request: Request, call_next): async def enforce_jwt_auth(request: Request, call_next):
if request.url.path in {"/api/health", "/api/version"}:
return await call_next(request)
return await require_jwt_auth(request, call_next) return await require_jwt_auth(request, call_next)
@app.middleware("http") @app.middleware("http")
@@ -109,5 +112,12 @@ def health_check() -> dict[str, str]:
return {"status": "ok"} return {"status": "ok"}
@app.get("/api/version")
def version_info() -> dict[str, str]:
"""Expose backend version/build metadata for the UI."""
logger.debug("version requested")
return get_version_info()
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -0,0 +1,50 @@
"""Version helpers for the backend service."""
from __future__ import annotations
import os
from importlib.metadata import PackageNotFoundError, version as package_version
PACKAGE_NAME = "media-library-viewer-backend"
DEFAULT_VERSION = "0.1.0"
def get_backend_version() -> str:
"""Return the packaged backend version or a stable fallback."""
env_version = os.getenv("APP_VERSION", "").strip()
if env_version:
return env_version
try:
return package_version(PACKAGE_NAME)
except PackageNotFoundError:
return DEFAULT_VERSION
def get_backend_build_info() -> str:
"""Return a short build marker such as a git SHA or dev marker."""
return (
os.getenv("APP_BUILD_INFO", "").strip()
or os.getenv("GIT_COMMIT", "").strip()
or os.getenv("BUILD_COMMIT", "").strip()
or "dev"
)
def format_version_label(version: str, build_info: str) -> str:
"""Format a user-facing version label."""
label = version.strip() or DEFAULT_VERSION
build = build_info.strip()
if build and build != "dev":
return f"{label}+{build}"
return label
def get_version_info() -> dict[str, str]:
version = get_backend_version()
build_info = get_backend_build_info()
return {
"app": "Manage",
"backend_version": version,
"backend_build": build_info,
"backend_label": format_version_label(version, build_info),
}
+5
View File
@@ -3,6 +3,9 @@ services:
build: build:
context: . context: .
dockerfile: backend/Dockerfile dockerfile: backend/Dockerfile
args:
APP_VERSION: ${APP_VERSION:-0.1.0}
APP_BUILD_INFO: ${APP_BUILD_INFO:-dev}
environment: environment:
AUTH_ENABLED: ${AUTH_ENABLED:-true} AUTH_ENABLED: ${AUTH_ENABLED:-true}
OIDC_ISSUER_URL: ${OIDC_ISSUER_URL:?set OIDC_ISSUER_URL} OIDC_ISSUER_URL: ${OIDC_ISSUER_URL:?set OIDC_ISSUER_URL}
@@ -60,6 +63,8 @@ services:
VITE_OIDC_REDIRECT_URI: ${VITE_OIDC_REDIRECT_URI:?set VITE_OIDC_REDIRECT_URI} VITE_OIDC_REDIRECT_URI: ${VITE_OIDC_REDIRECT_URI:?set VITE_OIDC_REDIRECT_URI}
VITE_OIDC_POST_LOGOUT_REDIRECT_URI: ${VITE_OIDC_POST_LOGOUT_REDIRECT_URI:?set VITE_OIDC_POST_LOGOUT_REDIRECT_URI} VITE_OIDC_POST_LOGOUT_REDIRECT_URI: ${VITE_OIDC_POST_LOGOUT_REDIRECT_URI:?set VITE_OIDC_POST_LOGOUT_REDIRECT_URI}
VITE_DEV_API_PROXY_TARGET: ${VITE_DEV_API_PROXY_TARGET:-http://backend:8000} VITE_DEV_API_PROXY_TARGET: ${VITE_DEV_API_PROXY_TARGET:-http://backend:8000}
VITE_APP_VERSION: ${APP_VERSION:-0.1.0}
VITE_APP_BUILD_INFO: ${APP_BUILD_INFO:-dev}
depends_on: depends_on:
backend: backend:
condition: service_healthy condition: service_healthy
+2
View File
@@ -273,3 +273,5 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo
- 2026-05-06: Added an Actions tab for saved server tasks, with backend persistence, per-task run history, and support for shell/Python task types on either local or SSH machines. - 2026-05-06: Added an Actions tab for saved server tasks, with backend persistence, per-task run history, and support for shell/Python task types on either local or SSH machines.
- 2026-05-06: Reusable dialog footers now keep cancel on the left and confirm on the right, and hover edit buttons now appear on the right edge of editable list rows in Actions and Settings. - 2026-05-06: Reusable dialog footers now keep cancel on the left and confirm on the right, and hover edit buttons now appear on the right edge of editable list rows in Actions and Settings.
- 2026-05-06: Library stats, Jellyfin activity, and Monitoring overview now use shared section-container patterns so subcontainers stay consistent across the app. - 2026-05-06: Library stats, Jellyfin activity, and Monitoring overview now use shared section-container patterns so subcontainers stay consistent across the app.
- 2026-05-07: The app versioning scheme should be hybrid: auto-detect package/build metadata when available, but allow explicit overrides for deployments that need fixed labels.
- 2026-05-07: The shell should display both frontend and backend version labels so deployed builds are easy to identify without opening a separate diagnostics screen.
+8 -2
View File
@@ -15,6 +15,8 @@ ARG VITE_OIDC_SCOPE=openid profile email
ARG VITE_OIDC_REDIRECT_URI= ARG VITE_OIDC_REDIRECT_URI=
ARG VITE_OIDC_POST_LOGOUT_REDIRECT_URI= ARG VITE_OIDC_POST_LOGOUT_REDIRECT_URI=
ARG VITE_DEV_API_PROXY_TARGET=http://backend:8000 ARG VITE_DEV_API_PROXY_TARGET=http://backend:8000
ARG VITE_APP_VERSION=0.1.0
ARG VITE_APP_BUILD_INFO=dev
ENV VITE_API_URL=${VITE_API_URL} \ ENV VITE_API_URL=${VITE_API_URL} \
VITE_OIDC_ENABLED=${VITE_OIDC_ENABLED} \ VITE_OIDC_ENABLED=${VITE_OIDC_ENABLED} \
@@ -23,7 +25,9 @@ ENV VITE_API_URL=${VITE_API_URL} \
VITE_OIDC_SCOPE=${VITE_OIDC_SCOPE} \ VITE_OIDC_SCOPE=${VITE_OIDC_SCOPE} \
VITE_OIDC_REDIRECT_URI=${VITE_OIDC_REDIRECT_URI} \ VITE_OIDC_REDIRECT_URI=${VITE_OIDC_REDIRECT_URI} \
VITE_OIDC_POST_LOGOUT_REDIRECT_URI=${VITE_OIDC_POST_LOGOUT_REDIRECT_URI} \ VITE_OIDC_POST_LOGOUT_REDIRECT_URI=${VITE_OIDC_POST_LOGOUT_REDIRECT_URI} \
VITE_DEV_API_PROXY_TARGET=${VITE_DEV_API_PROXY_TARGET} VITE_DEV_API_PROXY_TARGET=${VITE_DEV_API_PROXY_TARGET} \
VITE_APP_VERSION=${VITE_APP_VERSION} \
VITE_APP_BUILD_INFO=${VITE_APP_BUILD_INFO}
RUN npm run build RUN npm run build
@@ -45,7 +49,9 @@ COPY frontend/ ./
ENV VITE_API_URL=/api \ ENV VITE_API_URL=/api \
VITE_OIDC_ENABLED=false \ VITE_OIDC_ENABLED=false \
VITE_DEV_API_PROXY_TARGET=http://backend:8000 VITE_DEV_API_PROXY_TARGET=http://backend:8000 \
VITE_APP_VERSION=0.1.0 \
VITE_APP_BUILD_INFO=dev
EXPOSE 5173 EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
+2 -1
View File
@@ -45,7 +45,7 @@ Output goes to `frontend/dist/`.
## Pages ## Pages
- **Dashboard** (`/`) — Now playing, backend-collected per-machine monitoring table with 10-minute averages/min/max, library stats - **Dashboard** (`/`) — Now playing, backend-collected per-machine monitoring table with 10-minute averages/min/max, library stats, frontend/backend version chips in the shell header
- **Monitoring** (`/monitoring`) — Per-machine CPU/IO wait/RAM/network/disk charts, collector controls, and backend-collected recent action history - **Monitoring** (`/monitoring`) — Per-machine CPU/IO wait/RAM/network/disk charts, collector controls, and backend-collected recent action history
- **Media** (`/media`) — Full-library table with sort/filter/search - **Media** (`/media`) — Full-library table with sort/filter/search
- **Users** (`/users`) — Read-only Jellyfin user list with optional Jellyseerr enrichment - **Users** (`/users`) — Read-only Jellyfin user list with optional Jellyseerr enrichment
@@ -55,6 +55,7 @@ Output goes to `frontend/dist/`.
## Environment Variables ## Environment Variables
Set `VITE_API_URL` and any OIDC variables directly in your shell or Compose build args if the API is not at `http://localhost:8000`. Set `VITE_API_URL` and any OIDC variables directly in your shell or Compose build args if the API is not at `http://localhost:8000`.
The frontend version defaults to the package.json version and can be overridden with `VITE_APP_VERSION` and `VITE_APP_BUILD_INFO` when you need explicit deployed labels.
```bash ```bash
VITE_API_URL=http://your-backend-host:8000 VITE_API_URL=http://your-backend-host:8000
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.0.0", "version": "0.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+23 -1
View File
@@ -5,7 +5,11 @@ import {
NavLink, NavLink,
useLocation, useLocation,
} from "react-router-dom"; } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import {
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/react-query";
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from "@mui/material/styles";
import { import {
AppBar, AppBar,
@@ -34,6 +38,8 @@ import { FileBrowser } from "./pages/FileBrowser";
import { Actions } from "./pages/Actions"; import { Actions } from "./pages/Actions";
import { getAppTheme } from "./theme"; import { getAppTheme } from "./theme";
import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth"; import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth";
import { fetchAppVersion } from "./api/client";
import { FRONTEND_VERSION_LABEL } from "./version";
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } }, defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } },
@@ -51,6 +57,12 @@ function Shell({
const location = useLocation(); const location = useLocation();
const current = location.pathname; const current = location.pathname;
const isMobile = useMediaQuery("(max-width: 900px)"); const isMobile = useMediaQuery("(max-width: 900px)");
const { data: appVersion } = useQuery({
queryKey: ["app-version"],
queryFn: fetchAppVersion,
staleTime: 60 * 60 * 1000,
});
const backendLabel = appVersion?.backend_label || "…";
return ( return (
<> <>
@@ -126,6 +138,16 @@ function Shell({
width: { xs: "100%", md: "auto" }, width: { xs: "100%", md: "auto" },
}} }}
> >
<Chip
label={`FE ${FRONTEND_VERSION_LABEL}`}
variant="outlined"
sx={{ maxWidth: { xs: "100%", sm: 220 } }}
/>
<Chip
label={`BE ${backendLabel}`}
variant="outlined"
sx={{ maxWidth: { xs: "100%", sm: 220 } }}
/>
{authLabel && ( {authLabel && (
<Chip <Chip
label={authLabel} label={authLabel}
+2
View File
@@ -12,6 +12,7 @@ import type {
NowPlayingSession, NowPlayingSession,
MonitoringPollerStatus, MonitoringPollerStatus,
MonitoringOverviewResponse, MonitoringOverviewResponse,
AppVersionInfo,
MonitoringStatus, MonitoringStatus,
MonitoringMetrics, MonitoringMetrics,
DiskSpace, DiskSpace,
@@ -165,6 +166,7 @@ export const fetchMonitoringMachines = () =>
get<MonitoringMachine[]>("/api/monitoring/machines"); get<MonitoringMachine[]>("/api/monitoring/machines");
export const fetchMonitoringPoller = () => export const fetchMonitoringPoller = () =>
get<MonitoringPollerStatus>("/api/monitoring/poller"); get<MonitoringPollerStatus>("/api/monitoring/poller");
export const fetchAppVersion = () => get<AppVersionInfo>("/api/version");
export const fetchMonitoringOverview = () => export const fetchMonitoringOverview = () =>
get<MonitoringOverviewResponse>("/api/dashboard/monitoring"); get<MonitoringOverviewResponse>("/api/dashboard/monitoring");
export const fetchDashboardShortcuts = () => export const fetchDashboardShortcuts = () =>
+7
View File
@@ -297,6 +297,13 @@ export interface MonitoringPollerStatus {
retention_days: number; retention_days: number;
} }
export interface AppVersionInfo {
app: string;
backend_version: string;
backend_build: string;
backend_label: string;
}
export interface MonitoringMachineOverview { export interface MonitoringMachineOverview {
machine: MonitoringMachine; machine: MonitoringMachine;
status: string; status: string;
+19
View File
@@ -0,0 +1,19 @@
declare const __APP_VERSION__: string;
declare const __APP_BUILD_INFO__: string;
export const FRONTEND_VERSION = __APP_VERSION__;
export const FRONTEND_BUILD_INFO = __APP_BUILD_INFO__;
export function formatVersionLabel(version: string, buildInfo: string): string {
const trimmedVersion = version.trim() || "0.1.0";
const trimmedBuild = buildInfo.trim();
if (trimmedBuild && trimmedBuild !== "dev") {
return `${trimmedVersion}+${trimmedBuild}`;
}
return trimmedVersion;
}
export const FRONTEND_VERSION_LABEL = formatVersionLabel(
FRONTEND_VERSION,
FRONTEND_BUILD_INFO,
);
+11
View File
@@ -7,8 +7,19 @@ export default defineConfig(({ mode }) => {
process.env.VITE_DEV_API_PROXY_TARGET || process.env.VITE_DEV_API_PROXY_TARGET ||
env.VITE_DEV_API_PROXY_TARGET || env.VITE_DEV_API_PROXY_TARGET ||
"http://localhost:8000"; "http://localhost:8000";
const appVersion =
env.VITE_APP_VERSION || process.env.npm_package_version || "0.1.0";
const appBuildInfo =
env.VITE_APP_BUILD_INFO ||
process.env.GIT_COMMIT ||
process.env.BUILD_COMMIT ||
"dev";
return { return {
define: {
__APP_VERSION__: JSON.stringify(appVersion),
__APP_BUILD_INFO__: JSON.stringify(appBuildInfo),
},
plugins: [react()], plugins: [react()],
server: { server: {
proxy: { proxy: {