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>
476 lines
15 KiB
TypeScript
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);
|
|
}
|