@@ -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.
32973303const 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+
33013309function 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
1064810667function 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.
1074910810async 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
1149911563function 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