Phase 16: System-Monitor (Debug-Panel)

Neue Komponenten:
- MonitorPanel.svelte: Live-Event-Stream mit Farbcodierung
- MonitorEvent Store in app.ts (Ringbuffer max 1000)

Features:
- 🔵 API-Events (Request/Response, Token, Kosten, Latenz)
- 🟡 Tool-Events (Name, Parameter, Dauer)
- 🟠 Agent-Events (Start/Stop)
- 🔴 Error-Events
- Filter nach Event-Typ
- Auto-Scroll toggle
- Detail-Ansicht bei Klick
- Copy-Button für Details
- Statistik-Anzeige (API-Calls, Errors, Avg Latency)

claude-bridge.js:
- sendMonitorEvent() Funktion
- summarizeToolInput() für kompakte Zusammenfassung
- Events bei API-Start, API-Ende, Tool-Start, Fehler

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-14 12:40:29 +02:00
parent 6cfcdb2c79
commit adb11fd121
5 changed files with 668 additions and 8 deletions

View file

@ -68,6 +68,52 @@ function sendError(id, error) {
sendToTauri({ type: 'response', id, error }); sendToTauri({ type: 'response', id, error });
} }
// ============ Monitor-Events ============
// Sendet ein Event für den System-Monitor
function sendMonitorEvent(type, summary, details = {}, options = {}) {
sendEvent('monitor', {
type, // 'api' | 'hook' | 'tool' | 'mcp' | 'agent' | 'error' | 'debug'
summary, // Einzeiler für Kompakt-Ansicht
details, // Vollständige Daten
agentId: options.agentId || currentAgentId,
durationMs: options.durationMs,
error: options.error,
});
}
// Tool-Input für Logging kürzen (sensitive Daten maskieren)
function summarizeToolInput(tool, input) {
if (!input) return '';
// Bestimmte Tools speziell behandeln
if (tool === 'Read') {
return input.file_path || '';
}
if (tool === 'Edit' || tool === 'Write') {
const path = input.file_path || '';
const size = input.content ? `(${input.content.length} chars)` : '';
return `${path} ${size}`;
}
if (tool === 'Grep') {
return `"${input.pattern}" in ${input.path || '.'}`;
}
if (tool === 'Bash') {
const cmd = input.command || '';
return cmd.length > 50 ? cmd.substring(0, 50) + '...' : cmd;
}
if (tool === 'Task') {
return input.description || input.prompt || '';
}
// Default: Erstes String-Feld nehmen
const firstString = Object.values(input).find(v => typeof v === 'string');
if (firstString) {
return firstString.length > 50 ? firstString.substring(0, 50) + '...' : firstString;
}
return '';
}
// ============ Claude Agent SDK ============ // ============ Claude Agent SDK ============
async function sendMessage(message, requestId, model = null) { async function sendMessage(message, requestId, model = null) {
@ -84,6 +130,20 @@ async function sendMessage(message, requestId, model = null) {
model: useModel, model: useModel,
}); });
// Monitor: Agent gestartet
sendMonitorEvent('agent', `Main Agent gestartet (${useModel})`, {
agentId: currentAgentId,
model: useModel,
task: message.substring(0, 100),
});
// Monitor: API-Request
sendMonitorEvent('api', `${useModel}`, {
model: useModel,
promptLength: message.length,
maxTurns: 25,
});
sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel }); sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel });
const startTime = Date.now(); const startTime = Date.now();
@ -159,6 +219,14 @@ async function sendMessage(message, requestId, model = null) {
input: toolInput, input: toolInput,
agentId: currentAgentId, agentId: currentAgentId,
}); });
// Monitor: Tool gestartet
const toolSummary = summarizeToolInput(toolName, toolInput);
sendMonitorEvent('tool', `${toolName} ${toolSummary}`, {
toolId,
tool: toolName,
input: toolInput,
});
break; break;
} }
@ -185,20 +253,33 @@ async function sendMessage(message, requestId, model = null) {
break; break;
} }
case 'result': case 'result': {
// Endergebnis // Endergebnis
const durationMs = Date.now() - startTime;
const inputTokens = event.usage?.input_tokens || 0;
const outputTokens = event.usage?.output_tokens || 0;
const cost = event.total_cost_usd || 0;
sendEvent('result', { sendEvent('result', {
text: fullText, text: fullText,
cost: event.total_cost_usd || 0, cost,
tokens: { tokens: { input: inputTokens, output: outputTokens },
input: event.usage?.input_tokens || 0,
output: event.usage?.output_tokens || 0,
},
session_id: event.session_id || '', session_id: event.session_id || '',
duration_ms: Date.now() - startTime, duration_ms: durationMs,
model: usedModel, model: usedModel,
}); });
// Monitor: API-Response
const tokenK = ((inputTokens + outputTokens) / 1000).toFixed(1);
sendMonitorEvent('api', `${usedModel} [${durationMs}ms] ${tokenK}k tok $${cost.toFixed(4)}`, {
model: usedModel,
inputTokens,
outputTokens,
cost,
sessionId: event.session_id,
}, { durationMs });
break; break;
}
default: default:
// Andere Events still ignorieren // Andere Events still ignorieren
@ -208,8 +289,16 @@ async function sendMessage(message, requestId, model = null) {
} catch (err) { } catch (err) {
if (err.name === 'AbortError') { if (err.name === 'AbortError') {
// Abgebrochen — kein Fehler // Abgebrochen — kein Fehler
sendMonitorEvent('agent', 'Abgebrochen (User)', { reason: 'abort' });
} else { } else {
sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` }); sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` });
// Monitor: Fehler
sendMonitorEvent('error', `${err.message || err}`, {
name: err.name,
message: err.message,
stack: err.stack,
}, { error: err.message || String(err) });
} }
} finally { } finally {
// Alle noch aktiven Subagents stoppen // Alle noch aktiven Subagents stoppen

View file

@ -0,0 +1,438 @@
<script lang="ts">
import {
filteredMonitorEvents,
monitorFilter,
monitorAutoScroll,
selectedMonitorEventId,
monitorStats,
monitorEventColors,
clearMonitorEvents,
type MonitorEvent,
type MonitorEventType
} from '$lib/stores/app';
import { tick } from 'svelte';
// Event-Liste Container für Auto-Scroll
let eventListEl: HTMLDivElement;
// Auto-Scroll bei neuen Events
$effect(() => {
if ($monitorAutoScroll && $filteredMonitorEvents.length > 0) {
tick().then(() => {
if (eventListEl) {
eventListEl.scrollTop = eventListEl.scrollHeight;
}
});
}
});
// Event auswählen
function selectEvent(id: string) {
$selectedMonitorEventId = $selectedMonitorEventId === id ? null : id;
}
// Zeit formatieren
function formatTime(date: Date): string {
return date.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
}
// Details formatieren
function formatDetails(details: Record<string, unknown>): string {
return JSON.stringify(details, null, 2);
}
// Event-Typ Labels
const typeLabels: Record<MonitorEventType, string> = {
api: 'API',
hook: 'HOOK',
tool: 'TOOL',
mcp: 'MCP',
agent: 'AGENT',
error: 'ERROR',
debug: 'DEBUG',
};
// Filter-Optionen
const filterOptions: Array<{ value: MonitorEventType | 'all'; label: string }> = [
{ value: 'all', label: 'Alle' },
{ value: 'api', label: '🔵 API' },
{ value: 'tool', label: '🟡 Tools' },
{ value: 'agent', label: '🟠 Agents' },
{ value: 'mcp', label: '🟣 MCP' },
{ value: 'hook', label: '🟢 Hooks' },
{ value: 'error', label: '🔴 Fehler' },
{ value: 'debug', label: '⚪ Debug' },
];
</script>
<div class="monitor-panel">
<!-- Header mit Stats und Filter -->
<div class="monitor-header">
<div class="monitor-title">
<h2>📊 System-Monitor</h2>
<div class="monitor-stats">
<span class="stat" title="Alle Events">{$monitorStats.totalEvents}</span>
<span class="stat api" title="API-Calls">{$monitorStats.apiCalls} API</span>
{#if $monitorStats.errors > 0}
<span class="stat error" title="Fehler">{$monitorStats.errors} Err</span>
{/if}
{#if $monitorStats.avgLatencyMs > 0}
<span class="stat latency" title="Durchschnittliche Latenz">{$monitorStats.avgLatencyMs}ms</span>
{/if}
</div>
</div>
<div class="monitor-controls">
<select bind:value={$monitorFilter} class="filter-select">
{#each filterOptions as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
<label class="auto-scroll-toggle" title="Auto-Scroll">
<input type="checkbox" bind:checked={$monitorAutoScroll} />
<span></span>
</label>
<button class="clear-btn" on:click={clearMonitorEvents} title="Events löschen">
🗑️
</button>
</div>
</div>
<!-- Event-Liste -->
<div class="event-list" bind:this={eventListEl}>
{#if $filteredMonitorEvents.length === 0}
<div class="empty-state">
<p>Keine Events.</p>
<p class="hint">Events erscheinen hier wenn Claude arbeitet.</p>
</div>
{:else}
{#each $filteredMonitorEvents as event (event.id)}
<button
class="event-item"
class:selected={$selectedMonitorEventId === event.id}
class:error={event.type === 'error'}
on:click={() => selectEvent(event.id)}
>
<span class="event-time">{formatTime(event.timestamp)}</span>
<span class="event-icon">{monitorEventColors[event.type]}</span>
<span class="event-type">{typeLabels[event.type]}</span>
<span class="event-summary">{event.summary}</span>
{#if event.durationMs}
<span class="event-duration">[{event.durationMs}ms]</span>
{/if}
</button>
{/each}
{/if}
</div>
<!-- Detail-Ansicht -->
{#if $selectedMonitorEventId}
{@const selectedEvent = $filteredMonitorEvents.find((e) => e.id === $selectedMonitorEventId)}
{#if selectedEvent}
<div class="event-details">
<div class="details-header">
<h3>
{monitorEventColors[selectedEvent.type]} {typeLabels[selectedEvent.type]}
</h3>
<button class="copy-btn" on:click={() => navigator.clipboard.writeText(formatDetails(selectedEvent.details))} title="Kopieren">
📋
</button>
</div>
<div class="details-row">
<span class="label">Zeit:</span>
<span class="value">{selectedEvent.timestamp.toLocaleString('de-DE')}</span>
</div>
<div class="details-row">
<span class="label">Summary:</span>
<span class="value">{selectedEvent.summary}</span>
</div>
{#if selectedEvent.durationMs}
<div class="details-row">
<span class="label">Dauer:</span>
<span class="value">{selectedEvent.durationMs}ms</span>
</div>
{/if}
{#if selectedEvent.agentId}
<div class="details-row">
<span class="label">Agent:</span>
<span class="value mono">{selectedEvent.agentId.substring(0, 8)}...</span>
</div>
{/if}
{#if selectedEvent.error}
<div class="details-row error">
<span class="label">Fehler:</span>
<span class="value">{selectedEvent.error}</span>
</div>
{/if}
<h4>Details</h4>
<pre class="details-json">{formatDetails(selectedEvent.details)}</pre>
</div>
{/if}
{/if}
</div>
<style>
.monitor-panel {
display: flex;
flex-direction: column;
height: 100%;
font-family: var(--font-mono, monospace);
font-size: 0.75rem;
}
.monitor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.monitor-title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.monitor-title h2 {
font-size: 0.875rem;
font-weight: 600;
margin: 0;
}
.monitor-stats {
display: flex;
gap: var(--spacing-xs);
}
.stat {
padding: 2px 6px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
font-size: 0.625rem;
}
.stat.api {
color: #4a9eff;
}
.stat.error {
background: var(--error);
color: white;
}
.stat.latency {
color: var(--text-secondary);
}
.monitor-controls {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.filter-select {
padding: 4px 8px;
font-size: 0.75rem;
background: var(--bg-tertiary);
border: 1px solid var(--bg-tertiary);
border-radius: var(--radius-sm);
color: var(--text-primary);
}
.auto-scroll-toggle {
display: flex;
align-items: center;
cursor: pointer;
}
.auto-scroll-toggle input {
display: none;
}
.auto-scroll-toggle span {
padding: 4px 8px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
opacity: 0.5;
}
.auto-scroll-toggle input:checked + span {
opacity: 1;
background: var(--accent);
color: white;
}
.clear-btn {
padding: 4px 8px;
background: var(--bg-tertiary);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
}
.clear-btn:hover {
background: var(--error);
}
.event-list {
flex: 1;
overflow-y: auto;
padding: var(--spacing-xs);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
text-align: center;
}
.empty-state .hint {
font-size: 0.625rem;
margin-top: var(--spacing-xs);
}
.event-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
width: 100%;
text-align: left;
padding: 4px var(--spacing-sm);
margin-bottom: 2px;
background: var(--bg-secondary);
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.1s ease;
}
.event-item:hover {
background: var(--bg-tertiary);
}
.event-item.selected {
border-color: var(--accent);
}
.event-item.error {
background: rgba(239, 68, 68, 0.1);
}
.event-time {
color: var(--text-secondary);
font-size: 0.625rem;
flex-shrink: 0;
}
.event-icon {
flex-shrink: 0;
}
.event-type {
font-weight: 600;
flex-shrink: 0;
min-width: 50px;
}
.event-summary {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-secondary);
}
.event-duration {
font-size: 0.625rem;
color: var(--text-secondary);
flex-shrink: 0;
}
/* Detail-Ansicht */
.event-details {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary);
border-top: 1px solid var(--bg-tertiary);
max-height: 40%;
overflow-y: auto;
}
.details-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-sm);
}
.details-header h3 {
font-size: 0.875rem;
margin: 0;
}
.copy-btn {
padding: 4px 8px;
background: var(--bg-tertiary);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
}
.copy-btn:hover {
background: var(--accent);
}
.details-row {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.details-row .label {
color: var(--text-secondary);
min-width: 60px;
}
.details-row .value {
flex: 1;
}
.details-row .mono {
font-family: var(--font-mono, monospace);
}
.details-row.error .value {
color: var(--error);
}
.event-details h4 {
font-size: 0.75rem;
margin: var(--spacing-sm) 0 var(--spacing-xs);
}
.details-json {
padding: var(--spacing-sm);
background: var(--bg-primary);
border-radius: var(--radius-sm);
overflow-x: auto;
font-size: 0.625rem;
max-height: 200px;
overflow-y: auto;
}
</style>

View file

@ -261,3 +261,108 @@ export function dbToMessage(db: DbMessage): Message {
export function setMessagesFromDb(dbMessages: DbMessage[]) { export function setMessagesFromDb(dbMessages: DbMessage[]) {
messages.set(dbMessages.map(dbToMessage)); messages.set(dbMessages.map(dbToMessage));
} }
// ============ System-Monitor ============
export type MonitorEventType = 'api' | 'hook' | 'tool' | 'mcp' | 'agent' | 'error' | 'debug';
export interface MonitorEvent {
id: string;
timestamp: Date;
type: MonitorEventType;
summary: string; // Einzeiler für Kompakt-Ansicht
details: Record<string, unknown>; // Vollständige Daten
sessionId?: string;
agentId?: string;
durationMs?: number;
error?: string;
}
// Farbcodierung für Event-Typen
export const monitorEventColors: Record<MonitorEventType, string> = {
api: '🔵',
hook: '🟢',
tool: '🟡',
mcp: '🟣',
agent: '🟠',
error: '🔴',
debug: '⚪',
};
// Monitor Store — Ringbuffer mit max 1000 Events
const MAX_MONITOR_EVENTS = 1000;
export const monitorEvents = writable<MonitorEvent[]>([]);
// Filter für Monitor-Ansicht
export const monitorFilter = writable<MonitorEventType | 'all'>('all');
export const monitorAutoScroll = writable(true);
export const selectedMonitorEventId = writable<string | null>(null);
// Gefilterte Events
export const filteredMonitorEvents = derived(
[monitorEvents, monitorFilter],
([$events, $filter]) => {
if ($filter === 'all') return $events;
return $events.filter((e) => e.type === $filter);
}
);
// Monitor-Statistiken
export const monitorStats = derived(monitorEvents, ($events) => {
const last100 = $events.slice(-100);
const apiEvents = last100.filter((e) => e.type === 'api');
const errorEvents = last100.filter((e) => e.type === 'error');
const avgLatency = apiEvents.length > 0
? apiEvents.reduce((sum, e) => sum + (e.durationMs || 0), 0) / apiEvents.length
: 0;
return {
totalEvents: $events.length,
apiCalls: apiEvents.length,
errors: errorEvents.length,
avgLatencyMs: Math.round(avgLatency),
};
});
// Monitor-Event hinzufügen
export function addMonitorEvent(
type: MonitorEventType,
summary: string,
details: Record<string, unknown> = {},
options?: Partial<Omit<MonitorEvent, 'id' | 'timestamp' | 'type' | 'summary' | 'details'>>
) {
const event: MonitorEvent = {
id: crypto.randomUUID(),
timestamp: new Date(),
type,
summary,
details,
...options,
};
monitorEvents.update((events) => {
const updated = [...events, event];
// Ringbuffer: Alte Events entfernen wenn zu viele
if (updated.length > MAX_MONITOR_EVENTS) {
return updated.slice(-MAX_MONITOR_EVENTS);
}
return updated;
});
return event.id;
}
// Monitor leeren
export function clearMonitorEvents() {
monitorEvents.set([]);
}
// Sensitive Daten maskieren
export function maskSensitive(data: string): string {
return data
.replace(/password[=:]\s*\S+/gi, 'password=***')
.replace(/api[_-]?key[=:]\s*\S+/gi, 'api_key=***')
.replace(/bearer\s+\S+/gi, 'Bearer ***')
.replace(/sk-[a-zA-Z0-9]+/g, 'sk-***')
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '***@***.***');
}

View file

@ -20,8 +20,10 @@ import {
sessionStats, sessionStats,
currentSessionId, currentSessionId,
messageToDb, messageToDb,
addMonitorEvent,
type Message, type Message,
type Agent type Agent,
type MonitorEventType
} from './app'; } from './app';
// Event-Typen vom Backend // Event-Typen vom Backend
@ -66,6 +68,15 @@ interface ResultEvent {
model?: string; model?: string;
} }
interface MonitorEventPayload {
type: MonitorEventType;
summary: string;
details: Record<string, unknown>;
agentId?: string;
durationMs?: number;
error?: string;
}
// Listener-Handles // Listener-Handles
let listeners: UnlistenFn[] = []; let listeners: UnlistenFn[] = [];
@ -267,6 +278,19 @@ export async function initEventListeners(): Promise<void> {
}) })
); );
// Monitor-Events — für System-Monitor Panel
listeners.push(
await listen<MonitorEventPayload>('monitor', (event) => {
const { type, summary, details, agentId, durationMs, error } = event.payload;
addMonitorEvent(type, summary, details, {
agentId,
durationMs,
error,
});
})
);
console.log('✅ Event-Listener initialisiert'); console.log('✅ Event-Listener initialisiert');
} }

View file

@ -8,12 +8,14 @@
import AuditLog from '$lib/components/AuditLog.svelte'; import AuditLog from '$lib/components/AuditLog.svelte';
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte'; import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
import SettingsPanel from '$lib/components/SettingsPanel.svelte'; import SettingsPanel from '$lib/components/SettingsPanel.svelte';
import MonitorPanel from '$lib/components/MonitorPanel.svelte';
let activeMiddleTab = 'activity'; let activeMiddleTab = 'activity';
let activeRightTab = 'agents'; let activeRightTab = 'agents';
const middleTabs = [ const middleTabs = [
{ id: 'activity', label: 'Aktivität', icon: '📋' }, { id: 'activity', label: 'Aktivität', icon: '📋' },
{ id: 'monitor', label: 'Monitor', icon: '📊' },
{ id: 'memory', label: 'Gedächtnis', icon: '🧠' }, { id: 'memory', label: 'Gedächtnis', icon: '🧠' },
{ id: 'audit', label: 'Historie', icon: '📝' }, { id: 'audit', label: 'Historie', icon: '📝' },
]; ];
@ -60,6 +62,8 @@
<div class="panel-content"> <div class="panel-content">
{#if activeMiddleTab === 'activity'} {#if activeMiddleTab === 'activity'}
<ActivityPanel /> <ActivityPanel />
{:else if activeMiddleTab === 'monitor'}
<MonitorPanel />
{:else if activeMiddleTab === 'memory'} {:else if activeMiddleTab === 'memory'}
<MemoryPanel /> <MemoryPanel />
{:else if activeMiddleTab === 'audit'} {:else if activeMiddleTab === 'audit'}