diff --git a/src/frontend/web/von_interface/static/js/domUtils.js b/src/frontend/web/von_interface/static/js/domUtils.js
index 85963eb0..aadb91e6 100644
--- a/src/frontend/web/von_interface/static/js/domUtils.js
+++ b/src/frontend/web/von_interface/static/js/domUtils.js
@@ -1009,6 +1009,7 @@ function ensureFooterDbLoadingBadge(footer) {
const badge = document.createElement('span');
badge.className = 'db-conn-badge loading';
+ badge.tabIndex = 0;
badge.style.marginLeft = '12px';
badge.innerHTML = 'π Mongo: loading... RTT β¦ DB β¦';
setKeptNativeTitle(badge, 'Loading database status...');
@@ -1031,6 +1032,7 @@ function attachFooterDbBadge(footer, dbInfo, initialServerRoundTripMs = null) {
const badge = document.createElement('span');
const baseClass = classification === 'local' ? 'local' : (classification === 'atlas' ? 'atlas' : 'remote');
badge.className = `db-conn-badge ${baseClass}`;
+ badge.tabIndex = 0;
let labelIcon = 'π';
if (classification === 'local') labelIcon = 'π ';
else if (classification === 'atlas') labelIcon = 'πΊοΈ';
@@ -1138,6 +1140,13 @@ function attachFooterDbBadge(footer, dbInfo, initialServerRoundTripMs = null) {
rttSpan.classList.remove('fatal', 'warn', 'slow');
dbSpan.classList.remove('fatal', 'warn', 'slow');
+ // Only known, fast measurements may collapse. Outages and missing values
+ // remain visible; reuse the existing warning thresholds below.
+ rttSpan.classList.toggle('footer-quiet-metric', state !== 'server_down_unknown'
+ && Number.isFinite(serverRoundTripMs) && serverRoundTripMs <= 250);
+ dbSpan.classList.toggle('footer-quiet-metric', state !== 'server_down_unknown'
+ && state !== 'fatal_atlas' && Number.isFinite(mongoPingLatencyMs)
+ && mongoPingLatencyMs <= 100);
if (state === 'fatal_atlas') {
dbSpan.textContent = 'DB offline';
diff --git a/src/frontend/web/von_interface/static/styles.css b/src/frontend/web/von_interface/static/styles.css
index 5c2e86e5..d05c46df 100644
--- a/src/frontend/web/von_interface/static/styles.css
+++ b/src/frontend/web/von_interface/static/styles.css
@@ -18690,3 +18690,27 @@ html[data-footer-diagnostics="false"] .footer-container #serverPidFooter { displ
html[data-footer-uptime="false"][data-footer-runtimecost="false"][data-footer-build="false"] #serverUptimeFooter { display: none; }
.footer-container [hidden] { display: none !important; }
+
+/* Compact status details expand in the row, preserving its scroll behaviour.
+ Keep clipped text available to assistive technology even before expansion. */
+.footer-container .db-conn-badge:not(:hover):not(:focus-within) .footer-quiet-metric,
+.footer-container :is(.conversation-model-controls, .footer-user-segment, .footer-org-switcher):not(:hover):not(:focus-within) > .footer-label-inline {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+}
+.footer-container :is(.conversation-model-controls, .footer-user-segment, .footer-org-switcher):not(:hover):not(:focus-within) .concept-footer-button {
+ max-width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.footer-container .db-conn-badge:focus-visible {
+ outline: 2px solid #2563eb;
+ outline-offset: 2px;
+}
diff --git a/tests/browser/footerCompactCartouches.cjs b/tests/browser/footerCompactCartouches.cjs
new file mode 100644
index 00000000..399a4fd5
--- /dev/null
+++ b/tests/browser/footerCompactCartouches.cjs
@@ -0,0 +1,141 @@
+// Candidate-source layout acceptance. API fixtures make latency states repeatable;
+// no authentication, database, model request or live service is used.
+const { chromium, expect } = require('@playwright/test');
+const fs = require('node:fs');
+const path = require('node:path');
+const http = require('node:http');
+const { execFileSync } = require('node:child_process');
+const nativeZoom = process.env.NATIVE_FOOTER_ZOOM === '1';
+const root = path.resolve(__dirname, '../../src/frontend/web/von_interface');
+const template = fs.readFileSync(path.join(root, 'templates/von_interface.html'), 'utf8');
+const footer = template.slice(template.indexOf(' '), template.indexOf(' ')).replace(/{%.*?%}/g, '');
+const html = `${footer}`;
+const server = http.createServer((req, res) => {
+ if (req.url === '/') { res.setHeader('Content-Type', 'text/html'); return res.end(html); }
+ if (!req.url.startsWith('/static/') || req.url.includes('..')) { res.writeHead(404); return res.end(); }
+ const file = path.join(root, req.url.split('?')[0]);
+ res.setHeader('Content-Type', file.endsWith('.js') ? 'text/javascript' : 'text/css');
+ try { res.end(fs.readFileSync(file)); } catch { res.writeHead(404); res.end(); }
+});
+(async () => {
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ const browser = await chromium.launch({headless:!nativeZoom, args:['--window-size=1600,1000']});
+ const page = await browser.newPage({viewport:nativeZoom ? null : {width:1600,height:800}});
+ const errors = [];
+ page.on('pageerror', error => errors.push(error.message));
+ let delay = 5, db = 0.8;
+ await page.route('**/api/**', async route => {
+ const url = new URL(route.request().url()).pathname;
+ let body = {};
+ if (/\/settings\/?$/.test(url)) body = {resolved_llm:{model:'gpt-6-astra',provider:'openai'}};
+ if (url.includes('/llm/info')) body = {model:'gpt-6-astra',provider:'openai',status:'ready',details:{}};
+ if (url.includes('/auth/status')) body = {authenticated:true,email:'footer.researcher.with.long.name@example.test'};
+ if (url.includes('capability-index/status')) body = {ready:true,status:'ready'};
+ if (url.includes('my_organisations')) body = {organisations:[{concept_id:'#V#footer_org',name:'Footer Fixture Organisation',role:'member'}]};
+ if (url.includes('/db/info')) {
+ await new Promise(resolve => setTimeout(resolve, delay));
+ body = {classification:'local',ping_ok:true,mongo_ping_latency_ms:db};
+ }
+ await route.fulfill({json:body});
+ });
+ const output = process.argv[2] || '.run/footer-compact-cartouches';
+ fs.mkdirSync(output,{recursive:true});
+ const evidence = [];
+ const clipped = locator => locator.evaluate(el => getComputedStyle(el).clipPath === 'inset(50%)');
+ try {
+ for (const scenario of [
+ {name:'normal',delay:5,db:0.8,rttQuiet:true,dbQuiet:true},
+ {name:'slow-rtt',delay:304,db:0.8,rttQuiet:false,dbQuiet:true},
+ {name:'slow-db',delay:5,db:501,rttQuiet:true,dbQuiet:false},
+ {name:'partial',delay:5,db:null,rttQuiet:true,dbQuiet:false},
+ ]) {
+ delay = scenario.delay; db = scenario.db;
+ await page.goto(`http://127.0.0.1:${server.address().port}`);
+ await page.waitForFunction(() => window.fixtureReady);
+ await expect(page.locator('.conversation-model-controls')).toContainText('gpt-6-astra');
+ const badge = page.locator('.db-conn-badge');
+ const rtt = badge.locator('.server-rtt-latency');
+ const ping = badge.locator('.db-latency');
+ await expect(badge).not.toHaveClass(/loading/);
+ await page.mouse.move(0,0);
+ await expect.poll(() => clipped(rtt), {message:scenario.name}).toBe(scenario.rttQuiet);
+ await expect.poll(() => clipped(ping)).toBe(scenario.dbQuiet);
+ const compact = (await badge.boundingBox()).width;
+ await badge.hover();
+ expect(await clipped(rtt)).toBe(false);
+ expect(await clipped(ping)).toBe(false);
+ const expanded = (await badge.boundingBox()).width;
+ expect(expanded).toBeGreaterThan(compact);
+ await page.mouse.move(0,0);
+ // Reach the informational badge through actual sequential keyboard navigation.
+ for (let i=0; i<20 && !await badge.evaluate(el => el === document.activeElement); i++) await page.keyboard.press('Tab');
+ expect(await badge.evaluate(el => el === document.activeElement)).toBe(true);
+ expect(await clipped(ping)).toBe(false);
+ await page.screenshot({path:path.join(output,`${scenario.name}-focus.png`)});
+ evidence.push({scenario:scenario.name,compact,expanded,rtt:await rtt.textContent(),db:await ping.textContent()});
+ await page.evaluate(() => window.setReachability(false));
+ expect(await clipped(rtt)).toBe(false);
+ expect(await clipped(ping)).toBe(false);
+ await expect(ping).toHaveText('DB unknown');
+ }
+ if (nativeZoom) {
+ if (!process.env.DISPLAY || !process.env.XDOTOOL) throw new Error('Native zoom requires an isolated display and xdotool');
+ const key = (...args) => execFileSync(process.env.XDOTOOL,args,{encoding:'utf8'});
+ await page.bringToFront();
+ const windows = key('search','--onlyvisible','--class','Chromium').trim().split('\n');
+ expect(windows).toHaveLength(1);
+ key('windowfocus','--sync',windows[0]);
+ key('key','ctrl+0');
+ const cdp = await page.context().newCDPSession(page);
+ for (const [steps,ratio] of [[0,1],[2,1.25],[1,1.5],[2,2]]) {
+ for (let i=0;i page.evaluate(() => devicePixelRatio)).toBe(ratio);
+ await page.locator('.db-conn-badge').focus();
+ expect(await clipped(page.locator('.db-latency'))).toBe(false);
+ const measurement = await page.locator('.footer-container').evaluate(el => ({zoom:devicePixelRatio,width:innerWidth,height:el.getBoundingClientRect().height,scrollWidth:el.scrollWidth}));
+ expect(measurement.height).toBeLessThan(80);
+ evidence.push(measurement);
+ const shot = await cdp.send('Page.captureScreenshot',{format:'png',fromSurface:false});
+ fs.writeFileSync(path.join(output,`zoom-${ratio}.png`),Buffer.from(shot.data,'base64'));
+ }
+ key('key','ctrl+0');
+ await expect.poll(() => page.evaluate(() => devicePixelRatio)).toBe(1);
+ }
+ delay = 1500; db = 0.8;
+ await page.goto(`http://127.0.0.1:${server.address().port}`);
+ await expect(page.locator('.db-conn-badge.loading')).toBeVisible();
+ expect(await clipped(page.locator('.db-latency'))).toBe(false);
+ await page.waitForFunction(() => window.fixtureReady);
+ for (const width of [1600,800,493,320]) {
+ await page.setViewportSize({width,height:800});
+ for (const selector of ['.conversation-model-controls','.footer-user-segment','.footer-org-switcher']) {
+ const segment = page.locator(selector).first();
+ await segment.locator(selector === '.footer-org-switcher' ? 'summary' : 'button:not(:disabled)').first().focus();
+ expect(await clipped(segment.locator('.footer-label-inline').first()), `${width} ${selector}`).toBe(false);
+ const box = await segment.boundingBox();
+ expect(box.height).toBeLessThan(60);
+ }
+ await page.locator('.db-conn-badge').focus();
+ const measure = await page.locator('.footer-container').evaluate(el => {
+ const badge = el.querySelector('.db-conn-badge').getBoundingClientRect();
+ const previous = el.querySelector('.footer-org-switcher').getBoundingClientRect();
+ return {width:innerWidth,scrollWidth:el.scrollWidth,clientWidth:el.clientWidth,height:el.getBoundingClientRect().height,noOverlap:badge.x >= previous.right};
+ });
+ expect(measure.noOverlap).toBe(true);
+ expect(measure.height).toBeLessThan(80);
+ evidence.push(measure);
+ }
+ expect(errors).toEqual([]);
+ fs.writeFileSync(path.join(output,'measurements.json'),JSON.stringify({fixture:'candidate source; mocked API reads',nativeZoom,evidence},null,2));
+ console.log(JSON.stringify(evidence));
+ } finally { await browser.close(); server.close(); }
+})().catch(error => { console.error(error); server.close(); process.exitCode=1; });
diff --git a/tests/frontend/footerDbBadgeOutageStates.test.js b/tests/frontend/footerDbBadgeOutageStates.test.js
index 1ff9927d..c2cf9698 100644
--- a/tests/frontend/footerDbBadgeOutageStates.test.js
+++ b/tests/frontend/footerDbBadgeOutageStates.test.js
@@ -137,4 +137,32 @@ describe('footer DB badge outage state handling', () => {
expect(serverRtt?.title).toContain('Browser β Von server round trip');
expect(dbLatency?.title).toContain('measured inside the Von server');
});
+
+ test.each([
+ [250, 100, true, true],
+ [251, 0.8, false, true],
+ [601, 101, false, false],
+ [50, 501, true, false],
+ [50, null, true, false],
+ ])('collapses only fast known measurements (RTT %s, DB %s)', async (rtt, db, quietRtt, quietDb) => {
+ let clock = 0;
+ jest.spyOn(performance, 'now').mockImplementation(() => clock);
+ installFetchMock(buildDbInfo({ mongoPingLatencyMs: db }));
+ const fetchMock = global.fetch;
+ global.fetch = jest.fn((url, options) => {
+ if (String(url).includes('/db/info')) clock += rtt;
+ return fetchMock(url, options);
+ });
+ const { setModelInfoFooterText, setFooterServerReachability } = require(domUtilsPath);
+ setFooterServerReachability(true);
+ await setModelInfoFooterText();
+ const badge = await waitForDbBadgeReady();
+ expect(badge.tabIndex).toBe(0);
+ expect(badge.querySelector('.server-rtt-latency').classList.contains('footer-quiet-metric')).toBe(quietRtt);
+ expect(badge.querySelector('.db-latency').classList.contains('footer-quiet-metric')).toBe(quietDb);
+ setFooterServerReachability(false);
+ expect(badge.querySelectorAll('.footer-quiet-metric')).toHaveLength(0);
+ setFooterServerReachability(true);
+ expect(badge.querySelector('.db-latency').classList.contains('footer-quiet-metric')).toBe(quietDb);
+ });
});