openvibers/openvibe.today Commits

Today's menu card shows the week's holidays when nothing else is on (#117)

OP OpenVibers committed 7 days ago · 048965b987 · 3 files, +35 −11

This week's only public rows are holidays, which the card left out, so it was empty and the menu dropped Today. Now
a quiet week shows its holidays (a real event always comes first), and a holiday reads 'Wed 11 Nov · Holiday', not a
00:00 start time. The loader moves into menu-card.js (upcomingRows) so it is tested.

Changes

ddiff --git a/server/app.js b/server/app.js
iindex f834080..db044fb 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -182,13 +182,9 @@ async function createApp(opts = {}) {
// schedule already shows (events.listPublic) is read, so private and followers-only events never reach it; the
// loader reads the window and the card keeps the upcoming events, in start order.
{
-- const { createMenuCard, DAY, WINDOW_DAYS } = require('./menu-card');
++ const { createMenuCard, upcomingRows } = require('./menu-card');
const menuCard = createMenuCard({
-- load: async (at) => await events.listPublic({
-- from: new Date(at).toISOString(),
-- to: new Date(at + WINDOW_DAYS * DAY).toISOString(),
-- limit: 50, exceptKinds: ['holiday'],
-- }),
++ load: (at) => upcomingRows(events, at),
});
app.get('/.well-known/openvibe-menu.json', menuCard.handler);
}
ddiff --git a/server/menu-card.js b/server/menu-card.js
iindex 2a6df09..ad54512 100644
---- a/server/menu-card.js
++++ b/server/menu-card.js
@@@ -7,7 +7,8 @@
// with no items, and Network skips it: the schedule is honest rather than filled with stale events.
//
// Only public, non-cancelled events the public schedule already shows (events.listPublic) are read. Personal
--// agendas, reminders, followers-only events and holidays never reach the card. The file is CORS *, cached for
++// agendas, reminders and followers-only events never reach the card, and holidays only when no other public event
++// is on this week (server/app.js). The file is CORS *, cached for
// five minutes; a failed database keeps the last good card.
const BASE = 'https://openvibe.today';
const CARD_ID = 'today';
@@@ -88,8 +89,10 @@ function cardFrom(events, at = Date.now()) {
const title = plain(e.title, 80);
if (!title) continue;
if (firstDay === null) firstDay = dayKey(e.starts_at);
-- const when = [dayLabel(e.starts_at, at), timeLabel(e.starts_at, e.tz)].filter(Boolean).join(' · ');
-- const meta = [when, plain(placeOf(e), 60)].filter(Boolean).join(' · ');
++ // A holiday is a whole day: its day and the word, not a 00:00 start time or a place.
++ const meta = e.kind === 'holiday'
++ ? [dayLabel(e.starts_at, at), 'Holiday'].filter(Boolean).join(' · ')
++ : [[dayLabel(e.starts_at, at), timeLabel(e.starts_at, e.tz)].filter(Boolean).join(' · '), plain(placeOf(e), 60)].filter(Boolean).join(' · ');
items.push({ title, ...(meta ? { meta: plain(meta, 120) } : {}), url: `${BASE}/e/${encodeURIComponent(String(e.id))}` });
if (items.length >= LIMIT) break;
}
@@@ -140,4 +143,14 @@ function createMenuCard({ load, now = Date.now } = {}) {
return { card, handler, cardFrom, cacheMs: CACHE_MS };
}
--module.exports = { createMenuCard, cardFrom, dayLabel, timeLabel, placeOf, dayKey, startOfDay, plain, BASE, CARD_ID, SITE, CARD_TTL_S, DAY, WINDOW_DAYS, LIMIT };
++/**
++ * The rows the card reads: this week's public events, and holidays only when no other public event is on (11-07:
++ * the only rows were holidays, so the card was empty and the menu left Today out). A real event always comes first.
++ */
++async function upcomingRows(events, at = Date.now()) {
++ const window = { from: new Date(at).toISOString(), to: new Date(at + WINDOW_DAYS * DAY).toISOString(), limit: 50 };
++ const rows = await events.listPublic({ ...window, exceptKinds: ['holiday'] });
++ return rows.length ? rows : await events.listPublic({ ...window, kinds: ['holiday'] });
++}
++
++module.exports = { upcomingRows, createMenuCard, cardFrom, dayLabel, timeLabel, placeOf, dayKey, startOfDay, plain, BASE, CARD_ID, SITE, CARD_TTL_S, DAY, WINDOW_DAYS, LIMIT };
ddiff --git a/test/menu-card.test.js b/test/menu-card.test.js
iindex 754fac3..ee0ccb3 100644
---- a/test/menu-card.test.js
++++ b/test/menu-card.test.js
@@@ -6,7 +6,7 @@
// empty: the schedule is honest.
const assert = require('assert');
const contracts = require('openvibe-contracts');
--const { createMenuCard, cardFrom, dayLabel, timeLabel, placeOf, CARD_TTL_S } = require('../server/menu-card');
++const { createMenuCard, cardFrom, dayLabel, timeLabel, placeOf, upcomingRows, CARD_TTL_S } = require('../server/menu-card');
const NOW = Date.parse('2026-11-07T12:00:00Z');
const event = (over = {}) => ({
@@@ -28,6 +28,21 @@ const card = cardFrom([
event({ id: 'blank', title: ' ' }),
], NOW);
const checked = contracts.validate('frame.menu-cards@1', [card]);
++
++// ── Holidays only when nothing else is on this week (11-07) ──
++(async () => {
++ const asked = [];
++ const store = (rows) => ({ listPublic: async (q) => { asked.push(q); return rows(q); } });
++ const holiday = event({ id: 'h1', title: 'Veterans Day', kind: 'holiday', starts_at: '2026-11-11T00:00:00Z' });
++ const quiet = await upcomingRows(store((q) => (q.kinds ? [holiday] : [])), NOW);
++ assert.deepStrictEqual(quiet.map((r) => r.id), ['h1'], 'a quiet week shows its holidays');
++ assert.deepStrictEqual(cardFrom(quiet, NOW).items[0].meta, 'Wed 11 Nov · Holiday', 'a holiday reads as a day, not 00:00');
++ assert.deepStrictEqual(asked.map((q) => [q.exceptKinds || null, q.kinds || null]), [[['holiday'], null], [null, ['holiday']]]);
++ asked.length = 0;
++ const busy = await upcomingRows(store((q) => (q.kinds ? [holiday] : [event()])), NOW);
++ assert.deepStrictEqual(busy.map((r) => r.id), ['ev1'], 'a real event comes first, and holidays are not asked for');
++ assert.strictEqual(asked.length, 1);
++})().catch((e) => { console.error(e); process.exit(1); });
assert.ok(checked.valid, JSON.stringify(checked.errors));
assert.deepStrictEqual(Object.keys(card).sort(), ['id', 'items', 'kind', 'more', 'site', 'title', 'ttl_s', 'updated_at']);
assert.strictEqual(card.id, 'today');