From 6d458e37f39a70d031f15a04bde45cb391ad5ade Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:27:55 +0200 Subject: [PATCH] fix: stabilize the header on first paint Pass the installed version with the startup language so the renderer can display it before asynchronous initialization. Seed and start the upload sparkline immediately, keep a fixed update-action slot across all update states, and remove the delayed automatic update check. Add first-frame regression coverage for version, speed baseline, update geometry, and startup query behavior. --- lib/speed-history.js | 6 +++++- lib/startup-renderer.js | 7 ++++++- main.js | 10 +++++----- renderer/app.js | 13 ++++++++++--- renderer/index.html | 2 +- renderer/styles.css | 23 +++++++++++++++++------ tests/speed-history.test.js | 6 +++++- tests/startup-renderer.test.js | 29 ++++++++++++++++++++++++++++- tests/ui-smoke.js | 12 ++++++------ 9 files changed, 83 insertions(+), 25 deletions(-) diff --git a/lib/speed-history.js b/lib/speed-history.js index 68f0143..881cdf0 100644 --- a/lib/speed-history.js +++ b/lib/speed-history.js @@ -1,6 +1,10 @@ (function (root) { 'use strict'; + function createInitialSpeedHistoryState() { + return { display: 0, history: [0, 0] }; + } + function updateSpeedHistory(state, target, maxSamples = 160) { const nextTarget = Number.isFinite(Number(target)) ? Math.max(0, Number(target)) : 0; const previous = Number.isFinite(Number(state.display)) ? Math.max(0, Number(state.display)) : 0; @@ -14,7 +18,7 @@ return state; } - const api = { updateSpeedHistory }; + const api = { updateSpeedHistory, createInitialSpeedHistoryState }; if (typeof module !== 'undefined' && module.exports) module.exports = api; else if (root) root.SpeedHistory = api; })(typeof window !== 'undefined' ? window : this); diff --git a/lib/startup-renderer.js b/lib/startup-renderer.js index aea6694..e6dff20 100644 --- a/lib/startup-renderer.js +++ b/lib/startup-renderer.js @@ -8,6 +8,11 @@ function resolveStartupLanguage(config) { return config && config.globalSettings && config.globalSettings.language === 'de' ? 'de' : 'en'; } +function createStartupQuery(config, version) { + const normalizedVersion = /^\d+\.\d+\.\d+$/.test(String(version || '').trim()) ? String(version).trim() : ''; + return { language: resolveStartupLanguage(config), version: normalizedVersion }; +} + function createStartupWindow(BrowserWindow, options) { const window = new BrowserWindow({ ...options, show: false }); window.once('ready-to-show', () => { @@ -22,4 +27,4 @@ function createStartupWindow(BrowserWindow, options) { }; } -module.exports = { configureStartupRenderer, createStartupWindow, resolveStartupLanguage }; +module.exports = { configureStartupRenderer, createStartupWindow, resolveStartupLanguage, createStartupQuery }; diff --git a/main.js b/main.js index 5c9848a..b220a37 100644 --- a/main.js +++ b/main.js @@ -1,7 +1,7 @@ process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8'; const { monitorEventLoopDelay, PerformanceObserver } = require('perf_hooks'); const { app, BrowserWindow, ipcMain, dialog, clipboard, nativeTheme, Tray, Menu, nativeImage } = require('electron'); -const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage } = require('./lib/startup-renderer'); +const { configureStartupRenderer, createStartupWindow, createStartupQuery } = require('./lib/startup-renderer'); configureStartupRenderer(app); nativeTheme.themeSource = 'dark'; const path = require('path'); @@ -1508,12 +1508,12 @@ function createWindow() { debugLog(`CHILD PROCESS GONE: type=${details.type} reason=${details.reason} exitCode=${details.exitCode}`); }); - let startupLanguage = 'en'; - try { startupLanguage = resolveStartupLanguage(configStore.load()); } catch {} + let startupQuery = createStartupQuery(null, app.getVersion()); + try { startupQuery = createStartupQuery(configStore.load(), app.getVersion()); } catch {} startupWindow.load(path.join(__dirname, 'renderer', 'index.html'), (err) => { _writeCrashLog('LOAD FILE FAILED', err); debugLog(`LOAD FILE FAILED: ${err && err.stack ? err.stack : err}`); - }, { query: { language: startupLanguage } }); + }, { query: startupQuery }); } function createTray() { @@ -1652,7 +1652,7 @@ app.whenReady().then(async () => { } } catch {} - setTimeout(() => { void runAutomaticUpdateCheck(true); }, 3000); + void runAutomaticUpdateCheck(true); updateCheckInterval = setInterval(() => { void runAutomaticUpdateCheck(true); }, 5 * 60 * 1000); updateCheckInterval.unref?.(); }); diff --git a/renderer/app.js b/renderer/app.js index f6b347e..708ee6d 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -309,7 +309,7 @@ let settingsBaseline = ''; let settingsDirty = false; let settingsSaving = false; let lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: 0, elapsed: 0, activeJobs: 0 }; -const uploadSpeedState = { display: 0, history: [] }; +const uploadSpeedState = window.SpeedHistory.createInitialSpeedHistoryState(); let uploadSpeedTimer = null; const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload'; const QUEUE_COL_WIDTHS_KEY = 'queueColumnWidthsPx'; @@ -417,7 +417,6 @@ async function init() { renderAccounts(); setupListeners(); importEntryCoordinator.ready(); - initUploadSpeedSparkline(); restoreQueueColumnWidths(); loadHistory(); _refreshSessionFailedSnapshot(); @@ -808,7 +807,6 @@ function _syncHeaderUpdateState() { ? `Update v${version || 'unbekannt'} verfügbar. Klicken zum Installieren.` : 'Nach Aktualisierungen suchen'; if (button) { - button.hidden = !available; button.classList.toggle('update-available', available); button.classList.toggle('is-checking', _updateCheckBusy); button.disabled = _updateCheckBusy; @@ -4195,6 +4193,14 @@ function initUploadSpeedSparkline() { window.addEventListener('beforeunload', () => window.clearInterval(uploadSpeedTimer), { once: true }); } +function initializeStaticHeader() { + const version = new URLSearchParams(window.location.search).get('version') || ''; + const versionLabel = document.getElementById('versionLabel'); + if (versionLabel && /^\d+\.\d+\.\d+$/.test(version)) versionLabel.textContent = `v${version}`; + initUploadSpeedSparkline(); + _syncHeaderUpdateState(); +} + function updateStatusBar() { const stats = _computeQueueStats(); @@ -8116,6 +8122,7 @@ function updateStatsPanel() { window.api.onUpdateAvailable(showUpdateBanner); window.api.onUpdateProgress(handleUpdateProgress); window.api.onPrepareClose(prepareForWindowClose); +initializeStaticHeader(); setupDragDrop(); init().then(() => { window.api.signalCloseHandshakeReady(); diff --git a/renderer/index.html b/renderer/index.html index ce1c85d..537a915 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -58,7 +58,7 @@ 0 B/s - diff --git a/renderer/styles.css b/renderer/styles.css index 7e3bbfb..b5e102e 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -2091,17 +2091,18 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } .header-update-button { position: relative; - min-width: 106px; + width: 146px; + flex: 0 0 146px; height: 32px; display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 0 12px; - border: 1px solid transparent; + border: 1px solid var(--border); border-radius: 6px; - background: var(--accent); - color: var(--accent-ink); + background: var(--bg-secondary); + color: var(--text-muted); cursor: pointer; font: inherit; font-size: 12px; @@ -2109,8 +2110,10 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } transition: background-color .14s, transform .14s; } -.header-update-button[hidden] { - display: none; +.header-update-button.update-available { + border-color: transparent; + background: var(--accent); + color: var(--accent-ink); } .header-update-button::after { @@ -2145,7 +2148,13 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } } .header-update-button:hover { + background: var(--bg-card-hover); + color: var(--text); +} + +.header-update-button.update-available:hover { background: var(--accent-end); + color: var(--accent-ink); } .header-update-button:active { @@ -2276,6 +2285,7 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; } } .version-badge { + min-width: 48px; height: 32px; display: flex; align-items: center; @@ -3987,6 +3997,7 @@ input[type="checkbox"] { .header-update-button { min-width: 34px; width: 34px; + flex: 0 0 34px; padding: 0; } diff --git a/tests/speed-history.test.js b/tests/speed-history.test.js index 68220da..2cfea4e 100644 --- a/tests/speed-history.test.js +++ b/tests/speed-history.test.js @@ -1,6 +1,10 @@ const test = require('node:test'); const assert = require('node:assert/strict'); -const { updateSpeedHistory } = require('../lib/speed-history'); +const { updateSpeedHistory, createInitialSpeedHistoryState } = require('../lib/speed-history'); + +test('initial speed history contains a drawable zero baseline', () => { + assert.deepEqual(createInitialSpeedHistoryState(), { display: 0, history: [0, 0] }); +}); test('speed history smooths rising and falling samples independently', () => { const state = { display: 0, history: [] }; diff --git a/tests/startup-renderer.test.js b/tests/startup-renderer.test.js index 637889e..5a01af3 100644 --- a/tests/startup-renderer.test.js +++ b/tests/startup-renderer.test.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict'); const { EventEmitter } = require('node:events'); const fs = require('node:fs'); const path = require('node:path'); -const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage } = require('../lib/startup-renderer'); +const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage, createStartupQuery } = require('../lib/startup-renderer'); class TestBrowserWindow extends EventEmitter { constructor(options) { @@ -50,6 +50,14 @@ test('resolveStartupLanguage accepts only the supported persisted language', () assert.equal(resolveStartupLanguage(null), 'en'); }); +test('startup query carries language and installed version into the first renderer frame', () => { + assert.deepEqual(createStartupQuery({ globalSettings: { language: 'de' } }, '2.1.25'), { + language: 'de', + version: '2.1.25' + }); + assert.deepEqual(createStartupQuery(null, 'invalid'), { language: 'en', version: '' }); +}); + test('createStartupWindow forces the main window to start hidden', () => { const startup = createStartupWindow(TestBrowserWindow, { width: 1100, show: true }); @@ -124,3 +132,22 @@ test('upload sidebar renders and updates the remaining upload size', () => { assert.match(html, /Verbleibende Größe[\s\S]*id="uploadTelemetryRemainingSize"[^>]*>0 B { + const projectRoot = path.join(__dirname, '..'); + const html = fs.readFileSync(path.join(projectRoot, 'renderer', 'index.html'), 'utf8'); + const appSource = fs.readFileSync(path.join(projectRoot, 'renderer', 'app.js'), 'utf8'); + const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8'); + const css = fs.readFileSync(path.join(projectRoot, 'renderer', 'styles.css'), 'utf8'); + const updateButton = html.match(/