obsidian-logfire/src/main.ts
tolvitty 4936bfd94f Query-Management in main.ts verdrahtet, Schema/Modal CSS
History, Favorites, Templates und Schema-Browser initialisiert
und mit Commands verknuepft. CSS fuer Schema-Browser-Sidebar,
Template-Picker und Favoriten-Modal ergaenzt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 11:24:28 +01:00

476 lines
15 KiB
TypeScript

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<void> {
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<void> {
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<void> {
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<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);
}
}
// ---------------------------------------------------------------------------
// Dashboard view
// ---------------------------------------------------------------------------
private async activateDashboard(): Promise<void> {
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<void> {
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<void> {
const saved = (await this.loadData()) as Partial<LogfireSettings> | null;
this.settings = deepMerge(DEFAULT_SETTINGS, saved ?? {});
}
async saveSettings(): Promise<void> {
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);
}