diff --git a/web/common.py b/web/common.py
index 35d7e39..0c2372b 100644
--- a/web/common.py
+++ b/web/common.py
@@ -24,6 +24,7 @@ import notifications
from apply_client import ApplyClient, _row_to_profile
from auth import issue_csrf_token
from settings import APPLY_FAILURE_THRESHOLD, GIT_COMMIT, INTERNAL_API_KEY
+from version import commits_behind_main
logger = logging.getLogger("web")
@@ -108,6 +109,13 @@ def require_internal(x_internal_api_key: str | None = Header(default=None)) -> N
def base_context(request: Request, user, active_tab: str) -> dict:
+ behind = commits_behind_main(GIT_COMMIT)
+ if behind is None:
+ git_status = ""
+ elif behind == 0:
+ git_status = "latest"
+ else:
+ git_status = f"{behind} behind"
return {
"request": request,
"user": user,
@@ -116,6 +124,7 @@ def base_context(request: Request, user, active_tab: str) -> dict:
"is_admin": bool(user["is_admin"]),
"git_commit": GIT_COMMIT,
"git_commit_short": GIT_COMMIT[:7] if GIT_COMMIT and GIT_COMMIT != "dev" else GIT_COMMIT,
+ "git_status": git_status,
}
diff --git a/web/templates/_layout.html b/web/templates/_layout.html
index f93a029..a01bd17 100644
--- a/web/templates/_layout.html
+++ b/web/templates/_layout.html
@@ -33,7 +33,7 @@
Mit ♥ programmiert für Annika
{% if git_commit_short %}
·
- build {{ git_commit_short }}
+ build {{ git_commit_short }}{% if git_status %} ({{ git_status }}){% endif %}
{% endif %}
{% endblock %}
diff --git a/web/version.py b/web/version.py
new file mode 100644
index 0000000..5f7ccf8
--- /dev/null
+++ b/web/version.py
@@ -0,0 +1,37 @@
+"""How far behind origin/main the running build is.
+
+Hits Gitea's compare endpoint with a short per-SHA cache. Network errors
+return None and the footer just hides the status — the SHA itself still
+renders, so a flaky git host never breaks the page.
+"""
+import logging
+import time
+
+import requests
+
+
+_GITEA_REPO_API = "https://git.moritz.run/api/v1/repos/moritz/lazyflat"
+_CACHE_TTL = 60.0
+_HTTP_TIMEOUT = 1.5
+_cache: dict[str, tuple[int, float]] = {}
+
+logger = logging.getLogger("web.version")
+
+
+def commits_behind_main(sha: str) -> int | None:
+ """Return how many commits `sha` is behind origin/main, or None on error/dev."""
+ if not sha or sha == "dev":
+ return None
+ now = time.monotonic()
+ cached = _cache.get(sha)
+ if cached and (now - cached[1]) < _CACHE_TTL:
+ return cached[0]
+ try:
+ r = requests.get(f"{_GITEA_REPO_API}/compare/{sha}...main", timeout=_HTTP_TIMEOUT)
+ r.raise_for_status()
+ behind = int(r.json().get("total_commits", 0))
+ except (requests.RequestException, ValueError, KeyError) as e:
+ logger.debug("commits_behind_main fetch failed: %s", e)
+ return cached[0] if cached else None
+ _cache[sha] = (behind, now)
+ return behind