openvibers/openvibe.network Commits
openvibe.link/machines is the Machines hub alone (#845)
Changes
@@@ -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; }@@@ -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);}}@@@ -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.@@@ -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.@@@ -58,6 +58,11 @@ const tokenFor = (id, name) => jwt.sign({ sub: id, id, username: name }, keys.prr = 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');
@@ -752,3 +752,15 @@ body.my-needs-visible #ovdk-pill { display:none !important; }
@@ -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; }
.mc-count { margin:0 0 8px; color:var(--text-muted); font-size:12px; }
#sec-machines { scroll-margin-top: 90px; }
#sec-machines { scroll-margin-top: 90px; }
body.my-machines-open { padding-bottom: max(112px, calc(100vh - 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; }
@@ -191,7 +191,9 @@ function openInitialSection() {
@@ -191,7 +191,9 @@ function openInitialSection() {
}
}
if (document.getElementById('sec-' + routeSection)) {
if (document.getElementById('sec-' + routeSection)) {
showSection(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);
if (routeSection === 'machines' && hash === 'pair') setTimeout(() => mcPair(), 0);
}
}
}
}
@@ -592,7 +592,7 @@ async function loadMachines() {
@@ -592,7 +592,7 @@ async function loadMachines() {
machines.sort((a, b) => (b.online === true) - (a.online === true));
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 (count) count.textContent = `${machines.length} paired · ${machines.filter((m) => m.online).length} online now`;
if (!machines.length) {
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;
return;
}
}
// Pools are an extra: when they cannot be read, the machines still show.
// Pools are an extra: when they cannot be read, the machines still show.
@@ -27,6 +27,22 @@ const HUB_PATH = '/machines';
@@ -27,6 +27,22 @@ const HUB_PATH = '/machines';
const HUB_PAGE = pathLib.join(__dirname, '..', '..', 'public', 'my.html');
const HUB_PAGE = pathLib.join(__dirname, '..', '..', 'public', 'my.html');
const BOT_RE = /bot|crawl|spider|slurp|headless/i;
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. */
/** The request's host, lowercased, without the port. */
function hostOf(req) {
function hostOf(req) {
return String((req.headers && req.headers.host) || '').split(':')[0].toLowerCase();
return String((req.headers && req.headers.host) || '').split(':')[0].toLowerCase();
@@ -144,7 +160,7 @@ function createLinkRoutes({ releaseId, releaseApi = release, db = null, config =
@@ -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.
// 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') {
if (sessionUser || BOT_RE.test(userAgent) || params.get('sso') === 'none') {
noStore();
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;
// 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.
// the login page only shows when they are truly signed out. Pair intent survives in ?pair=1.
@@ -58,6 +58,11 @@ const tokenFor = (id, name) => jwt.sign({ sub: id, id, username: name }, keys.pr
@@ -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}`);
r = await get(`/machines?ov_handoff=${encodeURIComponent(spare)}`, `ov_token=${session}`);
assert.strictEqual(r.status, 200, 'a session serves the hub');
assert.strictEqual(r.status, 200, 'a session serves the hub');
assert.ok(r.body.includes('sec-machines'), 'the hub page');
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));
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');
assert.strictEqual(spareRow.used, 0, 'no silent account switch: the ticket stays unused');