openvibers/openvibe.chat Commits

Deploy notices: a new deploy folds into the last card only if nobody has spoken in ANY room since (the global feed shows stream messages too, so cards kept folding past them and their time ranges ran past the messages below), and only within 3 hours

OP OpenVibers committed 23 Sept 2026 · 789768806b · 2 files, +18 −4


Changes

ddiff --git a/server/chat/deploy-notice.js b/server/chat/deploy-notice.js
iindex 392a979..08d02d6 100644
---- a/server/chat/deploy-notice.js
++++ b/server/chat/deploy-notice.js
@@@ -15,7 +15,8 @@
// • Late joiners of this announcement get it once (replayTo, from the join handler).
// ═══════════════════════════════════════════════════════════════
--const FOLD_WINDOW_MS = 12 * 60 * 60 * 1000;
++// A card covers at most 3 hours of deploys, so its time range stays readable.
++const FOLD_WINDOW_MS = 3 * 60 * 60 * 1000;
const MAX_COMMITS = 40;
const ATTEMPTS_MS = [5000, 20000, 45000]; // clients reconnect with backoff after a restart
@@@ -30,10 +31,15 @@ const parseLog = (raw) => raw.trim().split('\n').filter(Boolean).map((line) => {
const plainText = (meta) => `🚀 ${meta.commits.length} update${meta.commits.length === 1 ? '' : 's'} shipped: ${meta.commits.slice(0, 3).map(c => c.subject).join(' · ')}${meta.commits.length > 3 ? ` · and ${meta.commits.length - 3} more` : ''}`;
--/** Insert a notice, or fold into the newest row when that row is itself a recent deploy notice. */
++/**
++ * Insert a notice, or fold into the newest row when that row is itself a recent deploy notice.
++ * "Newest" is across EVERY room: the global feed shows stream and channel messages too, so a card
++ * that kept folding while people chatted in a stream ended up with a time range running past the
++ * messages under it. Anyone speaking anywhere since the last card starts a new card.
++ */
function persist(db, commits) {
const nowIso = new Date().toISOString();
-- const newest = db.get('SELECT id, message_type, metadata FROM chat_messages WHERE is_global = 1 AND is_deleted = 0 ORDER BY id DESC LIMIT 1');
++ const newest = db.get('SELECT id, message_type, metadata FROM chat_messages WHERE is_deleted = 0 ORDER BY id DESC LIMIT 1');
let prev = null;
if (newest && newest.message_type === 'system' && newest.metadata) {
try { const m = JSON.parse(newest.metadata); if (m && m.kind === 'deploy' && Date.now() - Date.parse(m.first_at) < FOLD_WINDOW_MS) prev = m; } catch { /* not ours */ }
ddiff --git a/test/bridge.test.js b/test/bridge.test.js
iindex 52d09b4..fcaf4ab 100644
---- a/test/bridge.test.js
++++ b/test/bridge.test.js
@@@ -174,11 +174,19 @@ t('deploy notices: stored as one rolling global row, shown to everyone, folded o
r = await calls([{ op: 'deployNotice', args: [[commit(2)]] }]);
assert.strictEqual(r.body.results[0].result.id, id1, 'folded into the same row');
assert.strictEqual(JSON.parse(h.db.getChatMessageById(id1).metadata).deploys, 2);
++ // Someone speaks in a stream room (the global feed shows it): the next deploy is a new card,
++ // so no card's time range runs past a message under it.
++ h.db.saveChatMessage({ stream_id: streamId, user_id: viewer.id, username: viewer.username, message: 'hello brother', message_type: 'chat', is_global: false });
++ r = await calls([{ op: 'deployNotice', args: [[commit(3)]] }]);
++ const id3 = r.body.results[0].result.id;
++ assert.notStrictEqual(id3, id1, 'a message since the last card starts a new card');
++ assert.strictEqual(JSON.parse(h.db.getChatMessageById(id3).metadata).deploys, 1);
++ assert.strictEqual(JSON.parse(h.db.getChatMessageById(id1).metadata).deploys, 2, 'the old card is left as it was');
// A late joiner gets it once.
const late = await h.ws({ ip: '198.51.100.52' });
late.sendJson({ type: 'join' });
const upd = await late.next((m) => m.type === 'update', 4000);
-- assert.strictEqual(upd.id, id1);
++ assert.strictEqual(upd.id, id3);
late.close();
});