openvibers/openvibe.mediahub Commits

menu card: build and serve it with openvibe-sdk/menu (#312)

OP OpenVibers committed 6 days ago · 55367e4b68 · 5 files, +21 −27

* menu card: build and serve it with openvibe-sdk/menu

Video's 'Trending' card and route were hand-rolled against
frame.menu-card@1. menuCard() builds the same rows (plain text, six items,
own-origin and openvibe.* links, optional 640x360 thumbnails) and
menuRoute() answers /.well-known/openvibe-menu.json with the same 60 s
browser cache and CORS *. A failed build now answers [] (never a 500), as
the SDK route specifies. Pin openvibe-sdk v0.97.0.

* Pin openvibe-sdk v0.97.1

The menu handler types now accept plain node:http messages under
exactOptionalPropertyTypes; no runtime change.

Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex c5ef977..a311481 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -9,7 +9,7 @@
"domain": "openvibe.download and openvibe.video (openvibe.pics is parked: it redirects to openvibe.network)",
"packages": [
"openvibe-contracts v0.320.0",
-- "openvibe-sdk v0.94.0",
++ "openvibe-sdk v0.97.1",
"openvibe-shared v4.133.0"
],
"summary": "OpenVibe.MediaHub: openvibe.download as a private drive. Account-only uploads stream into OpenVibe.Media (a plain form up to 256 MB, resumable parts beyond). Files are private. Share links expire in 1 hour to 7 days, need a signed-in downloader and can be limited to named accounts (matched by subject). Reports suspend a share for staff. Downloads are always sandboxed attachments. Per-person quotas, and account export and deletion (ADR-033). openvibe.video serves public watch pages and playlists for Live's recordings and clips (no uploads); openvibe.pics is parked and redirects to openvibe.network.",
@@@ -43,7 +43,7 @@
"deploy files"
],
"contracts": "openvibe-contracts v0.320.0",
-- "sdk": "openvibe-sdk v0.94.0",
++ "sdk": "openvibe-sdk v0.97.1",
"shared": "openvibe-shared v4.133.0",
"deployment": "openvibe-ovh: unit openvibe-media-hub.service on 127.0.0.1:4990 (git checkout /opt/openvibe.download, env /etc/openvibe/media-hub.env, database ov_media_hub), deployed with `sudo ovhost deploy media-hub`; public at https://openvibe.download since 2026-10-08, https://openvibe.video since 2026-10-12 and https://openvibe.pics since 2026-10-15.",
"updated": "2026-11-07"
ddiff --git a/package-lock.json b/package-lock.json
iindex d93c8f3..6719a42 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -16,7 +16,7 @@
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.320.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.97.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.133.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
@@@ -1626,9 +1626,9 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.94.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0",
-- "integrity": "sha512-p3buZSOSFV81OdZgs5GoSIfMCQNJjW23eAIXIt0RVFIOvh3TByNNEMmuCanxRybFIbzh+9IDMbBwHJrBLl3JeA==",
++ "version": "0.97.1",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.97.1",
++ "integrity": "sha512-zCRmu7NoOi7kI0v04N8uypCgsuEcf9kCzkw88r0+dj9PG3AA/z+Pp4BZaX6Z1JUE3sdrtc17D5LcD05IbWsw8w==",
"license": "MIT",
"bin": {
"openvibe-download": "bin/openvibe-download.js"
ddiff --git a/package.json b/package.json
iindex 8a2c596..e329f36 100644
---- a/package.json
++++ b/package.json
@@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.320.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.97.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.133.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
ddiff --git a/server/video/menu-card.js b/server/video/menu-card.js
iindex 3716ce9..e269ff9 100644
---- a/server/video/menu-card.js
++++ b/server/video/menu-card.js
@@@ -10,9 +10,11 @@
* carries the item's own thumbnail (https on openvibe.media, asked for at 640 px and declared 640×360) when the
* feed has one; no picture is ever a placeholder.
*
-- * The file is public and CORS *, cached for two minutes. A failed feed keeps the last good card; with none yet it
-- * answers 503, and Network keeps the site's previous card for its own 30-minute window.
++ * The file is public and CORS *, cached for two minutes. A failed feed keeps the last good card; with none yet
++ * openvibe-sdk/menu answers [] (never a 500), and Network keeps the site's previous card for its 30-minute window.
*/
++const { menuCard, menuRoute } = require('openvibe-sdk/menu');
++
const SITE = 'media-hub';
const CARD_ID = 'trending';
const BASE = 'https://openvibe.video';
@@@ -78,16 +80,17 @@ function cardFrom(items, at = Date.now()) {
if (image) item.image = image;
return item;
});
-- return {
++ return menuCard({
id: CARD_ID,
site: SITE,
title: 'Trending',
kind: 'rows',
++ origin: BASE,
items: rows,
more: { label: 'Watch more', url: `${BASE}/` },
-- updated_at: new Date(at).toISOString(),
-- ttl_s: CARD_TTL_S,
-- };
++ ttlSeconds: CARD_TTL_S,
++ updatedAt: at,
++ });
}
/**
@@@ -120,18 +123,8 @@ function createMenuCardHandler({ feed, now = Date.now }) {
return pending;
}
-- return async function menuCard(req, res) {
-- try {
-- const body = await card();
-- res.set('Cache-Control', 'public, max-age=60')
-- .set('Access-Control-Allow-Origin', '*')
-- .set('Cross-Origin-Resource-Policy', 'cross-origin')
-- .json([body]);
-- } catch {
-- res.status(503).set('Cache-Control', 'no-store').set('Access-Control-Allow-Origin', '*')
-- .json({ error: 'menu_unavailable' });
-- }
-- };
++ // openvibe-sdk/menu answers the file (CORS *, the public cache) and turns a failed build into [], never a 500.
++ return menuRoute(async () => [await card()], { maxAgeSeconds: 60 });
}
module.exports = { createMenuCardHandler, cardFrom, imageOf, plain, SITE, CARD_ID, CARD_TTL_S };
ddiff --git a/test/video-menu-card.test.js b/test/video-menu-card.test.js
iindex 623a361..64b64e2 100644
---- a/test/video-menu-card.test.js
++++ b/test/video-menu-card.test.js
@@@ -126,8 +126,9 @@ function fakeRes() {
const never = createMenuCardHandler({ feed: async () => { throw new Error('down'); }, now: () => t });
const none = fakeRes();
await never({}, none.res);
-- assert.strictEqual(none.status(), 503);
-- assert.strictEqual(none.body().error, 'menu_unavailable');
++ assert.strictEqual(none.status(), 200);
++ assert.deepStrictEqual(none.body(), [], 'a failed build answers [] (never a 500)');
++ assert.strictEqual(none.headers['Cache-Control'], 'no-store');
console.log('video menu card: rows, week views, merge, cache, CORS, one source down and no card');
})().catch((err) => { console.error(err); process.exit(1); });