openvibers/openvibe.network Commits

openvibe.link/machines is the Machines hub alone (#845)

OP OpenVibers committed 1 days ago · 2fe00ff831 · 5 files, +38 −3

It served the whole account page and scrolled to Machines: the account home, an
email card that cannot load on Link's origin, and the settings tabs sat above it,
with a screen of padding below so the scroll could land. The page is now marked
link-hub on Link's host: only the Machines section shows, at the top, under its
own h1 and a Link title. The empty state no longer repeats the description and its
icon keeps its size.

Changes

ddiff --git a/public/css/my.css b/public/css/my.css
iindex 93d9cee7..5c8b8769 100644
---- a/public/css/my.css
++++ b/public/css/my.css
@@@ -752,3 +752,15 @@ body.my-needs-visible #ovdk-pill { display:none !important; }
.mc-count { margin:0 0 8px; color:var(--text-muted); font-size:12px; }
#sec-machines { scroll-margin-top: 90px; }
body.my-machines-open { padding-bottom: max(112px, calc(100vh - 90px)); }
++
++/* openvibe.link/machines: server/link/routes.js serves this page with body.link-hub, and it is the Machines hub alone.
++ The account home, the email card and the settings tabs stay on openvibe.network/my; nothing scrolls, and the
++ section's heading is the page's h1. An anonymous session still sees its own notice in place of the machines. */
++.link-hub :is(#page-header, #needs-you, #receipts, #me-acts, #today, #setup-fold, #account-emails-card, #settings, .section-tabs) { display: none !important; }
++.link-hub .settings { display: block; }
++.link-hub .settings > .section:not(#sec-machines, #sec-anon.active) { display: none; }
++.link-hub #sec-machines { display: block; }
++.link-hub .settings:has(#sec-anon.active) #sec-machines { display: none; }
++body.link-hub.my-machines-open { padding-bottom: 0; }
++.link-hub .mc-title { margin: 0 0 6px; font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2rem); line-height: 1.15; }
++.mc-empty > i { flex: none; }
ddiff --git a/public/js/my/core.js b/public/js/my/core.js
iindex e12a83c3..705d566c 100644
---- a/public/js/my/core.js
++++ b/public/js/my/core.js
@@@ -191,7 +191,9 @@ function openInitialSection() {
}
if (document.getElementById('sec-' + routeSection)) {
showSection(routeSection);
-- if (routeSection === 'machines') document.getElementById('sec-machines')?.scrollIntoView?.({ block: 'start' });
++ // On openvibe.network the section sits below the account home, so the page scrolls to it; openvibe.link's hub
++ // (body.link-hub) is the section alone and stays at the top.
++ if (routeSection === 'machines' && !document.body?.classList?.contains('link-hub')) document.getElementById('sec-machines')?.scrollIntoView?.({ block: 'start' });
if (routeSection === 'machines' && hash === 'pair') setTimeout(() => mcPair(), 0);
}
}
ddiff --git a/public/js/my/machines.js b/public/js/my/machines.js
iindex 7c24e7ed..7d9c784c 100644
---- a/public/js/my/machines.js
++++ b/public/js/my/machines.js
@@@ -592,7 +592,7 @@ async function loadMachines() {
machines.sort((a, b) => (b.online === true) - (a.online === true));
if (count) count.textContent = `${machines.length} paired · ${machines.filter((m) => m.online).length} online now`;
if (!machines.length) {
-- list.replaceChildren(mcEl('div', 'mc-empty', [mcIcon('fa-laptop'), mcEl('p', '', [mcEl('b', '', ['No machines yet.']), ' Pair your computer and it can run your Codes jobs, host your desks and serve your own models, under your account.'])]));
++ list.replaceChildren(mcEl('div', 'mc-empty', [mcIcon('fa-laptop'), mcEl('p', '', [mcEl('b', '', ['No machines yet.']), ' Pair a computer: you get one command to run on it, and it shows up here.'])]));
return;
}
// Pools are an extra: when they cannot be read, the machines still show.
ddiff --git a/server/link/routes.js b/server/link/routes.js
iindex e9d0ab29..740e0b07 100644
---- a/server/link/routes.js
++++ b/server/link/routes.js
@@@ -27,6 +27,22 @@ const HUB_PATH = '/machines';
const HUB_PAGE = pathLib.join(__dirname, '..', '..', 'public', 'my.html');
const BOT_RE = /bot|crawl|spider|slurp|headless/i;
++// The hub is the account page reduced to its Machines section: the body is marked link-hub (my.css hides the account
++// home, the email card and the settings tabs), the page is titled for Link, and the section's heading is the page's
++// one h1. Read once per file change; a heading the page no longer has is left alone rather than guessed at.
++let hubCache = { mtimeMs: -1, html: '' };
++function hubHtml() {
++ const { mtimeMs } = fs.statSync(HUB_PAGE);
++ if (hubCache.mtimeMs === mtimeMs) return hubCache.html;
++ const html = fs.readFileSync(HUB_PAGE, 'utf8')
++ .replace('<body>', () => '<body class="link-hub">')
++ .replace(/<title>[^<]*<\/title>/, () => '<title>Your machines - OpenVibe Link</title>')
++ .replace('<h1 id="user-name">Loading...</h1>', () => '<p class="me-name" id="user-name">Loading...</p>')
++ .replace(/<h3>(<span class="icon"><i class="fa-solid fa-laptop"><\/i><\/span> Your machines)<\/h3>/, (m, inner) => `<h1 class="mc-title">${inner}</h1>`);
++ hubCache = { mtimeMs, html };
++ return html;
++}
++
/** The request's host, lowercased, without the port. */
function hostOf(req) {
return String((req.headers && req.headers.host) || '').split(':')[0].toLowerCase();
@@@ -144,7 +160,7 @@ function createLinkRoutes({ releaseId, releaseApi = release, db = null, config =
// Signed in, a crawler, or back from a silent sign-in that found nobody: the hub itself.
if (sessionUser || BOT_RE.test(userAgent) || params.get('sso') === 'none') {
noStore();
-- return res.sendFile(HUB_PAGE);
++ return res.type('html').send(hubHtml());
}
// Nobody home: the silent sign-in (prompt=none) signs a Network-signed-in person in with no click;
// the login page only shows when they are truly signed out. Pair intent survives in ?pair=1.
ddiff --git a/test/link-machines-signin.test.js b/test/link-machines-signin.test.js
iindex 8c8bc152..0d0f884a 100644
---- a/test/link-machines-signin.test.js
++++ b/test/link-machines-signin.test.js
@@@ -58,6 +58,11 @@ const tokenFor = (id, name) => jwt.sign({ sub: id, id, username: name }, keys.pr
r = await get(`/machines?ov_handoff=${encodeURIComponent(spare)}`, `ov_token=${session}`);
assert.strictEqual(r.status, 200, 'a session serves the hub');
assert.ok(r.body.includes('sec-machines'), 'the hub page');
++ // The hub is the Machines section alone: marked for my.css, titled for Link, with one h1 (the section's).
++ assert.ok(r.body.includes('<body class="link-hub">'), 'the body is marked link-hub');
++ assert.ok(r.body.includes('<title>Your machines - OpenVibe Link</title>'), 'titled for Link');
++ assert.strictEqual((r.body.match(/<h1[\s>]/g) || []).length, 1, 'one h1');
++ assert.ok(/<h1 class="mc-title">[^]*?Your machines<\/h1>/.test(r.body), 'and it is the machines heading');
const spareRow = await db.prepare('SELECT used FROM handoff_tickets WHERE ticket_hash = ?').get(hashTicket(spare));
assert.strictEqual(spareRow.used, 0, 'no silent account switch: the ticket stays unused');