openvibers/openvibe.ai Commits

On a phone each model is a card, not a sideways table row (#255)

OP OpenVibers committed 11 days ago · 74639b70f8 · 2 files, +6 −2

Changes

ddiff --git a/server/home.js b/server/home.js
iindex 6a690a2..9ea27e4 100644
---- a/server/home.js
++++ b/server/home.js
@@@ -73,7 +73,7 @@ function models(stats) {
return `<section class="sc-sec" id="models" aria-labelledby="h-models"><h2 id="h-models">Models that answered this week</h2>
<p class="sc-lede">Every model that answered a call in the last ${esc(stats.days)} days, for OpenVibe's own sites and for apps, with what it does, how fast it usually answered and what it costs. Apps on the router get free and local capacity today (a small local model, without tools); name your project's own provider for anything bigger.</p>
<div class="sc-table-wrap" tabindex="0" role="region" aria-labelledby="h-models"><table class="sc-table"><thead><tr><th scope="col">Model</th><th scope="col">Provider</th><th scope="col">Does</th><th scope="col">Answered</th><th scope="col">Typical latency</th><th scope="col">Price</th></tr></thead><tbody>
--${rows.map((r) => `<tr><th scope="row"><code>${esc(r.model || '—')}</code></th><td><code>${esc(r.provider)}</code></td><td>${esc(caps(r.capability))}</td><td>${esc(n(r.ok))}</td><td>${r.p50 == null ? '—' : `under ${esc(r.p50 >= 1000 ? `${(r.p50 / 1000).toLocaleString('en-US', { maximumFractionDigits: 1 })} s` : `${n(r.p50)} ms`)}`}</td><td>${esc(price(r.cards))}</td></tr>`).join('\n')}
++${rows.map((r) => `<tr><th scope="row"><code>${esc(r.model || '—')}</code></th><td data-label="Provider"><code>${esc(r.provider)}</code></td><td data-label="Does">${esc(caps(r.capability))}</td><td data-label="Answered">${esc(n(r.ok))}</td><td data-label="Typical latency">${r.p50 == null ? '—' : `under ${esc(r.p50 >= 1000 ? `${(r.p50 / 1000).toLocaleString('en-US', { maximumFractionDigits: 1 })} s` : `${n(r.p50)} ms`)}`}</td><td data-label="Price">${esc(price(r.cards))}</td></tr>`).join('\n')}
</tbody></table></div><p class="sc-note">Latency is the upper bound of a fixed bucket on the slowest day. Every number here, with p95, success rates and free allowances, is on <a href="/stats">Prices and latency</a>.</p></section>`;
}
@@@ -224,7 +224,7 @@ const HOME_STYLE = `<style>
/* Phones (2026-10-29): a taste and a door. */
@media (max-width:599px){
.ai-num ul{display:flex;flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;gap:8px;margin:0 -16px;padding:0 16px 2px}.ai-num ul::-webkit-scrollbar{display:none}.ai-num li{flex:0 0 auto;width:148px}
--#get .sc-grid{display:flex;flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin:0 -16px;padding:0 16px 4px}#get .sc-grid::-webkit-scrollbar{display:none}#get .sc-grid>*{flex:0 0 80%;min-width:0;scroll-snap-align:start}
++@media (max-width:600px){#models .sc-table-wrap{overflow:visible}#models .sc-table-wrap{border:0;border-radius:0}#models table.sc-table{min-width:0;border-collapse:separate}#models .sc-table td{min-width:0!important;white-space:normal!important}#models .sc-table td code{overflow-wrap:anywhere}#models .sc-table,#models .sc-table tbody,#models .sc-table tr,#models .sc-table tbody th,#models .sc-table td{display:block;width:auto}#models .sc-table thead{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}#models .sc-table tr{margin:0 0 8px;padding:12px 14px;border:1px solid var(--border,color-mix(in srgb,currentColor 14%,transparent));border-radius:12px;background:var(--bg-card,color-mix(in srgb,currentColor 3%,transparent))}#models .sc-table tbody th{padding:0 0 6px;border:0;white-space:normal;overflow-wrap:anywhere}#models .sc-table td{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:3px 0;border:0;font-size:.86rem;text-align:right}#models .sc-table td::before{content:attr(data-label);flex:none;color:var(--text-muted,color-mix(in srgb,currentColor 62%,transparent));text-align:left}}#get .sc-grid{display:flex;flex-wrap:nowrap;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin:0 -16px;padding:0 16px 4px}#get .sc-grid::-webkit-scrollbar{display:none}#get .sc-grid>*{flex:0 0 80%;min-width:0;scroll-snap-align:start}
.ai-fold>summary span{display:none}
}
/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */
ddiff --git a/test/home.test.js b/test/home.test.js
iindex 7c7ccdc..476ebb2 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -58,6 +58,10 @@ t.test('the home: numbers and the model table come from the /stats rows; nothing
const html = renderHome({ siteUrl: SITE, stats: { days: 7, rows } });
assert.match(html, /<b>1,010<\/b><span>calls answered in the last 7 days<\/span>/);
assert.match(html, /<b>2<\/b><span>models answered them<\/span>/);
++ // On a phone each model is a card: every cell carries its column's name (2026-11-02: the table scrolled sideways
++ // with rows a screen tall).
++ assert.ok(/<td data-label="Provider">[\s\S]*?<td data-label="Price">/.test(html), 'each cell names its column');
++ assert.ok(/@media \(max-width:600px\)\{#models[^\n]*table\.sc-table\{min-width:0;/.test(html), 'and the phone layout undoes the table width');
assert.match(html, /<b>99\.4%<\/b><span>of provider calls succeeded<\/span>/);
assert.ok(html.indexOf('gpt-5-nano') < html.indexOf('qwen2.5-1.5b-instruct'), 'most used first');
assert.ok(!html.includes('never-answered'), 'a model with no answer is left out');