From 35e767b50692bc5b5d01d658b1d03802e367ff00 Mon Sep 17 00:00:00 2001 From: StyxX65 <150797939+StyxX65@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:09:34 +0200 Subject: [PATCH] Fix copy buttons doing nothing over plain HTTP navigator.clipboard is undefined in non-secure contexts, so the direct writeText() call threw synchronously and the execCommand fallback in its .catch() never ran. _copyText() now feature-detects the API, falls back to execCommand('copy'), then to a prompt() for manual copying. log.js reuses the helper; _getShareBaseUrl() caches the LAN-IP lookup so token Copy buttons stay within the click gesture execCommand requires. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 4 ++++ static/js/CLAUDE.md | 2 ++ static/js/log.js | 7 +++---- static/js/viewer.js | 38 +++++++++++++++++++++++++++----------- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a800d88..3bde90e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html ## [Unreleased] +### Fixed + +- **Copy buttons did nothing over plain HTTP** — the share modal's "Copy" buttons (new link + active links) and the log panel's copy button called `navigator.clipboard.writeText()` directly. The Clipboard API only exists in secure contexts (HTTPS or localhost), so when the scanner is reached at `http://:5100` the call threw synchronously and the intended `execCommand` fallback never ran — the button silently did nothing. `_copyText()` in `viewer.js` now feature-detects the API, falls back to `document.execCommand('copy')`, and as a last resort shows the link in a `prompt()` for manual copying; `log.js` reuses the same helper via `window._copyText`. `_getShareBaseUrl()` now caches the LAN-IP lookup so the token-list Copy buttons copy synchronously within the click gesture (required for `execCommand`). + --- ## [1.7.1] — 2026-06-10 diff --git a/static/js/CLAUDE.md b/static/js/CLAUDE.md index 0f6c263..61802b2 100644 --- a/static/js/CLAUDE.md +++ b/static/js/CLAUDE.md @@ -70,6 +70,8 @@ Never revert to `!!window._googleConnected` / `_fileSources.length > 0` — thos ## Gotchas +- **`navigator.clipboard` is `undefined` over plain HTTP** — the app is normally reached at `http://:5100`, a non-secure context where the Clipboard API does not exist, so calling `navigator.clipboard.writeText(...)` throws synchronously (a `.catch()` on it never runs). Always copy via `window._copyText(text, btn)` (defined in `viewer.js`) — it feature-detects the API and falls back to `document.execCommand('copy')`, then to a `prompt()`. Because `execCommand` needs a user gesture, don't `await` network calls between the click and the copy; `_getShareBaseUrl()` caches its result for this reason. + - **`scheduler.js` strings must use `t()`** — frequency labels, "Next", "Running...", "Disabled", empty-job text, and empty-history text all have translation keys. Do not hard-code English strings in `schedLoad()` or `schedRenderJobs()`. - **Scheduler UI — `schedToggleReportOnly()`** — dims the Profile row, shows/hides `#schedReportOnlyHint`, and forces `#schedAutoEmail` checked. Called from the checkbox `onchange` handler and at the start of `schedAddJob()` / `schedEditJob()`. - **Profile editor accounts** — default to unchecked. Only explicitly saved `user_ids` are checked. diff --git a/static/js/log.js b/static/js/log.js index 310b7b8..a859900 100644 --- a/static/js/log.js +++ b/static/js/log.js @@ -161,10 +161,9 @@ function copyLog() { document.querySelectorAll('#logPanel .log-line:not(#logLive)').forEach(function(d) { lines.push(d.textContent); }); - navigator.clipboard.writeText(lines.join('\n')).then(function() { - const btn = document.querySelector('.log-copy-btn'); - if (btn) { btn.textContent = '✓ Copied'; setTimeout(function(){ btn.textContent = '⎘ Copy'; }, 1500); } - }).catch(function() {}); + const btn = document.querySelector('.log-copy-btn'); + // _copyText (viewer.js) handles HTTP contexts where navigator.clipboard is undefined. + if (btn) window._copyText(lines.join('\n'), btn); } function _restoreLog() { diff --git a/static/js/viewer.js b/static/js/viewer.js index 6080b8a..7de5ecf 100644 --- a/static/js/viewer.js +++ b/static/js/viewer.js @@ -2,18 +2,23 @@ // Share button → modal to create, copy, and revoke read-only viewer links. import { S } from './state.js'; +let _shareBaseUrl = null; // cached so Copy buttons can build the URL synchronously + async function _getShareBaseUrl() { // Use the machine's LAN IP so links work for remote users, not just localhost. + if (_shareBaseUrl) return _shareBaseUrl; try { const r = await fetch('/api/local_ip'); if (r.ok) { const d = await r.json(); if (d.ip && d.ip !== '127.0.0.1') { - return 'http://' + d.ip + ':' + window.location.port; + _shareBaseUrl = 'http://' + d.ip + ':' + window.location.port; + return _shareBaseUrl; } } } catch(e) {} - return window.location.origin; + _shareBaseUrl = window.location.origin; + return _shareBaseUrl; } // ── User autocomplete for Share modal ──────────────────────────────────────── @@ -269,25 +274,35 @@ async function copyTokenLink(token, btn) { } function _copyText(text, btn) { - navigator.clipboard.writeText(text).then(() => { + const done = () => { const orig = btn.textContent; btn.textContent = t('share_copied', 'Copied!'); setTimeout(() => { btn.textContent = orig; }, 1800); - }).catch(() => { - // Fallback for HTTP contexts + }; + // Fallback for HTTP contexts, where navigator.clipboard is undefined + // (the Clipboard API only exists in secure contexts — HTTPS or localhost). + const fallback = () => { + let ok = false; try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; + ta.setAttribute('readonly', ''); document.body.appendChild(ta); + ta.focus(); ta.select(); - document.execCommand('copy'); + ok = document.execCommand('copy'); document.body.removeChild(ta); - const orig = btn.textContent; - btn.textContent = t('share_copied', 'Copied!'); - setTimeout(() => { btn.textContent = orig; }, 1800); - } catch(_) {} - }); + } catch(_) { ok = false; } + if (ok) done(); + // Last resort: show the link in a prompt so it can be copied manually. + else prompt(t('share_copy_link_prompt', 'Copy link:'), text); + }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(done).catch(fallback); + } else { + fallback(); + } } async function revokeToken(token, rowEl) { @@ -475,6 +490,7 @@ window.openShareModal = openShareModal; window.closeShareModal = closeShareModal; window.createShareLink = createShareLink; window.copyShareLink = copyShareLink; +window._copyText = _copyText; window.copyTokenLink = copyTokenLink; window.revokeToken = revokeToken; window.stLoadViewerPinStatus = stLoadViewerPinStatus;