openvibers/openvibe.live Commits
Clicking a name in chat never waits on a Loading box (#589)
Changes
@@@ -237,6 +237,10 @@ function authHeaders() {async function api(path, opts = {}) {// Caller headers add to the defaults (spreading opts after them used to replace the lot, auth included).const { headers, body, _retried, ...init } = opts;++ // A read gives up after 20 seconds unless the caller set its own signal: during a deploy round a request held at a++ // restarting service kept "Loading..." boxes up for minutes (11-03). The caller's error path takes it from there.++ const method = String(init.method || 'GET').toUpperCase();++ if (!init.signal && (method === 'GET' || method === 'HEAD') && typeof AbortSignal !== 'undefined' && AbortSignal.timeout) init.signal = AbortSignal.timeout(20000);const res = await fetch(`${API}/api${path}`, {...init,headers: { 'Content-Type': 'application/json', ...authHeaders(), ...headers },@@@ -3885,9 +3885,9 @@ function showChatContextMenu(event) {menu.dataset.msgId = msgId || '';menu.dataset.isRelay = isRelay ? '1' : '';menu.dataset.sourcePlatform = sourcePlatform;-- menu.innerHTML = `-- <div class="ctx-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>-- `;++ // The basic card at once (the name, Message, Channel), upgraded when the profile arrives: a click never waits on a++ // "Loading..." box (11-03: Maticus's sat for minutes while Chat restarted in a deploy round).++ menu.innerHTML = basicContextCard(username);// Position near the clickdocument.body.appendChild(menu);@@@ -3991,22 +3991,30 @@ async function loadContextMenuProfile(menu, username, userId, isAnon) {return;}try {-- const profile = await api(`/chat/user/${encodeURIComponent(username)}/profile`);++ // At most eight seconds: a profile that takes longer leaves the basic card, which already works.++ const profile = await api(`/chat/user/${encodeURIComponent(username)}/profile`, { signal: AbortSignal.timeout(8000) });++ if (!menu.isConnected) return; // dismissed meanwhilerenderContextMenu(menu, profile, username);} catch {-- menu.innerHTML = `++ if (menu.isConnected) menu.innerHTML = basicContextCard(username);++ }++}++++/** The name card before (or without) the profile: the name, Message and Channel. Shown at once, never a spinner. */++function basicContextCard(username) {++ const name = String(username || '?');++ return `<div class="ctx-header">-- <span class="ctx-avatar-letter">${esc(username)[0].toUpperCase()}</span>++ <span class="ctx-avatar-letter">${esc(name)[0].toUpperCase()}</span><div class="ctx-info">-- <span class="ctx-name">${esc(username)}</span>++ <span class="ctx-name">${esc(name)}</span></div></div><div class="ctx-actions">-- <button class="ctx-btn" data-username="${esc(username)}" onclick="ctxWhisper(this.dataset.username)"><i class="fa-solid fa-comment"></i> Message</button>-- <button class="ctx-btn" data-username="${esc(username)}" onclick="ctxViewChannel(this.dataset.username)"><i class="fa-solid fa-user"></i> Channel</button>++ <button class="ctx-btn" data-username="${esc(name)}" onclick="ctxWhisper(this.dataset.username)"><i class="fa-solid fa-comment"></i> Message</button>++ <button class="ctx-btn" data-username="${esc(name)}" onclick="ctxViewChannel(this.dataset.username)"><i class="fa-solid fa-user"></i> Channel</button></div>`;-- }}// Reserved usernames that represent the site itself (system/changelog messages), not a@@@ -4778,7 +4786,7 @@ function openChatLogsModal(username, userId, relay) {</div></div><div class="chat-logs-body" id="chat-logs-body">-- <div class="chat-logs-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>++ <div class="chat-logs-loading" role="status">Loading…</div></div><div class="chat-logs-footer" id="chat-logs-footer"></div></div>@@@ -4806,7 +4814,9 @@ async function loadChatLogs() {const footer = document.getElementById('chat-logs-footer');if (!body) return;-- body.innerHTML = '<div class="chat-logs-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>';++ body.innerHTML = '<div class="chat-logs-loading" role="status">Loading…</div>';++ // At most ten seconds for a page of logs (11-03: a request held while Chat restarted kept this up for minutes).++ const signal = AbortSignal.timeout(10000);try {let url, result;@@@ -4815,24 +4825,24 @@ async function loadChatLogs() {const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });if (ctx.query) params.set('q', ctx.query);url = `/chat/anon/${encodeURIComponent(ctx.anon)}/logs?${params}`;-- result = await api(url);++ result = await api(url, { signal });} else if (ctx.relay) {// Relay (external-platform) user history — matched by platform + username.const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });if (ctx.query) params.set('q', ctx.query);url = `/chat/relay-user/${encodeURIComponent(ctx.relay.platform)}/${encodeURIComponent(ctx.relay.username)}/logs?${params}`;-- result = await api(url);++ result = await api(url, { signal });} else if (ctx.userId && !ctx.query) {// User history modeurl = `/chat/user/${ctx.userId}/history?limit=50&offset=${ctx.offset}`;-- result = await api(url);++ result = await api(url, { signal });} else {// Search modeconst params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });if (ctx.query) params.set('q', ctx.query);if (ctx.userId) params.set('user_id', String(ctx.userId));url = `/chat/search?${params}`;-- result = await api(url);++ result = await api(url, { signal });}const msgs = result.messages || [];@@@ -4866,7 +4876,7 @@ async function loadChatLogs() {footer.innerHTML = total > 0 ? `<span class="log-page-info">${total} messages</span>` : '';}} catch (err) {-- body.innerHTML = '<p class="muted" style="text-align:center;padding:20px">Failed to load logs</p>';++ body.innerHTML = `<p class="muted" style="text-align:center;padding:20px">${err && err.name === 'TimeoutError' ? 'The logs are taking too long right now.' : 'Failed to load logs.'} <button type="button" class="btn btn-sm" onclick="loadChatLogs()">Try again</button></p>`;}}@@@ -22,7 +22,7 @@ const BUDGETS = {htmlRawKB: 128, // 119.3 with the icons drawn on the server (2026-11-01, plan L54 K4: the OpenVibe glyphs in the HTML, 20.8 KB brotli); measured 94.9 beforehtmlBrotliKB: 30,jsFiles: 26, // measured 23 (incl. route scripts for "/")-- jsRawKB: 1280, // 1279.2 (2026-11-03: openvibe-shared 4.107.0, open.<tld> brands; the home's reveal scroll listener hooked once); 1278.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack so the site's name reads whole on a phone); 1277.1 (2026-11-02: the home's money four as one 'None yet' chip while all four are zero, open item 135); 1275.2 (2026-11-02: route placeholders in ov-skeleton.js, L52 step 6: a loading route shows its shape, not an empty box); 1268.4 (2026-11-02: openvibe-shared 4.103.0, the puzzle glyph, two-line sidebar rows, boost's fix and eight solids; the 404 offers the channel for a bare /name); 1266.3 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 1262.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 1259.8 (2026-11-02: content labels on cards and the player, L1); 1257.5 (2026-11-01: the Catch up rails load by themselves as you swipe, no Load more on a phone); 1254.2 (2026-11-01: openvibe-shared 4.87.0), about 1255 with #534 (feed and card avatars at their size); 1253.8 (2026-11-01: avatars ask Media for their drawn width, ovImgW); 1252.3 (2026-11-01: Following in the topics row, the picked topic remembered); 1250.2 (2026-11-01: topics live now, readable chat names on light themes); 1247.7 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 1244.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 1243.5 with openvibe-shared 4.64.0: the look travels between sites, the Look panel in the menu (2026-10-31); 1242.7 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face, and the ring on page moves (2026-10-31); 1238.8 with the home's featured stream once and the intro closed on phones (2026-10-31); 1237.4 with the home's two more tab blocks on phones (2026-10-30); 1233.9 with the home's Catch up tabs on phones (2026-10-30); 1230.8 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 1227.8 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 1223.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 1215.4 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); measured 1203.9 (2026-10-28: openvibe-shared 4.30.0, the bell center: tabs with counts, the Unread filter, thumbnails, swipe, the phone sheet; 4.31.0's ✦ adds about 3 KB); 1187.2 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0: opaque themed toasts held while a menu is open, grouping, priority, System cards and the rich bell panel); 1158.8 (2026-10-28: openvibe-shared 4.25.0, the menu's orb, messages, the phone sheet and auto-hide in the navbar; the messenger opens from the bar)++ jsRawKB: 1290, // 1280.1 (2026-11-03: the chat name card shows at once, the logs give up after ten seconds; headroom for openvibe-shared 4.107.3); 1279.2 (2026-11-03: openvibe-shared 4.107.0, open.<tld> brands; the home's reveal scroll listener hooked once); 1278.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack so the site's name reads whole on a phone); 1277.1 (2026-11-02: the home's money four as one 'None yet' chip while all four are zero, open item 135); 1275.2 (2026-11-02: route placeholders in ov-skeleton.js, L52 step 6: a loading route shows its shape, not an empty box); 1268.4 (2026-11-02: openvibe-shared 4.103.0, the puzzle glyph, two-line sidebar rows, boost's fix and eight solids; the 404 offers the channel for a bare /name); 1266.3 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 1262.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 1259.8 (2026-11-02: content labels on cards and the player, L1); 1257.5 (2026-11-01: the Catch up rails load by themselves as you swipe, no Load more on a phone); 1254.2 (2026-11-01: openvibe-shared 4.87.0), about 1255 with #534 (feed and card avatars at their size); 1253.8 (2026-11-01: avatars ask Media for their drawn width, ovImgW); 1252.3 (2026-11-01: Following in the topics row, the picked topic remembered); 1250.2 (2026-11-01: topics live now, readable chat names on light themes); 1247.7 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 1244.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 1243.5 with openvibe-shared 4.64.0: the look travels between sites, the Look panel in the menu (2026-10-31); 1242.7 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face, and the ring on page moves (2026-10-31); 1238.8 with the home's featured stream once and the intro closed on phones (2026-10-31); 1237.4 with the home's two more tab blocks on phones (2026-10-30); 1233.9 with the home's Catch up tabs on phones (2026-10-30); 1230.8 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 1227.8 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 1223.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 1215.4 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); measured 1203.9 (2026-10-28: openvibe-shared 4.30.0, the bell center: tabs with counts, the Unread filter, thumbnails, swipe, the phone sheet; 4.31.0's ✦ adds about 3 KB); 1187.2 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0: opaque themed toasts held while a menu is open, grouping, priority, System cards and the rich bell panel); 1158.8 (2026-10-28: openvibe-shared 4.25.0, the menu's orb, messages, the phone sheet and auto-hide in the navbar; the messenger opens from the bar)jsBrotliKB: 300, // 299.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack); 298.4 (2026-11-02: route placeholders, L52 step 6); 296.7 (2026-11-02: openvibe-shared 4.103.0 and the 404's channel offer); 296.2 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 295.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 294.6 (2026-11-02: content labels on cards and the player, L1); 294.1 (2026-11-01: the Catch up rails load by themselves as you swipe); 292.6 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 291.1 with openvibe-shared 4.57.1 (the Dock's async token and scope-aware controls, 4.57.0; the tool frame's facts, 4.56.0) (2026-10-31); 290.6 with the home's two more tab blocks on phones (2026-10-30); 289.8 with the home's Catch up tabs on phones (2026-10-30); 288.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 287.4 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); measured 283.2 (2026-10-28: openvibe-shared 4.30.0, the bell center as above); 279.6 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0 as above); 273.1 (2026-10-28: openvibe-shared 4.25.0 as above); 270.5 (2026-10-28: openvibe-shared 4.22.0, the navbar's search magnifier with / and Ctrl+K and the More menu; search.js and loading.js load only when used); 269.6 (2026-10-28: openvibe-shared 4.19.0, the account menu's gear and people-group icons inline; 268 on 2026-10-27: openvibe-shared 4.16.0, the folded balance chip, Across OpenVibe with site marks and the icon preload in the navbar); 265.3 (2026-10-27: openvibe-shared 4.15.0, the compact footer opens into the whole one, and the pulse line); 263.3 on 10-23 (openvibe-shared 4.9.0, status text twins in every theme); 262.5 on 10-17 (openvibe-shared 4.4.0, every site in the launcher)cssRawKB: 270, // measured ~243 since the first-paint subset (style.core.css) replaced style.css (~717 before)cssBrotliKB: 55, // measured ~47.5 (was ~130)nnew file mode 100644@@@ -0,0 +1,34 @@++'use strict';++// The chat name card never waits (11-03: Maticus's "Loading..." box sat for minutes while Chat restarted in a deploy++// round). Clicking a name shows the basic card at once (the name, Message, Channel) and upgrades it when the profile++// arrives, within eight seconds; the chat logs give up after ten with a way to try again.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const vm = require('vm');++++const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'chat.js'), 'utf8');++assert.ok(!src.includes('<div class="ctx-loading">'), 'no "Loading..." box in the name card');++assert.match(src, /menu\.innerHTML = basicContextCard\(username\);/, 'the basic card is shown at once');++assert.match(src, /api\(`\/chat\/user\/\$\{encodeURIComponent\(username\)\}\/profile`, \{ signal: AbortSignal\.timeout\(8000\) \}\)/, 'the profile waits eight seconds at most');++assert.match(src, /if \(!menu\.isConnected\) return;/, 'a dismissed card is left alone');++++const fn = /function basicContextCard\(username\) \{[\s\S]*?\n\}/.exec(src);++assert.ok(fn, 'basicContextCard exists');++const ctx = { esc: (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') };++vm.createContext(ctx);++vm.runInContext(`${fn[0]}\nthis.card = basicContextCard;`, ctx);++const html = ctx.card('maticus');++assert.match(html, /<span class="ctx-avatar-letter">M<\/span>/);++assert.match(html, /ctxWhisper\(this\.dataset\.username\)[\s\S]*Message/);++assert.match(html, /ctxViewChannel\(this\.dataset\.username\)[\s\S]*Channel/);++assert.ok(!/spin|Loading/.test(html), 'nothing loading in it');++assert.ok(!ctx.card('<b>x</b>').includes('<b>'), 'the name is escaped');++++const logs = /async function loadChatLogs\(\) \{[\s\S]*?\n\}/.exec(src)[0];++assert.match(logs, /const signal = AbortSignal\.timeout\(10000\);/);++assert.strictEqual((logs.match(/await api\(url, \{ signal \}\)/g) || []).length, 4, 'every logs request has the limit');++assert.match(logs, /taking too long right now[\s\S]*onclick="loadChatLogs\(\)">Try again/);++const app = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');++assert.match(app, /if \(!init\.signal && \(method === 'GET' \|\| method === 'HEAD'\)[^\n]*AbortSignal\.timeout\(20000\)/, 'every read through api() gives up after 20 s unless the caller set a signal');++console.log('chat card never waits: all checks passed');
@@ -237,6 +237,10 @@ function authHeaders() {
@@ -237,6 +237,10 @@ function authHeaders() {
async function api(path, opts = {}) {
async function api(path, opts = {}) {
// Caller headers add to the defaults (spreading opts after them used to replace the lot, auth included).
// Caller headers add to the defaults (spreading opts after them used to replace the lot, auth included).
const { headers, body, _retried, ...init } = opts;
const { headers, body, _retried, ...init } = opts;
+ // A read gives up after 20 seconds unless the caller set its own signal: during a deploy round a request held at a
+ // restarting service kept "Loading..." boxes up for minutes (11-03). The caller's error path takes it from there.
+ const method = String(init.method || 'GET').toUpperCase();
+ if (!init.signal && (method === 'GET' || method === 'HEAD') && typeof AbortSignal !== 'undefined' && AbortSignal.timeout) init.signal = AbortSignal.timeout(20000);
const res = await fetch(`${API}/api${path}`, {
const res = await fetch(`${API}/api${path}`, {
...init,
...init,
headers: { 'Content-Type': 'application/json', ...authHeaders(), ...headers },
headers: { 'Content-Type': 'application/json', ...authHeaders(), ...headers },
@@ -3885,9 +3885,9 @@ function showChatContextMenu(event) {
@@ -3885,9 +3885,9 @@ function showChatContextMenu(event) {
menu.dataset.msgId = msgId || '';
menu.dataset.msgId = msgId || '';
menu.dataset.isRelay = isRelay ? '1' : '';
menu.dataset.isRelay = isRelay ? '1' : '';
menu.dataset.sourcePlatform = sourcePlatform;
menu.dataset.sourcePlatform = sourcePlatform;
- menu.innerHTML = `
- <div class="ctx-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>
- `;
+ // The basic card at once (the name, Message, Channel), upgraded when the profile arrives: a click never waits on a
+ // "Loading..." box (11-03: Maticus's sat for minutes while Chat restarted in a deploy round).
+ menu.innerHTML = basicContextCard(username);
// Position near the click
// Position near the click
document.body.appendChild(menu);
document.body.appendChild(menu);
@@ -3991,22 +3991,30 @@ async function loadContextMenuProfile(menu, username, userId, isAnon) {
@@ -3991,22 +3991,30 @@ async function loadContextMenuProfile(menu, username, userId, isAnon) {
return;
return;
}
}
try {
try {
- const profile = await api(`/chat/user/${encodeURIComponent(username)}/profile`);
+ // At most eight seconds: a profile that takes longer leaves the basic card, which already works.
+ const profile = await api(`/chat/user/${encodeURIComponent(username)}/profile`, { signal: AbortSignal.timeout(8000) });
+ if (!menu.isConnected) return; // dismissed meanwhile
renderContextMenu(menu, profile, username);
renderContextMenu(menu, profile, username);
} catch {
} catch {
- menu.innerHTML = `
+ if (menu.isConnected) menu.innerHTML = basicContextCard(username);
+ }
+}
+
+/** The name card before (or without) the profile: the name, Message and Channel. Shown at once, never a spinner. */
+function basicContextCard(username) {
+ const name = String(username || '?');
+ return `
<div class="ctx-header">
<div class="ctx-header">
- <span class="ctx-avatar-letter">${esc(username)[0].toUpperCase()}</span>
+ <span class="ctx-avatar-letter">${esc(name)[0].toUpperCase()}</span>
<div class="ctx-info">
<div class="ctx-info">
- <span class="ctx-name">${esc(username)}</span>
+ <span class="ctx-name">${esc(name)}</span>
</div>
</div>
</div>
</div>
<div class="ctx-actions">
<div class="ctx-actions">
- <button class="ctx-btn" data-username="${esc(username)}" onclick="ctxWhisper(this.dataset.username)"><i class="fa-solid fa-comment"></i> Message</button>
- <button class="ctx-btn" data-username="${esc(username)}" onclick="ctxViewChannel(this.dataset.username)"><i class="fa-solid fa-user"></i> Channel</button>
+ <button class="ctx-btn" data-username="${esc(name)}" onclick="ctxWhisper(this.dataset.username)"><i class="fa-solid fa-comment"></i> Message</button>
+ <button class="ctx-btn" data-username="${esc(name)}" onclick="ctxViewChannel(this.dataset.username)"><i class="fa-solid fa-user"></i> Channel</button>
</div>
</div>
`;
`;
- }
}
}
// Reserved usernames that represent the site itself (system/changelog messages), not a
// Reserved usernames that represent the site itself (system/changelog messages), not a
@@ -4778,7 +4786,7 @@ function openChatLogsModal(username, userId, relay) {
@@ -4778,7 +4786,7 @@ function openChatLogsModal(username, userId, relay) {
</div>
</div>
</div>
</div>
<div class="chat-logs-body" id="chat-logs-body">
<div class="chat-logs-body" id="chat-logs-body">
- <div class="chat-logs-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>
+ <div class="chat-logs-loading" role="status">Loading…</div>
</div>
</div>
<div class="chat-logs-footer" id="chat-logs-footer"></div>
<div class="chat-logs-footer" id="chat-logs-footer"></div>
</div>
</div>
@@ -4806,7 +4814,9 @@ async function loadChatLogs() {
@@ -4806,7 +4814,9 @@ async function loadChatLogs() {
const footer = document.getElementById('chat-logs-footer');
const footer = document.getElementById('chat-logs-footer');
if (!body) return;
if (!body) return;
- body.innerHTML = '<div class="chat-logs-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>';
+ body.innerHTML = '<div class="chat-logs-loading" role="status">Loading…</div>';
+ // At most ten seconds for a page of logs (11-03: a request held while Chat restarted kept this up for minutes).
+ const signal = AbortSignal.timeout(10000);
try {
try {
let url, result;
let url, result;
@@ -4815,24 +4825,24 @@ async function loadChatLogs() {
@@ -4815,24 +4825,24 @@ async function loadChatLogs() {
const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });
const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });
if (ctx.query) params.set('q', ctx.query);
if (ctx.query) params.set('q', ctx.query);
url = `/chat/anon/${encodeURIComponent(ctx.anon)}/logs?${params}`;
url = `/chat/anon/${encodeURIComponent(ctx.anon)}/logs?${params}`;
- result = await api(url);
+ result = await api(url, { signal });
} else if (ctx.relay) {
} else if (ctx.relay) {
// Relay (external-platform) user history — matched by platform + username.
// Relay (external-platform) user history — matched by platform + username.
const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });
const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });
if (ctx.query) params.set('q', ctx.query);
if (ctx.query) params.set('q', ctx.query);
url = `/chat/relay-user/${encodeURIComponent(ctx.relay.platform)}/${encodeURIComponent(ctx.relay.username)}/logs?${params}`;
url = `/chat/relay-user/${encodeURIComponent(ctx.relay.platform)}/${encodeURIComponent(ctx.relay.username)}/logs?${params}`;
- result = await api(url);
+ result = await api(url, { signal });
} else if (ctx.userId && !ctx.query) {
} else if (ctx.userId && !ctx.query) {
// User history mode
// User history mode
url = `/chat/user/${ctx.userId}/history?limit=50&offset=${ctx.offset}`;
url = `/chat/user/${ctx.userId}/history?limit=50&offset=${ctx.offset}`;
- result = await api(url);
+ result = await api(url, { signal });
} else {
} else {
// Search mode
// Search mode
const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });
const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });
if (ctx.query) params.set('q', ctx.query);
if (ctx.query) params.set('q', ctx.query);
if (ctx.userId) params.set('user_id', String(ctx.userId));
if (ctx.userId) params.set('user_id', String(ctx.userId));
url = `/chat/search?${params}`;
url = `/chat/search?${params}`;
- result = await api(url);
+ result = await api(url, { signal });
}
}
const msgs = result.messages || [];
const msgs = result.messages || [];
@@ -4866,7 +4876,7 @@ async function loadChatLogs() {
@@ -4866,7 +4876,7 @@ async function loadChatLogs() {
footer.innerHTML = total > 0 ? `<span class="log-page-info">${total} messages</span>` : '';
footer.innerHTML = total > 0 ? `<span class="log-page-info">${total} messages</span>` : '';
}
}
} catch (err) {
} catch (err) {
- body.innerHTML = '<p class="muted" style="text-align:center;padding:20px">Failed to load logs</p>';
+ body.innerHTML = `<p class="muted" style="text-align:center;padding:20px">${err && err.name === 'TimeoutError' ? 'The logs are taking too long right now.' : 'Failed to load logs.'} <button type="button" class="btn btn-sm" onclick="loadChatLogs()">Try again</button></p>`;
}
}
}
}
@@ -22,7 +22,7 @@ const BUDGETS = {
@@ -22,7 +22,7 @@ const BUDGETS = {
htmlRawKB: 128, // 119.3 with the icons drawn on the server (2026-11-01, plan L54 K4: the OpenVibe glyphs in the HTML, 20.8 KB brotli); measured 94.9 before
htmlRawKB: 128, // 119.3 with the icons drawn on the server (2026-11-01, plan L54 K4: the OpenVibe glyphs in the HTML, 20.8 KB brotli); measured 94.9 before
htmlBrotliKB: 30,
htmlBrotliKB: 30,
jsFiles: 26, // measured 23 (incl. route scripts for "/")
jsFiles: 26, // measured 23 (incl. route scripts for "/")
- jsRawKB: 1280, // 1279.2 (2026-11-03: openvibe-shared 4.107.0, open.<tld> brands; the home's reveal scroll listener hooked once); 1278.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack so the site's name reads whole on a phone); 1277.1 (2026-11-02: the home's money four as one 'None yet' chip while all four are zero, open item 135); 1275.2 (2026-11-02: route placeholders in ov-skeleton.js, L52 step 6: a loading route shows its shape, not an empty box); 1268.4 (2026-11-02: openvibe-shared 4.103.0, the puzzle glyph, two-line sidebar rows, boost's fix and eight solids; the 404 offers the channel for a bare /name); 1266.3 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 1262.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 1259.8 (2026-11-02: content labels on cards and the player, L1); 1257.5 (2026-11-01: the Catch up rails load by themselves as you swipe, no Load more on a phone); 1254.2 (2026-11-01: openvibe-shared 4.87.0), about 1255 with #534 (feed and card avatars at their size); 1253.8 (2026-11-01: avatars ask Media for their drawn width, ovImgW); 1252.3 (2026-11-01: Following in the topics row, the picked topic remembered); 1250.2 (2026-11-01: topics live now, readable chat names on light themes); 1247.7 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 1244.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 1243.5 with openvibe-shared 4.64.0: the look travels between sites, the Look panel in the menu (2026-10-31); 1242.7 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face, and the ring on page moves (2026-10-31); 1238.8 with the home's featured stream once and the intro closed on phones (2026-10-31); 1237.4 with the home's two more tab blocks on phones (2026-10-30); 1233.9 with the home's Catch up tabs on phones (2026-10-30); 1230.8 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 1227.8 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 1223.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 1215.4 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); measured 1203.9 (2026-10-28: openvibe-shared 4.30.0, the bell center: tabs with counts, the Unread filter, thumbnails, swipe, the phone sheet; 4.31.0's ✦ adds about 3 KB); 1187.2 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0: opaque themed toasts held while a menu is open, grouping, priority, System cards and the rich bell panel); 1158.8 (2026-10-28: openvibe-shared 4.25.0, the menu's orb, messages, the phone sheet and auto-hide in the navbar; the messenger opens from the bar)
+ jsRawKB: 1290, // 1280.1 (2026-11-03: the chat name card shows at once, the logs give up after ten seconds; headroom for openvibe-shared 4.107.3); 1279.2 (2026-11-03: openvibe-shared 4.107.0, open.<tld> brands; the home's reveal scroll listener hooked once); 1278.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack so the site's name reads whole on a phone); 1277.1 (2026-11-02: the home's money four as one 'None yet' chip while all four are zero, open item 135); 1275.2 (2026-11-02: route placeholders in ov-skeleton.js, L52 step 6: a loading route shows its shape, not an empty box); 1268.4 (2026-11-02: openvibe-shared 4.103.0, the puzzle glyph, two-line sidebar rows, boost's fix and eight solids; the 404 offers the channel for a bare /name); 1266.3 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 1262.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 1259.8 (2026-11-02: content labels on cards and the player, L1); 1257.5 (2026-11-01: the Catch up rails load by themselves as you swipe, no Load more on a phone); 1254.2 (2026-11-01: openvibe-shared 4.87.0), about 1255 with #534 (feed and card avatars at their size); 1253.8 (2026-11-01: avatars ask Media for their drawn width, ovImgW); 1252.3 (2026-11-01: Following in the topics row, the picked topic remembered); 1250.2 (2026-11-01: topics live now, readable chat names on light themes); 1247.7 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 1244.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 1243.5 with openvibe-shared 4.64.0: the look travels between sites, the Look panel in the menu (2026-10-31); 1242.7 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face, and the ring on page moves (2026-10-31); 1238.8 with the home's featured stream once and the intro closed on phones (2026-10-31); 1237.4 with the home's two more tab blocks on phones (2026-10-30); 1233.9 with the home's Catch up tabs on phones (2026-10-30); 1230.8 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 1227.8 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 1223.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 1215.4 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); measured 1203.9 (2026-10-28: openvibe-shared 4.30.0, the bell center: tabs with counts, the Unread filter, thumbnails, swipe, the phone sheet; 4.31.0's ✦ adds about 3 KB); 1187.2 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0: opaque themed toasts held while a menu is open, grouping, priority, System cards and the rich bell panel); 1158.8 (2026-10-28: openvibe-shared 4.25.0, the menu's orb, messages, the phone sheet and auto-hide in the navbar; the messenger opens from the bar)
jsBrotliKB: 300, // 299.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack); 298.4 (2026-11-02: route placeholders, L52 step 6); 296.7 (2026-11-02: openvibe-shared 4.103.0 and the 404's channel offer); 296.2 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 295.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 294.6 (2026-11-02: content labels on cards and the player, L1); 294.1 (2026-11-01: the Catch up rails load by themselves as you swipe); 292.6 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 291.1 with openvibe-shared 4.57.1 (the Dock's async token and scope-aware controls, 4.57.0; the tool frame's facts, 4.56.0) (2026-10-31); 290.6 with the home's two more tab blocks on phones (2026-10-30); 289.8 with the home's Catch up tabs on phones (2026-10-30); 288.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 287.4 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); measured 283.2 (2026-10-28: openvibe-shared 4.30.0, the bell center as above); 279.6 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0 as above); 273.1 (2026-10-28: openvibe-shared 4.25.0 as above); 270.5 (2026-10-28: openvibe-shared 4.22.0, the navbar's search magnifier with / and Ctrl+K and the More menu; search.js and loading.js load only when used); 269.6 (2026-10-28: openvibe-shared 4.19.0, the account menu's gear and people-group icons inline; 268 on 2026-10-27: openvibe-shared 4.16.0, the folded balance chip, Across OpenVibe with site marks and the icon preload in the navbar); 265.3 (2026-10-27: openvibe-shared 4.15.0, the compact footer opens into the whole one, and the pulse line); 263.3 on 10-23 (openvibe-shared 4.9.0, status text twins in every theme); 262.5 on 10-17 (openvibe-shared 4.4.0, every site in the launcher)
jsBrotliKB: 300, // 299.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack); 298.4 (2026-11-02: route placeholders, L52 step 6); 296.7 (2026-11-02: openvibe-shared 4.103.0 and the 404's channel offer); 296.2 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 295.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 294.6 (2026-11-02: content labels on cards and the player, L1); 294.1 (2026-11-01: the Catch up rails load by themselves as you swipe); 292.6 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 291.1 with openvibe-shared 4.57.1 (the Dock's async token and scope-aware controls, 4.57.0; the tool frame's facts, 4.56.0) (2026-10-31); 290.6 with the home's two more tab blocks on phones (2026-10-30); 289.8 with the home's Catch up tabs on phones (2026-10-30); 288.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 287.4 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); measured 283.2 (2026-10-28: openvibe-shared 4.30.0, the bell center as above); 279.6 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0 as above); 273.1 (2026-10-28: openvibe-shared 4.25.0 as above); 270.5 (2026-10-28: openvibe-shared 4.22.0, the navbar's search magnifier with / and Ctrl+K and the More menu; search.js and loading.js load only when used); 269.6 (2026-10-28: openvibe-shared 4.19.0, the account menu's gear and people-group icons inline; 268 on 2026-10-27: openvibe-shared 4.16.0, the folded balance chip, Across OpenVibe with site marks and the icon preload in the navbar); 265.3 (2026-10-27: openvibe-shared 4.15.0, the compact footer opens into the whole one, and the pulse line); 263.3 on 10-23 (openvibe-shared 4.9.0, status text twins in every theme); 262.5 on 10-17 (openvibe-shared 4.4.0, every site in the launcher)
cssRawKB: 270, // measured ~243 since the first-paint subset (style.core.css) replaced style.css (~717 before)
cssRawKB: 270, // measured ~243 since the first-paint subset (style.core.css) replaced style.css (~717 before)
cssBrotliKB: 55, // measured ~47.5 (was ~130)
cssBrotliKB: 55, // measured ~47.5 (was ~130)
new file mode 100644
new file mode 100644
@@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
+'use strict';
+// The chat name card never waits (11-03: Maticus's "Loading..." box sat for minutes while Chat restarted in a deploy
+// round). Clicking a name shows the basic card at once (the name, Message, Channel) and upgrades it when the profile
+// arrives, within eight seconds; the chat logs give up after ten with a way to try again.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+
+const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'chat.js'), 'utf8');
+assert.ok(!src.includes('<div class="ctx-loading">'), 'no "Loading..." box in the name card');
+assert.match(src, /menu\.innerHTML = basicContextCard\(username\);/, 'the basic card is shown at once');
+assert.match(src, /api\(`\/chat\/user\/\$\{encodeURIComponent\(username\)\}\/profile`, \{ signal: AbortSignal\.timeout\(8000\) \}\)/, 'the profile waits eight seconds at most');
+assert.match(src, /if \(!menu\.isConnected\) return;/, 'a dismissed card is left alone');
+
+const fn = /function basicContextCard\(username\) \{[\s\S]*?\n\}/.exec(src);
+assert.ok(fn, 'basicContextCard exists');
+const ctx = { esc: (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') };
+vm.createContext(ctx);
+vm.runInContext(`${fn[0]}\nthis.card = basicContextCard;`, ctx);
+const html = ctx.card('maticus');
+assert.match(html, /<span class="ctx-avatar-letter">M<\/span>/);
+assert.match(html, /ctxWhisper\(this\.dataset\.username\)[\s\S]*Message/);
+assert.match(html, /ctxViewChannel\(this\.dataset\.username\)[\s\S]*Channel/);
+assert.ok(!/spin|Loading/.test(html), 'nothing loading in it');
+assert.ok(!ctx.card('<b>x</b>').includes('<b>'), 'the name is escaped');
+
+const logs = /async function loadChatLogs\(\) \{[\s\S]*?\n\}/.exec(src)[0];
+assert.match(logs, /const signal = AbortSignal\.timeout\(10000\);/);
+assert.strictEqual((logs.match(/await api\(url, \{ signal \}\)/g) || []).length, 4, 'every logs request has the limit');
+assert.match(logs, /taking too long right now[\s\S]*onclick="loadChatLogs\(\)">Try again/);
+const app = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');
+assert.match(app, /if \(!init\.signal && \(method === 'GET' \|\| method === 'HEAD'\)[^\n]*AbortSignal\.timeout\(20000\)/, 'every read through api() gives up after 20 s unless the caller set a signal');
+console.log('chat card never waits: all checks passed');