diff --git a/backend/Dockerfile b/backend/Dockerfile index b8a7f06..03920c6 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,7 +1,12 @@ FROM python:3.11-slim +ARG APP_VERSION=0.1.0 +ARG APP_BUILD_INFO=dev + ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 + PYTHONUNBUFFERED=1 \ + APP_VERSION=${APP_VERSION} \ + APP_BUILD_INFO=${APP_BUILD_INFO} WORKDIR /app/backend diff --git a/backend/README.md b/backend/README.md index b2a75c0..b314329 100644 --- a/backend/README.md +++ b/backend/README.md @@ -71,7 +71,13 @@ SMTP_TIMEOUT=30 # 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. -# + +# 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 AUTH_ENABLED=true 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 - `POST /api/jobs/run` — Execute a job - `GET /api/users` — Jellyfin users with optional Jellyseerr enrichment +- `GET /api/version` — Backend version/build metadata for the UI diff --git a/backend/src/media_library_viewer_api/main.py b/backend/src/media_library_viewer_api/main.py index 4d65a51..b9d63d3 100644 --- a/backend/src/media_library_viewer_api/main.py +++ b/backend/src/media_library_viewer_api/main.py @@ -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.logging_utils import configure_logging, describe_settings 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.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 @@ -46,7 +47,7 @@ async def lifespan(app: FastAPI): app = FastAPI( 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.", lifespan=lifespan, ) @@ -67,6 +68,8 @@ app.add_middleware( @app.middleware("http") 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) @app.middleware("http") @@ -109,5 +112,12 @@ def health_check() -> dict[str, str]: 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__": - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/src/media_library_viewer_api/version.py b/backend/src/media_library_viewer_api/version.py new file mode 100644 index 0000000..7e5e790 --- /dev/null +++ b/backend/src/media_library_viewer_api/version.py @@ -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), + } diff --git a/docker-compose.yml b/docker-compose.yml index 74a9a27..c7966f8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,9 @@ services: build: context: . dockerfile: backend/Dockerfile + args: + APP_VERSION: ${APP_VERSION:-0.1.0} + APP_BUILD_INFO: ${APP_BUILD_INFO:-dev} environment: AUTH_ENABLED: ${AUTH_ENABLED:-true} 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_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_APP_VERSION: ${APP_VERSION:-0.1.0} + VITE_APP_BUILD_INFO: ${APP_BUILD_INFO:-dev} depends_on: backend: condition: service_healthy diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 522a62c..e214161 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -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: 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-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. diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 0f64f5c..9e3172e 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -15,6 +15,8 @@ ARG VITE_OIDC_SCOPE=openid profile email ARG VITE_OIDC_REDIRECT_URI= ARG VITE_OIDC_POST_LOGOUT_REDIRECT_URI= 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} \ 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_REDIRECT_URI=${VITE_OIDC_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 @@ -45,7 +49,9 @@ COPY frontend/ ./ ENV VITE_API_URL=/api \ 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 CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] diff --git a/frontend/README.md b/frontend/README.md index 5bc1dde..9e62af2 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -45,7 +45,7 @@ Output goes to `frontend/dist/`. ## 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 - **Media** (`/media`) — Full-library table with sort/filter/search - **Users** (`/users`) — Read-only Jellyfin user list with optional Jellyseerr enrichment @@ -55,6 +55,7 @@ Output goes to `frontend/dist/`. ## 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`. +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 VITE_API_URL=http://your-backend-host:8000 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2039b0f..6c16b52 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "0.0.0", + "version": "0.1.0", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", diff --git a/frontend/package.json b/frontend/package.json index 9e38f7b..5b8e496 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.0.0", + "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d577de3..91439ac 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,7 +5,11 @@ import { NavLink, useLocation, } 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 { AppBar, @@ -34,6 +38,8 @@ import { FileBrowser } from "./pages/FileBrowser"; import { Actions } from "./pages/Actions"; import { getAppTheme } from "./theme"; import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth"; +import { fetchAppVersion } from "./api/client"; +import { FRONTEND_VERSION_LABEL } from "./version"; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } }, @@ -51,6 +57,12 @@ function Shell({ const location = useLocation(); const current = location.pathname; 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 ( <> @@ -126,6 +138,16 @@ function Shell({ width: { xs: "100%", md: "auto" }, }} > + + {authLabel && ( get("/api/monitoring/machines"); export const fetchMonitoringPoller = () => get("/api/monitoring/poller"); +export const fetchAppVersion = () => get("/api/version"); export const fetchMonitoringOverview = () => get("/api/dashboard/monitoring"); export const fetchDashboardShortcuts = () => diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 99b3142..39f42f9 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -297,6 +297,13 @@ export interface MonitoringPollerStatus { retention_days: number; } +export interface AppVersionInfo { + app: string; + backend_version: string; + backend_build: string; + backend_label: string; +} + export interface MonitoringMachineOverview { machine: MonitoringMachine; status: string; diff --git a/frontend/src/version.ts b/frontend/src/version.ts new file mode 100644 index 0000000..c4ab5a8 --- /dev/null +++ b/frontend/src/version.ts @@ -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, +); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 3a3aafd..b814e72 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -7,8 +7,19 @@ export default defineConfig(({ mode }) => { process.env.VITE_DEV_API_PROXY_TARGET || env.VITE_DEV_API_PROXY_TARGET || "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 { + define: { + __APP_VERSION__: JSON.stringify(appVersion), + __APP_BUILD_INFO__: JSON.stringify(appBuildInfo), + }, plugins: [react()], server: { proxy: {