generated from nhcarrigan/template
feat: add debug console for frontend and backend logs
Added a comprehensive debug console feature that captures and displays logs from both the frontend and backend in a unified interface. Frontend changes: - Created DebugConsole component with real-time log display - Added debugConsoleStore for state management and console capture - Integrated console into main layout - Added toggle button in StatusBar with console icon - Implemented Ctrl+` keyboard shortcut to open/close console - Features: log filtering by level, auto-scroll, timestamps, colour-coding Backend changes: - Added tracing and tracing-subscriber dependencies - Created custom TauriLogLayer to emit Rust logs to frontend - Integrated tracing subscriber in lib.rs setup - Logs are forwarded via Tauri events (debug:log) Key features: - Circular buffer (max 1000 logs) prevents memory issues - Frontend logs captured via console method overrides - Backend logs forwarded from Rust tracing layer - Log level filtering (debug, info, warn, error, all) - Source badges distinguish frontend vs backend logs - Colour-coded log levels for easy identification - Auto-scroll toggle for inspecting older logs - Clear logs button for resetting the console - Beautiful dark-themed UI matching app aesthetic Closes #126
This commit is contained in:
Generated
+74
@@ -1658,6 +1658,8 @@ dependencies = [
|
|||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
"uuid 1.19.0",
|
"uuid 1.19.0",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
@@ -2245,6 +2247,15 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matches"
|
name = "matches"
|
||||||
version = "0.1.10"
|
version = "0.1.10"
|
||||||
@@ -2401,6 +2412,15 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -3879,6 +3899,15 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shared_child"
|
name = "shared_child"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -4695,6 +4724,15 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiff"
|
name = "tiff"
|
||||||
version = "0.10.3"
|
version = "0.10.3"
|
||||||
@@ -4986,6 +5024,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||||
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex-automata",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5167,6 +5235,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ semver = "1"
|
|||||||
chrono = { version = "0.4.43", features = ["serde"] }
|
chrono = { version = "0.4.43", features = ["serde"] }
|
||||||
discord-rich-presence = "0.2"
|
discord-rich-presence = "0.2"
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.62", features = [
|
windows = { version = "0.62", features = [
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
use tracing::{Level, Subscriber};
|
||||||
|
use tracing_subscriber::layer::{Context, Layer};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DebugLogEvent {
|
||||||
|
pub level: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TauriLogLayer {
|
||||||
|
app: Arc<AppHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TauriLogLayer {
|
||||||
|
pub fn new(app: AppHandle) -> Self {
|
||||||
|
Self {
|
||||||
|
app: Arc::new(app),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Layer<S> for TauriLogLayer
|
||||||
|
where
|
||||||
|
S: Subscriber,
|
||||||
|
{
|
||||||
|
fn on_event(
|
||||||
|
&self,
|
||||||
|
event: &tracing::Event<'_>,
|
||||||
|
_ctx: Context<'_, S>,
|
||||||
|
) {
|
||||||
|
let metadata = event.metadata();
|
||||||
|
let level = match *metadata.level() {
|
||||||
|
Level::ERROR => "error",
|
||||||
|
Level::WARN => "warn",
|
||||||
|
Level::INFO => "info",
|
||||||
|
Level::DEBUG => "debug",
|
||||||
|
Level::TRACE => "debug",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract message from the event
|
||||||
|
struct MessageVisitor {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tracing::field::Visit for MessageVisitor {
|
||||||
|
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||||
|
if field.name() == "message" {
|
||||||
|
self.message = format!("{:?}", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut visitor = MessageVisitor {
|
||||||
|
message: String::new(),
|
||||||
|
};
|
||||||
|
event.record(&mut visitor);
|
||||||
|
|
||||||
|
// If we couldn't extract a message, try to format the whole event
|
||||||
|
if visitor.message.is_empty() {
|
||||||
|
visitor.message = metadata.name().to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip quotes from the message
|
||||||
|
let message = visitor.message.trim_matches('"').to_string();
|
||||||
|
|
||||||
|
let log_event = DebugLogEvent {
|
||||||
|
level: level.to_string(),
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit to frontend
|
||||||
|
let _ = self.app.emit("debug:log", log_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-1
@@ -4,6 +4,7 @@ mod clipboard;
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod cost_tracking;
|
mod cost_tracking;
|
||||||
|
mod debug_logger;
|
||||||
mod discord_rpc;
|
mod discord_rpc;
|
||||||
mod git;
|
mod git;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
@@ -24,6 +25,7 @@ use bridge_manager::create_shared_bridge_manager;
|
|||||||
use clipboard::*;
|
use clipboard::*;
|
||||||
use commands::load_saved_achievements;
|
use commands::load_saved_achievements;
|
||||||
use commands::*;
|
use commands::*;
|
||||||
|
use debug_logger::TauriLogLayer;
|
||||||
use discord_rpc::DiscordRpcManager;
|
use discord_rpc::DiscordRpcManager;
|
||||||
use git::*;
|
use git::*;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
@@ -33,6 +35,8 @@ use snippets::*;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use temp_manager::create_shared_temp_manager;
|
use temp_manager::create_shared_temp_manager;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
use tray::{setup_tray, should_minimize_to_tray};
|
use tray::{setup_tray, should_minimize_to_tray};
|
||||||
use vbs_notification::*;
|
use vbs_notification::*;
|
||||||
use windows_toast::*;
|
use windows_toast::*;
|
||||||
@@ -58,6 +62,13 @@ pub fn run() {
|
|||||||
.manage(temp_manager.clone())
|
.manage(temp_manager.clone())
|
||||||
.manage(discord_rpc.clone())
|
.manage(discord_rpc.clone())
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
|
// Initialize tracing with custom layer that emits to frontend
|
||||||
|
let tauri_layer = TauriLogLayer::new(app.handle().clone());
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.with(tauri_layer)
|
||||||
|
.init();
|
||||||
|
|
||||||
// Initialize the app handle in the bridge manager
|
// Initialize the app handle in the bridge manager
|
||||||
bridge_manager.lock().set_app_handle(app.handle().clone());
|
bridge_manager.lock().set_app_handle(app.handle().clone());
|
||||||
|
|
||||||
@@ -67,10 +78,12 @@ pub fn run() {
|
|||||||
// Clean up any orphaned temp files from previous sessions
|
// Clean up any orphaned temp files from previous sessions
|
||||||
if let Ok(count) = temp_manager.lock().cleanup_orphaned_files() {
|
if let Ok(count) = temp_manager.lock().cleanup_orphaned_files() {
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
println!("Cleaned up {} orphaned temp files", count);
|
tracing::info!("Cleaned up {} orphaned temp files", count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::info!("Hikari Desktop started successfully");
|
||||||
|
|
||||||
// Set up system tray
|
// Set up system tray
|
||||||
if let Err(e) = setup_tray(app.handle()) {
|
if let Err(e) = setup_tray(app.handle()) {
|
||||||
eprintln!("Failed to set up system tray: {}", e);
|
eprintln!("Failed to set up system tray: {}", e);
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { debugConsoleStore, filteredLogs, type LogLevel } from "$lib/stores/debugConsole";
|
||||||
|
|
||||||
|
let isOpen = $state(false);
|
||||||
|
let logs = $state($filteredLogs);
|
||||||
|
let filterLevel = $state<LogLevel | "all">("all");
|
||||||
|
let autoScroll = $state(true);
|
||||||
|
let logContainerElement: HTMLDivElement | undefined = $state();
|
||||||
|
|
||||||
|
// Watch for log changes and auto-scroll
|
||||||
|
$effect(() => {
|
||||||
|
logs = $filteredLogs;
|
||||||
|
|
||||||
|
// Auto-scroll to bottom when logs change
|
||||||
|
if (autoScroll && logContainerElement) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (logContainerElement) {
|
||||||
|
logContainerElement.scrollTop = logContainerElement.scrollHeight;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Set up console capture and backend listener
|
||||||
|
debugConsoleStore.setupConsoleCapture();
|
||||||
|
debugConsoleStore.setupBackendLogsListener();
|
||||||
|
|
||||||
|
// Subscribe to store
|
||||||
|
const unsubscribe = debugConsoleStore.subscribe((state) => {
|
||||||
|
isOpen = state.isOpen;
|
||||||
|
filterLevel = state.filterLevel;
|
||||||
|
autoScroll = state.autoScroll;
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
debugConsoleStore.restoreConsole();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
debugConsoleStore.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
debugConsoleStore.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFilterChange(level: LogLevel | "all") {
|
||||||
|
debugConsoleStore.setFilterLevel(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAutoScrollToggle() {
|
||||||
|
debugConsoleStore.setAutoScroll(!autoScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(date: Date): string {
|
||||||
|
return date.toLocaleTimeString("en-US", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
fractionalSecondDigits: 3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLevelColor(level: LogLevel): string {
|
||||||
|
switch (level) {
|
||||||
|
case "debug":
|
||||||
|
return "#9CA3AF"; // gray
|
||||||
|
case "info":
|
||||||
|
return "#3B82F6"; // blue
|
||||||
|
case "warn":
|
||||||
|
return "#F59E0B"; // amber
|
||||||
|
case "error":
|
||||||
|
return "#EF4444"; // red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSourceBadgeColor(source: "frontend" | "backend"): string {
|
||||||
|
return source === "frontend" ? "#8B5CF6" : "#10B981"; // purple for frontend, green for backend
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div class="debug-console-overlay">
|
||||||
|
<div class="debug-console">
|
||||||
|
<div class="debug-console-header">
|
||||||
|
<h2>Debug Console</h2>
|
||||||
|
<div class="debug-console-controls">
|
||||||
|
<div class="filter-buttons">
|
||||||
|
<button
|
||||||
|
class="filter-btn"
|
||||||
|
class:active={filterLevel === "all"}
|
||||||
|
onclick={() => handleFilterChange("all")}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="filter-btn"
|
||||||
|
class:active={filterLevel === "debug"}
|
||||||
|
onclick={() => handleFilterChange("debug")}
|
||||||
|
style="color: {getLevelColor('debug')}"
|
||||||
|
>
|
||||||
|
Debug
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="filter-btn"
|
||||||
|
class:active={filterLevel === "info"}
|
||||||
|
onclick={() => handleFilterChange("info")}
|
||||||
|
style="color: {getLevelColor('info')}"
|
||||||
|
>
|
||||||
|
Info
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="filter-btn"
|
||||||
|
class:active={filterLevel === "warn"}
|
||||||
|
onclick={() => handleFilterChange("warn")}
|
||||||
|
style="color: {getLevelColor('warn')}"
|
||||||
|
>
|
||||||
|
Warn
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="filter-btn"
|
||||||
|
class:active={filterLevel === "error"}
|
||||||
|
onclick={() => handleFilterChange("error")}
|
||||||
|
style="color: {getLevelColor('error')}"
|
||||||
|
>
|
||||||
|
Error
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="auto-scroll-btn"
|
||||||
|
class:active={autoScroll}
|
||||||
|
onclick={handleAutoScrollToggle}
|
||||||
|
>
|
||||||
|
{autoScroll ? "🔒" : "🔓"} Auto-scroll
|
||||||
|
</button>
|
||||||
|
<button class="clear-btn" onclick={handleClear}> 🗑️ Clear </button>
|
||||||
|
<button class="close-btn" onclick={handleClose}> ✕ </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="debug-console-content" bind:this={logContainerElement}>
|
||||||
|
{#if logs.length === 0}
|
||||||
|
<div class="empty-state">No logs yet...</div>
|
||||||
|
{:else}
|
||||||
|
{#each logs as log (log.id)}
|
||||||
|
<div class="log-entry" data-level={log.level}>
|
||||||
|
<span class="log-timestamp">{formatTimestamp(log.timestamp)}</span>
|
||||||
|
<span class="log-level" style="color: {getLevelColor(log.level)}">
|
||||||
|
[{log.level.toUpperCase()}]
|
||||||
|
</span>
|
||||||
|
<span class="log-source" style="background-color: {getSourceBadgeColor(log.source)}">
|
||||||
|
{log.source}
|
||||||
|
</span>
|
||||||
|
<span class="log-message">{log.message}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.debug-console-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-console {
|
||||||
|
width: 90%;
|
||||||
|
height: 80%;
|
||||||
|
max-width: 1400px;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-console-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: #252525;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-console-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-console-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active {
|
||||||
|
background-color: #444;
|
||||||
|
border-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-scroll-btn,
|
||||||
|
.clear-btn {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background-color: #333;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-scroll-btn:hover,
|
||||||
|
.clear-btn:hover {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-scroll-btn.active {
|
||||||
|
background-color: #10b981;
|
||||||
|
border-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background-color: #ef4444;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background-color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-console-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #0f0f0f;
|
||||||
|
font-family: "Fira Code", "Consolas", monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px solid #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:hover {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-timestamp {
|
||||||
|
color: #666;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level {
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-source {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
color: #e5e5e5;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
sanitizeForJson,
|
sanitizeForJson,
|
||||||
} from "$lib/utils/conversationUtils";
|
} from "$lib/utils/conversationUtils";
|
||||||
import { updateDiscordRpc, setSkipNextGreeting } from "$lib/tauri";
|
import { updateDiscordRpc, setSkipNextGreeting } from "$lib/tauri";
|
||||||
|
import { debugConsoleStore } from "$lib/stores/debugConsole";
|
||||||
|
|
||||||
const DISCORD_URL = "https://chat.nhcarrigan.com";
|
const DISCORD_URL = "https://chat.nhcarrigan.com";
|
||||||
const DONATE_URL = "https://donate.nhcarrigan.com";
|
const DONATE_URL = "https://donate.nhcarrigan.com";
|
||||||
@@ -507,6 +508,20 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => debugConsoleStore.toggle()}
|
||||||
|
class="p-1 text-gray-500 icon-trans-hover"
|
||||||
|
title="Debug Console (Ctrl+`)"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={configStore.openSidebar}
|
onclick={configStore.openSidebar}
|
||||||
class="p-1 text-gray-500 icon-trans-hover"
|
class="p-1 text-gray-500 icon-trans-hover"
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { writable, derived } from "svelte/store";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
id: string;
|
||||||
|
timestamp: Date;
|
||||||
|
level: LogLevel;
|
||||||
|
message: string;
|
||||||
|
source: "frontend" | "backend";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DebugConsoleState {
|
||||||
|
logs: LogEntry[];
|
||||||
|
isOpen: boolean;
|
||||||
|
maxLogs: number;
|
||||||
|
filterLevel: LogLevel | "all";
|
||||||
|
autoScroll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_LOGS = 1000; // Circular buffer size
|
||||||
|
|
||||||
|
function createDebugConsoleStore() {
|
||||||
|
const { subscribe, update } = writable<DebugConsoleState>({
|
||||||
|
logs: [],
|
||||||
|
isOpen: false,
|
||||||
|
maxLogs: MAX_LOGS,
|
||||||
|
filterLevel: "all",
|
||||||
|
autoScroll: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let logCounter = 0;
|
||||||
|
|
||||||
|
function addLog(level: LogLevel, message: string, source: "frontend" | "backend") {
|
||||||
|
update((state) => {
|
||||||
|
const newLog: LogEntry = {
|
||||||
|
id: `log-${Date.now()}-${logCounter++}`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedLogs = [...state.logs, newLog];
|
||||||
|
|
||||||
|
// Implement circular buffer - remove oldest if exceeding max
|
||||||
|
if (updatedLogs.length > state.maxLogs) {
|
||||||
|
updatedLogs.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state, logs: updatedLogs };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override console methods to capture frontend logs
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log,
|
||||||
|
info: console.info,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error,
|
||||||
|
debug: console.debug,
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupConsoleCapture() {
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
originalConsole.log(...args);
|
||||||
|
addLog("info", args.map((arg) => String(arg)).join(" "), "frontend");
|
||||||
|
};
|
||||||
|
|
||||||
|
console.info = (...args: unknown[]) => {
|
||||||
|
originalConsole.info(...args);
|
||||||
|
addLog("info", args.map((arg) => String(arg)).join(" "), "frontend");
|
||||||
|
};
|
||||||
|
|
||||||
|
console.warn = (...args: unknown[]) => {
|
||||||
|
originalConsole.warn(...args);
|
||||||
|
addLog("warn", args.map((arg) => String(arg)).join(" "), "frontend");
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error = (...args: unknown[]) => {
|
||||||
|
originalConsole.error(...args);
|
||||||
|
addLog("error", args.map((arg) => String(arg)).join(" "), "frontend");
|
||||||
|
};
|
||||||
|
|
||||||
|
console.debug = (...args: unknown[]) => {
|
||||||
|
originalConsole.debug(...args);
|
||||||
|
addLog("debug", args.map((arg) => String(arg)).join(" "), "frontend");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreConsole() {
|
||||||
|
console.log = originalConsole.log;
|
||||||
|
console.info = originalConsole.info;
|
||||||
|
console.warn = originalConsole.warn;
|
||||||
|
console.error = originalConsole.error;
|
||||||
|
console.debug = originalConsole.debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for backend logs
|
||||||
|
async function setupBackendLogsListener() {
|
||||||
|
await listen<{ level: LogLevel; message: string }>("debug:log", (event) => {
|
||||||
|
addLog(event.payload.level, event.payload.message, "backend");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
toggle: () => update((state) => ({ ...state, isOpen: !state.isOpen })),
|
||||||
|
open: () => update((state) => ({ ...state, isOpen: true })),
|
||||||
|
close: () => update((state) => ({ ...state, isOpen: false })),
|
||||||
|
clear: () => update((state) => ({ ...state, logs: [] })),
|
||||||
|
setFilterLevel: (level: LogLevel | "all") =>
|
||||||
|
update((state) => ({ ...state, filterLevel: level })),
|
||||||
|
setAutoScroll: (enabled: boolean) => update((state) => ({ ...state, autoScroll: enabled })),
|
||||||
|
setupConsoleCapture,
|
||||||
|
restoreConsole,
|
||||||
|
setupBackendLogsListener,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const debugConsoleStore = createDebugConsoleStore();
|
||||||
|
|
||||||
|
// Derived store for filtered logs
|
||||||
|
export const filteredLogs = derived(debugConsoleStore, ($debugConsole) => {
|
||||||
|
if ($debugConsole.filterLevel === "all") {
|
||||||
|
return $debugConsole.logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelPriority: Record<LogLevel, number> = {
|
||||||
|
debug: 0,
|
||||||
|
info: 1,
|
||||||
|
warn: 2,
|
||||||
|
error: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const minPriority = levelPriority[$debugConsole.filterLevel];
|
||||||
|
|
||||||
|
return $debugConsole.logs.filter((log) => levelPriority[log.level] >= minPriority);
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
|
import DebugConsole from "$lib/components/DebugConsole.svelte";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
@@ -14,4 +15,5 @@
|
|||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
<DebugConsole />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
import AchievementNotification from "$lib/components/AchievementNotification.svelte";
|
import AchievementNotification from "$lib/components/AchievementNotification.svelte";
|
||||||
import AchievementsPanel from "$lib/components/AchievementsPanel.svelte";
|
import AchievementsPanel from "$lib/components/AchievementsPanel.svelte";
|
||||||
import UpdateNotification from "$lib/components/UpdateNotification.svelte";
|
import UpdateNotification from "$lib/components/UpdateNotification.svelte";
|
||||||
|
import { debugConsoleStore } from "$lib/stores/debugConsole";
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let updateNotification: UpdateNotification | undefined = $state(undefined);
|
let updateNotification: UpdateNotification | undefined = $state(undefined);
|
||||||
@@ -230,6 +231,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ctrl+` - Toggle debug console
|
||||||
|
if (event.ctrlKey && event.key === "`") {
|
||||||
|
event.preventDefault();
|
||||||
|
debugConsoleStore.toggle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Ctrl+E - Toggle editor panel (only when connected)
|
// Ctrl+E - Toggle editor panel (only when connected)
|
||||||
if (event.ctrlKey && event.key === "e") {
|
if (event.ctrlKey && event.key === "e") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
Reference in New Issue
Block a user