/* eslint-disable no-console -- этот файл — единственное место где console.* разрешён намеренно: дублирование лога в stderr для dev-режима. */ /** * Минимальный logger для main process. * * Пишет в файл `%APPDATA%/Exercise Reminder/logs/latest.log` + дублирует * в stderr через console.* (чтобы dev-режим оставался удобным). * * Ротация: при достижении 1MB latest.log переименовывается в prev.log * (предыдущий prev.log удаляется). Две сессии истории — этого достаточно * для воспроизведения «случилось вчера, а сегодня перезапустил». Никакой * remote-телеметрии: лог локальный, пользователь сам может вложить его в * issue если что-то сломалось. * * Уровни: * - debug: подробный traceback, видим только если LAUDE_DEBUG=1 * - info: значимые события (startup, GSI matched, updater progress) * - warn: recoverable issues (transient network, retry succeeded) * - error: что-то реально сломалось (atomic write fail, IPC validation) */ import { app } from 'electron' import { appendFileSync, existsSync, mkdirSync, renameSync, statSync, unlinkSync } from 'node:fs' import { join } from 'node:path' const ROTATE_AT_BYTES = 1 * 1024 * 1024 // 1 MB type Level = 'debug' | 'info' | 'warn' | 'error' let logDir = '' let logPath = '' let prevPath = '' function ensurePaths(): void { if (logDir) return try { logDir = join(app.getPath('userData'), 'logs') if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true }) logPath = join(logDir, 'latest.log') prevPath = join(logDir, 'prev.log') } catch { // app.getPath не готов (очень ранний boot) — отложим, console продолжит. } } function rotateIfNeeded(): void { if (!logPath) return try { if (!existsSync(logPath)) return const size = statSync(logPath).size if (size < ROTATE_AT_BYTES) return if (existsSync(prevPath)) unlinkSync(prevPath) renameSync(logPath, prevPath) } catch { // не критично — продолжим писать в latest.log с overflow } } function ts(): string { return new Date().toISOString() } function levelTag(l: Level): string { return l.toUpperCase().padEnd(5, ' ') } function write(level: Level, msg: string, extra?: unknown): void { // Always dup to console for dev. structuredClone-style serialize: const line = `[${ts()}] ${levelTag(level)} ${msg}${ extra !== undefined ? ' ' + safeStringify(extra) : '' }\n` switch (level) { case 'error': console.error(line.trimEnd()) break case 'warn': console.warn(line.trimEnd()) break case 'debug': case 'info': default: console.log(line.trimEnd()) } ensurePaths() rotateIfNeeded() if (!logPath) return try { appendFileSync(logPath, line, 'utf-8') } catch { // Если AV держит файл — переживём, в console уже залогировали. } } function safeStringify(v: unknown): string { if (v instanceof Error) { return v.stack ?? `${v.name}: ${v.message}` } try { return JSON.stringify(v) } catch { return String(v) } } const DEBUG_ENABLED = process.env.LAUDE_DEBUG === '1' export const log = { debug: (msg: string, extra?: unknown): void => { if (DEBUG_ENABLED) write('debug', msg, extra) }, info: (msg: string, extra?: unknown): void => write('info', msg, extra), warn: (msg: string, extra?: unknown): void => write('warn', msg, extra), error: (msg: string, extra?: unknown): void => write('error', msg, extra) } /** Путь к логам (для диагностики). Возвращает пустую строку до initLogger(). */ export function getLogDir(): string { return logDir }