From d720ba295a1cd83b9bd4f94cc98f27e7e4e951eb Mon Sep 17 00:00:00 2001 From: Administrator Date: Sat, 30 May 2026 14:41:06 +0200 Subject: [PATCH] feat(log): add per-session log mode (one file per app launch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a third choice next to the existing single-file and per-day modes: a new log file is created at every app start (process boot) and used until the app is closed. A close → reopen of the app starts a new session, hence a new file. File pattern: fileuploader-session-YYYY-MM-DD_HH-MM-SS-.log. The boolean sessionLog field — misnamed: it actually toggled daily mode — is replaced by a logMode enum: "single" | "daily" | "session". The misnomer made the migration the trap to watch: existing users with sessionLog:true must land on "daily", NOT "session". normalizeLogMode handles this and is unit-tested. - lib/log-mode.js (new, pure, dual CJS/window export): normalizeLogMode + resolveLogFileName + format helpers. No fs, no Date.now() at call time. - config-store.js: normalize at the single load() boundary so downstream readers consume logMode only. logMode is deliberately NOT seeded in DEFAULTS (would beat the legacy migration after merge). - main.js: stamp SESSION_ID once at process start (with pid hedge against same-second restart collisions); getLogFilePath and buildFallbackLogName switch on mode via the lib. _resolveUploadLogTarget cache key is now just the primary path, which already encodes mode/date/session — self-invalidates. - renderer: + + + Pro Session = neue Datei bei jedem App-Start; nach komplettem Schließen + erneutem Öffnen beginnt eine neue Session. `; @@ -2897,7 +2902,10 @@ async function saveSettings(options = {}) { const globalSettings = { ...(config.globalSettings || {}), logFilePath: (document.getElementById('logFilePathInput')?.value || '').trim(), - sessionLog: !!document.getElementById('sessionLogInput')?.checked, + logMode: (() => { + const v = document.getElementById('logModeInput')?.value; + return (v === 'single' || v === 'daily' || v === 'session') ? v : 'single'; + })(), resumeQueueOnLaunch: !!document.getElementById('resumeQueueOnLaunchInput')?.checked, parallelUploadCount: Math.max(0, Math.min(100, parseInt(document.getElementById('parallelUploadCountInput')?.value || '0', 10) || 0)), scaleParallelUploads: !!document.getElementById('scaleParallelUploadsInput')?.checked, diff --git a/renderer/index.html b/renderer/index.html index 8b0df96..78769af 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -332,6 +332,7 @@ + diff --git a/tests/config-store.test.js b/tests/config-store.test.js index 86b2af0..17c4198 100644 --- a/tests/config-store.test.js +++ b/tests/config-store.test.js @@ -56,6 +56,28 @@ describe('ConfigStore', () => { assert.equal(config.hosters['doodstream.com'][0].apiKey, 'test-key-123'); }); + it('default logMode is "single"', () => { + const config = store.load(); + assert.equal(config.globalSettings.logMode, 'single'); + }); + + it('regression: legacy sessionLog:true on disk normalizes to logMode "daily" (NOT "session")', async () => { + // Write a config with the legacy boolean only (what an existing user has). + await store.save({ globalSettings: { sessionLog: true } }); + const config = store.load(); + // The misnamed legacy field MUST map to daily — mapping to "session" would + // silently change every per-day user's behaviour on upgrade. + assert.equal(config.globalSettings.logMode, 'daily'); + }); + + it('logMode round-trips for all three values', async () => { + for (const mode of ['single', 'daily', 'session']) { + await store.save({ globalSettings: { logMode: mode } }); + const config = store.load(); + assert.equal(config.globalSettings.logMode, mode, `mode ${mode}`); + } + }); + it('load merges with defaults for missing hosters', () => { // Write partial config in old single-object format (triggers migration) fs.writeFileSync(store.filePath, JSON.stringify({ diff --git a/tests/log-mode.test.js b/tests/log-mode.test.js new file mode 100644 index 0000000..b686afc --- /dev/null +++ b/tests/log-mode.test.js @@ -0,0 +1,96 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const { normalizeLogMode, resolveLogFileName, formatDateStamp, formatSessionStamp } = require('../lib/log-mode'); + +// --- normalizeLogMode --- + +test('normalizeLogMode: default for empty/null/undefined is "single"', () => { + assert.equal(normalizeLogMode(), 'single'); + assert.equal(normalizeLogMode(null), 'single'); + assert.equal(normalizeLogMode({}), 'single'); +}); + +test('normalizeLogMode: explicit logMode wins for all three valid values', () => { + assert.equal(normalizeLogMode({ logMode: 'single' }), 'single'); + assert.equal(normalizeLogMode({ logMode: 'daily' }), 'daily'); + assert.equal(normalizeLogMode({ logMode: 'session' }), 'session'); +}); + +test('regression: legacy sessionLog:true maps to "daily", NOT "session"', () => { + // The legacy boolean field was named after a misnomer — it actually toggled + // per-day logging. Mapping it to "session" would silently flip every existing + // per-day user onto per-session, which is exactly the bug the migration trap + // exists to prevent. + assert.equal(normalizeLogMode({ sessionLog: true }), 'daily'); +}); + +test('normalizeLogMode: sessionLog:false / missing maps to "single"', () => { + assert.equal(normalizeLogMode({ sessionLog: false }), 'single'); +}); + +test('normalizeLogMode: explicit logMode beats the legacy sessionLog field', () => { + // Once a user picks a mode in 3.3.35+, the legacy boolean must NOT override. + assert.equal(normalizeLogMode({ logMode: 'session', sessionLog: true }), 'session'); + assert.equal(normalizeLogMode({ logMode: 'single', sessionLog: true }), 'single'); +}); + +test('normalizeLogMode: invalid logMode strings fall through to single (or legacy if present)', () => { + assert.equal(normalizeLogMode({ logMode: 'lolnope' }), 'single'); + assert.equal(normalizeLogMode({ logMode: '' }), 'single'); + assert.equal(normalizeLogMode({ logMode: 'lolnope', sessionLog: true }), 'daily'); +}); + +// --- resolveLogFileName --- + +test('resolveLogFileName: single mode → bare basename + ext', () => { + assert.equal( + resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode: 'single' }), + 'fileuploader.log' + ); +}); + +test('resolveLogFileName: daily mode → fileuploader-YYYY-MM-DD.log', () => { + const d = new Date(2026, 4, 28); // May 28, 2026 — month is 0-indexed + assert.equal( + resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode: 'daily', date: d }), + 'fileuploader-2026-05-28.log' + ); +}); + +test('resolveLogFileName: session mode → fileuploader-session-.log', () => { + assert.equal( + resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode: 'session', sessionId: '2026-05-28_22-44-52-12345' }), + 'fileuploader-session-2026-05-28_22-44-52-12345.log' + ); +}); + +test('resolveLogFileName: session mode with missing sessionId falls back to single (never emits malformed name)', () => { + assert.equal( + resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode: 'session' }), + 'fileuploader.log' + ); +}); + +test('resolveLogFileName: unknown mode is treated as single', () => { + assert.equal( + resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode: 'lolnope' }), + 'fileuploader.log' + ); +}); + +// --- format helpers --- + +test('formatDateStamp: zero-pads month and day', () => { + assert.equal(formatDateStamp(new Date(2026, 0, 3)), '2026-01-03'); + assert.equal(formatDateStamp(new Date(2026, 11, 31)), '2026-12-31'); +}); + +test('formatSessionStamp: produces YYYY-MM-DD_HH-MM-SS-pid', () => { + const d = new Date(2026, 4, 28, 7, 9, 5); + assert.equal(formatSessionStamp(d, 12345), '2026-05-28_07-09-05-12345'); +}); + +test('formatSessionStamp: omits the pid suffix when none provided', () => { + const d = new Date(2026, 4, 28, 22, 44, 52); + assert.equal(formatSessionStamp(d), '2026-05-28_22-44-52'); +});