openvibers/openvibe.network Commits

Your recent tools on my.openvibe.network (the tools.usage module OpenVibe.Tools keeps, named from the public Tools registry, built with DOM nodes); Tools may index its tools in Search (search.document.write on openvibe.search)

OP OpenVibers committed 24 Sept 2026 · 1de2e22920 · 3 files, +43 −0


Changes

ddiff --git a/public/my.html b/public/my.html
iindex 702c9838..0e73ba8e 100644
---- a/public/my.html
++++ b/public/my.html
@@@ -233,6 +233,11 @@ a.linked-item:hover{background:var(--bg-hover,var(--bg-card));transform:translat
.quick-link:nth-child(6) { animation-delay: 0.35s; }
.quick-link:hover { border-color: var(--accent); color: var(--accent-light); background: rgba(139,92,246,0.08); }
@media(max-width:600px){ .showcase-grid { grid-template-columns: repeat(2, 1fr); } .quick-access { grid-template-columns: repeat(3, 1fr); } }
++.recent-tools { margin-top: 14px; }
++.recent-tools h3 { font-size: 12px; font-weight: 700; color: var(--text-secondary); margin: 0 0 8px; display: flex; gap: 6px; align-items: center; }
++.recent-tools-list { display: flex; flex-wrap: wrap; gap: 6px; }
++.recent-tool { padding: 6px 10px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border); color: var(--text-secondary); font-size: 12px; font-weight: 600; text-decoration: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++.recent-tool:hover { border-color: var(--accent); color: var(--accent-light); }
</style>
</head>
<body>
@@@ -257,6 +262,10 @@ a.linked-item:hover{background:var(--bg-hover,var(--bg-card));transform:translat
</div>
<div class="showcase-grid" id="my-showcase-grid"></div>
<div class="quick-access" id="my-quick-access"></div>
++ <div class="recent-tools" id="my-recent-tools" hidden>
++ <h3><i class="fa-solid fa-screwdriver-wrench"></i> Your recent tools</h3>
++ <div class="recent-tools-list" id="my-recent-tools-list"></div>
++ </div>
</div>
<div class="section-tabs">
@@@ -644,6 +653,34 @@ function buildDashboardShowcase() {
quickAccess.innerHTML = quickLinks.map(l => `<a class="quick-link" onclick="${l.onclick}"><i class="fa-solid ${l.icon}"></i>${l.label}</a>`).join('');
}
++// Recently used tools: the tools.usage module OpenVibe.Tools keeps for this person, named from the
++// public Tools registry (a tool that is gone from it is skipped).
++async function loadRecentTools() {
++ const box = document.getElementById('my-recent-tools');
++ const list = document.getElementById('my-recent-tools-list');
++ if (!box || !list) return;
++ try {
++ const rec = await apiFetch('/api/modules/tools.usage').catch(() => null);
++ const recent = rec && rec.data && Array.isArray(rec.data.recent) ? rec.data.recent : [];
++ if (!recent.length) return;
++ const reg = await fetchWithTimeout('https://openvibe.tools/api/v1/tools', {}, 8000).then(r => (r.ok ? r.json() : { tools: [] }));
++ const byId = new Map((reg.tools || []).map(t => [t.id, t]));
++ list.replaceChildren();
++ for (const e of recent) {
++ const t = byId.get(e.tool);
++ if (!t || list.childElementCount >= 12) continue;
++ const a = document.createElement('a');
++ a.className = 'recent-tool';
++ a.href = t.hosts && t.hosts[0] ? 'https://' + t.hosts[0] : 'https://openvibe.tools/tool/' + encodeURIComponent(t.id);
++ a.target = '_blank'; a.rel = 'noopener';
++ a.textContent = t.name;
++ if (e.at) a.title = 'Last used ' + new Date(e.at).toLocaleString();
++ list.appendChild(a);
++ }
++ box.hidden = list.childElementCount === 0;
++ } catch { /* optional */ }
++}
++
// Section switching
function showSection(id, btn) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
@@@ -930,6 +967,7 @@ async function loadUser() {
loadSessions();
loadAnonInfo();
loadEmailStatus();
++ loadRecentTools();
// Init navbar
OpenVibeNavbar.init({ service: 'network', token: authToken, user: currentUser });
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex ed7c64e3..e2bfd53d 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -158,6 +158,8 @@ const DEFAULT_GRANTS = [
// Tools platform S9: products call the Tools run API (the kiosk's page titles, Chat's audio conversion,
// Community's save-as-paste) with their own token, on the service tier instead of the anonymous one.
...['live', 'chat', 'community'].flatMap(c => ['tools.tool.run', 'tools.job.read'].map(cap => [c, cap, 'openvibe.tools', []])),
++ // Tools indexes its own tool pages in Search through the owner API (owner tools only).
++ ['tools', 'search.document.write', 'openvibe.search', []],
];
// Grants withdrawn by decision; applied at every boot so an old default can't come back.
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex f4af78a0..48105507 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -139,6 +139,9 @@ const server = http.createServer(app);
assert.deepStrictEqual(cm.body.scope.split(' '), ['community.comment.moderate', 'community.comment.write', 'community.paste.create', 'community.paste.moderate', 'community.paste.write', 'community.pulse.write']);
const tl = await token({ client_id: 'live', client_secret: 'live-secret', audience: 'openvibe.tools' });
assert.deepStrictEqual(tl.body.scope.split(' '), ['tools.job.read', 'tools.tool.run'], 'Live runs Tools on the service tier, never probes');
++ db.prepare("UPDATE oauth_clients SET client_secret = 'tools-secret' WHERE client_id = 'tools'").run();
++ const ts = await token({ client_id: 'tools', client_secret: 'tools-secret', audience: 'openvibe.search' });
++ assert.strictEqual(ts.body.scope, 'search.document.write', 'Tools indexes its tools in Search');
// Go-live fan-out accepts Live's token (network.notifications.push); a narrower token is refused.
r = await post('/internal/events/stream-live', {}, { authorization: `Bearer ${full}` });