import { Plugin, FileSystemAdapter, TFile, MarkdownView, Notice } from 'obsidian'; import { LogfireSettings, DEFAULT_SETTINGS, deepMerge } from './types'; import { DatabaseManager } from './core/database'; import { EventBus } from './core/event-bus'; import { SessionManager } from './core/session-manager'; import { ContentAnalyzer } from './core/content-analyzer'; import { FileCollector } from './collectors/file-collector'; import { ContentCollector } from './collectors/content-collector'; import { NavCollector } from './collectors/nav-collector'; 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'; import { registerLogfireBlock, registerLogfireSqlBlock, registerLogfireDashboardBlock, cleanupAllRefreshTimers } from './query/processor'; import { QueryModal } from './query/query-modal'; import { VirtualTableManager } from './query/virtual-tables'; import { DashboardView, DASHBOARD_VIEW_TYPE } from './viz/dashboard'; import { HistoryManager } from './management/history'; import { FavoritesManager, SaveFavoriteModal } from './management/favorites'; import { TemplateManager, TemplatePickerModal } from './management/templates'; import { SchemaView, SCHEMA_VIEW_TYPE } from './ui/schema-view'; export default class LogfirePlugin extends Plugin { settings!: LogfireSettings; db!: DatabaseManager; eventBus!: EventBus; sessionManager!: SessionManager; contentAnalyzer!: ContentAnalyzer; historyManager!: HistoryManager; favoritesManager!: FavoritesManager; templateManager!: TemplateManager; private fileCollector!: FileCollector; private contentCollector!: ContentCollector; private navCollector!: NavCollector; private editorCollector!: EditorCollector; private systemCollector!: SystemCollector; private statusBar!: StatusBar; private virtualTables!: VirtualTableManager; private paused = false; async onload(): Promise { console.log('[Logfire] Lade Plugin...'); await this.loadSettings(); // Core infrastructure const dbPath = this.getDatabasePath(); this.db = new DatabaseManager(dbPath, this.settings); this.eventBus = new EventBus( this.db, this.settings.advanced.flushIntervalMs, this.settings.advanced.flushThreshold, ); const vaultName = this.app.vault.getName(); this.sessionManager = new SessionManager(this.eventBus, this.db, vaultName); // Query management this.historyManager = new HistoryManager(); this.favoritesManager = new FavoritesManager(); this.templateManager = new TemplateManager(); // Content analyzer this.contentAnalyzer = new ContentAnalyzer(); if (this.db.hasBaseline()) { this.contentAnalyzer.loadBaseline(this.db.loadBaseline()); } // Collectors const shouldTrack = (path: string) => this.shouldTrack(path); this.fileCollector = new FileCollector( this.app, this.eventBus, this.sessionManager, this.settings, shouldTrack, ); this.contentCollector = new ContentCollector( this.app, this.eventBus, this.sessionManager, this.settings, this.contentAnalyzer, shouldTrack, ); this.navCollector = new NavCollector( this.app, this.eventBus, this.sessionManager, this.settings, shouldTrack, ); this.editorCollector = new EditorCollector( this.eventBus, this.sessionManager, this.settings, shouldTrack, () => this.getActiveFilePath(), ); this.systemCollector = new SystemCollector( this.app, this.eventBus, this.sessionManager, this.settings, ); // Register CM6 editor extension this.registerEditorExtension(this.editorCollector.createExtension()); // 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: Dashboard view this.registerView( DASHBOARD_VIEW_TYPE, (leaf) => new DashboardView(leaf, this), ); // UI: Schema-Browser view this.registerView( SCHEMA_VIEW_TYPE, (leaf) => new SchemaView(leaf, this.db), ); // UI: Status bar this.statusBar = new StatusBar(this); this.statusBar.start(); // Query: Code-Block-Prozessoren registerLogfireBlock(this.db, (lang, handler) => { this.registerMarkdownCodeBlockProcessor(lang, handler); }); registerLogfireSqlBlock(this.db, (lang, handler) => { this.registerMarkdownCodeBlockProcessor(lang, handler); }); registerLogfireDashboardBlock(this, this.db, (lang, handler) => { this.registerMarkdownCodeBlockProcessor(lang, handler); }); // Ribbon icons this.addRibbonIcon('activity', 'Logfire: Event-Stream', () => { this.activateEventStream(); }); this.addRibbonIcon('layout-dashboard', 'Logfire: Dashboard', () => { this.activateDashboard(); }); // Commands this.registerCommands(); // Start tracking if (!this.settings.general.pauseOnStartup) { this.startTracking(); } else { this.paused = true; } // Initial scan + maintenance + virtual tables on startup (after layout ready) this.app.workspace.onLayoutReady(() => { if (!this.db.hasBaseline()) { this.runInitialScan(); } if (this.settings.advanced.retention.maintenanceOnStartup) { try { this.db.runMaintenance(this.settings.advanced.retention); } catch (err) { console.error('[Logfire] Wartung beim Start fehlgeschlagen:', err); } } // Virtual Tables this.virtualTables = new VirtualTableManager(this.app, this.db); this.virtualTables.initialize(); }); console.log('[Logfire] Plugin geladen. Session:', this.sessionManager.currentSessionId); } async onunload(): Promise { console.log('[Logfire] Entlade Plugin...'); cleanupAllRefreshTimers(); this.virtualTables?.destroy(); this.statusBar?.destroy(); this.stopTracking(); if (this.sessionManager) { this.sessionManager.endSession(); } if (this.eventBus) { this.eventBus.destroy(); } if (this.db) { this.db.close(); } console.log('[Logfire] Plugin entladen.'); } // --------------------------------------------------------------------------- // Tracking lifecycle // --------------------------------------------------------------------------- private startTracking(): void { this.sessionManager.startSession(); this.fileCollector.register(); this.contentCollector.register(); this.navCollector.register(); this.systemCollector.register(); } private stopTracking(): void { this.fileCollector?.unregister(); this.contentCollector?.unregister(); this.navCollector?.unregister(); this.systemCollector?.unregister(); } isPaused(): boolean { return this.paused; } pause(): void { if (!this.paused) { this.paused = true; this.stopTracking(); } } resume(): void { if (this.paused) { this.paused = false; this.startTracking(); } } // --------------------------------------------------------------------------- // Initial scan // --------------------------------------------------------------------------- private async runInitialScan(): Promise { const modal = new InitialScanModal( this.app, this.settings, this.contentAnalyzer, this.db, this.eventBus, this.sessionManager, (path) => this.shouldTrack(path), ); await modal.open(); } // --------------------------------------------------------------------------- // Track filtering // --------------------------------------------------------------------------- shouldTrack(path: string): boolean { if (this.paused) return false; if (!this.settings.general.enabled) return false; const logFolder = this.settings.general.logFolder; if (path.startsWith(logFolder + '/') || path === logFolder) return false; if (path.startsWith('.obsidian/')) return false; for (const folder of this.settings.tracking.excludedFolders) { if (path.startsWith(folder + '/') || path === folder) return false; } for (const pattern of this.settings.tracking.excludedPatterns) { if (matchGlob(path, pattern)) return false; } const file = this.app.vault.getAbstractFileByPath(path); if (file instanceof TFile) { const cache = this.app.metadataCache.getFileCache(file); if (cache?.frontmatter?.logfire === false) return false; } return true; } private getActiveFilePath(): string | null { const view = this.app.workspace.getActiveViewOfType(MarkdownView); return view?.file?.path ?? null; } // --------------------------------------------------------------------------- // Commands // --------------------------------------------------------------------------- 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', callback: () => { if (this.paused) { this.resume(); new Notice('Logfire: Tracking fortgesetzt.'); } else { this.pause(); new Notice('Logfire: Tracking pausiert.'); } }, }); this.addCommand({ id: 'rescan-vault', name: 'Vault erneut scannen', callback: () => this.runInitialScan(), }); this.addCommand({ id: 'run-maintenance', name: 'Wartung ausführen', callback: () => { this.db.runMaintenance(this.settings.advanced.retention); new Notice('Logfire: Wartung abgeschlossen.'); }, }); this.addCommand({ id: 'refresh-virtual-tables', name: 'Virtual Tables neu aufbauen', callback: () => { this.virtualTables?.rebuild(); new Notice('Logfire: Virtual Tables aktualisiert.'); }, }); this.addCommand({ id: 'show-dashboard', name: 'Dashboard anzeigen', callback: () => this.activateDashboard(), }); this.addCommand({ id: 'open-query', name: 'Query-Editor \u00f6ffnen', callback: () => { new QueryModal(this.app, this.db, this.historyManager).open(); }, }); this.addCommand({ id: 'show-schema', name: 'Schema-Browser anzeigen', callback: () => this.activateSchema(), }); this.addCommand({ id: 'show-templates', name: 'Query-Templates anzeigen', callback: () => { new TemplatePickerModal(this, this.templateManager, (sql) => { new QueryModal(this.app, this.db, this.historyManager, sql).open(); }).open(); }, }); this.addCommand({ id: 'save-favorite', name: 'Aktuelle Query als Favorit speichern', callback: () => { new SaveFavoriteModal(this, this.favoritesManager, '').open(); }, }); this.addCommand({ id: 'debug-info', name: 'Debug-Info', callback: () => { console.log('[Logfire] Debug-Info:', { paused: this.paused, sessionId: this.sessionManager.currentSessionId, sessionDuration: this.sessionManager.sessionDurationMs, bufferSize: this.eventBus.getBufferSize(), eventCount: this.db.getEventCount(), hasBaseline: this.db.hasBaseline(), dbSize: this.db.getDatabaseSizeBytes(), }); }, }); } // --------------------------------------------------------------------------- // Event stream view // --------------------------------------------------------------------------- private async activateEventStream(): Promise { 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); } } // --------------------------------------------------------------------------- // Dashboard view // --------------------------------------------------------------------------- private async activateDashboard(): Promise { const existing = this.app.workspace.getLeavesOfType(DASHBOARD_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: DASHBOARD_VIEW_TYPE, active: true }); this.app.workspace.revealLeaf(leaf); } } // --------------------------------------------------------------------------- // Schema view // --------------------------------------------------------------------------- private async activateSchema(): Promise { const existing = this.app.workspace.getLeavesOfType(SCHEMA_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: SCHEMA_VIEW_TYPE, active: true }); this.app.workspace.revealLeaf(leaf); } } // --------------------------------------------------------------------------- // Settings // --------------------------------------------------------------------------- private async loadSettings(): Promise { const saved = (await this.loadData()) as Partial | null; this.settings = deepMerge(DEFAULT_SETTINGS, saved ?? {}); } async saveSettings(): Promise { await this.saveData(this.settings); } // --------------------------------------------------------------------------- // Database path // --------------------------------------------------------------------------- private getDatabasePath(): string { const adapter = this.app.vault.adapter; if (!(adapter instanceof FileSystemAdapter)) { throw new Error('[Logfire] Benötigt einen Desktop-Vault mit Dateisystem-Zugriff.'); } const basePath = adapter.getBasePath(); return `${basePath}/${this.app.vault.configDir}/plugins/logfire/logfire.db`; } } // --------------------------------------------------------------------------- // Simple glob matching (supports * and **) // --------------------------------------------------------------------------- function matchGlob(path: string, pattern: string): boolean { const regex = pattern .replace(/[.+^${}()|[\]\\]/g, '\\$&') .replace(/\*\*/g, '{{GLOBSTAR}}') .replace(/\*/g, '[^/]*') .replace(/{{GLOBSTAR}}/g, '.*'); return new RegExp(`^${regex}$`).test(path); }