Skip to content

Commit 7c1a4ea

Browse files
ozgesolidkeyclaude
andcommitted
Add fatal/verbose log levels and quick level visibility toggle bar
- 7 log levels: fatal > error > warning > info > debug > verbose > trace - Level visibility bar in the status bar: click F/E/W/I/D/V/T/? pills to instantly hide/show any level without opening the filter dialog — state persists in localStorage - Fatal density tracked in minimap canvas (magenta color); verbose level color added (#5a8a6a) - Filter modal now includes fatal and verbose checkboxes; level pills sync with hidden levels when modal opens - Advanced filter level dropdown includes all 7 levels Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5472fbe commit 7c1a4ea

5 files changed

Lines changed: 183 additions & 23 deletions

File tree

src/main/analyzers/columnAwareAnalyzer.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,13 @@ export class ColumnAwareAnalyzer implements LogAnalyzer {
6262

6363
// Data collection
6464
const levelCounts: Record<string, number> = {
65-
error: 0, warning: 0, info: 0, debug: 0, trace: 0
65+
fatal: 0, error: 0, warning: 0, info: 0, debug: 0, verbose: 0, trace: 0
6666
};
6767

6868
// Density buckets — adaptive count based on file size (500–10,000)
6969
// Used by the minimap to draw a heat map by log level
7070
const DENSITY_BUCKETS = Math.min(Math.max(Math.ceil(fileSize / 500), 500), 10000);
71+
const densityFatal = new Uint32Array(DENSITY_BUCKETS);
7172
const densityError = new Uint32Array(DENSITY_BUCKETS);
7273
const densityWarning = new Uint32Array(DENSITY_BUCKETS);
7374
const densityInfo = new Uint32Array(DENSITY_BUCKETS);
@@ -130,7 +131,8 @@ export class ColumnAwareAnalyzer implements LogAnalyzer {
130131
levelCounts[level]++;
131132
// Update density bucket based on byte position
132133
const bucket = Math.min(DENSITY_BUCKETS - 1, Math.floor((bytesRead / fileSize) * DENSITY_BUCKETS));
133-
if (level === 'error') densityError[bucket]++;
134+
if (level === 'fatal') densityFatal[bucket]++;
135+
else if (level === 'error') densityError[bucket]++;
134136
else if (level === 'warning') densityWarning[bucket]++;
135137
else if (level === 'info') densityInfo[bucket]++;
136138
}
@@ -259,6 +261,7 @@ export class ColumnAwareAnalyzer implements LogAnalyzer {
259261
insights,
260262
density: {
261263
buckets: DENSITY_BUCKETS,
264+
fatal: Array.from(densityFatal),
262265
error: Array.from(densityError),
263266
warning: Array.from(densityWarning),
264267
info: Array.from(densityInfo),
@@ -352,21 +355,25 @@ export class ColumnAwareAnalyzer implements LogAnalyzer {
352355
}
353356

354357
private normalizeLevel(rawLevel: string): string | null {
355-
if (/^(error|fatal|critical|severe)$/.test(rawLevel)) return 'error';
356-
if (/^(warn|warning)$/.test(rawLevel)) return 'warning';
357-
if (/^(info|information)$/.test(rawLevel)) return 'info';
358-
if (/^debug$/.test(rawLevel)) return 'debug';
359-
if (/^(trace|verbose)$/.test(rawLevel)) return 'trace';
358+
if (/^(fatal|panic|emergency|alert|emerg)$/.test(rawLevel)) return 'fatal';
359+
if (/^(error|critical|severe|crit|exception)$/.test(rawLevel)) return 'error';
360+
if (/^(warn|warning|notice)$/.test(rawLevel)) return 'warning';
361+
if (/^(info|information|informational)$/.test(rawLevel)) return 'info';
362+
if (/^(debug|dbg|d)$/.test(rawLevel)) return 'debug';
363+
if (/^(verbose|verb|v|fine|finer)$/.test(rawLevel)) return 'verbose';
364+
if (/^(trace|finest|silly)$/.test(rawLevel)) return 'trace';
360365
return null;
361366
}
362367

363368
private detectLevelFromText(text: string): string | null {
364369
const upper = (text.length > 200 ? text.substring(0, 200) : text).toUpperCase();
365-
if (/\b(ERROR|FATAL|CRITICAL|EXCEPTION|PANIC)\b/.test(upper)) return 'error';
366-
if (/\b(WARN|WARNING)\b/.test(upper)) return 'warning';
370+
if (/\b(FATAL|PANIC|EMERGENCY|EMERG)\b/.test(upper)) return 'fatal';
371+
if (/\b(ERROR|CRITICAL|CRIT|EXCEPTION)\b/.test(upper)) return 'error';
372+
if (/\b(WARN|WARNING|NOTICE)\b/.test(upper)) return 'warning';
367373
if (/\b(INFO)\b/.test(upper)) return 'info';
368-
if (/\b(DEBUG)\b/.test(upper)) return 'debug';
369-
if (/\b(TRACE|VERBOSE)\b/.test(upper)) return 'trace';
374+
if (/\b(DEBUG|DBG)\b/.test(upper)) return 'debug';
375+
if (/\b(VERBOSE|VERB)\b/.test(upper)) return 'verbose';
376+
if (/\b(TRACE)\b/.test(upper)) return 'trace';
370377
return null;
371378
}
372379

src/main/analyzers/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export interface AnalysisResult {
6262
// Density buckets for minimap heat map (indexed by file byte position)
6363
density?: {
6464
buckets: number;
65+
fatal: number[];
6566
error: number[];
6667
warning: number[];
6768
info: number[];

src/renderer/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,16 @@ <h2>LOGAN</h2>
704704
</div>
705705
</div>
706706
<div class="status-right">
707+
<div id="level-visibility-bar" class="level-vis-bar hidden">
708+
<button class="lv-pill" data-level="fatal" title="fatal — click to hide">F</button>
709+
<button class="lv-pill" data-level="error" title="error — click to hide">E</button>
710+
<button class="lv-pill" data-level="warning" title="warning — click to hide">W</button>
711+
<button class="lv-pill" data-level="info" title="info — click to hide">I</button>
712+
<button class="lv-pill" data-level="debug" title="debug — click to hide">D</button>
713+
<button class="lv-pill" data-level="verbose" title="verbose — click to hide">V</button>
714+
<button class="lv-pill" data-level="trace" title="trace — click to hide">T</button>
715+
<button class="lv-pill" data-level="other" title="other — click to hide">?</button>
716+
</div>
707717
<span id="status-local-storage" class="status-local-storage hidden" title="Local .logan/ storage status"></span>
708718
<span id="status-cursor">Ln 1, Col 1</span>
709719
<span class="status-separator">|</span>
@@ -738,10 +748,12 @@ <h3>Filter Settings</h3>
738748
<div class="filter-group">
739749
<label>Log Levels</label>
740750
<div class="checkbox-group">
751+
<label class="checkbox-label"><input type="checkbox" name="level" value="fatal" checked> Fatal</label>
741752
<label class="checkbox-label"><input type="checkbox" name="level" value="error" checked> Error</label>
742753
<label class="checkbox-label"><input type="checkbox" name="level" value="warning" checked> Warning</label>
743754
<label class="checkbox-label"><input type="checkbox" name="level" value="info" checked> Info</label>
744755
<label class="checkbox-label"><input type="checkbox" name="level" value="debug"> Debug</label>
756+
<label class="checkbox-label"><input type="checkbox" name="level" value="verbose"> Verbose</label>
745757
<label class="checkbox-label"><input type="checkbox" name="level" value="trace"> Trace</label>
746758
<label class="checkbox-label"><input type="checkbox" name="level" value="other" checked> Other</label>
747759
</div>

src/renderer/renderer.ts

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ interface AnalysisResult {
7979
insights: AnalysisInsights;
8080
density?: {
8181
buckets: number;
82+
fatal: number[];
8283
error: number[];
8384
warning: number[];
8485
info: number[];
@@ -316,6 +317,9 @@ interface AppState {
316317
contextViewMode: 'tree' | 'lanes';
317318
contextGroupMode: 'separate' | 'combined';
318319
editingContextId: string | null;
320+
// Level visibility quick-toggle (persisted globally)
321+
hiddenLevels: Set<string>;
322+
levelBarFilterActive: boolean;
319323
// Traceback
320324
tracebackResult: any | null;
321325
tracebackSort: 'time' | 'score';
@@ -383,6 +387,8 @@ const state: AppState = {
383387
contextViewMode: 'tree' as 'tree' | 'lanes',
384388
contextGroupMode: 'separate' as 'separate' | 'combined',
385389
editingContextId: null,
390+
hiddenLevels: new Set<string>(JSON.parse(localStorage.getItem('logan-hidden-levels') || '[]')),
391+
levelBarFilterActive: false,
386392
tracebackResult: null,
387393
tracebackSort: 'time' as 'time' | 'score',
388394
tracebackFilterCat: 'all',
@@ -3295,9 +3301,11 @@ function escapeRegex(str: string): string {
32953301

32963302
// Severity rank used for worst-case sampling and density strip coloring.
32973303
const LEVEL_SEVERITY: Record<string, number> = {
3298-
fatal: 6, error: 5, warning: 4, warn: 4, info: 3, debug: 2, trace: 1,
3304+
fatal: 7, error: 6, warning: 5, warn: 5, info: 4, debug: 3, verbose: 2, trace: 1,
32993305
};
33003306

3307+
const ALL_LOG_LEVELS = ['fatal', 'error', 'warning', 'info', 'debug', 'verbose', 'trace', 'other'] as const;
3308+
33013309
function updateMinimapViewport(): void {
33023310
if (!logViewerElement || !minimapViewportElement || !minimapElement) return;
33033311

@@ -3541,30 +3549,37 @@ function renderMinimapCanvas(): void {
35413549
return;
35423550
}
35433551

3544-
const { buckets, error, warning, info } = density;
3552+
const { buckets, fatal: fatalArr, error, warning, info } = density;
3553+
const fatalData = fatalArr || [];
35453554

35463555
// Global max for brightness normalisation
35473556
let globalMax = 1;
35483557
for (let i = 0; i < buckets; i++) {
3549-
const t = error[i] + warning[i] + info[i];
3558+
const t = (fatalData[i] || 0) + error[i] + warning[i] + info[i];
35503559
if (t > globalMax) globalMax = t;
35513560
}
35523561

35533562
const pixels = new Uint8ClampedArray(cw * ch * 4);
35543563

35553564
for (let py = 0; py < ch; py++) {
35563565
const bi = Math.min(buckets - 1, Math.floor((py / ch) * buckets));
3566+
const f = fatalData[bi] || 0;
35573567
const e = error[bi] || 0;
35583568
const w = warning[bi] || 0;
35593569
const inf = info[bi] || 0;
3560-
const total = e + w + inf;
3570+
const total = f + e + w + inf;
35613571

35623572
// Base: very dark
35633573
let r = 18, g = 18, b = 26;
35643574

35653575
if (total > 0) {
35663576
const brightness = Math.min(1, Math.sqrt(total / globalMax));
35673577
const denom = Math.max(total, 1);
3578+
// Fatal: magenta (highest priority — drawn first, overrides error)
3579+
const fr = (f / denom) * brightness;
3580+
r = Math.min(255, r + fr * 224);
3581+
g = Math.min(255, g + fr * 18);
3582+
b = Math.min(255, b + fr * 251);
35683583
// Error: crimson
35693584
const er = (e / denom) * brightness;
35703585
r = Math.min(255, r + er * 210);
@@ -10643,6 +10658,10 @@ function showFilterModal(): void {
1064310658
addIncludePatternRow();
1064410659
}
1064510660
renderFilterPresets();
10661+
// Sync level checkboxes with current hidden levels
10662+
document.querySelectorAll<HTMLInputElement>('input[name="level"]').forEach(cb => {
10663+
cb.checked = !state.hiddenLevels.has(cb.value);
10664+
});
1064610665
}
1064710666

1064810667
function hideFilterModal(): void {
@@ -10734,6 +10753,7 @@ async function clearFilter(): Promise<void> {
1073410753
state.appliedFilterSuggestion = null;
1073510754
state.lastFilterConfig = null;
1073610755
state.filterSuspended = null;
10756+
state.levelBarFilterActive = false;
1073710757
cachedLines.clear();
1073810758

1073910759
await window.api.clearFilter();
@@ -10744,6 +10764,47 @@ async function clearFilter(): Promise<void> {
1074410764
updateLevelBadgeStyles();
1074510765
}
1074610766

10767+
// ─── Level Visibility Quick-Toggle ──────────────────────────────────────────
10768+
10769+
function saveHiddenLevels(): void {
10770+
localStorage.setItem('logan-hidden-levels', JSON.stringify([...state.hiddenLevels]));
10771+
}
10772+
10773+
function updateLevelVisibilityBar(): void {
10774+
const bar = document.getElementById('level-visibility-bar');
10775+
if (!bar) return;
10776+
bar.querySelectorAll<HTMLButtonElement>('.lv-pill').forEach(btn => {
10777+
const level = btn.dataset.level!;
10778+
const hidden = state.hiddenLevels.has(level);
10779+
btn.classList.toggle('hidden-level', hidden);
10780+
btn.title = hidden ? `${level} (hidden — click to show)` : `${level} — click to hide`;
10781+
});
10782+
}
10783+
10784+
async function applyLevelVisibilityFilter(): Promise<void> {
10785+
saveHiddenLevels();
10786+
updateLevelVisibilityBar();
10787+
10788+
if (state.hiddenLevels.size === 0) {
10789+
if (state.levelBarFilterActive) {
10790+
state.levelBarFilterActive = false;
10791+
await clearFilter();
10792+
}
10793+
return;
10794+
}
10795+
10796+
const visibleLevels = (ALL_LOG_LEVELS as readonly string[]).filter(l => !state.hiddenLevels.has(l));
10797+
state.levelBarFilterActive = true;
10798+
await applyFilter({
10799+
levels: visibleLevels,
10800+
includePatterns: [],
10801+
excludePatterns: [],
10802+
matchCase: false,
10803+
exactMatch: false,
10804+
contextLines: 0,
10805+
});
10806+
}
10807+
1074710808
// Suspend: keep config, temporarily show unfiltered view.
1074810809
// reason='auto' (navigation) → badge blinks; reason='manual' → badge solid.
1074910810
async function suspendFilter(reason: 'auto' | 'manual' = 'auto'): Promise<void> {
@@ -10823,6 +10884,7 @@ function updateLevelBadgeStyles(): void {
1082310884
badge.classList.remove('active-filter');
1082410885
}
1082510886
});
10887+
updateLevelVisibilityBar();
1082610888
}
1082710889

1082810890
// === Advanced Filter ===
@@ -10949,7 +11011,7 @@ function renderFilterRuleHTML(groupId: string, rule: FilterRule): string {
1094911011
const isLevelType = rule.type === 'level' || rule.type === 'not_level';
1095011012
const showCaseSensitive = !isLevelType;
1095111013

10952-
const levelOptions = ['error', 'warning', 'info', 'debug', 'trace']
11014+
const levelOptions = ['fatal', 'error', 'warning', 'info', 'debug', 'verbose', 'trace']
1095311015
.map(l => `<option value="${l}" ${rule.value === l ? 'selected' : ''}>${l}</option>`)
1095411016
.join('');
1095511017

@@ -11455,29 +11517,31 @@ function renderCompareHeader(): void {
1145511517
});
1145611518
}
1145711519

11458-
function renderCompareCanvas(id: string, density: { buckets: number; error: number[]; warning: number[]; info: number[] }): void {
11520+
function renderCompareCanvas(id: string, density: { buckets: number; fatal?: number[]; error: number[]; warning: number[]; info: number[] }): void {
1145911521
const canvas = document.getElementById(id) as HTMLCanvasElement | null;
1146011522
if (!canvas) return;
1146111523
const dpr = window.devicePixelRatio || 1;
1146211524
const cw = Math.floor(canvas.offsetWidth * dpr) || Math.round(30 * dpr);
1146311525
const ch = Math.floor(canvas.offsetHeight * dpr) || Math.round(120 * dpr);
1146411526
canvas.width = cw; canvas.height = ch;
1146511527
const ctx = canvas.getContext('2d')!;
11466-
const { buckets, error, warning, info } = density;
11528+
const { buckets, fatal: fatalArr, error, warning, info } = density;
11529+
const fD = fatalArr || [];
1146711530
let globalMax = 1;
1146811531
for (let i = 0; i < buckets; i++) {
11469-
const t = error[i] + warning[i] + info[i];
11532+
const t = (fD[i]||0) + error[i] + warning[i] + info[i];
1147011533
if (t > globalMax) globalMax = t;
1147111534
}
1147211535
const pixels = new Uint8ClampedArray(cw * ch * 4);
1147311536
for (let py = 0; py < ch; py++) {
1147411537
const bi = Math.min(buckets - 1, Math.floor((py / ch) * buckets));
11475-
const e = error[bi] || 0, w = warning[bi] || 0, inf = info[bi] || 0;
11476-
const total = e + w + inf;
11538+
const f = fD[bi]||0, e = error[bi] || 0, w = warning[bi] || 0, inf = info[bi] || 0;
11539+
const total = f + e + w + inf;
1147711540
let r = 18, g = 18, b = 26;
1147811541
if (total > 0) {
1147911542
const br = Math.min(1, Math.sqrt(total / globalMax));
1148011543
const d = Math.max(total, 1);
11544+
r = Math.min(255, r + (f/d)*br*224); g = Math.min(255, g + (f/d)*br*18); b = Math.min(255, b + (f/d)*br*251);
1148111545
r = Math.min(255, r + (e/d)*br*210);
1148211546
g = Math.min(255, g + (e/d)*br*28);
1148311547
b = Math.min(255, b + (e/d)*br*28);
@@ -11497,8 +11561,8 @@ function renderCompareCanvas(id: string, density: { buckets: number; error: numb
1149711561
}
1149811562

1149911563
function renderDiffCanvas(id: string,
11500-
densA: { buckets: number; error: number[]; warning: number[]; info: number[] },
11501-
densB: { buckets: number; error: number[]; warning: number[]; info: number[] }
11564+
densA: { buckets: number; fatal?: number[]; error: number[]; warning: number[]; info: number[] },
11565+
densB: { buckets: number; fatal?: number[]; error: number[]; warning: number[]; info: number[] }
1150211566
): void {
1150311567
const canvas = document.getElementById(id) as HTMLCanvasElement | null;
1150411568
if (!canvas) return;
@@ -12597,12 +12661,16 @@ function updateStatusBar(): void {
1259712661
badge.classList.add('hidden');
1259812662
badge.classList.remove('suspended', 'suspended-auto', 'suspended-manual');
1259912663
}
12664+
// Show level visibility bar when file is open
12665+
document.getElementById('level-visibility-bar')?.classList.remove('hidden');
12666+
updateLevelVisibilityBar();
1260012667
} else {
1260112668
elements.statusFile.textContent = 'No file';
1260212669
elements.statusLines.textContent = '0 lines';
1260312670
elements.statusSize.textContent = '0 B';
1260412671
elements.statusFiltered.classList.add('hidden');
1260512672
elements.statusFiltered.classList.remove('suspended', 'suspended-auto', 'suspended-manual');
12673+
document.getElementById('level-visibility-bar')?.classList.add('hidden');
1260612674
}
1260712675
}
1260812676

@@ -14587,6 +14655,20 @@ function init(): void {
1458714655
}
1458814656
});
1458914657
document.getElementById('btn-status-clear-filter')?.addEventListener('click', clearFilter);
14658+
14659+
// Level visibility quick-toggle
14660+
document.getElementById('level-visibility-bar')?.addEventListener('click', async (e) => {
14661+
const pill = (e.target as HTMLElement).closest<HTMLButtonElement>('.lv-pill');
14662+
if (!pill || !pill.dataset.level) return;
14663+
const level = pill.dataset.level;
14664+
if (state.hiddenLevels.has(level)) {
14665+
state.hiddenLevels.delete(level);
14666+
} else {
14667+
state.hiddenLevels.add(level);
14668+
}
14669+
await applyLevelVisibilityFilter();
14670+
});
14671+
1459014672
elements.btnAddIncludePattern.addEventListener('click', () => addIncludePatternRow());
1459114673

1459214674
elements.btnSaveFilterPreset?.addEventListener('click', async () => {

0 commit comments

Comments
 (0)