diff --git a/containers/novela/main.py b/containers/novela/main.py
index aa60208..8cc805c 100644
--- a/containers/novela/main.py
+++ b/containers/novela/main.py
@@ -53,6 +53,12 @@ app.include_router(changelog_router)
app.include_router(search_router)
+@app.get("/api/version")
+async def version():
+ from version import display_version
+ return JSONResponse({"version": display_version()})
+
+
@app.get("/health")
async def health():
try:
diff --git a/containers/novela/shared_templates.py b/containers/novela/shared_templates.py
index 54562ee..3f78142 100644
--- a/containers/novela/shared_templates.py
+++ b/containers/novela/shared_templates.py
@@ -1,6 +1,7 @@
from fastapi.templating import Jinja2Templates
from db import get_db_conn
+from version import display_version
def _develop_mode() -> bool:
@@ -16,3 +17,4 @@ def _develop_mode() -> bool:
templates = Jinja2Templates(directory="templates")
templates.env.globals["develop_mode"] = _develop_mode
+templates.env.globals["app_version"] = display_version
diff --git a/containers/novela/static/sidebar.css b/containers/novela/static/sidebar.css
index 8a79ee1..6608942 100644
--- a/containers/novela/static/sidebar.css
+++ b/containers/novela/static/sidebar.css
@@ -118,6 +118,18 @@ html {
.sidebar-bottom { margin-top: auto; }
+.sidebar-version {
+ display: block;
+ margin-top: 0.5rem;
+ text-align: center;
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ color: var(--text-dim);
+ text-decoration: none;
+ opacity: 0.75;
+}
+.sidebar-version:hover { opacity: 1; }
+
.disk-warning {
display: flex;
align-items: center;
diff --git a/containers/novela/templates/_sidebar.html b/containers/novela/templates/_sidebar.html
index a13fd41..ff4e686 100644
--- a/containers/novela/templates/_sidebar.html
+++ b/containers/novela/templates/_sidebar.html
@@ -289,6 +289,7 @@
Rescan library
+
diff --git a/containers/novela/version.py b/containers/novela/version.py
new file mode 100644
index 0000000..cbf3acb
--- /dev/null
+++ b/containers/novela/version.py
@@ -0,0 +1,26 @@
+"""Novela version metadata.
+
+The release version is the single source maintained in ``changelog.py``
+(``CHANGELOG[0]["version"]``). Dev/test builds append an explicit ``BUILD``
+segment that is incremented by ``scripts/bump-dev-build.py`` on every test
+build, so operators can see exactly which image build is running in the
+sidebar. ``BUILD`` is reset to 0 for releases.
+"""
+from __future__ import annotations
+
+from changelog import CHANGELOG
+
+BUILD = 2
+
+
+def _release_version() -> str:
+ """Return the semantic release version (e.g. v0.2.11)."""
+ return CHANGELOG[0]["version"] if CHANGELOG else "v0.0.0"
+
+
+def display_version() -> str:
+ """Return the user-visible Novela version (e.g. v0.2.11 or v0.2.11.3)."""
+ version = _release_version()
+ if BUILD > 0:
+ return f"{version}.{BUILD}"
+ return version