diff --git a/Cargo.lock b/Cargo.lock index 90cfc84..e2192a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2457,7 +2457,7 @@ dependencies = [ [[package]] name = "openeventviewer" -version = "0.1.0" +version = "1.0.0" dependencies = [ "chrono", "roxmltree", diff --git a/README.md b/README.md index c98887a..a20347d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ The built-in Event Viewer can answer most of these questions, eventually. This o the two things that actually take the time: narrowing fifty thousand records to the dozen that matter, and seeing when they happened. +![The Events page reading System and Application](docs/events.png) + - **Events** — a virtualised table over up to 50 000 records. Per-column filters that fit each column: tick lists with a search box and a count for level, provider, task, channel and computer; a from/to range for the time; an expression for the ID (`41, 6008`, `>7000`, `7000-7040`, diff --git a/docs/events.png b/docs/events.png new file mode 100644 index 0000000..543cd02 Binary files /dev/null and b/docs/events.png differ diff --git a/package.json b/package.json index f865076..3bf83fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openeventviewer-ui", - "version": "0.1.0", + "version": "1.0.0", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9487fa3..6c46725 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openeventviewer" -version = "0.1.0" +version = "1.0.0" description = "Read the Windows event logs and filter them down to what matters" authors = ["thorstenalpers"] repository = "https://github.com/thorstenalpers/OpenEventViewer" diff --git a/src-tauri/app-icon.svg b/src-tauri/app-icon.svg index 7b60e85..4fbd7fc 100644 --- a/src-tauri/app-icon.svg +++ b/src-tauri/app-icon.svg @@ -1,7 +1,7 @@ OpenEventViewer — a window watching the machine - - - - + + + + diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 84bd9a2..b090aa3 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index 7ae4832..1be2625 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index a5ddac2..de0fdb1 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png index d06f70d..27442b2 100644 Binary files a/src-tauri/icons/64x64.png and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 2fd5ef3..aba0964 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index 0e88db7..37a6802 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index d4a8b89..f5afb5f 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index bc89bbc..97468a8 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index 307eb82..ff3970d 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index f77bf10..d75887c 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index bed00ae..675b396 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index 607dd4e..f668c14 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index 1c0e76a..aa0b07e 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index f0a9f50..e2ac527 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index ebdd114..7940bbb 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index b79b2df..6963370 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index f2ba85a..642367c 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/eventlog.rs b/src-tauri/src/eventlog.rs index 25cdef8..4fc2b25 100644 --- a/src-tauri/src/eventlog.rs +++ b/src-tauri/src/eventlog.rs @@ -16,10 +16,11 @@ use windows::Win32::Foundation::{ ERROR_TIMEOUT, WIN32_ERROR, }; use windows::Win32::System::EventLog::{ - EvtClose, EvtFormatMessage, EvtFormatMessageEvent, EvtFormatMessageTask, EvtNext, - EvtNextChannelPath, EvtOpenChannelEnum, EvtOpenPublisherMetadata, EvtQuery, - EvtQueryChannelPath, EvtQueryReverseDirection, EvtQueryTolerateQueryErrors, EvtRender, - EvtRenderEventXml, EVT_HANDLE, + EvtChannelConfigType, EvtChannelTypeAnalytic, EvtChannelTypeDebug, EvtClose, EvtFormatMessage, + EvtFormatMessageEvent, EvtFormatMessageTask, EvtGetChannelConfigProperty, EvtNext, + EvtNextChannelPath, EvtOpenChannelConfig, EvtOpenChannelEnum, EvtOpenPublisherMetadata, + EvtQuery, EvtQueryChannelPath, EvtQueryReverseDirection, EvtQueryTolerateQueryErrors, + EvtRender, EvtRenderEventXml, EVT_HANDLE, EVT_VARIANT, }; use crate::error::{AppError, AppResult}; @@ -170,13 +171,48 @@ pub fn list_channels() -> AppResult> { let mut buffer = vec![0u16; used as usize]; unsafe { EvtNextChannelPath(enumerator.0, Some(&mut buffer), &mut used) } .map_err(|error| describe(&error, "listing the channels"))?; - channels.push(from_wide(&buffer)); + let channel = from_wide(&buffer); + if queryable(&channel) { + channels.push(channel); + } } channels.sort_unstable(); Ok(channels) } +/// Whether `EvtQuery` will take this channel at all. +/// +/// Analytic and Debug channels answer every query with ERROR_NOT_SUPPORTED, so offering them in +/// the picker sells a query that can only fail. A channel whose type will not say stays offered — +/// wrongly hiding a readable channel is the worse failure. +fn queryable(channel: &str) -> bool { + let path = wide(channel); + let Ok(config) = (unsafe { EvtOpenChannelConfig(None, PCWSTR(path.as_ptr()), 0) }) else { + return true; + }; + let config = Handle(config); + + let mut variant: EVT_VARIANT = unsafe { std::mem::zeroed() }; + let mut used = 0u32; + let read = unsafe { + EvtGetChannelConfigProperty( + config.0, + EvtChannelConfigType, + 0, + std::mem::size_of::() as u32, + Some(&mut variant), + &mut used, + ) + }; + if read.is_err() { + return true; + } + + let kind = unsafe { variant.Anonymous.UInt32Val } as i32; + kind != EvtChannelTypeAnalytic.0 && kind != EvtChannelTypeDebug.0 +} + pub fn query(filter: &Filter) -> AppResult { let started = std::time::Instant::now(); let wanted = filter.max.clamp(1, MAX_EVENTS); @@ -1061,6 +1097,20 @@ mod tests { assert!(channels.iter().any(|name| name == "System")); assert!(channels.iter().any(|name| name == "Application")); + // A Debug or Analytic name is only a convention — some such channels are declared + // Operational and answer fine. What must hold is that everything still on offer answers. + for name in channels + .iter() + .filter(|name| name.ends_with("/Debug") || name.ends_with("/Analytic")) + { + let filter = Filter { + channels: vec![name.clone()], + max: 1, + ..Filter::default() + }; + query(&filter) + .unwrap_or_else(|error| panic!("{name} is offered but refuses a query: {error:?}")); + } println!("{} channels", channels.len()); } } diff --git a/src/app.css b/src/app.css index a106af3..c687324 100644 --- a/src/app.css +++ b/src/app.css @@ -10,8 +10,8 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.52 0.22 262); - --primary-foreground: oklch(0.99 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); @@ -24,7 +24,7 @@ --warning: oklch(0.72 0.16 75); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); - --ring: oklch(0.52 0.22 262); + --ring: oklch(0.708 0 0); --radius: 0.75rem; --sidebar: oklch(0.985 0 0); @@ -44,8 +44,8 @@ --card-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.66 0.19 262); - --primary-foreground: oklch(0.15 0.02 264); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); @@ -58,7 +58,7 @@ --warning: oklch(0.8 0.16 80); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); - --ring: oklch(0.66 0.19 262); + --ring: oklch(0.556 0 0); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); diff --git a/src/lib/components/app-menu.svelte b/src/lib/components/app-menu.svelte deleted file mode 100644 index 1ae6cc5..0000000 --- a/src/lib/components/app-menu.svelte +++ /dev/null @@ -1,119 +0,0 @@ - - - { - const target = event.target as HTMLElement | null; - if (open && !target?.closest('[data-menu]')) open = null; - }} - onkeydown={(event: KeyboardEvent) => { - if (event.key === 'Escape') open = null; - }} -/> - - diff --git a/src/lib/components/events-histogram.svelte b/src/lib/components/events-histogram.svelte index adb97e1..0f28529 100644 --- a/src/lib/components/events-histogram.svelte +++ b/src/lib/components/events-histogram.svelte @@ -6,10 +6,12 @@ interface Props { events: EventRecord[]; + /** Pins the axis to the time filter's window; an open end falls back to the events. */ + span?: { from?: number; to?: number }; class?: string; } - let { events, class: className }: Props = $props(); + let { events, span = {}, class: className }: Props = $props(); const t = $derived(i18n.t); @@ -19,7 +21,7 @@ let width = $state(0); // One bar per ~9 px, so the chart is denser on a wide window rather than stretched. const columns = $derived(Math.max(12, Math.min(160, Math.floor(width / 9) || 60))); - const chart = $derived(histogram(events, columns)); + const chart = $derived(histogram(events, columns, span)); const hovered = $state<{ index: number | null; x: number; y: number }>({ index: null, x: 0, @@ -49,22 +51,26 @@ } /** - * How tall one bar stands, and how much of it is red. + * How tall one bar stands, and how much of it is red and amber. * * The bar is scaled by a square root — one spike of ten thousand would otherwise flatten every * other bar to a single pixel, and the quiet stretches are the interesting part. The split - * inside it is then a straight proportion: scaling the two segments separately would draw four + * inside it is then a straight proportion: scaling the segments separately would draw four * errors out of ten as two thirds of the bar. */ - function bar(entry: Bucket): { total: number; errors: number } { - if (chart.peak === 0 || entry.total === 0) return { total: 0, errors: 0 }; + function bar(entry: Bucket): { total: number; errors: number; warnings: number } { + if (chart.peak === 0 || entry.total === 0) return { total: 0, errors: 0, warnings: 0 }; const total = Math.max( 2, Math.round((Math.sqrt(entry.total) / Math.sqrt(chart.peak)) * HEIGHT) ); const errors = entry.errors === 0 ? 0 : Math.max(1, Math.round((entry.errors / entry.total) * total)); - return { total, errors }; + const warnings = + entry.warnings === 0 + ? 0 + : Math.min(total - errors, Math.max(1, Math.round((entry.warnings / entry.total) * total))); + return { total, errors, warnings }; } function spanOf(entry: Bucket): string { @@ -130,8 +136,9 @@ >
+
{/each} @@ -158,7 +165,7 @@ >

{spanOf(bucket)}

- {t.events.bucketCount(bucket.total, bucket.errors)} + {t.events.bucketCount(bucket.total, bucket.errors, bucket.warnings)}

{#if tally.length}
    diff --git a/src/lib/components/filters/time-filter.svelte b/src/lib/components/filters/time-filter.svelte index f9c41b9..05a9dc2 100644 --- a/src/lib/components/filters/time-filter.svelte +++ b/src/lib/components/filters/time-filter.svelte @@ -28,8 +28,21 @@ : t.events.filters.before(short(range.to)) ); + /** Now, in the wall-clock shape `datetime-local` speaks. */ + function nowLocal(): string { + const now = new Date(); + const pad = (part: number) => String(part).padStart(2, '0'); + return ( + `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` + + `T${pad(now.getHours())}:${pad(now.getMinutes())}` + ); + } + function set(edge: 'from' | 'to', value: string) { const next: TimeRange = { ...range, [edge]: value }; + // A lower bound alone almost always means "since then until now" — write the now down, so + // the reader sees the window they got and can move its end if they meant something else. + if (edge === 'from' && value && !next.to) next.to = nowLocal(); onChange(isEmptyTimeRange(next) ? undefined : next); } diff --git a/src/lib/components/level-badge.svelte b/src/lib/components/level-badge.svelte index d4a3439..7d72975 100644 --- a/src/lib/components/level-badge.svelte +++ b/src/lib/components/level-badge.svelte @@ -1,5 +1,5 @@ -{label} +{label} diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte index c74a3b9..5e0020a 100644 --- a/src/lib/components/ui/badge/badge.svelte +++ b/src/lib/components/ui/badge/badge.svelte @@ -7,6 +7,7 @@ variant: { neutral: 'border-border text-muted-foreground', accent: 'border-primary/40 text-primary', + warning: 'border-warning/50 text-warning', destructive: 'border-destructive/40 text-destructive' } }, diff --git a/src/lib/components/ui/select/select.svelte b/src/lib/components/ui/select/select.svelte index 528544c..1ddf900 100644 --- a/src/lib/components/ui/select/select.svelte +++ b/src/lib/components/ui/select/select.svelte @@ -23,7 +23,7 @@