mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
Merge pull request #39 from o3LL/feat/admin-build-provenance
feat(settings): show the running build's version and commit in the admin panel
This commit is contained in:
@@ -1169,6 +1169,38 @@ details.hwfit-serve-advanced label:has(.hwfit-sf[data-field="vllm_kv_cache_dtype
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
/* Build provenance at the bottom of the sidebar. The nav above it takes the
|
||||
free space, so this needs no absolute positioning to sit at the bottom.
|
||||
`[hidden]` needs restating: an author `display` rule outranks the UA
|
||||
stylesheet's [hidden] rule, so the attribute alone would not hide it. */
|
||||
.settings-sidebar-build {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
margin-top: 8px;
|
||||
padding: 4px 12px 2px;
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 9px;
|
||||
color: color-mix(in srgb, var(--fg) 45%, transparent);
|
||||
}
|
||||
.settings-sidebar-build[hidden] {
|
||||
display: none;
|
||||
}
|
||||
/* Same treatment as the "Admin" group label this sits under. */
|
||||
.settings-sidebar-build-version {
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.settings-sidebar-build-commit {
|
||||
overflow: hidden;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.settings-nav-search-wrap {
|
||||
position: relative;
|
||||
margin: 0 0 8px;
|
||||
@@ -1324,6 +1356,9 @@ body.settings-sidebar-resize-active {
|
||||
.settings-sidebar.settings-sidebar-collapsed .settings-sidebar-content {
|
||||
display: none;
|
||||
}
|
||||
.settings-sidebar.settings-sidebar-collapsed .settings-sidebar-build {
|
||||
display: none;
|
||||
}
|
||||
.settings-sidebar.settings-sidebar-collapsed .settings-sidebar-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
@@ -1354,7 +1389,8 @@ body.settings-sidebar-resize-active {
|
||||
|
||||
.settings-nav-search-wrap,
|
||||
.settings-sidebar-resize-handle,
|
||||
.settings-sidebar-toggle {
|
||||
.settings-sidebar-toggle,
|
||||
.settings-sidebar-build {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1382,7 +1418,8 @@ body.settings-sidebar-resize-active {
|
||||
|
||||
.settings-nav-search-wrap,
|
||||
.settings-sidebar-resize-handle,
|
||||
.settings-sidebar-toggle {
|
||||
.settings-sidebar-toggle,
|
||||
.settings-sidebar-build {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1549,6 +1549,15 @@
|
||||
<span>System</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Build provenance, pinned below the scrolling nav so it stays at
|
||||
the bottom-left of the panel: the version this build registers
|
||||
and the commit the running process actually loaded. Populated
|
||||
from /api/version; stays hidden until that answers. -->
|
||||
<div class="settings-sidebar-build admin-only" id="settings-sidebar-build" hidden>
|
||||
<span class="settings-sidebar-build-version" id="settings-sidebar-build-version"></span>
|
||||
<span class="settings-sidebar-build-commit" id="settings-sidebar-build-commit"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-panels">
|
||||
|
||||
|
||||
@@ -121,10 +121,88 @@ export function setSettingsSidebarWidth(modalEl, width, options = {}) {
|
||||
return next;
|
||||
}
|
||||
|
||||
/* ── Build provenance ──
|
||||
The bottom-left of the panel answers "which build am I actually looking
|
||||
at": the version this build registers and the commit its process loaded.
|
||||
Both come from /api/version, which resolves the commit once at import via
|
||||
`git rev-parse HEAD` and reports the string "unknown" when it cannot — a
|
||||
read-only Docker tree with no .git, for instance. */
|
||||
const BUILD_COMMIT_SHORT_LEN = 8;
|
||||
const BUILD_UNKNOWN = 'unknown';
|
||||
const BUILD_COMMIT_SHA = /^[0-9a-f]{7,64}$/i;
|
||||
|
||||
function buildField(value) {
|
||||
const text = String(value ?? '').trim();
|
||||
return text && text.toLowerCase() !== BUILD_UNKNOWN ? text : '';
|
||||
}
|
||||
|
||||
/** Turn an /api/version payload into the two sidebar lines, or null when it
|
||||
* carries nothing worth a footer. */
|
||||
export function formatSettingsBuildInfo(payload) {
|
||||
const version = buildField(payload?.version);
|
||||
const build = buildField(payload?.build);
|
||||
const commit = buildField(payload?.source_commit);
|
||||
|
||||
const parts = [];
|
||||
if (version) parts.push(`v${version}`);
|
||||
// The preview harness versions itself separately from the public semver, so
|
||||
// show both — but never the same number twice.
|
||||
if (build && build !== version) parts.push(`build ${build}`);
|
||||
|
||||
// An ODYSSEUS_SOURCE_COMMIT override need not be a sha; leave those intact
|
||||
// and let the CSS ellipsis deal with the width.
|
||||
const commitLabel = BUILD_COMMIT_SHA.test(commit)
|
||||
? commit.slice(0, BUILD_COMMIT_SHORT_LEN).toLowerCase()
|
||||
: commit;
|
||||
|
||||
if (!parts.length && !commitLabel) return null;
|
||||
|
||||
return {
|
||||
versionLabel: parts.join(' \u00b7 '),
|
||||
commitLabel,
|
||||
commitTitle: commit,
|
||||
};
|
||||
}
|
||||
|
||||
function paintSettingsBuildInfo(modalEl, info) {
|
||||
const host = modalEl?.querySelector('#settings-sidebar-build');
|
||||
if (!host) return;
|
||||
|
||||
const versionEl = host.querySelector('#settings-sidebar-build-version');
|
||||
const commitEl = host.querySelector('#settings-sidebar-build-commit');
|
||||
|
||||
if (!info) {
|
||||
host.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (versionEl) versionEl.textContent = info.versionLabel;
|
||||
if (commitEl) {
|
||||
commitEl.textContent = info.commitLabel;
|
||||
// The short hash is what fits; keep the full one reachable on hover.
|
||||
if (info.commitTitle) commitEl.title = info.commitTitle;
|
||||
}
|
||||
|
||||
host.hidden = false;
|
||||
}
|
||||
|
||||
async function loadSettingsBuildInfo(modalEl) {
|
||||
try {
|
||||
const res = await fetch('/api/version', { credentials: 'same-origin' });
|
||||
if (!res.ok) return;
|
||||
paintSettingsBuildInfo(modalEl, formatSettingsBuildInfo(await res.json()));
|
||||
} catch {
|
||||
// Provenance is informational: a failed probe leaves the footer hidden
|
||||
// rather than putting an error in the middle of the settings nav.
|
||||
}
|
||||
}
|
||||
|
||||
export function bindSettingsSidebar(modalEl) {
|
||||
if (!modalEl || _bound.has(modalEl)) return;
|
||||
_bound.add(modalEl);
|
||||
|
||||
loadSettingsBuildInfo(modalEl);
|
||||
|
||||
const sidebar = modalEl.querySelector('.settings-sidebar');
|
||||
const handle = modalEl.querySelector('#settings-sidebar-resize-handle');
|
||||
const toggle = modalEl.querySelector('#settings-sidebar-toggle');
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
"""The settings sidebar footer that names the running build.
|
||||
|
||||
The admin panel had no way to answer "which build is this?" from the UI: the
|
||||
version and the source commit were only reachable by hand-querying
|
||||
``/api/version``. A footer pinned under the sidebar nav now shows the version
|
||||
the build registers plus the commit its process loaded.
|
||||
|
||||
Three things can break independently, so each is covered on its own:
|
||||
|
||||
* ``formatSettingsBuildInfo`` decides what the two lines say, including what
|
||||
to do with the ``"unknown"`` the backend reports when it cannot resolve a
|
||||
commit. That is real logic, so it is exercised in node against the shipped
|
||||
module rather than asserted on as text.
|
||||
* the footer's *position* is the requirement — below the scrolling nav, inside
|
||||
the sidebar. Only the markup's structure records that.
|
||||
* the layouts that have no bottom-left to write in (the collapsed rail, the
|
||||
narrow tab rail) must hide it, and the ``[hidden]`` attribute only works
|
||||
here because the CSS restates it against the element's own ``display`` rule.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
|
||||
SIDEBAR_CSS = (
|
||||
ROOT / "static" / "css" / "cookbook-research-memory-settings.css"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
requires_node = pytest.mark.skipif(
|
||||
not shutil.which("node"), reason="node binary not on PATH"
|
||||
)
|
||||
|
||||
|
||||
def _format_build_info(payload):
|
||||
"""Run the shipped formatter over one payload and return its result."""
|
||||
source = (
|
||||
"import { formatSettingsBuildInfo } from "
|
||||
"'./static/js/settings/sidebar.js';\n"
|
||||
f"console.log(JSON.stringify(formatSettingsBuildInfo({json.dumps(payload)})));\n"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--input-type=module", "-e", source],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# What the footer says
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@requires_node
|
||||
def test_shows_registered_version_and_short_commit():
|
||||
info = _format_build_info(
|
||||
{
|
||||
"version": "1.0.3",
|
||||
"build": "0.20.19",
|
||||
"source_commit": "8a28c5f9c0ffee1234567890abcdef1234567890",
|
||||
}
|
||||
)
|
||||
|
||||
assert info["versionLabel"] == "v1.0.3 · build 0.20.19"
|
||||
assert info["commitLabel"] == "8a28c5f9"
|
||||
# The short hash is what fits the sidebar; the full one stays on hover.
|
||||
assert info["commitTitle"] == "8a28c5f9c0ffee1234567890abcdef1234567890"
|
||||
|
||||
|
||||
@requires_node
|
||||
def test_build_equal_to_version_is_not_printed_twice():
|
||||
info = _format_build_info(
|
||||
{"version": "1.0.3", "build": "1.0.3", "source_commit": "abcdef1234"}
|
||||
)
|
||||
|
||||
assert info["versionLabel"] == "v1.0.3"
|
||||
|
||||
|
||||
@requires_node
|
||||
def test_unresolvable_commit_leaves_only_the_version():
|
||||
"""`/api/version` reports the string "unknown" when `git rev-parse` fails
|
||||
— a read-only Docker tree with no `.git`. Printing it would be worse than
|
||||
printing nothing."""
|
||||
info = _format_build_info(
|
||||
{"version": "1.0.3", "build": "unknown", "source_commit": "unknown"}
|
||||
)
|
||||
|
||||
assert info["versionLabel"] == "v1.0.3"
|
||||
assert info["commitLabel"] == ""
|
||||
assert info["commitTitle"] == ""
|
||||
|
||||
|
||||
@requires_node
|
||||
def test_commit_only_build_still_gets_a_footer():
|
||||
info = _format_build_info({"source_commit": "0123456789abcdef"})
|
||||
|
||||
assert info["versionLabel"] == ""
|
||||
assert info["commitLabel"] == "01234567"
|
||||
|
||||
|
||||
@requires_node
|
||||
def test_non_sha_commit_override_is_left_intact():
|
||||
"""`ODYSSEUS_SOURCE_COMMIT` can be any string; truncating a tag to eight
|
||||
characters would corrupt it, so only real hashes get shortened."""
|
||||
info = _format_build_info(
|
||||
{"version": "1.0.3", "source_commit": "release-2026-09-30"}
|
||||
)
|
||||
|
||||
assert info["commitLabel"] == "release-2026-09-30"
|
||||
|
||||
|
||||
@requires_node
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{},
|
||||
{"version": "", "build": "", "source_commit": ""},
|
||||
{"version": "unknown", "build": "unknown", "source_commit": "unknown"},
|
||||
],
|
||||
)
|
||||
def test_nothing_to_report_means_no_footer(payload):
|
||||
assert _format_build_info(payload) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Where the footer sits
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _settings_sidebar_markup():
|
||||
start = INDEX_HTML.index('<div class="settings-sidebar">')
|
||||
end = INDEX_HTML.index('<div class="settings-panels">', start)
|
||||
return INDEX_HTML[start:end]
|
||||
|
||||
|
||||
def test_footer_is_the_last_thing_in_the_sidebar():
|
||||
"""The ask is bottom-left. The nav above it takes the free space, so the
|
||||
footer lands at the bottom by being the sidebar's final child — and it has
|
||||
to sit outside the nav's own scroll container to stay pinned there."""
|
||||
sidebar = _settings_sidebar_markup()
|
||||
|
||||
nav_start = sidebar.index('<div class="settings-sidebar-content">')
|
||||
footer_start = sidebar.index('id="settings-sidebar-build"')
|
||||
assert nav_start < footer_start, "footer must come after the nav"
|
||||
|
||||
# Nothing but the footer between the nav's closing tag and the sidebar's.
|
||||
tail = sidebar[footer_start:]
|
||||
assert '<div class="settings-nav' not in tail
|
||||
assert "settings-nav-item" not in tail
|
||||
|
||||
|
||||
def test_footer_carries_both_fields_and_is_admin_gated():
|
||||
sidebar = _settings_sidebar_markup()
|
||||
footer = sidebar[sidebar.index('id="settings-sidebar-build"') - 200:]
|
||||
|
||||
assert 'id="settings-sidebar-build-version"' in footer
|
||||
assert 'id="settings-sidebar-build-commit"' in footer
|
||||
# `.admin-only` is what syncAdminVisibility() hides for non-admins.
|
||||
assert "settings-sidebar-build admin-only" in footer
|
||||
# Hidden until /api/version answers, so no empty bordered strip appears.
|
||||
assert re.search(r'id="settings-sidebar-build"[^>]*\bhidden\b', footer)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# When the footer must disappear
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _rule_body(selector, css=SIDEBAR_CSS):
|
||||
match = re.search(
|
||||
re.escape(selector) + r"\s*\{([^}]*)\}", css
|
||||
)
|
||||
assert match, f"no rule for {selector}"
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def test_hidden_attribute_is_restated_against_the_display_rule():
|
||||
"""`.settings-sidebar-build { display: flex }` is an author rule, so it
|
||||
outranks the UA stylesheet's `[hidden]` rule. Without this the attribute
|
||||
would not hide anything."""
|
||||
assert "display: flex" in _rule_body(".settings-sidebar-build")
|
||||
assert "display: none" in _rule_body(".settings-sidebar-build[hidden]")
|
||||
|
||||
|
||||
def test_collapsed_sidebar_hides_the_footer():
|
||||
body = _rule_body(
|
||||
".settings-sidebar.settings-sidebar-collapsed .settings-sidebar-build"
|
||||
)
|
||||
assert "display: none" in body
|
||||
|
||||
|
||||
def test_both_narrow_tab_rail_layouts_hide_the_footer():
|
||||
"""At narrow widths the sidebar becomes a horizontal rail, where a stacked
|
||||
footer has no bottom to sit at. Both the container query and the media
|
||||
query switch to that layout, so both have to hide it."""
|
||||
rails = re.findall(
|
||||
r"([^{}]*\.settings-sidebar-build[^{}]*)\{\s*display:\s*none;\s*\}",
|
||||
SIDEBAR_CSS,
|
||||
)
|
||||
tab_rail_rules = [r for r in rails if ".settings-sidebar-toggle" in r]
|
||||
assert len(tab_rail_rules) == 2, tab_rail_rules
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The payload the footer reads
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_api_version_reports_version_build_and_commit():
|
||||
"""The footer renders these three keys. `/api/version` is auth-exempt, so
|
||||
it answers without a session — the same way the login page already reads
|
||||
it."""
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app import app
|
||||
|
||||
with TestClient(app) as client:
|
||||
payload = client.get("/api/version").json()
|
||||
|
||||
assert set(payload) >= {"version", "build", "source_commit"}
|
||||
# Absent values are reported as "unknown", never omitted or null, which is
|
||||
# the case formatSettingsBuildInfo() filters on.
|
||||
assert all(isinstance(payload[k], str) and payload[k] for k in payload)
|
||||
Reference in New Issue
Block a user