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).
76 lines
3 KiB
HTML
76 lines
3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}Data Analyst Portal{% endblock %}</title>
|
|
<link rel="stylesheet" href="{{ static_url('style.css') }}">
|
|
<link rel="stylesheet" href="{{ static_url('style-custom.css') }}">
|
|
{% include '_theme.html' %}
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<div class="logo">
|
|
<h1>Data Analyst Portal</h1>
|
|
<p class="subtitle">{{ config.INSTANCE_SUBTITLE }}</p>
|
|
</div>
|
|
{% if session.user %}
|
|
<nav>
|
|
<span class="user-info">
|
|
{% if session.user.picture %}
|
|
<img src="{{ session.user.picture }}" alt="Profile" class="avatar">
|
|
{% endif %}
|
|
{{ session.user.email }}
|
|
</span>
|
|
<a href="{{ url_for('auth.logout') }}" class="btn btn-secondary btn-sm">Logout</a>
|
|
</nav>
|
|
{% endif %}
|
|
</header>
|
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
<div class="flash-messages">
|
|
{% for category, message in messages %}
|
|
<div class="flash flash-{{ category }}">
|
|
{{ message }}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<main>
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<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>
|