openvibers/openvibe.live Commits

Load pastes.js and dashboard.js on demand; one closeModal and one escapeHtml for the site

OP OpenVibers committed 16 Sept 2026 · 5afa9aa71f · 5 files, +51 −26

Up-front JavaScript: 40 files / 511.0KB gzipped -> 38 files / 475.9KB (35KB off every page load;
with staff-console.js removed earlier today, 44KB). Both files were parsed on every page, and on the
home page almost none of either runs.

Two shared helpers had to move first, and both were hiding something:
- closeModal was defined in app.js and again in dashboard.js. The later script wins, so every modal
  on the site was already closing through the dashboard's copy (which also hides the clone-preset
  modal and tolerates a missing overlay). That fuller version is now the only one, in app.js.
- escapeHtml lived in pastes.js but settings.js relies on it. It moves to app.js, quote-safe.

Routes wait for their bundle: /pastes (including ?edit=), /paste and /p/:slug viewers, and
/dashboard, whose bundle includes pastes.js because it renders paste cards synchronously. Four
handlers are reachable from pages that do not load these files — Screenshot in the account menu,
the stream-key button on the broadcast desk, and the two control-profile modal buttons — so each
gets a stand-in that loads the bundle and calls the real function, which replaces the stand-in when
the script runs; a stand-in whose real function never arrives gives up rather than recursing.

Verified in a browser: neither file is requested at load; /pastes renders once its bundle arrives;
calling Screenshot from a fresh page loads pastes.js and opens the screenshot modal; a direct
/p/:slug load reaches the viewer; /dashboard loads both files; closeModal still closes the overlay;
no console errors. All test suites pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/public/index.html b/public/index.html
iindex 2da47cf8..3dd8c6d9 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -5114,7 +5114,10 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
(function () {
var BUNDLES = {
broadcast: ['/js/broadcast-workspace.js?v=16', '/js/broadcast.js?v=31'],
-- arena: ['/js/arena.js?v=18']
++ arena: ['/js/arena.js?v=18'],
++ pastes: ['/js/pastes.js?v=17'],
++ // The dashboard renders paste cards with pastes.js helpers, synchronously.
++ dashboard: ['/js/pastes.js?v=17', '/js/dashboard.js?v=17']
};
var started = Object.create(null);
function one(src) {
@@@ -5137,9 +5140,29 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
window.ovPrefetchRoutes = function () {
for (var k in BUNDLES) window.ovLoadRoute(k);
};
++ // A few handlers in those files are reachable from pages that do not load them: "Screenshot" in
++ // the account menu, the stream-key button on the broadcast desk, the control-profile modal
++ // buttons. Each gets a stand-in that loads its bundle and then calls the real function, which
++ // replaces the stand-in when the script runs (a top-level function declaration overwrites the
++ // window property). If the real function never arrives, the stand-in gives up instead of
++ // calling itself forever.
++ function stub(name, bundle) {
++ var fake = function () {
++ var args = arguments;
++ return window.ovLoadRoute(bundle).then(function () {
++ if (window[name] !== fake && typeof window[name] === 'function') return window[name].apply(window, args);
++ console.warn('[routes] ' + name + ' is unavailable');
++ });
++ };
++ window[name] = fake;
++ }
++ stub('captureScreenshot', 'pastes');
++ stub('regenerateDashStreamKey', 'dashboard');
++ stub('doCreateConfig', 'dashboard');
++ stub('doAddConfigButton', 'dashboard');
})();
</script>
--<script src="/js/app.js?v=183" defer></script>
++<script src="/js/app.js?v=184" defer></script>
<script src="/js/stream-player.js?v=22" defer></script>
<script src="/js/stream-pip.js?v=1" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
@@@ -5150,7 +5173,6 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/channel-uploads.js?v=12" defer></script>
<script src="/js/controls.js?v=8" defer></script>
<script src="/js/onvif-dashboard.js?v=6" defer></script>
--<script src="/js/dashboard.js?v=16" defer></script>
<!-- (js/admin.js removed — 130KB of unreachable code; /admin redirects off-site.) -->
<!-- ── Floating Global Chat Widget (visible on non-chat pages) ── -->
<div id="floating-chat-widget" class="floating-chat-widget" style="display:none">
@@@ -5211,7 +5233,6 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/recap.js?v=1" defer></script>
<script src="/js/context-cards.js?v=4" defer></script>
<script src="/js/powerchat.js?v=10" defer></script>
--<script src="/js/pastes.js?v=16" defer></script>
<script src="/js/draggable-fab.js?v=2" defer></script>
<script src="/js/messenger.js?v=4" defer></script>
<script src="/js/ov-mark.js?v=2" defer></script>
ddiff --git a/public/js/app.js b/public/js/app.js
iindex 0ecc3d48..891c7124 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -727,10 +727,24 @@ function showModal(id) {
if (id === 'donate' && typeof _initDonateModal === 'function') _initDonateModal();
}
++// Defined here once. dashboard.js used to carry a second, fuller copy that silently replaced this one
++// because it loaded later — so the site's modals were closing through the dashboard's version.
function closeModal() {
-- document.getElementById('modal-overlay').classList.remove('show');
++ const overlay = document.getElementById('modal-overlay');
++ if (overlay) overlay.classList.remove('show');
++ // The clone-preset modal toggles style.display rather than the .show class.
++ const cloneModal = document.getElementById('clone-preset-config-modal');
++ if (cloneModal) cloneModal.style.display = 'none';
++}
++
++// Escapes quotes as well as < > &. The previous version used the textContent/innerHTML trick,
++// which leaves " and ' untouched — safe in text, but these helpers are also interpolated into
++// attribute values, where an unescaped quote ends the attribute and starts a new one.
++function escapeHtml(str) {
++ return String(str == null ? '' : str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
++
/* ── Auth ──────────────────────────────────────────────────────── */
// Local login/register removed — all auth goes through OpenVibe SSO
function doLogin() { window.location.href = '/api/auth/sso/login'; }
@@@ -1213,7 +1227,7 @@ function routeFromURL() {
loadClipPlayer(segments[1]);
} else if (segments[0] === 'dashboard') {
showPage('dashboard');
-- loadDashboard();
++ ovLoadRoute('dashboard').then(() => { if (typeof loadDashboard === 'function') loadDashboard(); });
} else if (segments[0] === 'settings') {
// /settings was merged into the dashboard — redirect there.
navigate('/dashboard', true);
@@@ -1240,12 +1254,14 @@ function routeFromURL() {
return;
} else if (segments[0] === 'pastes') {
showPage('pastes');
-- loadPastesPage();
-- // Handle ?edit=slug
const editSlug = new URLSearchParams(window.location.search).get('edit');
++ ovLoadRoute('pastes').then(() => {
++ if (typeof loadPastesPage === 'function') loadPastesPage();
++ });
++ // Handle ?edit=slug
if (editSlug) {
-- api(`/pastes/${editSlug}`).then(data => {
-- if (data.paste) openNewPasteModal({
++ Promise.all([api(`/pastes/${editSlug}`), ovLoadRoute('pastes')]).then(([data]) => {
++ if (data.paste && typeof openNewPasteModal === 'function') openNewPasteModal({
title: data.paste.title,
content: data.paste.content,
language: data.paste.language,
@@@ -1275,7 +1291,7 @@ function routeFromURL() {
return;
} else if (segments[0] === 'p' && segments[1]) {
showPage('paste-viewer');
-- loadPasteViewer(segments[1]);
++ ovLoadRoute('pastes').then(() => { if (typeof loadPasteViewer === 'function') loadPasteViewer(segments[1]); });
} else if (segments[0] === 'documentation') {
showPage('documentation');
initDocsTabScroller();
ddiff --git a/public/js/dashboard.js b/public/js/dashboard.js
iindex 7ea5d5ef..b5d5376e 100644
---- a/public/js/dashboard.js
++++ b/public/js/dashboard.js
@@@ -380,14 +380,7 @@ function openClonePresetModal() {
};
}
--function closeModal() {
-- // Hide main modal (app.js uses classList 'show')
-- const overlay = document.getElementById('modal-overlay');
-- if (overlay) overlay.classList.remove('show');
-- // Also hide clone preset modal (uses style.display)
-- const cloneModal = document.getElementById('clone-preset-config-modal');
-- if (cloneModal) cloneModal.style.display = 'none';
--}
++// closeModal() lives in app.js — every modal on the site uses it, not only the dashboard's.
async function doClonePresetConfig() {
const select = document.getElementById('clone-preset-select');
ddiff --git a/public/js/pastes.js b/public/js/pastes.js
iindex 224e8034..eac40657 100644
---- a/public/js/pastes.js
++++ b/public/js/pastes.js
@@@ -1597,9 +1597,4 @@ function formatTimeAgo(dateStr) {
return date.toLocaleDateString();
}
--// Escapes quotes as well as < > &. The previous version used the textContent/innerHTML trick,
--// which leaves " and ' untouched — safe in text, but these helpers are also interpolated into
--// attribute values, where an unescaped quote ends the attribute and starts a new one.
--function escapeHtml(str) {
-- return String(str == null ? '' : str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
--}
++// escapeHtml() lives in app.js: settings.js and others use it on pages that never load this file.
ddiff --git a/test/security-redaction.test.js b/test/security-redaction.test.js
iindex 9b9b4f4e..310bf70f 100644
---- a/test/security-redaction.test.js
++++ b/test/security-redaction.test.js
@@@ -265,7 +265,7 @@ const ok = (name) => { pass++; console.log(' ok -', name); };
// straight from the uploader's request body) in href="…" on a public page, and a group DM name
// in value="…". Each helper is evaluated here against a breakout payload.
const cases = [
-- ['public/js/pastes.js', /function escapeHtml\(str\) \{[\s\S]*?\n\}/, 'escapeHtml'],
++ ['public/js/app.js', /function escapeHtml\(str\) \{[\s\S]*?\n\}/, 'escapeHtml'],
['public/js/messenger.js', /function esc\(s\) \{[\s\S]*?\n \}/, 'esc'],
['public/js/vibes.js', /function escHb\(s\) \{[^\n]*\}/, 'escHb'],
['public/js/call.js', /function _esc\(str\) \{[\s\S]*?\n\}/, '_esc'],