From 36b25b321bd87a1dc67476f6da9ee99e0667e45d Mon Sep 17 00:00:00 2001 From: tolvitty Date: Thu, 12 Feb 2026 10:46:02 +0100 Subject: [PATCH] Plugin-Einstieg und Settings-Tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minimaler main.ts: onload/onunload, DB-Initialisierung, Settings laden/speichern, shouldTrack-Filter mit Glob-Matching. Settings-Tab mit Tracking-Toggles, Exclude-Patterns, erweitertem Bereich für Performance/Retention/DB-Info. Co-Authored-By: Claude Opus 4.6 --- src/main.ts | 99 ++++++++++++++++ src/ui/settings-tab.ts | 257 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+) create mode 100644 src/main.ts create mode 100644 src/ui/settings-tab.ts diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..15cb9e2 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,99 @@ +import { Plugin, FileSystemAdapter, TFile } from 'obsidian'; +import { LogfireSettings, DEFAULT_SETTINGS, deepMerge } from './types'; +import { DatabaseManager } from './core/database'; +import { LogfireSettingTab } from './ui/settings-tab'; + +export default class LogfirePlugin extends Plugin { + settings!: LogfireSettings; + db!: DatabaseManager; + + async onload(): Promise { + console.log('[Logfire] Lade Plugin...'); + + await this.loadSettings(); + + const dbPath = this.getDatabasePath(); + this.db = new DatabaseManager(dbPath, this.settings); + + this.addSettingTab(new LogfireSettingTab(this.app, this)); + + console.log('[Logfire] Plugin geladen.'); + } + + async onunload(): Promise { + console.log('[Logfire] Entlade Plugin...'); + + if (this.db) { + this.db.close(); + } + + console.log('[Logfire] Plugin entladen.'); + } + + // --------------------------------------------------------------------------- + // Track filtering (used by collectors in later features) + // --------------------------------------------------------------------------- + + shouldTrack(path: string): boolean { + 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; + } + + // --------------------------------------------------------------------------- + // 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); +} diff --git a/src/ui/settings-tab.ts b/src/ui/settings-tab.ts new file mode 100644 index 0000000..b97ccf3 --- /dev/null +++ b/src/ui/settings-tab.ts @@ -0,0 +1,257 @@ +import { App, PluginSettingTab, Setting, Notice } from 'obsidian'; +import type LogfirePlugin from '../main'; + +export class LogfireSettingTab extends PluginSettingTab { + constructor(app: App, private plugin: LogfirePlugin) { + super(app, plugin); + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + // ----- General ----- + containerEl.createEl('h2', { text: 'Allgemein' }); + + new Setting(containerEl) + .setName('Tracking aktiviert') + .setDesc('Hauptschalter für alle Event-Collector.') + .addToggle(t => t + .setValue(this.plugin.settings.general.enabled) + .onChange(async (v) => { + this.plugin.settings.general.enabled = v; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Log-Ordner') + .setDesc('Ordner für Markdown-Projektionen. Wird automatisch vom Tracking ausgeschlossen.') + .addText(t => t + .setPlaceholder('Logfire') + .setValue(this.plugin.settings.general.logFolder) + .onChange(async (v) => { + this.plugin.settings.general.logFolder = v || 'Logfire'; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Beim Start pausieren') + .setDesc('Startet im pausierten Zustand — keine Events bis manuell fortgesetzt.') + .addToggle(t => t + .setValue(this.plugin.settings.general.pauseOnStartup) + .onChange(async (v) => { + this.plugin.settings.general.pauseOnStartup = v; + await this.plugin.saveSettings(); + })); + + // ----- Tracking ----- + containerEl.createEl('h2', { text: 'Tracking' }); + + new Setting(containerEl) + .setName('Datei-Events') + .setDesc('Erstellen, Löschen, Umbenennen, Verschieben und Ändern von Dateien.') + .addToggle(t => t + .setValue(this.plugin.settings.tracking.fileEvents) + .onChange(async (v) => { + this.plugin.settings.tracking.fileEvents = v; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Content-Analyse') + .setDesc('Semantische Diffs: Wortzählung, Links, Tags, Überschriften, Frontmatter.') + .addToggle(t => t + .setValue(this.plugin.settings.tracking.contentAnalysis) + .onChange(async (v) => { + this.plugin.settings.tracking.contentAnalysis = v; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Navigation') + .setDesc('Aktive Datei-Wechsel, Datei öffnen/schließen mit Dauer.') + .addToggle(t => t + .setValue(this.plugin.settings.tracking.navigation) + .onChange(async (v) => { + this.plugin.settings.tracking.navigation = v; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Editor-Änderungen') + .setDesc('Tastenanschläge (debounced und aggregiert).') + .addToggle(t => t + .setValue(this.plugin.settings.tracking.editorChanges) + .onChange(async (v) => { + this.plugin.settings.tracking.editorChanges = v; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Kommando-Tracking') + .setDesc('Ausführung von Befehlen über Palette und Hotkeys.') + .addToggle(t => t + .setValue(this.plugin.settings.tracking.commandTracking) + .onChange(async (v) => { + this.plugin.settings.tracking.commandTracking = v; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Ausgeschlossene Ordner') + .setDesc('Komma-getrennte Liste von Ordnern, die nicht getrackt werden.') + .addText(t => t + .setPlaceholder('.obsidian, templates') + .setValue(this.plugin.settings.tracking.excludedFolders.join(', ')) + .onChange(async (v) => { + this.plugin.settings.tracking.excludedFolders = v + .split(',') + .map(s => s.trim()) + .filter(s => s.length > 0); + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Ausgeschlossene Patterns') + .setDesc('Komma-getrennte Glob-Patterns (z.B. **/*.excalidraw.md).') + .addText(t => t + .setPlaceholder('**/*.excalidraw.md') + .setValue(this.plugin.settings.tracking.excludedPatterns.join(', ')) + .onChange(async (v) => { + this.plugin.settings.tracking.excludedPatterns = v + .split(',') + .map(s => s.trim()) + .filter(s => s.length > 0); + await this.plugin.saveSettings(); + })); + + // ----- Advanced (collapsible) ----- + const advancedHeader = containerEl.createEl('details'); + advancedHeader.createEl('summary', { text: 'Erweiterte Einstellungen' }) + .style.cursor = 'pointer'; + + const advEl = advancedHeader.createDiv(); + advEl.createEl('p', { + text: 'Diese Einstellungen beeinflussen Performance und Speicher. Die Standardwerte sind für die meisten Vaults optimal.', + cls: 'setting-item-description', + }); + + // Performance + advEl.createEl('h3', { text: 'Performance' }); + + new Setting(advEl) + .setName('Editor-Debounce (ms)') + .setDesc('Wartezeit nach letztem Tastenanschlag vor editor:change Event.') + .addText(t => t + .setValue(String(this.plugin.settings.advanced.debounceMs)) + .onChange(async (v) => { + const n = parseInt(v, 10); + if (!isNaN(n) && n >= 500) { + this.plugin.settings.advanced.debounceMs = n; + await this.plugin.saveSettings(); + } + })); + + new Setting(advEl) + .setName('Flush-Intervall (ms)') + .setDesc('Wie oft gepufferte Events in die Datenbank geschrieben werden.') + .addText(t => t + .setValue(String(this.plugin.settings.advanced.flushIntervalMs)) + .onChange(async (v) => { + const n = parseInt(v, 10); + if (!isNaN(n) && n >= 1000) { + this.plugin.settings.advanced.flushIntervalMs = n; + await this.plugin.saveSettings(); + } + })); + + new Setting(advEl) + .setName('Flush-Schwellwert') + .setDesc('Maximale Events im Puffer vor erzwungenem Schreiben.') + .addText(t => t + .setValue(String(this.plugin.settings.advanced.flushThreshold)) + .onChange(async (v) => { + const n = parseInt(v, 10); + if (!isNaN(n) && n >= 10) { + this.plugin.settings.advanced.flushThreshold = n; + await this.plugin.saveSettings(); + } + })); + + // Retention + advEl.createEl('h3', { text: 'Aufbewahrung' }); + + new Setting(advEl) + .setName('Raw-Events aufbewahren (Tage)') + .setDesc('Danach werden Events in Tages-Statistiken aggregiert und gelöscht.') + .addText(t => t + .setValue(String(this.plugin.settings.advanced.retention.rawEventsDays)) + .onChange(async (v) => { + const n = parseInt(v, 10); + if (!isNaN(n) && n >= 1) { + this.plugin.settings.advanced.retention.rawEventsDays = n; + await this.plugin.saveSettings(); + } + })); + + new Setting(advEl) + .setName('Tages-Statistiken aufbewahren (Tage)') + .setDesc('Danach werden Tages-Statistiken in Monats-Statistiken aggregiert.') + .addText(t => t + .setValue(String(this.plugin.settings.advanced.retention.dailyStatsDays)) + .onChange(async (v) => { + const n = parseInt(v, 10); + if (!isNaN(n) && n >= 30) { + this.plugin.settings.advanced.retention.dailyStatsDays = n; + await this.plugin.saveSettings(); + } + })); + + new Setting(advEl) + .setName('Wartung beim Start') + .setDesc('Automatisch Retention-Cleanup beim Plugin-Start ausführen.') + .addToggle(t => t + .setValue(this.plugin.settings.advanced.retention.maintenanceOnStartup) + .onChange(async (v) => { + this.plugin.settings.advanced.retention.maintenanceOnStartup = v; + await this.plugin.saveSettings(); + })); + + // Database info + advEl.createEl('h3', { text: 'Datenbank' }); + + const dbInfoEl = advEl.createDiv(); + try { + const eventCount = this.plugin.db.getEventCount(); + const dbSize = this.plugin.db.getDatabaseSizeBytes(); + const oldest = this.plugin.db.getOldestEventTimestamp(); + + dbInfoEl.createEl('p', { + text: `Events: ${eventCount.toLocaleString()} | Größe: ${formatBytes(dbSize)} | Ältestes: ${oldest ? new Date(oldest).toLocaleDateString() : 'k.A.'}`, + }); + } catch { + dbInfoEl.createEl('p', { text: 'Datenbank-Info nicht verfügbar.' }); + } + + new Setting(advEl) + .setName('Datenbank-Aktionen') + .addButton(b => b + .setButtonText('Wartung ausführen') + .onClick(() => { + try { + this.plugin.db.runMaintenance(this.plugin.settings.advanced.retention); + new Notice('Logfire: Wartung abgeschlossen.'); + this.display(); + } catch (err) { + new Notice('Logfire: Wartung fehlgeschlagen.'); + console.error('[Logfire] Wartung fehlgeschlagen:', err); + } + })); + } +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +}