openvibers/openvibe.community Commits
Pulse times in one spelling, so the Feed reads day by day in order (#320)
Changes
nnew file mode 100644@@@ -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__:__:__%';@@@ -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, followereturn { rows, hasMore };}--module.exports = { upsertItem, upsertLiveItem, removeItem, listItems };++module.exports = { pulseTime, upsertItem, upsertLiveItem, removeItem, listItems };@@@ -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');@@@ -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);})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -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__:__:__%';
@@ -14,8 +14,27 @@
@@ -14,8 +14,27 @@
*/
*/
const LIVE_WINDOW_MS = 6 * 60 * 60 * 1000;
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 } */
/** A live.stream.started item: one row per broadcast, not per reconnect. → { item, created } */
async function upsertLiveItem(db, it) {
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 = ?');
const find = db.prepare('SELECT * FROM pulse_items WHERE source_service = ? AND source_type = ? AND source_id = ?');
return await db.tx(async () => {
return await db.tx(async () => {
const key = [it.source_service, it.source_type, String(it.source_id)];
const key = [it.source_service, it.source_type, String(it.source_id)];
@@ -24,7 +43,7 @@ async function upsertLiveItem(db, it) {
@@ -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);
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 };
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;
const actor = it.actor_subject || null;
if (Number.isFinite(at)) {
if (Number.isFinite(at)) {
// The same person (or channel URL) and title within six hours of this start is the same
// 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) {
@@ -39,7 +58,7 @@ async function upsertLiveItem(db, it) {
ORDER BY occurred_at DESC, id DESC`).all(it.title, it.url);
ORDER BY occurred_at DESC, id DESC`).all(it.title, it.url);
let prior = null, priorAt = -Infinity;
let prior = null, priorAt = -Infinity;
for (const c of candidates) {
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 (Number.isFinite(t) && t <= at && at - t <= LIVE_WINDOW_MS && t > priorAt) { prior = c; priorAt = t; }
}
}
if (prior) {
if (prior) {
@@ -55,6 +74,7 @@ async function upsertLiveItem(db, it) {
@@ -55,6 +74,7 @@ async function upsertLiveItem(db, it) {
/** → { item, created } */
/** → { item, created } */
async function upsertItem(db, it) {
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 = ?');
const find = db.prepare('SELECT * FROM pulse_items WHERE source_service = ? AND source_type = ? AND source_id = ?');
return await db.tx(async () => {
return await db.tx(async () => {
const key = [it.source_service, it.source_type, String(it.source_id)];
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
@@ -128,4 +148,4 @@ async function listItems(db, { origin = null, before = null, limit = 30, followe
return { rows, hasMore };
return { rows, hasMore };
}
}
-module.exports = { upsertItem, upsertLiveItem, removeItem, listItems };
+module.exports = { pulseTime, upsertItem, upsertLiveItem, removeItem, listItems };
@@ -103,7 +103,7 @@ let SECRET, db, app, vipDrops, USR, seq, evt;
@@ -103,7 +103,7 @@ let SECRET, db, app, vipDrops, USR, seq, evt;
let shown = await shows(REPEAT);
let shown = await shows(REPEAT);
assert.strictEqual(shown.length, 1, 'one item, not one per session');
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].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'));
r = await post(liveAt(9003, T + 30 * 60_000, 'New show'));
assert.strictEqual(r.json.outcome, 'pulse:created');
assert.strictEqual(r.json.outcome, 'pulse:created');
@@ -85,5 +85,17 @@ const day = (ms) => new Date(ms).toISOString().slice(0, 10);
@@ -85,5 +85,17 @@ const day = (ms) => new Date(ms).toISOString().slice(0, 10);
assert.strictEqual(r.item.text || '', '');
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);
await done(t);
})();
})();