feat(webapp): show running version in Mini App footer
Compute `git describe --tags --always --dirty` at server startup and expose via unauthenticated /api/version. Render as small muted text at the bottom of the Mini App so the running version can be confirmed at a glance. Once tags exist, the badge will show e.g. v0.1.0 or v0.1.0-3-gSHA. Until then it shows the short SHA. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
31d426d53e
commit
b4d76b0eca
4 changed files with 56 additions and 1 deletions
27
server.py
27
server.py
|
|
@ -9,6 +9,8 @@ import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
@ -23,6 +25,24 @@ logging.basicConfig(
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Version (computed once at startup) ───────────────────────────
|
||||||
|
|
||||||
|
def _compute_version() -> str:
|
||||||
|
"""Return `git describe --tags --always --dirty`, or 'unknown'."""
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
["git", "describe", "--tags", "--always", "--dirty"],
|
||||||
|
cwd=pathlib.Path(__file__).parent,
|
||||||
|
capture_output=True, text=True, timeout=2, check=True,
|
||||||
|
)
|
||||||
|
return out.stdout.strip() or "unknown"
|
||||||
|
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
_VERSION = _compute_version()
|
||||||
|
|
||||||
|
|
||||||
# ── Token (injected by start.py via env) ─────────────────────────
|
# ── Token (injected by start.py via env) ─────────────────────────
|
||||||
|
|
||||||
def _get_bot_token() -> str:
|
def _get_bot_token() -> str:
|
||||||
|
|
@ -206,6 +226,11 @@ async def api_export_json(request: web.Request):
|
||||||
return web.json_response({"records": data, "count": len(data)})
|
return web.json_response({"records": data, "count": len(data)})
|
||||||
|
|
||||||
|
|
||||||
|
async def api_version(request: web.Request):
|
||||||
|
"""Return the running server version. Unauthenticated."""
|
||||||
|
return web.json_response({"version": _VERSION})
|
||||||
|
|
||||||
|
|
||||||
@require_auth
|
@require_auth
|
||||||
async def api_export_csv(request: web.Request):
|
async def api_export_csv(request: web.Request):
|
||||||
"""Export all workouts as CSV."""
|
"""Export all workouts as CSV."""
|
||||||
|
|
@ -239,9 +264,9 @@ def create_app() -> web.Application:
|
||||||
app.router.add_get("/api/stats", api_get_stats)
|
app.router.add_get("/api/stats", api_get_stats)
|
||||||
app.router.add_get("/api/export/json", api_export_json)
|
app.router.add_get("/api/export/json", api_export_json)
|
||||||
app.router.add_get("/api/export/csv", api_export_csv)
|
app.router.add_get("/api/export/csv", api_export_csv)
|
||||||
|
app.router.add_get("/api/version", api_version)
|
||||||
|
|
||||||
# Serve the webapp/ folder
|
# Serve the webapp/ folder
|
||||||
import pathlib
|
|
||||||
webapp_dir = pathlib.Path(__file__).parent / "webapp"
|
webapp_dir = pathlib.Path(__file__).parent / "webapp"
|
||||||
|
|
||||||
async def index_handler(request):
|
async def index_handler(request):
|
||||||
|
|
|
||||||
|
|
@ -767,8 +767,22 @@ function fmtWeight(w) {
|
||||||
return w === Math.floor(w) ? Math.floor(w).toString() : w.toString();
|
return w === Math.floor(w) ? Math.floor(w).toString() : w.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Version badge ───────────────────────────────────────────────
|
||||||
|
async function loadVersion() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(API + "/version");
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
const badge = document.getElementById("version-badge");
|
||||||
|
if (badge && data.version) badge.textContent = data.version;
|
||||||
|
} catch (e) {
|
||||||
|
// Silent — footer just stays empty if unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Init ────────────────────────────────────────────────────────
|
// ── Init ────────────────────────────────────────────────────────
|
||||||
async function init() {
|
async function init() {
|
||||||
|
loadVersion();
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
try {
|
try {
|
||||||
const data = await api("GET", "/exercises");
|
const data = await api("GET", "/exercises");
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,10 @@
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer id="app-footer">
|
||||||
|
<span id="version-badge"></span>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -472,3 +472,15 @@ details[open] .raw-toggle::before {
|
||||||
transform: translateX(-50%) translateY(0);
|
transform: translateX(-50%) translateY(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Footer / version badge ──────────────────────────────────── */
|
||||||
|
|
||||||
|
#app-footer {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 12px 16px 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tg-theme-hint-color, #999);
|
||||||
|
opacity: 0.6;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue