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"])
|
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")
|
@router.get("/api/health")
|
||||||
async def health_check(conn: duckdb.DuckDBPyConnection = Depends(_get_db)):
|
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,
|
"status": overall,
|
||||||
"version": os.environ.get("AGNES_VERSION", "dev"),
|
"version": os.environ.get("AGNES_VERSION", "dev"),
|
||||||
"channel": os.environ.get("RELEASE_CHANNEL", "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,
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"deployed_at": _DEPLOYED_AT,
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
"services": checks,
|
"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>
|
<footer>
|
||||||
<p>© {{ now().year if now is defined else 2024 }} {{ config.INSTANCE_COPYRIGHT or 'AI Data Analyst' }}</p>
|
<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>
|
</footer>
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,25 @@ if [ "$DATA_SOURCE" = "keboola" ]; then
|
||||||
fi
|
fi
|
||||||
JWT_KEY=$(gcloud secrets versions access latest --secret=agnes-$${CUSTOMER_NAME}-jwt-secret)
|
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
|
cat > "$APP_DIR/.env" <<ENVEOF
|
||||||
JWT_SECRET_KEY=$JWT_KEY
|
JWT_SECRET_KEY=$JWT_KEY
|
||||||
DATA_DIR=$DATA_MNT
|
DATA_DIR=$DATA_MNT
|
||||||
|
|
@ -78,6 +97,9 @@ SEED_ADMIN_EMAIL=$SEED_ADMIN_EMAIL
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
DOMAIN=$DOMAIN
|
DOMAIN=$DOMAIN
|
||||||
AGNES_TAG=$IMAGE_TAG
|
AGNES_TAG=$IMAGE_TAG
|
||||||
|
AGNES_VERSION=$AGNES_VERSION
|
||||||
|
RELEASE_CHANNEL=$RELEASE_CHANNEL
|
||||||
|
AGNES_COMMIT_SHA=$IMAGE_DIGEST
|
||||||
ACME_EMAIL=$ACME_EMAIL
|
ACME_EMAIL=$ACME_EMAIL
|
||||||
ENVEOF
|
ENVEOF
|
||||||
chmod 600 "$APP_DIR/.env"
|
chmod 600 "$APP_DIR/.env"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue