Merge feature/echtzeit-ui: Echtzeit-UI

Event-Stream-Sidebar mit Suche und Kategorie-Filtern,
Status-Bar mit Live-Metriken und Klick-Pause,
Ribbon-Icon, Styles im System-Monitor-Design.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Oelfke 2026-02-12 10:55:34 +01:00
commit 3a64d723d5
4 changed files with 575 additions and 1 deletions

View file

@ -11,6 +11,8 @@ import { EditorCollector } from './collectors/editor-collector';
import { SystemCollector } from './collectors/system-collector';
import { LogfireSettingTab } from './ui/settings-tab';
import { InitialScanModal } from './ui/initial-scan-modal';
import { StatusBar } from './ui/status-bar';
import { EventStreamView, EVENT_STREAM_VIEW_TYPE } from './ui/event-stream-view';
export default class LogfirePlugin extends Plugin {
settings!: LogfireSettings;
@ -24,6 +26,7 @@ export default class LogfirePlugin extends Plugin {
private navCollector!: NavCollector;
private editorCollector!: EditorCollector;
private systemCollector!: SystemCollector;
private statusBar!: StatusBar;
private paused = false;
@ -77,6 +80,21 @@ export default class LogfirePlugin extends Plugin {
// UI: Settings tab
this.addSettingTab(new LogfireSettingTab(this.app, this));
// UI: Event stream view
this.registerView(
EVENT_STREAM_VIEW_TYPE,
(leaf) => new EventStreamView(leaf, this.eventBus),
);
// UI: Status bar
this.statusBar = new StatusBar(this);
this.statusBar.start();
// Ribbon icon
this.addRibbonIcon('activity', 'Logfire: Event-Stream', () => {
this.activateEventStream();
});
// Commands
this.registerCommands();
@ -107,6 +125,7 @@ export default class LogfirePlugin extends Plugin {
async onunload(): Promise<void> {
console.log('[Logfire] Entlade Plugin...');
this.statusBar?.destroy();
this.stopTracking();
if (this.sessionManager) {
@ -217,6 +236,12 @@ export default class LogfirePlugin extends Plugin {
// ---------------------------------------------------------------------------
private registerCommands(): void {
this.addCommand({
id: 'show-event-stream',
name: 'Event-Stream anzeigen',
callback: () => this.activateEventStream(),
});
this.addCommand({
id: 'toggle-tracking',
name: 'Tracking pausieren/fortsetzen',
@ -263,6 +288,23 @@ export default class LogfirePlugin extends Plugin {
});
}
// ---------------------------------------------------------------------------
// Event stream view
// ---------------------------------------------------------------------------
private async activateEventStream(): Promise<void> {
const existing = this.app.workspace.getLeavesOfType(EVENT_STREAM_VIEW_TYPE);
if (existing.length > 0) {
this.app.workspace.revealLeaf(existing[0]);
return;
}
const leaf = this.app.workspace.getRightLeaf(false);
if (leaf) {
await leaf.setViewState({ type: EVENT_STREAM_VIEW_TYPE, active: true });
this.app.workspace.revealLeaf(leaf);
}
}
// ---------------------------------------------------------------------------
// Settings
// ---------------------------------------------------------------------------

150
src/ui/event-stream-view.ts Normal file
View file

@ -0,0 +1,150 @@
import { ItemView, WorkspaceLeaf } from 'obsidian';
import { LogfireEvent, EventCategory } from '../types';
import { EventBus } from '../core/event-bus';
export const EVENT_STREAM_VIEW_TYPE = 'logfire-event-stream';
const MAX_VISIBLE_ENTRIES = 500;
export class EventStreamView extends ItemView {
private entries: LogfireEvent[] = [];
private unsubscribe: (() => void) | null = null;
private listEl!: HTMLElement;
private viewPaused = false;
private filterText = '';
private enabledCategories = new Set<EventCategory>([
'file', 'content', 'navigation', 'editor', 'vault', 'plugin', 'system',
]);
constructor(leaf: WorkspaceLeaf, private eventBus: EventBus) {
super(leaf);
}
getViewType(): string {
return EVENT_STREAM_VIEW_TYPE;
}
getDisplayText(): string {
return 'Logfire Events';
}
getIcon(): string {
return 'activity';
}
async onOpen(): Promise<void> {
const container = this.containerEl.children[1] as HTMLElement;
container.empty();
container.addClass('logfire-event-stream');
// Toolbar
const toolbar = container.createDiv({ cls: 'logfire-stream-toolbar' });
const searchInput = toolbar.createEl('input', {
type: 'text',
placeholder: 'Nach Quelle filtern...',
cls: 'logfire-stream-search',
});
searchInput.addEventListener('input', () => {
this.filterText = searchInput.value.toLowerCase();
this.renderList();
});
const btnGroup = toolbar.createDiv({ cls: 'logfire-stream-btn-group' });
const pauseBtn = btnGroup.createEl('button', { text: 'Pause' });
pauseBtn.addEventListener('click', () => {
this.viewPaused = !this.viewPaused;
pauseBtn.textContent = this.viewPaused ? 'Weiter' : 'Pause';
pauseBtn.toggleClass('is-active', this.viewPaused);
});
const clearBtn = btnGroup.createEl('button', { text: 'Leeren' });
clearBtn.addEventListener('click', () => {
this.entries = [];
this.renderList();
});
// Category toggles
const categoryBar = container.createDiv({ cls: 'logfire-stream-categories' });
const categories: EventCategory[] = ['file', 'content', 'navigation', 'editor', 'vault', 'plugin', 'system'];
for (const cat of categories) {
const label = categoryBar.createEl('label', { cls: 'logfire-cat-toggle' });
const cb = label.createEl('input', { type: 'checkbox' }) as HTMLInputElement;
cb.checked = true;
label.createSpan({ text: cat });
cb.addEventListener('change', () => {
if (cb.checked) {
this.enabledCategories.add(cat);
} else {
this.enabledCategories.delete(cat);
}
this.renderList();
});
}
// Event list
this.listEl = container.createDiv({ cls: 'logfire-stream-list' });
// Subscribe to events
this.unsubscribe = this.eventBus.onEvent('*', (event) => {
if (this.viewPaused) return;
this.entries.unshift(event);
if (this.entries.length > MAX_VISIBLE_ENTRIES) {
this.entries.length = MAX_VISIBLE_ENTRIES;
}
this.renderList();
});
}
async onClose(): Promise<void> {
this.unsubscribe?.();
this.unsubscribe = null;
}
private renderList(): void {
this.listEl.empty();
const filtered = this.entries.filter(e => {
if (!this.enabledCategories.has(e.category)) return false;
if (this.filterText && !e.source.toLowerCase().includes(this.filterText)) return false;
return true;
});
for (const event of filtered.slice(0, MAX_VISIBLE_ENTRIES)) {
const row = this.listEl.createDiv({ cls: 'logfire-stream-row' });
const time = new Date(event.timestamp);
const timeStr = `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}:${String(time.getSeconds()).padStart(2, '0')}`;
const payloadSummary = this.summarizePayload(event);
row.createSpan({ text: timeStr, cls: 'logfire-stream-time' });
row.createSpan({ text: event.type, cls: 'logfire-stream-type' });
row.createSpan({ text: event.source || '', cls: 'logfire-stream-source' });
if (payloadSummary) {
row.createSpan({ text: payloadSummary, cls: 'logfire-stream-payload' });
}
}
}
private summarizePayload(event: LogfireEvent): string {
const p = event.payload;
switch (event.type) {
case 'content:words-changed':
return `+${p.wordsAdded ?? 0}/-${p.wordsRemoved ?? 0} Wörter`;
case 'editor:change':
return `+${p.insertedChars ?? 0}/-${p.deletedChars ?? 0} Zeichen`;
case 'nav:file-close':
return typeof p.duration === 'number' ? `${Math.round(p.duration / 1000)}s` : '';
case 'file:rename':
return `${p.newName ?? ''}`;
case 'file:move':
return `${p.newFolder ?? ''}`;
case 'plugin:command-executed':
return String(p.commandName ?? '');
default:
return '';
}
}
}

67
src/ui/status-bar.ts Normal file
View file

@ -0,0 +1,67 @@
import type LogfirePlugin from '../main';
export class StatusBar {
private el: HTMLElement;
private intervalId: ReturnType<typeof setInterval> | null = null;
private wordsAdded = 0;
private eventCount = 0;
constructor(private plugin: LogfirePlugin) {
this.el = plugin.addStatusBarItem();
this.el.addClass('logfire-status-bar');
this.el.addEventListener('click', () => this.onClick());
}
start(): void {
this.plugin.eventBus.onEvent('*', (event) => {
this.eventCount++;
const wa = event.payload.wordsAdded;
if (typeof wa === 'number') {
this.wordsAdded += wa;
}
});
this.update();
this.intervalId = setInterval(() => this.update(), 1000);
}
destroy(): void {
if (this.intervalId !== null) {
clearInterval(this.intervalId);
this.intervalId = null;
}
this.el.remove();
}
private update(): void {
const paused = this.plugin.isPaused();
const indicator = paused ? '\u23F8' : '\u{1F534}';
const state = paused ? 'Pausiert' : 'Aufnahme';
const duration = this.formatDuration(this.plugin.sessionManager.sessionDurationMs);
const words = this.wordsAdded > 0 ? ` | +${this.wordsAdded}w` : '';
this.el.textContent = `${indicator} ${state} | ${this.eventCount} Events${words} | ${duration}`;
}
private onClick(): void {
if (this.plugin.isPaused()) {
this.plugin.resume();
} else {
this.plugin.pause();
}
this.update();
}
private formatDuration(ms: number): string {
if (ms <= 0) return '0:00';
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
return `${minutes}:${String(seconds).padStart(2, '0')}`;
}
}

View file

@ -1 +1,316 @@
/* Logfire Styles */
/*
Logfire Obsidian Plugin Styles
Aesthetic: Utilitarian System Monitor
*/
/* ---------------------------------------------------------------------------
Event Stream View Container
--------------------------------------------------------------------------- */
.logfire-event-stream {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background: var(--background-primary);
}
/* ---------------------------------------------------------------------------
Event Stream Toolbar
--------------------------------------------------------------------------- */
.logfire-stream-toolbar {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border-bottom: 1px solid var(--background-modifier-border);
background: var(--background-secondary);
flex-shrink: 0;
}
.logfire-stream-search {
flex: 1;
min-width: 0;
padding: 4px 8px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background: var(--background-primary);
color: var(--text-normal);
font-family: var(--font-monospace);
font-size: 12px;
outline: none;
transition: border-color 120ms ease;
}
.logfire-stream-search:focus {
border-color: var(--interactive-accent);
}
.logfire-stream-search::placeholder {
color: var(--text-faint);
font-style: italic;
}
.logfire-stream-btn-group {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.logfire-stream-btn-group button {
padding: 3px 10px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background: var(--background-primary);
color: var(--text-muted);
font-size: 11px;
font-family: var(--font-monospace);
cursor: pointer;
transition: background 100ms ease, color 100ms ease, border-color 100ms ease;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.logfire-stream-btn-group button:hover {
background: var(--background-modifier-hover);
color: var(--text-normal);
border-color: var(--text-faint);
}
.logfire-stream-btn-group button.is-active {
background: var(--interactive-accent);
color: var(--text-on-accent);
border-color: var(--interactive-accent);
}
/* ---------------------------------------------------------------------------
Event Stream Category Filter Bar
--------------------------------------------------------------------------- */
.logfire-stream-categories {
display: flex;
gap: 2px;
padding: 6px 10px;
border-bottom: 1px solid var(--background-modifier-border);
background: var(--background-secondary);
flex-shrink: 0;
flex-wrap: wrap;
}
.logfire-cat-toggle {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 7px;
border-radius: 3px;
font-family: var(--font-monospace);
font-size: 10px;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--text-muted);
cursor: pointer;
transition: background 80ms ease, color 80ms ease;
user-select: none;
line-height: 1;
}
.logfire-cat-toggle:hover {
background: var(--background-modifier-hover);
color: var(--text-normal);
}
.logfire-cat-toggle input[type="checkbox"] {
margin: 0;
width: 10px;
height: 10px;
accent-color: var(--interactive-accent);
cursor: pointer;
}
.logfire-cat-toggle span {
pointer-events: none;
}
/* ---------------------------------------------------------------------------
Event Stream List
--------------------------------------------------------------------------- */
.logfire-stream-list {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
font-family: var(--font-monospace);
font-size: 11.5px;
line-height: 1.5;
padding: 2px 0;
}
/* Subtle scrollbar */
.logfire-stream-list::-webkit-scrollbar {
width: 6px;
}
.logfire-stream-list::-webkit-scrollbar-track {
background: transparent;
}
.logfire-stream-list::-webkit-scrollbar-thumb {
background: var(--background-modifier-border);
border-radius: 3px;
}
.logfire-stream-list::-webkit-scrollbar-thumb:hover {
background: var(--text-faint);
}
/* ---------------------------------------------------------------------------
Event Stream Row
--------------------------------------------------------------------------- */
.logfire-stream-row {
display: flex;
gap: 8px;
padding: 2px 10px;
border-bottom: 1px solid transparent;
transition: background 60ms ease;
white-space: nowrap;
overflow: hidden;
}
.logfire-stream-row:hover {
background: var(--background-secondary);
border-bottom-color: var(--background-modifier-border);
}
/* Alternating row tint for scanability */
.logfire-stream-row:nth-child(even) {
background: color-mix(in srgb, var(--background-secondary) 30%, transparent);
}
.logfire-stream-row:nth-child(even):hover {
background: var(--background-secondary);
}
/* ---------------------------------------------------------------------------
Event Stream Row Segments
--------------------------------------------------------------------------- */
.logfire-stream-time {
color: var(--text-faint);
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.logfire-stream-type {
color: var(--text-accent);
flex-shrink: 0;
font-weight: 600;
min-width: 0;
}
.logfire-stream-source {
color: var(--text-normal);
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
flex-shrink: 1;
}
.logfire-stream-payload {
color: var(--text-faint);
flex-shrink: 0;
margin-left: auto;
font-variant-numeric: tabular-nums;
}
/* ---------------------------------------------------------------------------
Status Bar
--------------------------------------------------------------------------- */
.logfire-status-bar {
cursor: pointer;
font-family: var(--font-monospace);
font-size: 11px;
letter-spacing: 0.01em;
padding: 0 4px;
font-variant-numeric: tabular-nums;
transition: color 100ms ease;
}
.logfire-status-bar:hover {
color: var(--text-accent);
}
/* ---------------------------------------------------------------------------
Scan Modal
--------------------------------------------------------------------------- */
.logfire-scan-modal {
font-family: var(--font-monospace);
}
.logfire-scan-modal h2 {
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
margin-bottom: 8px;
color: var(--text-normal);
}
.logfire-scan-modal > p {
color: var(--text-muted);
font-size: 12px;
line-height: 1.5;
margin-bottom: 16px;
}
.logfire-scan-progress {
margin-bottom: 12px;
}
.logfire-scan-progress progress {
width: 100%;
height: 6px;
border: none;
border-radius: 3px;
overflow: hidden;
appearance: none;
-webkit-appearance: none;
}
.logfire-scan-progress progress::-webkit-progress-bar {
background: var(--background-modifier-border);
border-radius: 3px;
}
.logfire-scan-progress progress::-webkit-progress-value {
background: var(--interactive-accent);
border-radius: 3px;
transition: width 200ms ease;
}
.logfire-scan-status {
font-size: 11.5px;
line-height: 1.5;
color: var(--text-normal);
word-break: break-all;
}
.logfire-scan-status > div:last-child {
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
.logfire-scan-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
.logfire-scan-buttons button {
font-family: var(--font-monospace);
font-size: 12px;
letter-spacing: 0.02em;
}