add(ui/ux): show the running version and build stamp in the header

This commit is contained in:
Steppenstreuner
2026-09-01 17:50:50 +02:00
parent d9b4c95ca6
commit 8e7be708b2
10 changed files with 73 additions and 4 deletions
+3 -1
View File
@@ -6,8 +6,10 @@ COPY wordarr ./wordarr
COPY static ./static
RUN pip install --no-cache-dir .
ARG WORDARR_BUILD=""
ENV WORDARR_CONFIG_DIR=/config \
WORDARR_DOWNLOAD_DIR=/mnt/downloads
WORDARR_DOWNLOAD_DIR=/mnt/downloads \
WORDARR_BUILD=$WORDARR_BUILD
VOLUME /config
EXPOSE 8787
+7
View File
@@ -25,6 +25,13 @@ the same mount, otherwise moving turns into copy + delete.
| `WORDARR_EXTRA_EBOOK_EXTENSIONS` | | extra ebook formats the scan should offer, e.g. `.kepub,.lit,.rtf` |
| `WORDARR_HARDCOVER_TOKEN` | | Hardcover API token, the best ebook source; without it Hardcover is skipped |
| `WORDARR_GOOGLE_BOOKS_KEY` | | optional, only needed when the keyless Google Books quota runs dry |
| `WORDARR_BUILD` | | shown next to the version, e.g. the git sha; also a build arg of the image |
The header shows the running version and, behind it, the build stamp (the newest
source file, or `WORDARR_BUILD` when the image was built with it) - the quickest
way to see whether a redeploy actually arrived. `GET /api/version` returns the
same three values. Build with the git sha via
`WORDARR_BUILD=$(git rev-parse --short HEAD) docker compose up -d --build`.
The UI speaks German and English; the button next to the navigation switches
between them and remembers the choice. Without one, the browser language decides.
+5 -1
View File
@@ -1,6 +1,10 @@
services:
wordarr:
build: .
build:
context: .
args:
# e.g. WORDARR_BUILD=$(git rev-parse --short HEAD) docker compose up -d --build
WORDARR_BUILD: ${WORDARR_BUILD:-}
container_name: wordarr
ports:
- "8787:8787"
+12
View File
@@ -1619,9 +1619,21 @@ function onLangChange() {
if (scanItems.length) renderImportTable();
}
// ---- version ----
// the build stamp is the quickest answer to "did the redeploy arrive?"
async function showVersion() {
try {
const v = await api("/api/version");
// the version alone rarely moves, so the stamp is what tells deploys apart
$("#app-version").textContent = `v${v.version} · ${v.build || v.built_at}`;
$("#app-version").title = `${v.built_at} UTC${v.build ? " · " + v.build : ""}`;
} catch {}
}
// ---- init ----
applyI18n();
updateLangButton();
endLibraryEdit();
loadLibraries();
refreshMissingBadge();
showVersion();
+1 -1
View File
@@ -11,7 +11,7 @@
</head>
<body>
<header>
<h1>📚 wordarr</h1>
<h1>📚 wordarr <span id="app-version"></span></h1>
<nav>
<button data-view="search" class="active" data-i18n="nav.search"></button>
<button data-view="missing">
+1
View File
@@ -10,6 +10,7 @@ header {
padding: 0.8rem; background: var(--panel); border-bottom: 1px solid var(--border);
}
h1 { font-size: 1.2rem; }
#app-version { color: var(--muted); font-size: 0.72rem; font-weight: 400; font-variant-numeric: tabular-nums; }
nav { display: flex; gap: 0.4rem; flex-wrap: wrap; }
nav button { background: none; border: none; color: var(--muted); padding: 0.5rem 0.9rem; cursor: pointer; border-radius: 4px; font-size: 0.95rem; }
nav button.active, nav button:hover { color: var(--text); background: var(--bg); }
+7
View File
@@ -1135,3 +1135,10 @@ def test_extra_ebook_extensions_are_configurable(client, monkeypatch):
finally:
monkeypatch.delenv("WORDARR_EXTRA_EBOOK_EXTENSIONS")
importlib.reload(config_module)
def test_version_endpoint_reports_a_build_stamp(client):
data = client.get("/api/version").json()
assert data["version"]
# "2026-09-01 15:43" - the newest source file, so a stale deploy stands out
assert len(data["built_at"]) == 16 and data["built_at"][4] == "-"
+6
View File
@@ -153,3 +153,9 @@ def test_english_reaches_the_rendered_rows(page):
page.click("#lang-toggle")
assert "— überspringen —" in page.locator("#import-table tbody select").first.text_content()
assert "2,0 KB" in " ".join(page.locator("#import-table .tag.fmt").all_text_contents())
def test_the_header_shows_the_running_version(page):
stamp = page.locator("#app-version").text_content()
assert stamp.startswith("v")
assert len(stamp) > 3 # version plus the build stamp behind it
+7 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from . import db
from . import db, version
from .api import fs, imports, libraries, requests, search
@@ -23,6 +23,12 @@ app.include_router(imports.router)
app.include_router(fs.router)
@app.get("/api/version")
def read_version():
return {"version": version.VERSION, "build": version.BUILD,
"built_at": version.BUILT_AT}
class RevalidatingStatics(StaticFiles):
"""Without a Cache-Control header the browser caches heuristically and does
not ask again, so a redeployed app.js kept rendering the old UI. no-cache
+24
View File
@@ -0,0 +1,24 @@
"""Version and build stamp, so the UI can say what is actually deployed."""
import os
from datetime import datetime, timezone
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as package_version
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def _built_at() -> str:
"""Newest source file - a redeploy that did not take keeps the old stamp."""
files = [*(ROOT / "wordarr").rglob("*.py"), *(ROOT / "static").glob("*")]
newest = max((f.stat().st_mtime for f in files), default=0.0)
return datetime.fromtimestamp(newest, timezone.utc).strftime("%Y-%m-%d %H:%M")
try:
VERSION = package_version("wordarr")
except PackageNotFoundError: # running from a checkout without an install
VERSION = "0.0.0+src"
BUILD = os.environ.get("WORDARR_BUILD", "").strip() # git sha, set by the image build
BUILT_AT = _built_at()