feat(ui): version badge in footer + /api/version endpoint
UI now shows a small footer badge with: - release channel + CalVer version (e.g. 'stable-2026.04.47') - floating image tag (e.g. 'stable') - time since last container restart (proxy for 'last deployed') Backend: - app/api/health.py: /api/health returns image_tag, commit_sha, deployed_at - app/api/health.py: new /api/version endpoint (lightweight, no DB hit, for footer badge polling) Infra: - startup-script.sh.tpl: resolves image digest from ghcr pull, derives channel + version from the tag name, and writes AGNES_VERSION / RELEASE_CHANNEL / AGNES_COMMIT_SHA into .env so the app can surface them to the UI. UI: - app/web/templates/base.html: footer loads /api/version asynchronously and renders '<channel>-<version> · <tag> · deployed <relative> (<UTC>)'. Tooltip shows full detail (commit sha, schema version).
This commit is contained in:
parent
cdd959b19f
commit
b091cf7003
3 changed files with 67 additions and 0 deletions
|
|
@ -12,6 +12,11 @@ from src.repositories.sync_state import SyncStateRepository
|
|||
|
||||
router = APIRouter(tags=["health"])
|
||||
|
||||
# Captured at module import (i.e., app process start) — proxy for "deployed at".
|
||||
# When the cron auto-upgrade pulls a new digest and recreates the container,
|
||||
# this resets. Accurate enough for a UI "last updated" badge.
|
||||
_DEPLOYED_AT = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@router.get("/api/health")
|
||||
async def health_check(conn: duckdb.DuckDBPyConnection = Depends(_get_db)):
|
||||
|
|
@ -73,7 +78,23 @@ async def health_check(conn: duckdb.DuckDBPyConnection = Depends(_get_db)):
|
|||
"status": overall,
|
||||
"version": os.environ.get("AGNES_VERSION", "dev"),
|
||||
"channel": os.environ.get("RELEASE_CHANNEL", "dev"),
|
||||
"image_tag": os.environ.get("AGNES_TAG", "unknown"),
|
||||
"commit_sha": os.environ.get("AGNES_COMMIT_SHA", "unknown"),
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"deployed_at": _DEPLOYED_AT,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"services": checks,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/version")
|
||||
async def version_info():
|
||||
"""Lightweight version info — cacheable, no DB touch. Used by UI footer badge."""
|
||||
return {
|
||||
"version": os.environ.get("AGNES_VERSION", "dev"),
|
||||
"channel": os.environ.get("RELEASE_CHANNEL", "dev"),
|
||||
"image_tag": os.environ.get("AGNES_TAG", "unknown"),
|
||||
"commit_sha": os.environ.get("AGNES_COMMIT_SHA", "unknown"),
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"deployed_at": _DEPLOYED_AT,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,31 @@
|
|||
|
||||
<footer>
|
||||
<p>© {{ now().year if now is defined else 2024 }} {{ config.INSTANCE_COPYRIGHT or 'AI Data Analyst' }}</p>
|
||||
<p class="version-badge" style="font-size: 0.75rem; color: var(--muted, #888); margin-top: 0.5rem;">
|
||||
<span id="agnes-version-badge">Loading version…</span>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
<script>
|
||||
// Version badge — fetched from /api/version, no auth needed.
|
||||
(function() {
|
||||
fetch('/api/version').then(r => r.ok ? r.json() : null).then(v => {
|
||||
if (!v) return;
|
||||
const el = document.getElementById('agnes-version-badge');
|
||||
if (!el) return;
|
||||
const deployed = new Date(v.deployed_at);
|
||||
const relative = (() => {
|
||||
const s = Math.floor((Date.now() - deployed.getTime()) / 1000);
|
||||
if (s < 60) return s + 's ago';
|
||||
if (s < 3600) return Math.floor(s/60) + 'm ago';
|
||||
if (s < 86400) return Math.floor(s/3600) + 'h ago';
|
||||
return Math.floor(s/86400) + 'd ago';
|
||||
})();
|
||||
const tag = v.image_tag && v.image_tag !== 'unknown' ? ' · ' + v.image_tag : '';
|
||||
el.textContent = `${v.channel}-${v.version}${tag} · deployed ${relative} (${deployed.toISOString().slice(0,19).replace('T',' ')}Z)`;
|
||||
el.title = `version ${v.version}\nchannel ${v.channel}\nimage tag ${v.image_tag}\ncommit ${v.commit_sha}\nschema v${v.schema_version}\ndeployed at ${v.deployed_at}`;
|
||||
}).catch(() => {});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -68,6 +68,25 @@ if [ "$DATA_SOURCE" = "keboola" ]; then
|
|||
fi
|
||||
JWT_KEY=$(gcloud secrets versions access latest --secret=agnes-$${CUSTOMER_NAME}-jwt-secret)
|
||||
|
||||
# Resolve the actual version/commit behind the requested tag so the UI can
|
||||
# show specific `stable-2026.04.47` + commit SHA instead of just `stable`.
|
||||
IMAGE_DIGEST=$(docker pull "$IMAGE_REPO:$IMAGE_TAG" 2>/dev/null | grep -o 'sha256:[a-f0-9]*' | head -1 || echo "unknown")
|
||||
IMAGE_INFO=$(curl -fsSL "https://ghcr.io/v2/keboola/agnes-the-ai-analyst/manifests/$IMAGE_TAG" -H "Accept: application/vnd.oci.image.manifest.v1+json" 2>/dev/null || echo "{}")
|
||||
|
||||
# Channel derived from tag prefix (stable-*/dev-*/release-*) — simple heuristic.
|
||||
case "$IMAGE_TAG" in
|
||||
stable*) RELEASE_CHANNEL="stable" ;;
|
||||
dev*) RELEASE_CHANNEL="dev" ;;
|
||||
release*) RELEASE_CHANNEL="release" ;;
|
||||
*) RELEASE_CHANNEL="custom" ;;
|
||||
esac
|
||||
|
||||
# Version extracted from versioned tags (stable-2026.04.N); floating tags stay "dev".
|
||||
case "$IMAGE_TAG" in
|
||||
*-[0-9]*.[0-9]*.[0-9]*) AGNES_VERSION="$${IMAGE_TAG#*-}" ;;
|
||||
*) AGNES_VERSION="$IMAGE_TAG" ;;
|
||||
esac
|
||||
|
||||
cat > "$APP_DIR/.env" <<ENVEOF
|
||||
JWT_SECRET_KEY=$JWT_KEY
|
||||
DATA_DIR=$DATA_MNT
|
||||
|
|
@ -78,6 +97,9 @@ SEED_ADMIN_EMAIL=$SEED_ADMIN_EMAIL
|
|||
LOG_LEVEL=info
|
||||
DOMAIN=$DOMAIN
|
||||
AGNES_TAG=$IMAGE_TAG
|
||||
AGNES_VERSION=$AGNES_VERSION
|
||||
RELEASE_CHANNEL=$RELEASE_CHANNEL
|
||||
AGNES_COMMIT_SHA=$IMAGE_DIGEST
|
||||
ACME_EMAIL=$ACME_EMAIL
|
||||
ENVEOF
|
||||
chmod 600 "$APP_DIR/.env"
|
||||
|
|
|
|||
Loading…
Reference in a new issue