Multi-Hoster-Upload/lib/log-mode.js
Administrator d720ba295a feat(log): add per-session log mode (one file per app launch)
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-<pid>.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: <select> with three German labels replaces the old checkbox;
  saveSettings writes logMode; index.html loads the lib so window.LogMode is
  available in renderSettings.
- Tests: 14 log-mode tests (incl. legacy-migration regression), 3 config-store
  tests (defaults, legacy migration, round-trip all three values). 200/200.

End-to-end simulated locally: two launches → two distinct session files; PID
hedge produces distinct names even within the same second.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 14:41:06 +02:00

86 lines
3.7 KiB
JavaScript

// Log-file mode resolution for fileuploader.log:
// - "single" → one file: fileuploader.log
// - "daily" → per-day: fileuploader-YYYY-MM-DD.log
// - "session" → per-launch: fileuploader-session-YYYY-MM-DD_HH-MM-SS-<pid>.log
//
// Pure functions only — no fs, no Date.now() at call time — so they unit-test
// cleanly and the main.js call sites pass in `new Date()` + the session stamp.
//
// MIGRATION TRAP this lib protects against: the legacy boolean was named
// `sessionLog` but actually toggled *daily* mode. A naive rename would silently
// flip every per-day user onto per-session. normalizeLogMode below maps the
// legacy `sessionLog: true` to "daily", NOT "session". Read logMode everywhere
// downstream; do not derive from sessionLog at call sites.
//
// Loaded both as CommonJS (main.js, tests) and as a browser global
// (renderer/app.js via index.html script tag) so a single implementation backs
// runtime and tests — same pattern as queue-prune.js / queue-dedup.js.
(function (root) {
'use strict';
const VALID_MODES = new Set(['single', 'daily', 'session']);
function normalizeLogMode(globalSettings) {
const gs = globalSettings && typeof globalSettings === 'object' ? globalSettings : {};
if (typeof gs.logMode === 'string' && VALID_MODES.has(gs.logMode)) {
return gs.logMode;
}
// Legacy boolean migration: sessionLog *named* like "session" but actually
// implemented "daily" — preserve daily users on the migration path.
if (gs.sessionLog === true) return 'daily';
return 'single';
}
function _two(n) { return String(n).padStart(2, '0'); }
function formatDateStamp(date) {
return `${date.getFullYear()}-${_two(date.getMonth() + 1)}-${_two(date.getDate())}`;
}
function formatSessionStamp(date, pid) {
const d = `${date.getFullYear()}-${_two(date.getMonth() + 1)}-${_two(date.getDate())}`;
const t = `${_two(date.getHours())}-${_two(date.getMinutes())}-${_two(date.getSeconds())}`;
// PID disambiguates a same-second close→reopen — a human can't but two
// automated runs might. Cheap belt to a suspenders-not-required problem.
const pidStr = pid !== undefined && pid !== null ? `-${pid}` : '';
return `${d}_${t}${pidStr}`;
}
/**
* Compute the log filename for the given mode + clock.
* @param {Object} args
* @param {string} args.baseName e.g. "fileuploader"
* @param {string} args.ext e.g. ".log"
* @param {string} args.mode "single" | "daily" | "session"
* @param {Date} args.date current timestamp
* @param {string} [args.sessionId] required when mode === "session"
* @returns {string} the bare filename (no directory)
*/
function resolveLogFileName(args) {
const a = args || {};
const base = String(a.baseName || 'fileuploader');
const ext = String(a.ext || '.log');
const mode = VALID_MODES.has(a.mode) ? a.mode : 'single';
if (mode === 'single') return `${base}${ext}`;
if (mode === 'daily') {
const date = a.date instanceof Date ? a.date : new Date();
return `${base}-${formatDateStamp(date)}${ext}`;
}
// session
const sid = a.sessionId && String(a.sessionId).trim();
if (sid) return `${base}-session-${sid}${ext}`;
// Defensive: if a session-id wasn't passed, fall back to single rather
// than emit a malformed name. main.js always supplies one.
return `${base}${ext}`;
}
const api = { normalizeLogMode, resolveLogFileName, formatDateStamp, formatSessionStamp, VALID_MODES };
if (typeof module !== 'undefined' && module.exports) {
module.exports = api;
} else if (root) {
root.LogMode = api;
}
})(typeof window !== 'undefined' ? window : this);