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.
+
+
- **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 @@
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;
- }}
-/>
-
-
{}}
->
- {#each MENUS as menu (menu.id)}
-
-
-
- {#if open === menu.id}
-
- {#each menu.groups as group, index (index)}
- {#if index > 0}
-
- {/if}
- {#each group as item (item.id)}
-
- {/each}
- {/each}
-
- {/if}
-
- {/each}
-
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 @@
>