## Overview ClioDeck has **three** logging-adjacent mechanisms, not two as an earlier version of this page said — and one of the three previously documented here turned out to be dead code. - **Global console filter** (`src/shared/console-filter.ts`) — intercepts every raw `console.*` call, and is imported by both `src/renderer/src/main.tsx` and `src/main/index.ts`. But its env-var branching (`CLIODESK_DEBUG`/`CLIODESK_LOG_LEVEL`) only has an observable effect **in the main process**: the `BrowserWindow` is created with `contextIsolation: true, nodeIntegration: false, sandbox: true` (`src/main/index.ts`, unconditionally, dev and prod alike), so the renderer has no `process` global at runtime — this file's own `isProduction()`/`isDebugEnabled()` checks (`typeof process !== 'undefined'`) can never be true there, env vars or not. Separately, Vite's `esbuild.pure` setting (see below) already strips raw `console.log`/`info`/`debug` calls from the production renderer bundle regardless. `restoreConsole`/`rawConsole`/`getFilterStatus` are real exports of this file, but in the renderer they operate on a filter that was never actually engaged. - **Renderer logger** (`src/renderer/src/utils/logger.ts`) — the structured logger actually imported throughout the renderer (stores, components: `MainLayout.tsx`, `similarityStore.ts`, `editorStore.ts`, and more — `grep -rn "import { logger }" src/renderer/` shows every hit resolves here). See "Renderer Logger (for Developers)" below for its real API. - **Main-process logger** (`src/main/utils/logger.ts`) — used throughout the main process (IPC handlers, services). Different API (`logger.info(module, action, data)`), different output format (`[2026-…] INFO [ipc] fs:read-directory {...}`), and — important — **it does not read `CLIODESK_LOG_LEVEL` or any other env var**. It hardcodes `level = 'info'` and nothing in the codebase ever calls its `setLevel()`. Setting `CLIODESK_LOG_LEVEL=debug` raises verbosity for the console filter only; main-process `debug`-level logs from IPC handlers stay suppressed regardless. **`@shared/logger` (`src/shared/logger.ts`) is dead code.** An earlier version of this page documented it as "the" renderer logger, with a `logger.debug(context, message, data)` API and a `createContextLogger()` helper. Neither has any call site anywhere in the repo outside the file's own definition (`grep -rln "@shared/logger" src/ backend/` returns only `src/shared/logger.ts` itself) — every real renderer import goes through `src/renderer/src/utils/logger.ts` instead, a different file with a different API. If you see this page's older text elsewhere describing `logger.createContextLogger`, it no longer reflects real code. If you're debugging something in `src/main/` (IPC, services), the env vars below won't help — there is currently no way to raise that logger's level short of editing `src/main/utils/logger.ts`. ## Default Behavior | Environment | console.log | console.info | console.warn | console.error | |-------------|-------------|--------------|--------------|---------------| | **Development** | Shown | Shown | Shown | Shown | | **Production** | Filtered | Filtered | Shown | Shown | In production, only `console.warn` and `console.error` are displayed to reduce console noise. > This table describes the **observable outcome**, not one uniform mechanism. In the **main process**, it's genuinely this file's `getFilterConfig()` doing the filtering. In the **renderer**, this file's filter never actually engages at all — `isProduction()` always returns `false` there (no `process` global, sandboxed), so `installConsoleFilter()` installs a no-op pass-through regardless of environment. The renderer's production row holds anyway, but for an unrelated reason: Vite's `esbuild.pure` setting (see below) deletes `console.log`/`info`/`debug` calls from the bundle at build time. In renderer **development** builds, neither mechanism filters anything, which is why raw `console.log` calls show there too. ## Enabling Debug Logs in Production > **⚠️ These env vars do NOT revive the renderer logger's `log`/`info`/`debug` > calls in a packaged build — only raw `console.*` calls.** The renderer > logger's prod-gate (`isProd = import.meta.env?.PROD === true`, > `src/renderer/src/utils/logger.ts:28`) is a **build-time** Vite constant, > not a runtime check. Worse, `vite.config.mts:79` marks `console.log`, > `console.info`, and `console.debug` as `pure` for esbuild — in a > production build, the real branch of `isProd ? noop : realCall` is > physically removed from the shipped bundle before the app ever runs. No > environment variable can bring it back. If you're debugging a packaged > build and relying on `logger.debug()`/`logger.log()`/`logger.info()` > calls (see "Renderer Logger" below), you will get silence — build from > source in development mode instead, or add temporary `rawConsole.log()` > calls (see "Restoring Logs" below), which bypass this entirely. > > The env vars below genuinely do work for the **separate** console-filter > mechanism (raw, un-wrapped `console.log`/`console.info` calls) — but only > in the **main process**. The renderer is sandboxed (`contextIsolation`, > no `nodeIntegration`) and has no `process` global, so the filter's own > env-var checks can never fire there; and any raw console call that > survives esbuild's `pure` stripping in a renderer production build is > vanishingly rare to begin with. Either way, they never affect this > renderer logger's own methods. ### Method 1: CLIODESK_DEBUG Environment Variable ```bash # macOS / Linux CLIODESK_DEBUG=1 /path/to/ClioDeck.app/Contents/MacOS/ClioDeck # Windows set CLIODESK_DEBUG=1 "C:\Program Files\ClioDeck\ClioDeck.exe" ``` ### Method 2: CLIODESK_LOG_LEVEL Environment Variable ```bash # Available levels: debug, info, warn, error CLIODESK_LOG_LEVEL=debug /path/to/ClioDeck ``` ### Method 3: Standard DEBUG Variable ```bash DEBUG=1 /path/to/ClioDeck ``` ## DevTools in Production By default, Electron DevTools are **disabled** in production. To enable them, `src/main/index.ts` checks `CLIODESK_DEBUG=1` or `DEBUG=1` specifically — **not** `CLIODESK_LOG_LEVEL`, which only affects the console filter (see above), not this check: ```bash # macOS / Linux CLIODESK_DEBUG=1 /path/to/ClioDeck.app/Contents/MacOS/ClioDeck # Windows set CLIODESK_DEBUG=1 "C:\Program Files\ClioDeck\ClioDeck.exe" ``` This opens the DevTools window on the renderer's `webContents` — but that window will **not** show revived renderer `console.log`/`info`/`debug` calls. Those were already removed from the production renderer bundle at build time by esbuild's `pure` stripping, regardless of any env var, so there's nothing left for DevTools to display on that front. What `CLIODESK_DEBUG=1` genuinely does, in addition to opening DevTools, is un-suppress raw `console.log`/`info` calls in the **main process** (the ~40+ files under `src/main/` that call `console.log` directly rather than through the structured main-process logger) — those print to the terminal you launched ClioDeck from, not to the DevTools panel. ## Renderer Logger (for Developers) For renderer code, use `src/renderer/src/utils/logger.ts` — this is the one actually imported throughout the renderer, not `@shared/logger`: ```typescript import { logger } from '../utils/logger'; // path relative to caller // Variadic, mirrors console.* — stripped in production (both by esbuild's // `pure` annotation on console.log/info/debug in vite.config.mts, and by // an explicit no-op gate inside the module itself) logger.log('Debug message', { data }); logger.info('Important information'); logger.debug('Verbose detail'); // warn/error always emit, in prod too — diagnostic surface for triage logger.warn('Warning'); logger.error('MyComponent', 'Something failed', error); // Typed channels, kept for existing call sites logger.ipc('fs:read-directory', { dirPath }); logger.store('similarityStore', 'analyze', { segments: 12 }); logger.component('SimilarityCard', 'Inserting citation', { zoteroKey }); ``` ### Renderer Logger Benefits - `log`/`info`/`debug` are compiled out of the production bundle entirely (not just hidden — removed), so expensive argument construction never runs in prod either - `warn`/`error` always reach the console, in every environment - Typed channel helpers (`ipc`/`store`/`component`/`error`) give consistent, greppable prefixes without extra ceremony ## Main-Process Logger (for Developers) For code under `src/main/` (IPC handlers, services), use the separate main-process logger instead: ```typescript import { logger } from '../../utils/logger.js'; logger.info('ipc', 'fs:read-directory', { dirPath }); logger.error('ipc', 'fs:read-directory', { error: error.message }); // Measuring duration: const elapsed = logger.startTimer(); await someWork(); logger.info('ipc', 'pdf:index', { durationMs: elapsed() }); ``` Output: `[2026-02-19T20:00:00.000Z] INFO [ipc] fs:read-directory { dirPath: "/home/user" }`. Level defaults to `info` and **cannot currently be changed at runtime** — see the caveat above. ## Architecture ``` src/shared/ ├── logger.ts # Dead code — no import anywhere outside this file └── console-filter.ts # Global console.* filter, env-var-controlled — # only actually live in the main process; the # renderer's copy never engages (see Overview above) src/renderer/src/utils/ └── logger.ts # Real renderer logger — what every renderer import resolves to src/main/utils/ └── logger.ts # Main-process logger, fixed at 'info', no env var ``` The console filter is automatically imported at application startup: - Main process: `src/main/index.ts` - Renderer process: `src/renderer/src/main.tsx` ## Environment Detection Environment is automatically detected via: 1. `process.env.NODE_ENV === 'production'` 2. `process.env.ELECTRON_IS_PACKAGED === 'true'` Both checks start with `typeof process !== 'undefined'` (`isProduction()` in `console-filter.ts`). As explained above, that's only ever true in the **main process** — the sandboxed renderer has no `process` global, so this detection always resolves to "not production" there, regardless of whether the app is actually a packaged build or not. ## Restoring Logs (for Testing) ```typescript import { restoreConsole, rawConsole } from '@shared/console-filter'; // Restore all console.* restoreConsole(); // Or use rawConsole to bypass filter rawConsole.log('This message will always be displayed'); ``` ## Checking Filter Status ```typescript import { getFilterStatus } from '@shared/console-filter'; const status = getFilterStatus(); console.log(status); // { isProduction: true, isDebugEnabled: false, isFiltering: true } ``` This example result is only reachable from **main-process** code in a packaged build. Calling `getFilterStatus()` from the renderer always returns `{ isProduction: false, isDebugEnabled: false, isFiltering: false }` — regardless of whether the build is actually production or not, for the same `process`-is-undefined reason covered above.