From 1dff043a066aa54e4f34b0f4bcc736a57f92bd7f Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:51:33 -0400 Subject: [PATCH] Fix: reload refreshing iframes without adding Back entries Reload with contentWindow.location.replace and fall back to setting src when the frame has no window. --- ui/admin/index.html | 2 +- ui/index.html | 2 +- ui/js/admin-app-form.js | 8 ++++---- ui/js/admin-auth.js | 4 ++-- ui/js/admin-color-control.js | 4 ++-- ui/js/admin-drag.js | 2 +- ui/js/admin-list.js | 8 ++++---- ui/js/admin-settings.js | 8 ++++---- ui/js/admin-shared.js | 2 +- ui/js/admin-widget-form.js | 8 ++++---- ui/js/admin.js | 18 +++++++++--------- ui/js/background.js | 2 +- ui/js/dashboard.js | 8 ++++---- ui/js/listbox.js | 2 +- ui/js/spotlight.js | 2 +- ui/js/ui.js | 2 +- ui/js/utils.js | 9 ++++++++- ui/js/widget-config-form.js | 8 ++++---- ui/test/widget-teardown.test.mjs | 29 +++++++++++++++++++++++++++++ 19 files changed, 82 insertions(+), 46 deletions(-) diff --git a/ui/admin/index.html b/ui/admin/index.html index bcd31ca1..df36076f 100644 --- a/ui/admin/index.html +++ b/ui/admin/index.html @@ -300,6 +300,6 @@

General

- + diff --git a/ui/index.html b/ui/index.html index 24daf46b..95e7fbfb 100644 --- a/ui/index.html +++ b/ui/index.html @@ -56,5 +56,5 @@ - + diff --git a/ui/js/admin-app-form.js b/ui/js/admin-app-form.js index d2c0f702..1652e90d 100644 --- a/ui/js/admin-app-form.js +++ b/ui/js/admin-app-form.js @@ -1,4 +1,4 @@ -import { clr, el, focusFirst, initial, inp as inpById, isolate, q as qSel, qa, qi, tgt } from '/js/utils.js?v=dc4ac52e'; +import { clr, el, focusFirst, initial, inp as inpById, isolate, q as qSel, qa, qi, tgt } from '/js/utils.js?v=705ca896'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { loadLocalIcons, @@ -31,10 +31,10 @@ import { reveal, setTogDisabled, swapContent, -} from '/js/admin-shared.js?v=5e6a023a'; -import { createListbox } from '/js/listbox.js?v=7d7867ae'; +} from '/js/admin-shared.js?v=1c01be9a'; +import { createListbox } from '/js/listbox.js?v=f814170f'; import { MAX_LABELS } from '/js/badge-logic.js?v=5c72bd4c'; -import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=64eef54d'; +import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=ef504f96'; import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=b0881957'; import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; diff --git a/ui/js/admin-auth.js b/ui/js/admin-auth.js index 6fc95c7a..cc91ba9e 100644 --- a/ui/js/admin-auth.js +++ b/ui/js/admin-auth.js @@ -1,8 +1,8 @@ -import { apiGet, apiPost } from '/js/admin-shared.js?v=5e6a023a'; +import { apiGet, apiPost } from '/js/admin-shared.js?v=1c01be9a'; import { initI18n, t } from '/js/i18n.js?v=ed7c5565'; import { loginErrorKey } from '/js/admin-error.js?v=b0881957'; import { pwStrength } from '/js/password-strength.js?v=389e0ed0'; -import { el, inp as inpById, qa } from '/js/utils.js?v=dc4ac52e'; +import { el, inp as inpById, qa } from '/js/utils.js?v=705ca896'; import { blockingScreenFor } from '/js/config-recovery.js?v=85138697'; export async function checkAuth(onLogin) { diff --git a/ui/js/admin-color-control.js b/ui/js/admin-color-control.js index 873e2c38..87c16bd3 100644 --- a/ui/js/admin-color-control.js +++ b/ui/js/admin-color-control.js @@ -1,7 +1,7 @@ -import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=5e6a023a'; +import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=1c01be9a'; import { t } from '/js/i18n.js?v=ed7c5565'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { qa, q } from '/js/utils.js?v=dc4ac52e'; +import { qa, q } from '/js/utils.js?v=705ca896'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; import { HUE_NAMES, ROLE_NAMES, TILE_KEYWORDS, pageTheme, tileColor } from '/js/palette.js?v=3fb8ae43'; diff --git a/ui/js/admin-drag.js b/ui/js/admin-drag.js index bbe67258..cd139f43 100644 --- a/ui/js/admin-drag.js +++ b/ui/js/admin-drag.js @@ -12,7 +12,7 @@ import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76'; import { state } from '/js/admin-state.js?v=af772a1b'; import { snapshotItems } from '/js/admin-save-logic.js?v=5613a9fa'; -import { qa } from '/js/utils.js?v=dc4ac52e'; +import { qa } from '/js/utils.js?v=705ca896'; /** @type {(before: unknown) => void} */ let _save = () => {}; diff --git a/ui/js/admin-list.js b/ui/js/admin-list.js index 7b724752..b33e31b3 100644 --- a/ui/js/admin-list.js +++ b/ui/js/admin-list.js @@ -14,12 +14,12 @@ import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b'; import { snapshotItems } from '/js/admin-save-logic.js?v=5613a9fa'; import { reorderItems } from '/js/admin-logic.js?v=1cca36f5'; -import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=29e09aad'; -import { paintIcon } from '/js/admin-shared.js?v=5e6a023a'; -import { clr, el, focusFirst, initial, qa, setUserText } from '/js/utils.js?v=dc4ac52e'; +import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=8fa6fbae'; +import { paintIcon } from '/js/admin-shared.js?v=1c01be9a'; +import { clr, el, focusFirst, initial, qa, setUserText } from '/js/utils.js?v=705ca896'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { t } from '/js/i18n.js?v=ed7c5565'; -import { sizeLabel } from '/js/admin-widget-form.js?v=e0966417'; +import { sizeLabel } from '/js/admin-widget-form.js?v=c66544d4'; import { widgetGlyph } from '/js/widget-glyphs.js?v=b383b7c8'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; diff --git a/ui/js/admin-settings.js b/ui/js/admin-settings.js index d3bae93c..2a627df2 100644 --- a/ui/js/admin-settings.js +++ b/ui/js/admin-settings.js @@ -1,4 +1,4 @@ -import { toast, apiGet, apiPost, errorText, reveal, swapContent, wireInlineEdit } from '/js/admin-shared.js?v=5e6a023a'; +import { toast, apiGet, apiPost, errorText, reveal, swapContent, wireInlineEdit } from '/js/admin-shared.js?v=1c01be9a'; import { socketProbeAdvice } from '/js/admin-error.js?v=b0881957'; import { pwStrength } from '/js/password-strength.js?v=389e0ed0'; import { t } from '/js/i18n.js?v=ed7c5565'; @@ -11,11 +11,11 @@ import { BLOCK, } from '/js/admin-logic.js?v=1cca36f5'; import { confirmText, promptModal } from '/js/modal.js?v=6b0320bd'; -import { el, inp, setUserText } from '/js/utils.js?v=dc4ac52e'; +import { el, inp, setUserText } from '/js/utils.js?v=705ca896'; import { formatNumber } from '/js/format-number.js?v=abcd4ae4'; import { serialWrites } from '/js/admin-save-logic.js?v=5613a9fa'; -import { renderColorControl } from '/js/admin-color-control.js?v=64eef54d'; -import { BACKDROP } from '/js/background.js?v=d6f2e701'; +import { renderColorControl } from '/js/admin-color-control.js?v=ef504f96'; +import { BACKDROP } from '/js/background.js?v=25428bf2'; import { ALLOWED_HOSTS_MAX, firstBadHost, diff --git a/ui/js/admin-shared.js b/ui/js/admin-shared.js index 177ddf86..4fd10a1f 100644 --- a/ui/js/admin-shared.js +++ b/ui/js/admin-shared.js @@ -1,6 +1,6 @@ /* Stateless helpers shared by the admin modules. Mutable state stays out. */ import { recoversSession, toastHoldMs } from '/js/admin-logic.js?v=1cca36f5'; -import { el, isolate, q } from '/js/utils.js?v=dc4ac52e'; +import { el, isolate, q } from '/js/utils.js?v=705ca896'; import { t } from '/js/i18n.js?v=ed7c5565'; import { errorAdvice } from '/js/admin-error.js?v=b0881957'; import { formatList, formatNumber } from '/js/format-number.js?v=abcd4ae4'; diff --git a/ui/js/admin-widget-form.js b/ui/js/admin-widget-form.js index c1c63da1..cc7f6f23 100644 --- a/ui/js/admin-widget-form.js +++ b/ui/js/admin-widget-form.js @@ -1,11 +1,11 @@ import { state } from '/js/admin-state.js?v=af772a1b'; -import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=5e6a023a'; -import { createListbox } from '/js/listbox.js?v=7d7867ae'; -import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=294805cc'; +import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=1c01be9a'; +import { createListbox } from '/js/listbox.js?v=f814170f'; +import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=54fa233f'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=1cca36f5'; import { t } from '/js/i18n.js?v=ed7c5565'; -import { focusFirst, q, qi, qa } from '/js/utils.js?v=dc4ac52e'; +import { focusFirst, q, qi, qa } from '/js/utils.js?v=705ca896'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; const SIZES_WITH_ICONS = new Set(['small', 'medium', 'large', 'xlarge']); diff --git a/ui/js/admin.js b/ui/js/admin.js index 0d47b958..ee88351e 100644 --- a/ui/js/admin.js +++ b/ui/js/admin.js @@ -1,7 +1,7 @@ -import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=deccbdee'; -import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=104be7f0'; +import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=4853370b'; +import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=5246ff39'; import { recoveryShown } from '/js/config-recovery.js?v=85138697'; -import { focusRow, initList, render, syncFilterUI } from '/js/admin-list.js?v=14782161'; +import { focusRow, initList, render, syncFilterUI } from '/js/admin-list.js?v=87efcfb7'; import { resolveAdminSection } from '/js/admin-logic.js?v=1cca36f5'; import { afterImport, @@ -23,7 +23,7 @@ import { showBgFields, showWallpaperFile, wireUnsplashKey, -} from '/js/admin-settings.js?v=584aae78'; +} from '/js/admin-settings.js?v=41547198'; import { apiGet, apiPost, @@ -37,13 +37,13 @@ import { setReauthHandler, toast, withheldText, -} from '/js/admin-shared.js?v=5e6a023a'; +} from '/js/admin-shared.js?v=1c01be9a'; import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b'; -import { buildWidgetForm } from '/js/admin-widget-form.js?v=e0966417'; +import { buildWidgetForm } from '/js/admin-widget-form.js?v=c66544d4'; import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86'; import { formatNumber } from '/js/format-number.js?v=abcd4ae4'; import { initGlideSelect, syncGlideSelect } from '/js/glide-select.js?v=8b39e9d0'; -import { createListbox } from '/js/listbox.js?v=7d7867ae'; +import { createListbox } from '/js/listbox.js?v=f814170f'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=ed7c5565'; import { loadLocalIcons } from '/js/icons.js?v=5ac01d72'; @@ -82,8 +82,8 @@ import { storeGet, storeSet, tgt, -} from '/js/utils.js?v=dc4ac52e'; -import { applyBackground, resolveBackground } from '/js/background.js?v=d6f2e701'; +} from '/js/utils.js?v=705ca896'; +import { applyBackground, resolveBackground } from '/js/background.js?v=25428bf2'; import { parseYamlTolerant, YamlLiteError, YamlTooLargeError } from '/js/yaml-lite.js?v=a42ef914'; ensureSprite(); diff --git a/ui/js/background.js b/ui/js/background.js index 776c931d..95515192 100644 --- a/ui/js/background.js +++ b/ui/js/background.js @@ -1,5 +1,5 @@ // @ts-check -import { cssColor, sanitizeCssUrl } from '/js/utils.js?v=dc4ac52e'; +import { cssColor, sanitizeCssUrl } from '/js/utils.js?v=705ca896'; import { loadWallpaper, saveWallpaper } from '/js/wallpaper-cache.js?v=c5f8a3e6'; export const BACKDROP = '#0d1117'; diff --git a/ui/js/dashboard.js b/ui/js/dashboard.js index 1834c7fb..97fba979 100644 --- a/ui/js/dashboard.js +++ b/ui/js/dashboard.js @@ -28,9 +28,9 @@ import { storeSet, teardownWidgets, titleWhenTruncated, -} from '/js/utils.js?v=dc4ac52e'; +} from '/js/utils.js?v=705ca896'; import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86'; -import { initSpotlight } from '/js/spotlight.js?v=c76fb899'; +import { initSpotlight } from '/js/spotlight.js?v=ad660003'; import { html, setHtml, raw } from '/js/html.js?v=c71f8903'; import { initI18n, t, currentLang } from '/js/i18n.js?v=ed7c5565'; import { blockingScreenFor, showBlockingScreen } from '/js/config-recovery.js?v=85138697'; @@ -46,7 +46,7 @@ import { buildMobile, resetMobileChrome, mkFolderGlyph, -} from '/js/ui.js?v=4265d299'; +} from '/js/ui.js?v=049178d3'; import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=5c72bd4c'; import { formatNumber } from '/js/format-number.js?v=abcd4ae4'; import { closeBadgePopover, wireBadgePopover } from '/js/badge-popover.js?v=23793736'; @@ -59,7 +59,7 @@ import { restorePage, wheelPager, } from '/js/dashboard-logic.js?v=6a29cedc'; -import { applyBackground, BACKDROP, resolveBackground } from '/js/background.js?v=d6f2e701'; +import { applyBackground, BACKDROP, resolveBackground } from '/js/background.js?v=25428bf2'; import { repeatJittered } from '/js/jitter.js?v=087a1fcf'; import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=e9f4b607'; import { startWakeLock } from '/js/wake-lock.js?v=6b9591cf'; diff --git a/ui/js/listbox.js b/ui/js/listbox.js index ef9a591e..67bc726a 100644 --- a/ui/js/listbox.js +++ b/ui/js/listbox.js @@ -10,7 +10,7 @@ import { nextActiveIndex, singleChoice } from '/js/admin-logic.js?v=1cca36f5'; import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86'; import { formatList } from '/js/format-number.js?v=abcd4ae4'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { qa } from '/js/utils.js?v=dc4ac52e'; +import { qa } from '/js/utils.js?v=705ca896'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; const CHEV = iconSvg('chevrons', 22, 'solid', 'dd-chev'); diff --git a/ui/js/spotlight.js b/ui/js/spotlight.js index 8072ec13..209d6ef4 100644 --- a/ui/js/spotlight.js +++ b/ui/js/spotlight.js @@ -1,5 +1,5 @@ import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86'; -import { mk, clr, el, initial, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=dc4ac52e'; +import { mk, clr, el, initial, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=705ca896'; import { t } from '/js/i18n.js?v=ed7c5565'; /* Attached to the window so a re-open can undo the previous one. */ diff --git a/ui/js/ui.js b/ui/js/ui.js index 51ffd9de..8acfe974 100644 --- a/ui/js/ui.js +++ b/ui/js/ui.js @@ -16,7 +16,7 @@ import { q, qa, setUserText, -} from '/js/utils.js?v=dc4ac52e'; +} from '/js/utils.js?v=705ca896'; import { t, currentLang } from '/js/i18n.js?v=ed7c5565'; import { toneForColor } from '/js/label-contrast.js?v=0b1ebb19'; import { mobileMetrics, gridColumnWidth, gridCellCount } from '/js/mobile-metrics.js?v=349f4300'; diff --git a/ui/js/utils.js b/ui/js/utils.js index 57623268..404721d7 100644 --- a/ui/js/utils.js +++ b/ui/js/utils.js @@ -410,8 +410,15 @@ export function mountScaledWidget(card, { src, title, design, iframeOpts, overla const refreshInterval = Number(o.refreshInterval) || 0; if (refreshInterval >= 250) { const base = refreshInterval; + /* Not ifr.src alone: each assignment adds an entry to the tab's Back + history. */ const reload = () => { - ifr.src = src + (src.includes('?') ? '&' : '?') + '_r=' + Date.now(); + const next = src + (src.includes('?') ? '&' : '?') + '_r=' + Date.now(); + try { + ifr.contentWindow.location.replace(next); + } catch { + ifr.src = next; + } }; const jit = () => Math.round(base * (1 + (Math.random() * 2 - 1) * 0.15)); /* The handle is reassigned every tick. The cleanup must read the current diff --git a/ui/js/widget-config-form.js b/ui/js/widget-config-form.js index d6f1e097..86ee9d91 100644 --- a/ui/js/widget-config-form.js +++ b/ui/js/widget-config-form.js @@ -3,9 +3,9 @@ import { t } from '/js/i18n.js?v=ed7c5565'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=5e6a023a'; -import { createListbox } from '/js/listbox.js?v=7d7867ae'; -import { renderColorControl } from '/js/admin-color-control.js?v=64eef54d'; +import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=1c01be9a'; +import { createListbox } from '/js/listbox.js?v=f814170f'; +import { renderColorControl } from '/js/admin-color-control.js?v=ef504f96'; import { seedCarried, applyOptionSet, @@ -15,7 +15,7 @@ import { visibleFieldFlags, } from '/js/admin-logic.js?v=1cca36f5'; import { optionsErrorAdvice, TONE } from '/js/admin-error.js?v=b0881957'; -import { qi } from '/js/utils.js?v=dc4ac52e'; +import { qi } from '/js/utils.js?v=705ca896'; import { formatNumber } from '/js/format-number.js?v=abcd4ae4'; const PE = diff --git a/ui/test/widget-teardown.test.mjs b/ui/test/widget-teardown.test.mjs index 482edf9b..e826ff63 100644 --- a/ui/test/widget-teardown.test.mjs +++ b/ui/test/widget-teardown.test.mjs @@ -228,6 +228,35 @@ test('the rebuild is driven by the shared layout rule', () => { assert.match(src, /onLayoutChange\(mobile => \{/, 'the layout change is what triggers a rebuild'); }); +test('a reload replaces the frame location instead of adding a Back entry', async () => { + const h = harness(); + const frames = []; + const make = globalThis.document.createElement; + globalThis.document.createElement = tag => { + const e = make(tag); + if (tag === 'iframe') { + e.replaced = []; + e.contentWindow = { location: { replace: url => e.replaced.push(url) } }; + frames.push(e); + } + return e; + }; + globalThis.document.hidden = false; + const wait = ms => new Promise(r => h.realSetTimeout(r, ms)); + const { mountScaledWidget, teardownWidgets } = await import('../js/utils.js'); + try { + mountScaledWidget(h.card(), { ...OPTS, iframeOpts: { refreshInterval: 250 } }); + const first = frames[0].src; + await wait(800); + assert.ok(frames[0].replaced.length > 0, 'no reload ran'); + assert.match(frames[0].replaced[0], /^http:\/\/svc\/w\?_r=\d+$/); + assert.equal(frames[0].src, first, 'src was reassigned, which adds a Back entry'); + } finally { + teardownWidgets(); + h.done(); + } +}); + test('a hidden tab does not reload its widgets, and catches up on return', async () => { const h = harness(); const frames = [];