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(/