openvibers/openvibe.community Commits

Pulse times in one spelling, so the Feed reads day by day in order (#320)

OP OpenVibers committed 4 days ago · 701ad9a050 · 4 files, +44 −4

Changes

ddiff --git a/migrations/0025_pulse_time_spelling.sql b/migrations/0025_pulse_time_spelling.sql
nnew file mode 100644
iindex 0000000..7345d0b
---- /dev/null
++++ b/migrations/0025_pulse_time_spelling.sql
@@@ -0,0 +1,8 @@
++-- phase: migrate
++-- Pulse times in one spelling (server/pulse/store.js pulseTime): "YYYY-MM-DD HH:MM:SS" in UTC. Items from OpenVibe.Events
++-- were stored as ISO ("2026-11-08T16:39:00.000Z"), which as text sorts above every same-day time written with a space,
++-- so the Feed put a day's quest badges and Community's own posts below that day's events (11-09). Rewrites the ISO ones;
++-- the times themselves do not change.
++UPDATE pulse_items
++ SET occurred_at = replace(substr(occurred_at, 1, 19), 'T', ' ')
++ WHERE occurred_at LIKE '____-__-__T__:__:__%';
ddiff --git a/server/pulse/store.js b/server/pulse/store.js
iindex 0018da8..9fb09f8 100644
---- a/server/pulse/store.js
++++ b/server/pulse/store.js
@@@ -14,8 +14,27 @@
*/
const LIVE_WINDOW_MS = 6 * 60 * 60 * 1000;
++/**
++ * One spelling for a Pulse time: "YYYY-MM-DD HH:MM:SS" in UTC (sqlTime's). The feed orders by this text, and an ISO
++ * time ("…T…Z") sorts above every same-day time written with a space, so a day's quest badges and Community's own posts
++ * fell below that day's events (11-09). Anything Date can read is accepted; nothing readable keeps what was given.
++ */
++function pulseTime(v) {
++ if (v == null || v === '') return v;
++ const s = String(v);
++ const t = Date.parse(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(s) ? `${s.replace(' ', 'T')}Z` : s);
++ return Number.isFinite(t) ? new Date(t).toISOString().replace('T', ' ').slice(0, 19) : s;
++}
++
++/** A Pulse time in milliseconds, the space spelling read as UTC (Date.parse would read it as local time). */
++function ms(v) {
++ const s = String(v == null ? '' : v);
++ return Date.parse(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(s) ? `${s.replace(' ', 'T')}Z` : s);
++}
++
/** A live.stream.started item: one row per broadcast, not per reconnect. → { item, created } */
async function upsertLiveItem(db, it) {
++ it = { ...it, occurred_at: pulseTime(it.occurred_at) };
const find = db.prepare('SELECT * FROM pulse_items WHERE source_service = ? AND source_type = ? AND source_id = ?');
return await db.tx(async () => {
const key = [it.source_service, it.source_type, String(it.source_id)];
@@@ -24,7 +43,7 @@ async function upsertLiveItem(db, it) {
await db.prepare('UPDATE pulse_items SET title = ?, url = ? WHERE source_service = ? AND source_type = ? AND source_id = ?').run(it.title, it.url, ...key);
return { item: await find.get(...key), created: false };
}
-- const at = Date.parse(String(it.occurred_at || ''));
++ const at = ms(it.occurred_at);
const actor = it.actor_subject || null;
if (Number.isFinite(at)) {
// The same person (or channel URL) and title within six hours of this start is the same
@@@ -39,7 +58,7 @@ async function upsertLiveItem(db, it) {
ORDER BY occurred_at DESC, id DESC`).all(it.title, it.url);
let prior = null, priorAt = -Infinity;
for (const c of candidates) {
-- const t = Date.parse(c.occurred_at);
++ const t = ms(c.occurred_at);
if (Number.isFinite(t) && t <= at && at - t <= LIVE_WINDOW_MS && t > priorAt) { prior = c; priorAt = t; }
}
if (prior) {
@@@ -55,6 +74,7 @@ async function upsertLiveItem(db, it) {
/** → { item, created } */
async function upsertItem(db, it) {
++ it = { ...it, occurred_at: pulseTime(it.occurred_at) };
const find = db.prepare('SELECT * FROM pulse_items WHERE source_service = ? AND source_type = ? AND source_id = ?');
return await db.tx(async () => {
const key = [it.source_service, it.source_type, String(it.source_id)];
@@@ -128,4 +148,4 @@ async function listItems(db, { origin = null, before = null, limit = 30, followe
return { rows, hasMore };
}
--module.exports = { upsertItem, upsertLiveItem, removeItem, listItems };
++module.exports = { pulseTime, upsertItem, upsertLiveItem, removeItem, listItems };
ddiff --git a/test/pulse-consumer.test.js b/test/pulse-consumer.test.js
iindex f18ba7a..29892b8 100644
---- a/test/pulse-consumer.test.js
++++ b/test/pulse-consumer.test.js
@@@ -103,7 +103,7 @@ let SECRET, db, app, vipDrops, USR, seq, evt;
let shown = await shows(REPEAT);
assert.strictEqual(shown.length, 1, 'one item, not one per session');
assert.strictEqual(shown[0].source_id, '9001', 'the item keeps its id (and source)');
-- assert.strictEqual(shown[0].occurred_at, new Date(T + 20 * 60_000).toISOString(), 'its time is the later start');
++ assert.strictEqual(shown[0].occurred_at, require('../server/pulse/store').pulseTime(new Date(T + 20 * 60_000).toISOString()), 'its time is the later start (stored in the one spelling)');
r = await post(liveAt(9003, T + 30 * 60_000, 'New show'));
assert.strictEqual(r.json.outcome, 'pulse:created');
ddiff --git a/test/pulse-sources.test.js b/test/pulse-sources.test.js
iindex 64ebf06..2cc03b7 100644
---- a/test/pulse-sources.test.js
++++ b/test/pulse-sources.test.js
@@@ -85,5 +85,17 @@ const day = (ms) => new Date(ms).toISOString().slice(0, 10);
assert.strictEqual(r.item.text || '', '');
});
++ await check('times are one spelling, so a day reads in order whatever wrote it (ISO from Events, a space locally)', async () => {
++ const store = require('../server/pulse/store');
++ const db = t.app.locals.db;
++ const base = { source_service: 'today', source_type: 'event', url: 'https://openvibe.today/e/x', origin: 'system' };
++ await store.upsertItem(db, { ...base, source_id: 'order_iso', title: 'Event: Morning (ISO)', occurred_at: '2030-01-02T08:00:00.000Z' });
++ await store.upsertItem(db, { ...base, source_id: 'order_space', title: 'Event: Evening (space)', occurred_at: '2030-01-02 20:00:00' });
++ const rows = await db.prepare("SELECT source_id, occurred_at FROM pulse_items WHERE source_id LIKE 'order_%' ORDER BY occurred_at DESC").all();
++ assert.deepStrictEqual(rows.map((r) => r.source_id), ['order_space', 'order_iso'], 'the evening one is newer');
++ assert.ok(rows.every((r) => /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(r.occurred_at)), rows.map((r) => r.occurred_at).join(', '));
++ assert.strictEqual(store.pulseTime('2030-01-02T08:00:00+02:00'), '2030-01-02 06:00:00', 'an offset is read, not dropped');
++ });
++
await done(t);
})();