Localize UI to English across all 22 source files

Translates all German user-facing strings (command names, notices,
settings, modal labels, template names/descriptions, error messages,
status bar, and code comments) to English.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Oelfke 2026-02-12 12:17:24 +01:00
parent 878b144ccc
commit 3c8c22ee07
23 changed files with 377 additions and 270 deletions

107
CLAUDE.md Normal file
View file

@ -0,0 +1,107 @@
# Logfire — Obsidian Plugin
Kombiniertes Plugin aus **Basefire** (SQLite-Query-Engine) und **Logfire** (Event-Logging).
Trackt alle Vault-Aktivitaeten, speichert in SQLite, macht per SQL abfragbar, visualisiert mit Charts/Dashboards.
## Technischer Stack
- **SQLite**: `better-sqlite3` (nativ, synchron, schnell)
- **Build**: esbuild mit `nativeModulePlugin` fuer Electron-Kompatibilitaet
- **Desktop-only** (FileSystemAdapter erforderlich)
- **Autor**: tolvitty
## Architektur
```
src/
├── main.ts # Plugin-Einstieg, Lifecycle
├── types.ts # Event-Typen, Settings, Query-Interfaces
├── core/
│ ├── database.ts # better-sqlite3, Schema, Retention, Maintenance
│ ├── event-bus.ts # Circular Buffer, Pub/Sub, Auto-Flush
│ ├── session-manager.ts # Session-Start/End, Dauer-Tracking
│ ├── content-analyzer.ts # Snapshot-Cache, Wort-/Link-/Tag-Diffs
│ └── query-builder.ts # QueryConfig → parametrisiertes SQL
├── collectors/
│ ├── file-collector.ts # File CRUD Events
│ ├── content-collector.ts # Semantische Content-Analyse
│ ├── nav-collector.ts # Navigation-Tracking
│ ├── editor-collector.ts # CM6 ViewPlugin, Debouncing
│ └── system-collector.ts # Command-Tracking
├── query/
│ ├── processor.ts # Code-Block-Prozessoren (logfire, logfire-sql, logfire-dashboard)
│ ├── query-modal.ts # Interaktiver SQL-Editor (Shorthand + SQL)
│ └── virtual-tables.ts # _files, _links, _tags, _headings
├── viz/
│ ├── table-renderer.ts # Table, Timeline, Summary, Metric, List, Heatmap
│ ├── chart-renderer.ts # 10 SVG-Chart-Typen (Bar, Line, Pie, Gauge, ...)
│ └── dashboard.ts # Multi-Widget-Dashboards, Grid-Layout
├── management/
│ ├── history.ts # Automatische Query-History mit Metriken
│ ├── favorites.ts # Gespeicherte Queries, Kategorien, Tags
│ └── templates.ts # Built-in + Custom Templates, Parameter-Substitution
├── projection/
│ ├── formatters.ts # Query-Results → Markdown (Timeline, Table, Summary, Metric, Heatmap)
│ ├── template-registry.ts # Built-in + Custom ProjectionTemplate Verwaltung
│ ├── projection-engine.ts # Kern-Engine: Scheduling, Session-End-Listener, PickerModal
│ └── presets/
│ ├── daily-log.ts # Tagesprotokoll-Preset
│ ├── session-log.ts # Session-Protokoll-Preset
│ └── weekly-digest.ts # Wochenuebersicht-Preset
├── ui/
│ ├── settings-tab.ts # Obsidian-native Settings
│ ├── status-bar.ts # Live-Status-Widget (Recording/Paused)
│ ├── event-stream-view.ts # Echtzeit-Event-Sidebar
│ ├── schema-view.ts # Schema-Browser (Tabellen, Spalten, Indizes)
│ └── initial-scan-modal.ts # Initialer Vault-Scan mit Fortschritt
```
## DB-Schema
**Kern-Tabellen** (database.ts):
- `events` — id, timestamp, type, category, source, target, payload, session
- `sessions` — id, start_time, end_time, vault_name
- `baseline` — file_path, word_count, char_count, links, tags, headings, ...
- `daily_stats` — date, file_path, events_count, words_added/removed, time_active_ms
- `monthly_stats` — wie daily_stats, aggregiert pro Monat
**Virtual Tables** (virtual-tables.ts):
- `_files` — path, name, basename, extension, size, created, modified, folder
- `_links` — from_path, to_path, display_text, link_type
- `_tags` — path, tag
- `_headings` — path, level, heading
## Konventionen
- **Sprache**: Code und Variablennamen auf Englisch, UI-Texte und Commits auf Deutsch
- **Commits**: Kleinschrittig, atomar, deutsch. Niemals pushen (Nutzer pusht manuell)
- **Branching**: Feature-Branches (`feature/<name>`), Merge mit `--no-ff` in `main`
- **CSS**: Ausschliesslich Obsidian-Variablen, Monospace, "Utilitarian System Monitor" Aesthetic
- **Charts**: Reines SVG, keine externen Bibliotheken
- **Queries**: `Record<string, unknown>[]` Format (better-sqlite3 Rueckgabe)
- **Storage**: History/Favorites/Templates in localStorage, Plugin-Daten via loadData/saveData
## Feature-Roadmap
### Abgeschlossen
- [x] **Feature 1**: Projekt-Grundgeruest & Datenbank (`feature/grundgeruest`)
- [x] **Feature 2**: Event-System & Collectors (`feature/event-system`)
- [x] **Feature 3**: Echtzeit-UI (`feature/echtzeit-ui`)
- [x] **Feature 4**: SQL-Query-Engine (`feature/sql-engine`)
- [x] **Feature 5**: Virtual Tables (`feature/virtual-tables`)
- [x] **Feature 6**: Datenvisualisierung (`feature/visualisierung`)
- [x] **Feature 7**: Query-Management (`feature/query-management`)
- [x] **Feature 8**: Projections & Reports (`feature/projections`)
- [x] **Feature 9**: Polish & Extras (`feature/polish`)
## Build & Test
```bash
npm run build # Production-Build (esbuild)
npm run dev # Watch-Mode
```
Build-Output: `main.js` (aktuell ~86KB), `styles.css`, `manifest.json`
Zum Testen: Plugin-Ordner in `.obsidian/plugins/logfire/` eines Vaults verlinken/kopieren.

View file

@ -92,7 +92,7 @@ export class ContentCollector {
}); });
} }
} catch (err) { } catch (err) {
console.error('[Logfire] ContentCollector-Fehler:', err); console.error('[Logfire] ContentCollector error:', err);
} }
} }

View file

@ -49,7 +49,7 @@ export class EventBus {
try { try {
this.db.insertEvents(batch); this.db.insertEvents(batch);
} catch (err) { } catch (err) {
console.error('[Logfire] Flush fehlgeschlagen:', err); console.error('[Logfire] Flush failed:', err);
this.buffer.unshift(...batch); this.buffer.unshift(...batch);
} }
} }
@ -93,7 +93,7 @@ export class EventBus {
try { try {
cb(event); cb(event);
} catch (err) { } catch (err) {
console.error('[Logfire] Subscriber-Fehler:', err); console.error('[Logfire] Subscriber error:', err);
} }
} }
} }

View file

@ -49,7 +49,7 @@ export default class LogfirePlugin extends Plugin {
private paused = false; private paused = false;
async onload(): Promise<void> { async onload(): Promise<void> {
console.log('[Logfire] Lade Plugin...'); console.log('[Logfire] Loading plugin...');
await this.loadSettings(); await this.loadSettings();
@ -125,7 +125,7 @@ export default class LogfirePlugin extends Plugin {
this.statusBar = new StatusBar(this); this.statusBar = new StatusBar(this);
this.statusBar.start(); this.statusBar.start();
// Query: Code-Block-Prozessoren // Query: Code-block processors
registerLogfireBlock(this.db, (lang, handler) => { registerLogfireBlock(this.db, (lang, handler) => {
this.registerMarkdownCodeBlockProcessor(lang, handler); this.registerMarkdownCodeBlockProcessor(lang, handler);
}); });
@ -137,7 +137,7 @@ export default class LogfirePlugin extends Plugin {
}); });
// Ribbon icons // Ribbon icons
this.addRibbonIcon('activity', 'Logfire: Event-Stream', () => { this.addRibbonIcon('activity', 'Logfire: Event Stream', () => {
this.activateEventStream(); this.activateEventStream();
}); });
this.addRibbonIcon('layout-dashboard', 'Logfire: Dashboard', () => { this.addRibbonIcon('layout-dashboard', 'Logfire: Dashboard', () => {
@ -163,7 +163,7 @@ export default class LogfirePlugin extends Plugin {
try { try {
this.db.runMaintenance(this.settings.advanced.retention); this.db.runMaintenance(this.settings.advanced.retention);
} catch (err) { } catch (err) {
console.error('[Logfire] Wartung beim Start fehlgeschlagen:', err); console.error('[Logfire] Startup maintenance failed:', err);
} }
} }
@ -175,18 +175,18 @@ export default class LogfirePlugin extends Plugin {
this.projectionEngine = new ProjectionEngine(this.app, this.db, this.eventBus, this.settings); this.projectionEngine = new ProjectionEngine(this.app, this.db, this.eventBus, this.settings);
this.projectionEngine.start(); this.projectionEngine.start();
// Autocomplete (nach Virtual Tables) // Autocomplete (after virtual tables)
this.autocomplete = new SqlAutocomplete(this.db); this.autocomplete = new SqlAutocomplete(this.db);
// Keyboard Navigator // Keyboard Navigator
this.keyboardNav = new KeyboardNavigator(this.app); this.keyboardNav = new KeyboardNavigator(this.app);
}); });
console.log('[Logfire] Plugin geladen. Session:', this.sessionManager.currentSessionId); console.log('[Logfire] Plugin loaded. Session:', this.sessionManager.currentSessionId);
} }
async onunload(): Promise<void> { async onunload(): Promise<void> {
console.log('[Logfire] Entlade Plugin...'); console.log('[Logfire] Unloading plugin...');
cleanupAllRefreshTimers(); cleanupAllRefreshTimers();
this.projectionEngine?.destroy(); this.projectionEngine?.destroy();
@ -206,7 +206,7 @@ export default class LogfirePlugin extends Plugin {
this.db.close(); this.db.close();
} }
console.log('[Logfire] Plugin entladen.'); console.log('[Logfire] Plugin unloaded.');
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -304,57 +304,57 @@ export default class LogfirePlugin extends Plugin {
private registerCommands(): void { private registerCommands(): void {
this.addCommand({ this.addCommand({
id: 'show-event-stream', id: 'show-event-stream',
name: 'Event-Stream anzeigen', name: 'Show event stream',
callback: () => this.activateEventStream(), callback: () => this.activateEventStream(),
}); });
this.addCommand({ this.addCommand({
id: 'toggle-tracking', id: 'toggle-tracking',
name: 'Tracking pausieren/fortsetzen', name: 'Toggle tracking',
callback: () => { callback: () => {
if (this.paused) { if (this.paused) {
this.resume(); this.resume();
new Notice('Logfire: Tracking fortgesetzt.'); new Notice('Logfire: Tracking resumed.');
} else { } else {
this.pause(); this.pause();
new Notice('Logfire: Tracking pausiert.'); new Notice('Logfire: Tracking paused.');
} }
}, },
}); });
this.addCommand({ this.addCommand({
id: 'rescan-vault', id: 'rescan-vault',
name: 'Vault erneut scannen', name: 'Rescan vault',
callback: () => this.runInitialScan(), callback: () => this.runInitialScan(),
}); });
this.addCommand({ this.addCommand({
id: 'run-maintenance', id: 'run-maintenance',
name: 'Wartung ausführen', name: 'Run maintenance',
callback: () => { callback: () => {
this.db.runMaintenance(this.settings.advanced.retention); this.db.runMaintenance(this.settings.advanced.retention);
new Notice('Logfire: Wartung abgeschlossen.'); new Notice('Logfire: Maintenance complete.');
}, },
}); });
this.addCommand({ this.addCommand({
id: 'refresh-virtual-tables', id: 'refresh-virtual-tables',
name: 'Virtual Tables neu aufbauen', name: 'Rebuild virtual tables',
callback: () => { callback: () => {
this.virtualTables?.rebuild(); this.virtualTables?.rebuild();
new Notice('Logfire: Virtual Tables aktualisiert.'); new Notice('Logfire: Virtual tables updated.');
}, },
}); });
this.addCommand({ this.addCommand({
id: 'show-dashboard', id: 'show-dashboard',
name: 'Dashboard anzeigen', name: 'Show dashboard',
callback: () => this.activateDashboard(), callback: () => this.activateDashboard(),
}); });
this.addCommand({ this.addCommand({
id: 'open-query', id: 'open-query',
name: 'Query-Editor \u00f6ffnen', name: 'Open query editor',
callback: () => { callback: () => {
new QueryModal(this.app, this.db, this.historyManager, undefined, this.autocomplete).open(); new QueryModal(this.app, this.db, this.historyManager, undefined, this.autocomplete).open();
}, },
@ -362,13 +362,13 @@ export default class LogfirePlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'show-schema', id: 'show-schema',
name: 'Schema-Browser anzeigen', name: 'Show schema browser',
callback: () => this.activateSchema(), callback: () => this.activateSchema(),
}); });
this.addCommand({ this.addCommand({
id: 'show-templates', id: 'show-templates',
name: 'Query-Templates anzeigen', name: 'Show query templates',
callback: () => { callback: () => {
new TemplatePickerModal(this, this.templateManager, (sql) => { new TemplatePickerModal(this, this.templateManager, (sql) => {
new QueryModal(this.app, this.db, this.historyManager, sql, this.autocomplete).open(); new QueryModal(this.app, this.db, this.historyManager, sql, this.autocomplete).open();
@ -378,7 +378,7 @@ export default class LogfirePlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'save-favorite', id: 'save-favorite',
name: 'Aktuelle Query als Favorit speichern', name: 'Save current query as favorite',
callback: () => { callback: () => {
new SaveFavoriteModal(this, this.favoritesManager, '').open(); new SaveFavoriteModal(this, this.favoritesManager, '').open();
}, },
@ -402,7 +402,7 @@ export default class LogfirePlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'run-projection', id: 'run-projection',
name: 'Projektion manuell ausführen', name: 'Run projection',
callback: () => { callback: () => {
new ProjectionPickerModal(this.app, this.projectionEngine).open(); new ProjectionPickerModal(this.app, this.projectionEngine).open();
}, },
@ -410,7 +410,7 @@ export default class LogfirePlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'run-all-projections', id: 'run-all-projections',
name: 'Alle Projektionen ausführen', name: 'Run all projections',
callback: () => { callback: () => {
this.projectionEngine.runAllProjections(); this.projectionEngine.runAllProjections();
}, },
@ -418,33 +418,33 @@ export default class LogfirePlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'export-csv', id: 'export-csv',
name: 'Letzte Query als CSV exportieren', name: 'Export last query as CSV',
callback: () => { callback: () => {
new Notice('CSV-Export: Bitte Query-Editor öffnen und dort exportieren.'); new Notice('CSV export: Please open the query editor and export from there.');
}, },
}); });
this.addCommand({ this.addCommand({
id: 'export-json', id: 'export-json',
name: 'Letzte Query als JSON exportieren', name: 'Export last query as JSON',
callback: () => { callback: () => {
new Notice('JSON-Export: Bitte Query-Editor öffnen und dort exportieren.'); new Notice('JSON export: Please open the query editor and export from there.');
}, },
}); });
this.addCommand({ this.addCommand({
id: 'toggle-vim-nav', id: 'toggle-vim-nav',
name: 'Vim-Navigation umschalten', name: 'Toggle Vim navigation',
callback: () => { callback: () => {
if (this.keyboardNav?.isActive()) { if (this.keyboardNav?.isActive()) {
this.keyboardNav.detach(); this.keyboardNav.detach();
new Notice('Logfire: Vim-Navigation deaktiviert.'); new Notice('Logfire: Vim navigation disabled.');
} else { } else {
const active = document.querySelector('.logfire-qm-results, .logfire-dash-widget-content'); const active = document.querySelector('.logfire-qm-results, .logfire-dash-widget-content');
if (active instanceof HTMLElement && this.keyboardNav?.attach(active)) { if (active instanceof HTMLElement && this.keyboardNav?.attach(active)) {
new Notice('Logfire: Vim-Navigation aktiviert (j/k/h/l, gg/G, /, y, Enter, Esc).'); new Notice('Logfire: Vim navigation enabled (j/k/h/l, gg/G, /, y, Enter, Esc).');
} else { } else {
new Notice('Logfire: Keine Tabelle gefunden.'); new Notice('Logfire: No table found.');
} }
} }
}, },
@ -522,7 +522,7 @@ export default class LogfirePlugin extends Plugin {
private getDatabasePath(): string { private getDatabasePath(): string {
const adapter = this.app.vault.adapter; const adapter = this.app.vault.adapter;
if (!(adapter instanceof FileSystemAdapter)) { if (!(adapter instanceof FileSystemAdapter)) {
throw new Error('[Logfire] Benötigt einen Desktop-Vault mit Dateisystem-Zugriff.'); throw new Error('[Logfire] Requires a desktop vault with filesystem access.');
} }
const basePath = adapter.getBasePath(); const basePath = adapter.getBasePath();
return `${basePath}/${this.app.vault.configDir}/plugins/logfire/logfire.db`; return `${basePath}/${this.app.vault.configDir}/plugins/logfire/logfire.db`;

View file

@ -25,9 +25,9 @@ export interface FavoriteCategory {
const STORAGE_KEY = 'logfire-favorites'; const STORAGE_KEY = 'logfire-favorites';
const DEFAULT_CATEGORIES: FavoriteCategory[] = [ const DEFAULT_CATEGORIES: FavoriteCategory[] = [
{ id: 'allgemein', name: 'Allgemein', color: '#4C78A8' }, { id: 'general', name: 'General', color: '#4C78A8' },
{ id: 'analyse', name: 'Analyse', color: '#F58518' }, { id: 'analysis', name: 'Analysis', color: '#F58518' },
{ id: 'wartung', name: 'Wartung', color: '#E45756' }, { id: 'maintenance', name: 'Maintenance', color: '#E45756' },
]; ];
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -75,7 +75,7 @@ export class FavoritesManager {
// Favorites CRUD // Favorites CRUD
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
add(name: string, sql: string, category = 'allgemein', tags: string[] = []): Favorite { add(name: string, sql: string, category = 'general', tags: string[] = []): Favorite {
const fav: Favorite = { const fav: Favorite = {
id: `fav-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, id: `fav-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name, name,
@ -154,7 +154,7 @@ export class SaveFavoriteModal extends Modal {
private manager: FavoritesManager; private manager: FavoritesManager;
private sql: string; private sql: string;
private name = ''; private name = '';
private category = 'allgemein'; private category = 'general';
private tags: string[] = []; private tags: string[] = [];
constructor(plugin: LogfirePlugin, manager: FavoritesManager, sql: string) { constructor(plugin: LogfirePlugin, manager: FavoritesManager, sql: string) {
@ -167,41 +167,41 @@ export class SaveFavoriteModal extends Modal {
onOpen(): void { onOpen(): void {
const { contentEl } = this; const { contentEl } = this;
contentEl.addClass('logfire-save-fav-modal'); contentEl.addClass('logfire-save-fav-modal');
contentEl.createEl('h3', { text: 'Als Favorit speichern' }); contentEl.createEl('h3', { text: 'Save as Favorite' });
new Setting(contentEl).setName('Name').addText(text => new Setting(contentEl).setName('Name').addText(text =>
text.setPlaceholder('Meine Query').onChange(v => { this.name = v; }), text.setPlaceholder('My Query').onChange(v => { this.name = v; }),
); );
new Setting(contentEl).setName('Kategorie').addDropdown(dd => { new Setting(contentEl).setName('Category').addDropdown(dd => {
for (const cat of this.manager.getCategories()) dd.addOption(cat.id, cat.name); for (const cat of this.manager.getCategories()) dd.addOption(cat.id, cat.name);
dd.setValue(this.category); dd.setValue(this.category);
dd.onChange(v => { this.category = v; }); dd.onChange(v => { this.category = v; });
}); });
new Setting(contentEl).setName('Tags').setDesc('Komma-getrennt').addText(text => new Setting(contentEl).setName('Tags').setDesc('Comma-separated').addText(text =>
text.setPlaceholder('select, events').onChange(v => { text.setPlaceholder('select, events').onChange(v => {
this.tags = v.split(',').map(s => s.trim()).filter(Boolean); this.tags = v.split(',').map(s => s.trim()).filter(Boolean);
}), }),
); );
contentEl.createEl('h4', { text: 'SQL-Vorschau' }); contentEl.createEl('h4', { text: 'SQL Preview' });
contentEl.createEl('pre', { contentEl.createEl('pre', {
cls: 'logfire-sql-preview', cls: 'logfire-sql-preview',
text: this.sql.length > 200 ? this.sql.slice(0, 200) + '...' : this.sql, text: this.sql.length > 200 ? this.sql.slice(0, 200) + '...' : this.sql,
}); });
const btns = contentEl.createDiv({ cls: 'logfire-modal-buttons' }); const btns = contentEl.createDiv({ cls: 'logfire-modal-buttons' });
const saveBtn = btns.createEl('button', { text: 'Speichern', cls: 'mod-cta' }); const saveBtn = btns.createEl('button', { text: 'Save', cls: 'mod-cta' });
saveBtn.addEventListener('click', () => this.doSave()); saveBtn.addEventListener('click', () => this.doSave());
const cancelBtn = btns.createEl('button', { text: 'Abbrechen' }); const cancelBtn = btns.createEl('button', { text: 'Cancel' });
cancelBtn.addEventListener('click', () => this.close()); cancelBtn.addEventListener('click', () => this.close());
} }
private doSave(): void { private doSave(): void {
if (!this.name.trim()) { new Notice('Bitte einen Namen eingeben.'); return; } if (!this.name.trim()) { new Notice('Please enter a name.'); return; }
this.manager.add(this.name.trim(), this.sql, this.category, this.tags); this.manager.add(this.name.trim(), this.sql, this.category, this.tags);
new Notice(`Favorit "${this.name}" gespeichert.`); new Notice(`Favorite "${this.name}" saved.`);
this.close(); this.close();
} }

View file

@ -54,7 +54,7 @@ export class HistoryManager {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
addEntry(sql: string, executionTimeMs?: number, rowCount?: number): HistoryEntry { addEntry(sql: string, executionTimeMs?: number, rowCount?: number): HistoryEntry {
// Deduplizieren: gleiche SQL aktualisiert vorhandenen Eintrag // Deduplicate: same SQL updates existing entry
for (const entry of this.entries.values()) { for (const entry of this.entries.values()) {
if (entry.sql === sql) { if (entry.sql === sql) {
entry.executedAt = new Date().toISOString(); entry.executedAt = new Date().toISOString();
@ -75,7 +75,7 @@ export class HistoryManager {
}; };
this.entries.set(entry.id, entry); this.entries.set(entry.id, entry);
// Limit: aelteste nicht-favorisierte loeschen // Limit: delete oldest non-favorited entries
if (this.entries.size > MAX_ENTRIES) { if (this.entries.size > MAX_ENTRIES) {
const sorted = this.getAll(); const sorted = this.getAll();
for (const e of sorted.slice(MAX_ENTRIES)) { for (const e of sorted.slice(MAX_ENTRIES)) {

View file

@ -42,7 +42,7 @@ export class TemplateManager {
for (const t of arr) this.templates.set(t.id, t); for (const t of arr) this.templates.set(t.id, t);
} catch { /* ignore */ } } catch { /* ignore */ }
} }
// Immer Built-in Templates sicherstellen // Ensure built-in templates are always present
for (const t of BUILTIN_TEMPLATES) { for (const t of BUILTIN_TEMPLATES) {
if (!this.templates.has(t.id)) this.templates.set(t.id, t); if (!this.templates.has(t.id)) this.templates.set(t.id, t);
} }
@ -123,15 +123,15 @@ function parseParameters(sql: string): TemplateParameter[] {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Built-in Templates (angepasst an Logfire-Schema) // Built-in Templates (adapted for Logfire schema)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const BUILTIN_TEMPLATES: QueryTemplate[] = [ const BUILTIN_TEMPLATES: QueryTemplate[] = [
{ {
id: 'builtin-events-today', id: 'builtin-events-today',
name: 'Events heute', name: 'Events Today',
description: 'Alle Events seit Mitternacht', description: 'All events since midnight',
sql: `SELECT type, source, datetime(timestamp/1000, 'unixepoch', 'localtime') as zeit sql: `SELECT type, source, datetime(timestamp/1000, 'unixepoch', 'localtime') as time
FROM events FROM events
WHERE timestamp > (strftime('%s', 'now', 'start of day') * 1000) WHERE timestamp > (strftime('%s', 'now', 'start of day') * 1000)
ORDER BY timestamp DESC ORDER BY timestamp DESC
@ -141,9 +141,9 @@ LIMIT {{limit:100}}`,
}, },
{ {
id: 'builtin-active-files', id: 'builtin-active-files',
name: 'Aktivste Dateien', name: 'Most Active Files',
description: 'Dateien mit den meisten Events', description: 'Files with the most events',
sql: `SELECT source as datei, COUNT(*) as events sql: `SELECT source as file, COUNT(*) as events
FROM events FROM events
WHERE source IS NOT NULL WHERE source IS NOT NULL
GROUP BY source GROUP BY source
@ -155,14 +155,14 @@ LIMIT {{limit:20}}`,
{ {
id: 'builtin-session-overview', id: 'builtin-session-overview',
name: 'Sessions', name: 'Sessions',
description: 'Letzte Sessions mit Dauer', description: 'Recent sessions with duration',
sql: `SELECT sql: `SELECT
id, id,
datetime(start_time/1000, 'unixepoch', 'localtime') as start, datetime(start_time/1000, 'unixepoch', 'localtime') as start,
CASE WHEN end_time IS NOT NULL CASE WHEN end_time IS NOT NULL
THEN ROUND((end_time - start_time) / 60000.0, 1) || ' min' THEN ROUND((end_time - start_time) / 60000.0, 1) || ' min'
ELSE 'aktiv' ELSE 'active'
END as dauer END as duration
FROM sessions FROM sessions
ORDER BY start_time DESC ORDER BY start_time DESC
LIMIT {{limit:10}}`, LIMIT {{limit:10}}`,
@ -171,23 +171,23 @@ LIMIT {{limit:10}}`,
}, },
{ {
id: 'builtin-daily-stats', id: 'builtin-daily-stats',
name: 'Tagesstatistik', name: 'Daily Statistics',
description: 'Aggregierte Statistiken pro Tag', description: 'Aggregated statistics per day',
sql: `SELECT date as tag, SUM(events_count) as events, sql: `SELECT date as day, SUM(events_count) as events,
SUM(words_added) as woerter_hinzu, SUM(words_removed) as woerter_entfernt SUM(words_added) as words_added, SUM(words_removed) as words_removed
FROM daily_stats FROM daily_stats
GROUP BY date GROUP BY date
ORDER BY date DESC ORDER BY date DESC
LIMIT {{limit:14}}`, LIMIT {{limit:14}}`,
parameters: [{ name: 'limit', label: 'Tage', defaultValue: '14', type: 'number' }], parameters: [{ name: 'limit', label: 'Days', defaultValue: '14', type: 'number' }],
builtIn: true, builtIn: true,
}, },
{ {
id: 'builtin-recent-files', id: 'builtin-recent-files',
name: 'Zuletzt geaenderte Dateien', name: 'Recently Modified Files',
description: 'Dateien nach Aenderungsdatum', description: 'Files by modification date',
sql: `SELECT name, extension, sql: `SELECT name, extension,
datetime(modified/1000, 'unixepoch', 'localtime') as geaendert, datetime(modified/1000, 'unixepoch', 'localtime') as modified_at,
folder folder
FROM _files FROM _files
WHERE extension = 'md' WHERE extension = 'md'
@ -198,22 +198,22 @@ LIMIT {{limit:20}}`,
}, },
{ {
id: 'builtin-tag-stats', id: 'builtin-tag-stats',
name: 'Tag-Statistiken', name: 'Tag Statistics',
description: 'Notizen pro Tag zaehlen', description: 'Count notes per tag',
sql: `SELECT tag, COUNT(DISTINCT path) as notizen sql: `SELECT tag, COUNT(DISTINCT path) as notes
FROM _tags FROM _tags
GROUP BY tag GROUP BY tag
ORDER BY notizen DESC ORDER BY notes DESC
LIMIT {{limit:30}}`, LIMIT {{limit:30}}`,
parameters: [{ name: 'limit', label: 'Limit', defaultValue: '30', type: 'number' }], parameters: [{ name: 'limit', label: 'Limit', defaultValue: '30', type: 'number' }],
builtIn: true, builtIn: true,
}, },
{ {
id: 'builtin-orphan-notes', id: 'builtin-orphan-notes',
name: 'Verwaiste Notizen', name: 'Orphan Notes',
description: 'Notizen ohne eingehende Links', description: 'Notes without incoming links',
sql: `SELECT f.path, f.name, sql: `SELECT f.path, f.name,
datetime(f.modified/1000, 'unixepoch', 'localtime') as geaendert datetime(f.modified/1000, 'unixepoch', 'localtime') as modified_at
FROM _files f FROM _files f
WHERE f.extension = 'md' WHERE f.extension = 'md'
AND f.path NOT IN (SELECT DISTINCT to_path FROM _links) AND f.path NOT IN (SELECT DISTINCT to_path FROM _links)
@ -223,9 +223,9 @@ ORDER BY f.modified DESC`,
}, },
{ {
id: 'builtin-broken-links', id: 'builtin-broken-links',
name: 'Defekte Links', name: 'Broken Links',
description: 'Links zu nicht-existierenden Notizen', description: 'Links to non-existent notes',
sql: `SELECT l.from_path as von, l.to_path as nach, l.display_text sql: `SELECT l.from_path as from_file, l.to_path as to_file, l.display_text
FROM _links l FROM _links l
WHERE l.to_path NOT IN (SELECT path FROM _files) WHERE l.to_path NOT IN (SELECT path FROM _files)
AND l.link_type = 'link'`, AND l.link_type = 'link'`,
@ -234,12 +234,12 @@ WHERE l.to_path NOT IN (SELECT path FROM _files)
}, },
{ {
id: 'builtin-event-types', id: 'builtin-event-types',
name: 'Event-Typen', name: 'Event Types',
description: 'Verteilung der Event-Typen', description: 'Distribution of event types',
sql: `SELECT type as typ, category as kategorie, COUNT(*) as anzahl sql: `SELECT type, category, COUNT(*) as count
FROM events FROM events
GROUP BY type, category GROUP BY type, category
ORDER BY anzahl DESC`, ORDER BY count DESC`,
parameters: [], parameters: [],
builtIn: true, builtIn: true,
}, },
@ -262,7 +262,7 @@ export class TemplatePickerModal extends Modal {
onOpen(): void { onOpen(): void {
const { contentEl } = this; const { contentEl } = this;
contentEl.addClass('logfire-template-picker'); contentEl.addClass('logfire-template-picker');
contentEl.createEl('h2', { text: 'Query-Templates' }); contentEl.createEl('h2', { text: 'Query Templates' });
const list = contentEl.createDiv({ cls: 'logfire-template-list' }); const list = contentEl.createDiv({ cls: 'logfire-template-list' });
@ -285,7 +285,7 @@ export class TemplatePickerModal extends Modal {
}); });
const actions = item.createDiv({ cls: 'logfire-template-actions' }); const actions = item.createDiv({ cls: 'logfire-template-actions' });
const useBtn = actions.createEl('button', { text: 'Verwenden', cls: 'mod-cta' }); const useBtn = actions.createEl('button', { text: 'Use', cls: 'mod-cta' });
useBtn.addEventListener('click', () => { useBtn.addEventListener('click', () => {
if (tpl.parameters.length > 0) { if (tpl.parameters.length > 0) {
this.close(); this.close();
@ -299,11 +299,11 @@ export class TemplatePickerModal extends Modal {
}); });
if (!tpl.builtIn) { if (!tpl.builtIn) {
const delBtn = actions.createEl('button', { text: 'Entfernen' }); const delBtn = actions.createEl('button', { text: 'Remove' });
delBtn.addEventListener('click', () => { delBtn.addEventListener('click', () => {
this.manager.delete(tpl.id); this.manager.delete(tpl.id);
item.remove(); item.remove();
new Notice('Template entfernt.'); new Notice('Template removed.');
}); });
} }
} }
@ -334,7 +334,7 @@ class ParameterModal extends Modal {
onOpen(): void { onOpen(): void {
const { contentEl } = this; const { contentEl } = this;
contentEl.createEl('h3', { text: `Parameter: ${this.template.name}` }); contentEl.createEl('h3', { text: `Parameters: ${this.template.name}` });
for (const param of this.template.parameters) { for (const param of this.template.parameters) {
new Setting(contentEl).setName(param.label).addText(text => { new Setting(contentEl).setName(param.label).addText(text => {
@ -346,9 +346,9 @@ class ParameterModal extends Modal {
} }
const btns = contentEl.createDiv({ cls: 'logfire-modal-buttons' }); const btns = contentEl.createDiv({ cls: 'logfire-modal-buttons' });
const runBtn = btns.createEl('button', { text: 'Ausfuehren', cls: 'mod-cta' }); const runBtn = btns.createEl('button', { text: 'Execute', cls: 'mod-cta' });
runBtn.addEventListener('click', () => { this.onSubmit(this.values); this.close(); }); runBtn.addEventListener('click', () => { this.onSubmit(this.values); this.close(); });
const cancelBtn = btns.createEl('button', { text: 'Abbrechen' }); const cancelBtn = btns.createEl('button', { text: 'Cancel' });
cancelBtn.addEventListener('click', () => this.close()); cancelBtn.addEventListener('click', () => this.close());
} }

View file

@ -6,7 +6,7 @@ import { toMarkdownTable } from '../viz/table-renderer';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function formatSection(rows: Record<string, unknown>[], section: SectionConfig): string { export function formatSection(rows: Record<string, unknown>[], section: SectionConfig): string {
if (rows.length === 0) return `### ${section.heading}\n\n*Keine Daten.*\n`; if (rows.length === 0) return `### ${section.heading}\n\n*No data.*\n`;
const heading = `### ${section.heading}\n\n`; const heading = `### ${section.heading}\n\n`;

View file

@ -2,8 +2,8 @@ import { ProjectionTemplate } from '../../types';
export const dailyLogTemplate: ProjectionTemplate = { export const dailyLogTemplate: ProjectionTemplate = {
id: 'builtin:daily-log', id: 'builtin:daily-log',
name: 'Tagesprotokoll', name: 'Daily Log',
description: 'Sessions, aktive Dateien, Events und Zeitleiste des Tages.', description: 'Sessions, active files, events, and timeline of the day.',
enabled: false, enabled: false,
trigger: { type: 'manual' }, trigger: { type: 'manual' },
output: { output: {
@ -30,8 +30,8 @@ export const dailyLogTemplate: ProjectionTemplate = {
name: 'table', name: 'table',
columns: [ columns: [
{ header: 'Session', value: 'session' }, { header: 'Session', value: 'session' },
{ header: 'Typ', value: 'type' }, { header: 'Type', value: 'type' },
{ header: 'Zeit', value: 'timestamp' }, { header: 'Time', value: 'timestamp' },
], ],
}, },
}, },
@ -39,7 +39,7 @@ export const dailyLogTemplate: ProjectionTemplate = {
}, },
{ {
id: 'active-files', id: 'active-files',
heading: 'Aktive Dateien', heading: 'Active Files',
type: 'table', type: 'table',
query: { query: {
timeRange: { type: 'relative', value: 'today' }, timeRange: { type: 'relative', value: 'today' },
@ -53,10 +53,10 @@ export const dailyLogTemplate: ProjectionTemplate = {
builtin: { builtin: {
name: 'table', name: 'table',
columns: [ columns: [
{ header: 'Datei', value: 'group' }, { header: 'File', value: 'group' },
{ header: 'Events', value: 'count' }, { header: 'Events', value: 'count' },
{ header: 'Woerter+', value: 'words_added' }, { header: 'Words+', value: 'words_added' },
{ header: 'Woerter-', value: 'words_removed' }, { header: 'Words-', value: 'words_removed' },
], ],
}, },
}, },
@ -64,7 +64,7 @@ export const dailyLogTemplate: ProjectionTemplate = {
}, },
{ {
id: 'event-summary', id: 'event-summary',
heading: 'Event-Uebersicht', heading: 'Event Summary',
type: 'table', type: 'table',
query: { query: {
timeRange: { type: 'relative', value: 'today' }, timeRange: { type: 'relative', value: 'today' },
@ -77,8 +77,8 @@ export const dailyLogTemplate: ProjectionTemplate = {
builtin: { builtin: {
name: 'table', name: 'table',
columns: [ columns: [
{ header: 'Typ', value: 'group' }, { header: 'Type', value: 'group' },
{ header: 'Anzahl', value: 'count' }, { header: 'Count', value: 'count' },
], ],
}, },
}, },
@ -86,7 +86,7 @@ export const dailyLogTemplate: ProjectionTemplate = {
}, },
{ {
id: 'timeline', id: 'timeline',
heading: 'Zeitleiste', heading: 'Timeline',
type: 'timeline', type: 'timeline',
query: { query: {
timeRange: { type: 'relative', value: 'today' }, timeRange: { type: 'relative', value: 'today' },

View file

@ -2,8 +2,8 @@ import { ProjectionTemplate } from '../../types';
export const sessionLogTemplate: ProjectionTemplate = { export const sessionLogTemplate: ProjectionTemplate = {
id: 'builtin:session-log', id: 'builtin:session-log',
name: 'Session-Protokoll', name: 'Session Log',
description: 'Einzelne Session: Dauer, bearbeitete Dateien, ausgefuehrte Befehle.', description: 'Single session: duration, edited files, executed commands.',
enabled: false, enabled: false,
trigger: { type: 'on-session-end' }, trigger: { type: 'on-session-end' },
output: { output: {
@ -19,7 +19,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
sections: [ sections: [
{ {
id: 'overview', id: 'overview',
heading: 'Session-Uebersicht', heading: 'Session Overview',
type: 'summary', type: 'summary',
query: { query: {
timeRange: { type: 'session' }, timeRange: { type: 'session' },
@ -29,7 +29,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
builtin: { builtin: {
name: 'summary', name: 'summary',
metrics: [ metrics: [
{ label: 'Events gesamt', aggregate: 'count' }, { label: 'Total events', aggregate: 'count' },
], ],
}, },
}, },
@ -37,7 +37,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
}, },
{ {
id: 'files', id: 'files',
heading: 'Bearbeitete Dateien', heading: 'Edited Files',
type: 'table', type: 'table',
query: { query: {
timeRange: { type: 'session' }, timeRange: { type: 'session' },
@ -51,10 +51,10 @@ export const sessionLogTemplate: ProjectionTemplate = {
builtin: { builtin: {
name: 'table', name: 'table',
columns: [ columns: [
{ header: 'Datei', value: 'group' }, { header: 'File', value: 'group' },
{ header: 'Events', value: 'count' }, { header: 'Events', value: 'count' },
{ header: 'Woerter+', value: 'words_added' }, { header: 'Words+', value: 'words_added' },
{ header: 'Woerter-', value: 'words_removed' }, { header: 'Words-', value: 'words_removed' },
], ],
}, },
}, },
@ -62,7 +62,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
}, },
{ {
id: 'commands', id: 'commands',
heading: 'Ausgefuehrte Befehle', heading: 'Executed Commands',
type: 'table', type: 'table',
query: { query: {
timeRange: { type: 'session' }, timeRange: { type: 'session' },
@ -74,8 +74,8 @@ export const sessionLogTemplate: ProjectionTemplate = {
builtin: { builtin: {
name: 'table', name: 'table',
columns: [ columns: [
{ header: 'Zeit', value: 'timestamp' }, { header: 'Time', value: 'timestamp' },
{ header: 'Befehl', value: 'source' }, { header: 'Command', value: 'source' },
], ],
}, },
}, },

View file

@ -2,8 +2,8 @@ import { ProjectionTemplate } from '../../types';
export const weeklyDigestTemplate: ProjectionTemplate = { export const weeklyDigestTemplate: ProjectionTemplate = {
id: 'builtin:weekly-digest', id: 'builtin:weekly-digest',
name: 'Wochen-Digest', name: 'Weekly Digest',
description: 'Wochenuebersicht: Tages-Summary, Top-Dateien, Schreib-Statistiken, Heatmap.', description: 'Weekly overview: daily summary, top files, writing statistics, heatmap.',
enabled: false, enabled: false,
trigger: { type: 'manual' }, trigger: { type: 'manual' },
output: { output: {
@ -18,7 +18,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
sections: [ sections: [
{ {
id: 'daily-summary', id: 'daily-summary',
heading: 'Tages-Uebersicht', heading: 'Daily Overview',
type: 'table', type: 'table',
query: { query: {
timeRange: { type: 'relative', value: 'last-7-days' }, timeRange: { type: 'relative', value: 'last-7-days' },
@ -31,10 +31,10 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
builtin: { builtin: {
name: 'table', name: 'table',
columns: [ columns: [
{ header: 'Tag', value: 'group' }, { header: 'Day', value: 'group' },
{ header: 'Events', value: 'count' }, { header: 'Events', value: 'count' },
{ header: 'Woerter+', value: 'words_added' }, { header: 'Words+', value: 'words_added' },
{ header: 'Woerter-', value: 'words_removed' }, { header: 'Words-', value: 'words_removed' },
], ],
}, },
}, },
@ -42,7 +42,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
}, },
{ {
id: 'top-files', id: 'top-files',
heading: 'Top-Dateien', heading: 'Top Files',
type: 'table', type: 'table',
query: { query: {
timeRange: { type: 'relative', value: 'last-7-days' }, timeRange: { type: 'relative', value: 'last-7-days' },
@ -56,9 +56,9 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
builtin: { builtin: {
name: 'table', name: 'table',
columns: [ columns: [
{ header: 'Datei', value: 'group' }, { header: 'File', value: 'group' },
{ header: 'Events', value: 'count' }, { header: 'Events', value: 'count' },
{ header: 'Woerter+', value: 'words_added' }, { header: 'Words+', value: 'words_added' },
], ],
}, },
}, },
@ -66,7 +66,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
}, },
{ {
id: 'write-stats', id: 'write-stats',
heading: 'Schreib-Statistiken', heading: 'Writing Statistics',
type: 'summary', type: 'summary',
query: { query: {
timeRange: { type: 'relative', value: 'last-7-days' }, timeRange: { type: 'relative', value: 'last-7-days' },
@ -77,9 +77,9 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
builtin: { builtin: {
name: 'summary', name: 'summary',
metrics: [ metrics: [
{ label: 'Tage aktiv', aggregate: 'count' }, { label: 'Days active', aggregate: 'count' },
{ label: 'Woerter geschrieben', aggregate: 'sum', field: 'words_added' }, { label: 'Words written', aggregate: 'sum', field: 'words_added' },
{ label: 'Woerter geloescht', aggregate: 'sum', field: 'words_removed' }, { label: 'Words deleted', aggregate: 'sum', field: 'words_removed' },
], ],
}, },
}, },
@ -87,7 +87,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
}, },
{ {
id: 'activity-heatmap', id: 'activity-heatmap',
heading: 'Aktivitaets-Heatmap', heading: 'Activity Heatmap',
type: 'chart-data', type: 'chart-data',
query: { query: {
timeRange: { type: 'relative', value: 'last-7-days' }, timeRange: { type: 'relative', value: 'last-7-days' },

View file

@ -32,7 +32,7 @@ export class ProjectionEngine {
start(): void { start(): void {
if (!this.settings.projections.enabled) return; if (!this.settings.projections.enabled) return;
// Session-End-Listener fuer session-log // Session-end listener for session-log
if (this.settings.projections.sessionLog.enabled) { if (this.settings.projections.sessionLog.enabled) {
this.sessionEndUnsub = this.eventBus.onEvent('system:session-end', (event) => { this.sessionEndUnsub = this.eventBus.onEvent('system:session-end', (event) => {
const templates = this.registry.getByTrigger('on-session-end'); const templates = this.registry.getByTrigger('on-session-end');
@ -42,7 +42,7 @@ export class ProjectionEngine {
}); });
} }
// Scheduler fuer daily + weekly (prueft alle 60s) // Scheduler for daily + weekly (checks every 60s)
this.schedulerTimer = setInterval(() => this.checkSchedule(), 60_000); this.schedulerTimer = setInterval(() => this.checkSchedule(), 60_000);
} }
@ -75,7 +75,7 @@ export class ProjectionEngine {
if (template) this.runProjection(template); if (template) this.runProjection(template);
} }
// Weekly Digest (dayOfWeek: 0=So, 1=Mo, ...) // Weekly Digest (dayOfWeek: 0=Sun, 1=Mon, ...)
if ( if (
this.settings.projections.weeklyDigest.enabled && this.settings.projections.weeklyDigest.enabled &&
now.getDay() === this.settings.projections.weeklyDigest.dayOfWeek && now.getDay() === this.settings.projections.weeklyDigest.dayOfWeek &&
@ -115,7 +115,7 @@ export class ProjectionEngine {
const queryConfig = { ...section.query }; const queryConfig = { ...section.query };
// Session-Kontext einsetzen // Inject session context
if (queryConfig.timeRange.type === 'session' && context?.sessionId) { if (queryConfig.timeRange.type === 'session' && context?.sessionId) {
queryConfig.timeRange = { ...queryConfig.timeRange, sessionId: context.sessionId }; queryConfig.timeRange = { ...queryConfig.timeRange, sessionId: context.sessionId };
} }
@ -133,10 +133,10 @@ export class ProjectionEngine {
}); });
const filePath = `${outputFolder}/${fileName}`; const filePath = `${outputFolder}/${fileName}`;
// Ordner erstellen falls noetig // Create folder if needed
await this.ensureFolder(outputFolder); await this.ensureFolder(outputFolder);
// Datei schreiben // Write file
const existing = this.app.vault.getAbstractFileByPath(filePath); const existing = this.app.vault.getAbstractFileByPath(filePath);
if (existing) { if (existing) {
if (template.output.mode === 'append') { if (template.output.mode === 'append') {
@ -151,8 +151,8 @@ export class ProjectionEngine {
return filePath; return filePath;
} catch (err) { } catch (err) {
console.error('[Logfire] Projektion fehlgeschlagen:', err); console.error('[Logfire] Projection failed:', err);
new Notice(`Logfire: Projektion "${template.name}" fehlgeschlagen.`); new Notice(`Logfire: Projection "${template.name}" failed.`);
return null; return null;
} }
} }
@ -166,7 +166,7 @@ export class ProjectionEngine {
if (result) count++; if (result) count++;
} }
} }
new Notice(`Logfire: ${count} Projektion(en) ausgefuehrt.`); new Notice(`Logfire: ${count} projection(s) executed.`);
} }
private async ensureFolder(path: string): Promise<void> { private async ensureFolder(path: string): Promise<void> {
@ -198,7 +198,7 @@ export class ProjectionPickerModal extends Modal {
contentEl.empty(); contentEl.empty();
contentEl.addClass('logfire-projection-picker'); contentEl.addClass('logfire-projection-picker');
contentEl.createEl('h2', { text: 'Projektion ausfuehren' }); contentEl.createEl('h2', { text: 'Run Projection' });
const list = contentEl.createDiv({ cls: 'logfire-template-list' }); const list = contentEl.createDiv({ cls: 'logfire-template-list' });
const templates = this.engine.getRegistry().getAll(); const templates = this.engine.getRegistry().getAll();
@ -221,11 +221,11 @@ export class ProjectionPickerModal extends Modal {
}); });
const actions = item.createDiv({ cls: 'logfire-template-actions' }); const actions = item.createDiv({ cls: 'logfire-template-actions' });
const runBtn = actions.createEl('button', { text: 'Ausfuehren', cls: 'mod-cta' }); const runBtn = actions.createEl('button', { text: 'Execute', cls: 'mod-cta' });
runBtn.addEventListener('click', async () => { runBtn.addEventListener('click', async () => {
const path = await this.engine.runProjection(template); const path = await this.engine.runProjection(template);
if (path) { if (path) {
new Notice(`Projektion geschrieben: ${path}`); new Notice(`Projection written: ${path}`);
} }
this.close(); this.close();
}); });

View file

@ -63,7 +63,7 @@ export class SqlAutocomplete {
) as { name: string }[]; ) as { name: string }[];
this.columnCache.set(table, rows.map(r => r.name)); this.columnCache.set(table, rows.map(r => r.name));
} catch { } catch {
// Virtual tables oder nicht existente Tabellen ueberspringen // Skip virtual tables or non-existent tables
} }
} }
} }
@ -91,7 +91,7 @@ export class SqlAutocomplete {
case 'where': case 'where':
case 'group-by': case 'group-by':
case 'order-by': { case 'order-by': {
// Spalten aus aktiven Tabellen // Columns from active tables
const tables = extractTables(textBefore); const tables = extractTables(textBefore);
for (const table of tables) { for (const table of tables) {
const cols = this.columnCache.get(table) ?? []; const cols = this.columnCache.get(table) ?? [];

View file

@ -76,14 +76,14 @@ export function registerLogfireSqlBlock(
const { sql, refresh } = parseSqlBlock(source); const { sql, refresh } = parseSqlBlock(source);
if (!sql) { if (!sql) {
renderError(el, new Error('Leere Query.')); renderError(el, new Error('Empty query.'));
return; return;
} }
// Safety: only SELECT/WITH // Safety: only SELECT/WITH
const firstWord = sql.split(/\s+/)[0].toUpperCase(); const firstWord = sql.split(/\s+/)[0].toUpperCase();
if (firstWord !== 'SELECT' && firstWord !== 'WITH') { if (firstWord !== 'SELECT' && firstWord !== 'WITH') {
renderError(el, new Error('Nur SELECT- und WITH-Queries sind erlaubt.')); renderError(el, new Error('Only SELECT and WITH queries are allowed.'));
return; return;
} }
@ -92,7 +92,7 @@ export function registerLogfireSqlBlock(
try { try {
const rows = db.queryReadOnly(sql) as Record<string, unknown>[]; const rows = db.queryReadOnly(sql) as Record<string, unknown>[];
if (!Array.isArray(rows) || rows.length === 0) { if (!Array.isArray(rows) || rows.length === 0) {
el.createEl('p', { text: 'Keine Ergebnisse.', cls: 'logfire-empty' }); el.createEl('p', { text: 'No results.', cls: 'logfire-empty' });
return; return;
} }
@ -108,7 +108,7 @@ export function registerLogfireSqlBlock(
try { try {
const freshRows = db.queryReadOnly(sql) as Record<string, unknown>[]; const freshRows = db.queryReadOnly(sql) as Record<string, unknown>[];
if (!Array.isArray(freshRows) || freshRows.length === 0) { if (!Array.isArray(freshRows) || freshRows.length === 0) {
el.createEl('p', { text: 'Keine Ergebnisse.', cls: 'logfire-empty' }); el.createEl('p', { text: 'No results.', cls: 'logfire-empty' });
return; return;
} }
if (chartConfig) { if (chartConfig) {
@ -139,7 +139,7 @@ export function registerLogfireDashboardBlock(
registerFn('logfire-dashboard', (source, el, ctx) => { registerFn('logfire-dashboard', (source, el, ctx) => {
const dashboard = parseDashboardBlock(source); const dashboard = parseDashboardBlock(source);
if (!dashboard) { if (!dashboard) {
renderError(el, new Error('Ungültige Dashboard-Definition.')); renderError(el, new Error('Invalid dashboard definition.'));
return; return;
} }
@ -170,7 +170,7 @@ export function registerLogfireDashboardBlock(
} else if (widget.sql) { } else if (widget.sql) {
const rows = db.queryReadOnly(widget.sql) as Record<string, unknown>[]; const rows = db.queryReadOnly(widget.sql) as Record<string, unknown>[];
if (rows.length === 0) { if (rows.length === 0) {
content.createDiv({ cls: 'logfire-empty', text: 'Keine Ergebnisse.' }); content.createDiv({ cls: 'logfire-empty', text: 'No results.' });
} else if (widget.type === 'chart' && widget.chartConfig) { } else if (widget.type === 'chart' && widget.chartConfig) {
renderChart(content, rows, widget.chartConfig); renderChart(content, rows, widget.chartConfig);
} else if (widget.type === 'stat') { } else if (widget.type === 'stat') {
@ -182,7 +182,7 @@ export function registerLogfireDashboardBlock(
} catch (err) { } catch (err) {
content.createDiv({ content.createDiv({
cls: 'logfire-error', cls: 'logfire-error',
text: `Fehler: ${err instanceof Error ? err.message : String(err)}`, text: `Error: ${err instanceof Error ? err.message : String(err)}`,
}); });
} }
} }
@ -292,7 +292,7 @@ function parseSqlBlock(source: string): { sql: string; refresh?: number } {
function renderResult(el: HTMLElement, rows: Record<string, unknown>[], format: string, columns?: string[]): void { function renderResult(el: HTMLElement, rows: Record<string, unknown>[], format: string, columns?: string[]): void {
if (rows.length === 0) { if (rows.length === 0) {
el.createEl('p', { text: 'Keine Events gefunden.', cls: 'logfire-empty' }); el.createEl('p', { text: 'No events found.', cls: 'logfire-empty' });
return; return;
} }

View file

@ -35,17 +35,17 @@ export class QueryModal extends Modal {
// Mode toggle // Mode toggle
const toolbar = contentEl.createDiv({ cls: 'logfire-qm-toolbar' }); const toolbar = contentEl.createDiv({ cls: 'logfire-qm-toolbar' });
this.modeToggle = toolbar.createEl('button', { text: 'Modus: Shorthand' }); this.modeToggle = toolbar.createEl('button', { text: 'Mode: Shorthand' });
this.modeToggle.addEventListener('click', () => { this.modeToggle.addEventListener('click', () => {
this.mode = this.mode === 'shorthand' ? 'sql' : 'shorthand'; this.mode = this.mode === 'shorthand' ? 'sql' : 'shorthand';
this.modeToggle.textContent = this.mode === 'shorthand' ? 'Modus: Shorthand' : 'Modus: SQL'; this.modeToggle.textContent = this.mode === 'shorthand' ? 'Mode: Shorthand' : 'Mode: SQL';
this.editorEl.placeholder = this.mode === 'shorthand' this.editorEl.placeholder = this.mode === 'shorthand'
? 'events today\nstats this-week group by file\nfiles modified yesterday' ? 'events today\nstats this-week group by file\nfiles modified yesterday'
: 'SELECT * FROM events\nWHERE type = \'file:create\'\nORDER BY timestamp DESC\nLIMIT 50'; : 'SELECT * FROM events\nWHERE type = \'file:create\'\nORDER BY timestamp DESC\nLIMIT 50';
}); });
const helpSpan = toolbar.createEl('span', { const helpSpan = toolbar.createEl('span', {
text: 'Ctrl+Enter: Ausf\u00fchren', text: 'Ctrl+Enter: Execute',
cls: 'logfire-qm-hint', cls: 'logfire-qm-hint',
}); });
@ -87,13 +87,13 @@ export class QueryModal extends Modal {
// Buttons // Buttons
const buttonRow = contentEl.createDiv({ cls: 'logfire-qm-buttons' }); const buttonRow = contentEl.createDiv({ cls: 'logfire-qm-buttons' });
const runBtn = buttonRow.createEl('button', { text: 'Ausf\u00fchren', cls: 'mod-cta' }); const runBtn = buttonRow.createEl('button', { text: 'Execute', cls: 'mod-cta' });
runBtn.addEventListener('click', () => this.executeQuery()); runBtn.addEventListener('click', () => this.executeQuery());
const copyBtn = buttonRow.createEl('button', { text: 'Als Markdown kopieren' }); const copyBtn = buttonRow.createEl('button', { text: 'Copy as Markdown' });
copyBtn.addEventListener('click', () => this.copyAsMarkdown()); copyBtn.addEventListener('click', () => this.copyAsMarkdown());
const insertBtn = buttonRow.createEl('button', { text: 'In Notiz einf\u00fcgen' }); const insertBtn = buttonRow.createEl('button', { text: 'Insert into note' });
insertBtn.addEventListener('click', () => this.insertInNote()); insertBtn.addEventListener('click', () => this.insertInNote());
const csvBtn = buttonRow.createEl('button', { text: 'CSV Export' }); const csvBtn = buttonRow.createEl('button', { text: 'CSV Export' });
@ -102,7 +102,7 @@ export class QueryModal extends Modal {
const jsonBtn = buttonRow.createEl('button', { text: 'JSON Export' }); const jsonBtn = buttonRow.createEl('button', { text: 'JSON Export' });
jsonBtn.addEventListener('click', () => this.exportJson()); jsonBtn.addEventListener('click', () => this.exportJson());
const clearBtn = buttonRow.createEl('button', { text: 'Leeren' }); const clearBtn = buttonRow.createEl('button', { text: 'Clear' });
clearBtn.addEventListener('click', () => { clearBtn.addEventListener('click', () => {
this.editorEl.value = ''; this.editorEl.value = '';
this.resultEl.empty(); this.resultEl.empty();
@ -113,10 +113,10 @@ export class QueryModal extends Modal {
// Results // Results
this.resultEl = contentEl.createDiv({ cls: 'logfire-qm-results' }); this.resultEl = contentEl.createDiv({ cls: 'logfire-qm-results' });
// Initial SQL setzen // Set initial SQL
if (this.initialSql) { if (this.initialSql) {
this.mode = 'sql'; this.mode = 'sql';
this.modeToggle.textContent = 'Modus: SQL'; this.modeToggle.textContent = 'Mode: SQL';
this.editorEl.value = this.initialSql; this.editorEl.value = this.initialSql;
this.editorEl.placeholder = 'SELECT * FROM events\nWHERE type = \'file:create\'\nORDER BY timestamp DESC\nLIMIT 50'; this.editorEl.placeholder = 'SELECT * FROM events\nWHERE type = \'file:create\'\nORDER BY timestamp DESC\nLIMIT 50';
} }
@ -147,7 +147,7 @@ export class QueryModal extends Modal {
const firstWord = input.split(/\s+/)[0].toUpperCase(); const firstWord = input.split(/\s+/)[0].toUpperCase();
if (firstWord !== 'SELECT' && firstWord !== 'WITH') { if (firstWord !== 'SELECT' && firstWord !== 'WITH') {
this.resultEl.createEl('pre', { this.resultEl.createEl('pre', {
text: 'Nur SELECT- und WITH-Queries sind erlaubt.', text: 'Only SELECT and WITH queries are allowed.',
cls: 'logfire-error', cls: 'logfire-error',
}); });
return; return;
@ -170,7 +170,7 @@ export class QueryModal extends Modal {
this.renderResults(rows); this.renderResults(rows);
} catch (err) { } catch (err) {
this.resultEl.createEl('pre', { this.resultEl.createEl('pre', {
text: `Fehler: ${err instanceof Error ? err.message : String(err)}`, text: `Error: ${err instanceof Error ? err.message : String(err)}`,
cls: 'logfire-error', cls: 'logfire-error',
}); });
} }
@ -180,7 +180,7 @@ export class QueryModal extends Modal {
this.resultEl.empty(); this.resultEl.empty();
if (rows.length === 0) { if (rows.length === 0) {
this.resultEl.createEl('p', { text: 'Keine Ergebnisse.' }); this.resultEl.createEl('p', { text: 'No results.' });
return; return;
} }
@ -189,7 +189,7 @@ export class QueryModal extends Modal {
if (rows.length > 200) { if (rows.length > 200) {
this.resultEl.createEl('p', { this.resultEl.createEl('p', {
text: `${rows.length} Ergebnisse, 200 angezeigt.`, text: `${rows.length} results, 200 shown.`,
cls: 'logfire-qm-truncated', cls: 'logfire-qm-truncated',
}); });
} }
@ -197,22 +197,22 @@ export class QueryModal extends Modal {
private copyAsMarkdown(): void { private copyAsMarkdown(): void {
if (this.lastRows.length === 0) { if (this.lastRows.length === 0) {
new Notice('Keine Ergebnisse zum Kopieren.'); new Notice('No results to copy.');
return; return;
} }
const md = toMarkdownTable(this.lastKeys, this.lastRows); const md = toMarkdownTable(this.lastKeys, this.lastRows);
navigator.clipboard.writeText(md); navigator.clipboard.writeText(md);
new Notice('In Zwischenablage kopiert.'); new Notice('Copied to clipboard.');
} }
private insertInNote(): void { private insertInNote(): void {
if (this.lastRows.length === 0) { if (this.lastRows.length === 0) {
new Notice('Keine Ergebnisse zum Einf\u00fcgen.'); new Notice('No results to insert.');
return; return;
} }
const view = this.app.workspace.getActiveViewOfType(MarkdownView); const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) { if (!view) {
new Notice('Kein aktiver Editor zum Einf\u00fcgen.'); new Notice('No active editor to insert into.');
return; return;
} }
const md = toMarkdownTable(this.lastKeys, this.lastRows); const md = toMarkdownTable(this.lastKeys, this.lastRows);
@ -226,7 +226,7 @@ export class QueryModal extends Modal {
private exportCsv(): void { private exportCsv(): void {
if (this.lastRows.length === 0) { if (this.lastRows.length === 0) {
new Notice('Keine Ergebnisse zum Exportieren.'); new Notice('No results to export.');
return; return;
} }
const bom = '\uFEFF'; const bom = '\uFEFF';
@ -235,17 +235,17 @@ export class QueryModal extends Modal {
this.lastKeys.map(k => csvEscape(row[k])).join(',') this.lastKeys.map(k => csvEscape(row[k])).join(',')
).join('\n'); ).join('\n');
downloadBlob(bom + header + '\n' + body, 'logfire-export.csv', 'text/csv;charset=utf-8'); downloadBlob(bom + header + '\n' + body, 'logfire-export.csv', 'text/csv;charset=utf-8');
new Notice('CSV exportiert.'); new Notice('CSV exported.');
} }
private exportJson(): void { private exportJson(): void {
if (this.lastRows.length === 0) { if (this.lastRows.length === 0) {
new Notice('Keine Ergebnisse zum Exportieren.'); new Notice('No results to export.');
return; return;
} }
const json = JSON.stringify(this.lastRows, null, 2); const json = JSON.stringify(this.lastRows, null, 2);
downloadBlob(json, 'logfire-export.json', 'application/json'); downloadBlob(json, 'logfire-export.json', 'application/json');
new Notice('JSON exportiert.'); new Notice('JSON exported.');
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -303,7 +303,7 @@ export class QueryModal extends Modal {
const before = text.substring(0, cursorPos); const before = text.substring(0, cursorPos);
const after = text.substring(cursorPos); const after = text.substring(cursorPos);
// Letztes Wort ersetzen // Replace last word
const lastWordMatch = before.match(/[\w._:-]+$/); const lastWordMatch = before.match(/[\w._:-]+$/);
const replaceStart = lastWordMatch ? cursorPos - lastWordMatch[0].length : cursorPos; const replaceStart = lastWordMatch ? cursorPos - lastWordMatch[0].length : cursorPos;
const newBefore = text.substring(0, replaceStart) + suggestion.text; const newBefore = text.substring(0, replaceStart) + suggestion.text;

View file

@ -41,7 +41,7 @@ export class EventStreamView extends ItemView {
const searchInput = toolbar.createEl('input', { const searchInput = toolbar.createEl('input', {
type: 'text', type: 'text',
placeholder: 'Nach Quelle filtern...', placeholder: 'Filter by source...',
cls: 'logfire-stream-search', cls: 'logfire-stream-search',
}); });
searchInput.addEventListener('input', () => { searchInput.addEventListener('input', () => {
@ -54,11 +54,11 @@ export class EventStreamView extends ItemView {
const pauseBtn = btnGroup.createEl('button', { text: 'Pause' }); const pauseBtn = btnGroup.createEl('button', { text: 'Pause' });
pauseBtn.addEventListener('click', () => { pauseBtn.addEventListener('click', () => {
this.viewPaused = !this.viewPaused; this.viewPaused = !this.viewPaused;
pauseBtn.textContent = this.viewPaused ? 'Weiter' : 'Pause'; pauseBtn.textContent = this.viewPaused ? 'Resume' : 'Pause';
pauseBtn.toggleClass('is-active', this.viewPaused); pauseBtn.toggleClass('is-active', this.viewPaused);
}); });
const clearBtn = btnGroup.createEl('button', { text: 'Leeren' }); const clearBtn = btnGroup.createEl('button', { text: 'Clear' });
clearBtn.addEventListener('click', () => { clearBtn.addEventListener('click', () => {
this.entries = []; this.entries = [];
this.renderList(); this.renderList();
@ -132,9 +132,9 @@ export class EventStreamView extends ItemView {
const p = event.payload; const p = event.payload;
switch (event.type) { switch (event.type) {
case 'content:words-changed': case 'content:words-changed':
return `+${p.wordsAdded ?? 0}/-${p.wordsRemoved ?? 0} Wörter`; return `+${p.wordsAdded ?? 0}/-${p.wordsRemoved ?? 0} words`;
case 'editor:change': case 'editor:change':
return `+${p.insertedChars ?? 0}/-${p.deletedChars ?? 0} Zeichen`; return `+${p.insertedChars ?? 0}/-${p.deletedChars ?? 0} chars`;
case 'nav:file-close': case 'nav:file-close':
return typeof p.duration === 'number' ? `${Math.round(p.duration / 1000)}s` : ''; return typeof p.duration === 'number' ? `${Math.round(p.duration / 1000)}s` : '';
case 'file:rename': case 'file:rename':

View file

@ -39,9 +39,9 @@ export class InitialScanModal extends Modal {
const files = this.app.vault.getMarkdownFiles() const files = this.app.vault.getMarkdownFiles()
.filter(f => this.shouldTrack(f.path)); .filter(f => this.shouldTrack(f.path));
contentEl.createEl('h2', { text: 'Logfire Vault-Scan' }); contentEl.createEl('h2', { text: 'Logfire Vault Scan' });
contentEl.createEl('p', { contentEl.createEl('p', {
text: `${files.length} Markdown-Dateien gefunden. Der Scan erstellt die Baseline für Content-Tracking.`, text: `${files.length} Markdown files found. The scan creates the baseline for content tracking.`,
}); });
const progressContainer = contentEl.createDiv({ cls: 'logfire-scan-progress' }); const progressContainer = contentEl.createDiv({ cls: 'logfire-scan-progress' });
@ -62,15 +62,15 @@ export class InitialScanModal extends Modal {
buttonContainer.style.gap = '8px'; buttonContainer.style.gap = '8px';
buttonContainer.style.marginTop = '16px'; buttonContainer.style.marginTop = '16px';
const cancelBtn = buttonContainer.createEl('button', { text: 'Abbrechen' }); const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel' });
cancelBtn.addEventListener('click', () => { cancelBtn.addEventListener('click', () => {
this.cancelled = true; this.cancelled = true;
}); });
const startBtn = buttonContainer.createEl('button', { text: 'Scan starten', cls: 'mod-cta' }); const startBtn = buttonContainer.createEl('button', { text: 'Start scan', cls: 'mod-cta' });
startBtn.addEventListener('click', async () => { startBtn.addEventListener('click', async () => {
startBtn.disabled = true; startBtn.disabled = true;
cancelBtn.textContent = 'Stoppen'; cancelBtn.textContent = 'Stop';
let scanned = 0; let scanned = 0;
let totalWords = 0; let totalWords = 0;
@ -103,9 +103,9 @@ export class InitialScanModal extends Modal {
progressBar.value = scanned; progressBar.value = scanned;
currentFileEl.textContent = file.path; currentFileEl.textContent = file.path;
totalsEl.textContent = `${scanned}/${files.length} Dateien | ${totalWords.toLocaleString()} Wörter | ${totalLinks} Links | ${totalTags} Tags`; totalsEl.textContent = `${scanned}/${files.length} files | ${totalWords.toLocaleString()} words | ${totalLinks} links | ${totalTags} tags`;
} catch (err) { } catch (err) {
console.error(`[Logfire] Scan-Fehler bei ${file.path}:`, err); console.error(`[Logfire] Scan error at ${file.path}:`, err);
} }
} }
@ -129,11 +129,11 @@ export class InitialScanModal extends Modal {
}); });
currentFileEl.textContent = this.cancelled currentFileEl.textContent = this.cancelled
? `Scan gestoppt. ${scanned} von ${files.length} Dateien gescannt.` ? `Scan stopped. ${scanned} of ${files.length} files scanned.`
: `Scan abgeschlossen! ${scanned} Dateien gescannt.`; : `Scan complete! ${scanned} files scanned.`;
cancelBtn.style.display = 'none'; cancelBtn.style.display = 'none';
const closeBtn = buttonContainer.createEl('button', { text: 'Schließen', cls: 'mod-cta' }); const closeBtn = buttonContainer.createEl('button', { text: 'Close', cls: 'mod-cta' });
closeBtn.addEventListener('click', () => { closeBtn.addEventListener('click', () => {
this.close(); this.close();
this.resolve(true); this.resolve(true);

View file

@ -46,7 +46,7 @@ export class SchemaView extends ItemView {
} }
getDisplayText(): string { getDisplayText(): string {
return 'Schema-Browser'; return 'Schema Browser';
} }
getIcon(): string { getIcon(): string {
@ -63,7 +63,7 @@ export class SchemaView extends ItemView {
const refreshBtn = header.createEl('button', { const refreshBtn = header.createEl('button', {
cls: 'logfire-dash-btn clickable-icon', cls: 'logfire-dash-btn clickable-icon',
attr: { 'aria-label': 'Aktualisieren' }, attr: { 'aria-label': 'Refresh' },
text: '\u21bb', text: '\u21bb',
}); });
refreshBtn.addEventListener('click', () => this.refresh()); refreshBtn.addEventListener('click', () => this.refresh());
@ -77,7 +77,7 @@ export class SchemaView extends ItemView {
const tables = this.introspect(); const tables = this.introspect();
if (tables.length === 0) { if (tables.length === 0) {
this.contentContainer.createDiv({ cls: 'logfire-empty', text: 'Keine Tabellen gefunden.' }); this.contentContainer.createDiv({ cls: 'logfire-empty', text: 'No tables found.' });
return; return;
} }
@ -144,11 +144,11 @@ export class SchemaView extends ItemView {
this.refresh(); this.refresh();
}); });
// Context menu: SQL kopieren // Context menu: copy SQL
header.addEventListener('contextmenu', (e) => { header.addEventListener('contextmenu', (e) => {
e.preventDefault(); e.preventDefault();
navigator.clipboard.writeText(`SELECT * FROM "${table.name}" LIMIT 100;`); navigator.clipboard.writeText(`SELECT * FROM "${table.name}" LIMIT 100;`);
new Notice(`SELECT auf "${table.name}" kopiert.`); new Notice(`SELECT for "${table.name}" copied.`);
}); });
if (!isExpanded) return; if (!isExpanded) return;
@ -166,7 +166,7 @@ export class SchemaView extends ItemView {
// Indexes // Indexes
if (table.indexes.length > 0) { if (table.indexes.length > 0) {
details.createDiv({ cls: 'logfire-schema-section-header', text: 'Indizes' }); details.createDiv({ cls: 'logfire-schema-section-header', text: 'Indexes' });
for (const idx of table.indexes) { for (const idx of table.indexes) {
const row = details.createDiv({ cls: 'logfire-schema-idx' }); const row = details.createDiv({ cls: 'logfire-schema-idx' });
row.createSpan({ text: `${idx.unique ? 'UNIQUE ' : ''}${idx.name}` }); row.createSpan({ text: `${idx.unique ? 'UNIQUE ' : ''}${idx.name}` });

View file

@ -11,11 +11,11 @@ export class LogfireSettingTab extends PluginSettingTab {
containerEl.empty(); containerEl.empty();
// ----- General ----- // ----- General -----
containerEl.createEl('h2', { text: 'Allgemein' }); containerEl.createEl('h2', { text: 'General' });
new Setting(containerEl) new Setting(containerEl)
.setName('Tracking aktiviert') .setName('Tracking enabled')
.setDesc('Hauptschalter für alle Event-Collector.') .setDesc('Master switch for all event collectors.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.general.enabled) .setValue(this.plugin.settings.general.enabled)
.onChange(async (v) => { .onChange(async (v) => {
@ -24,8 +24,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Log-Ordner') .setName('Log folder')
.setDesc('Ordner für Markdown-Projektionen. Wird automatisch vom Tracking ausgeschlossen.') .setDesc('Folder for Markdown projections. Automatically excluded from tracking.')
.addText(t => t .addText(t => t
.setPlaceholder('Logfire') .setPlaceholder('Logfire')
.setValue(this.plugin.settings.general.logFolder) .setValue(this.plugin.settings.general.logFolder)
@ -35,8 +35,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Beim Start pausieren') .setName('Pause on startup')
.setDesc('Startet im pausierten Zustand — keine Events bis manuell fortgesetzt.') .setDesc('Starts in paused state — no events until manually resumed.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.general.pauseOnStartup) .setValue(this.plugin.settings.general.pauseOnStartup)
.onChange(async (v) => { .onChange(async (v) => {
@ -48,8 +48,8 @@ export class LogfireSettingTab extends PluginSettingTab {
containerEl.createEl('h2', { text: 'Tracking' }); containerEl.createEl('h2', { text: 'Tracking' });
new Setting(containerEl) new Setting(containerEl)
.setName('Datei-Events') .setName('File events')
.setDesc('Erstellen, Löschen, Umbenennen, Verschieben und Ändern von Dateien.') .setDesc('Create, delete, rename, move, and modify files.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.tracking.fileEvents) .setValue(this.plugin.settings.tracking.fileEvents)
.onChange(async (v) => { .onChange(async (v) => {
@ -58,8 +58,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Content-Analyse') .setName('Content analysis')
.setDesc('Semantische Diffs: Wortzählung, Links, Tags, Überschriften, Frontmatter.') .setDesc('Semantic diffs: word count, links, tags, headings, frontmatter.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.tracking.contentAnalysis) .setValue(this.plugin.settings.tracking.contentAnalysis)
.onChange(async (v) => { .onChange(async (v) => {
@ -69,7 +69,7 @@ export class LogfireSettingTab extends PluginSettingTab {
new Setting(containerEl) new Setting(containerEl)
.setName('Navigation') .setName('Navigation')
.setDesc('Aktive Datei-Wechsel, Datei öffnen/schließen mit Dauer.') .setDesc('Active file switches, file open/close with duration.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.tracking.navigation) .setValue(this.plugin.settings.tracking.navigation)
.onChange(async (v) => { .onChange(async (v) => {
@ -78,8 +78,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Editor-Änderungen') .setName('Editor changes')
.setDesc('Tastenanschläge (debounced und aggregiert).') .setDesc('Keystrokes (debounced and aggregated).')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.tracking.editorChanges) .setValue(this.plugin.settings.tracking.editorChanges)
.onChange(async (v) => { .onChange(async (v) => {
@ -88,8 +88,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Kommando-Tracking') .setName('Command tracking')
.setDesc('Ausführung von Befehlen über Palette und Hotkeys.') .setDesc('Execution of commands via palette and hotkeys.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.tracking.commandTracking) .setValue(this.plugin.settings.tracking.commandTracking)
.onChange(async (v) => { .onChange(async (v) => {
@ -98,8 +98,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Ausgeschlossene Ordner') .setName('Excluded folders')
.setDesc('Komma-getrennte Liste von Ordnern, die nicht getrackt werden.') .setDesc('Comma-separated list of folders to exclude from tracking.')
.addText(t => t .addText(t => t
.setPlaceholder('.obsidian, templates') .setPlaceholder('.obsidian, templates')
.setValue(this.plugin.settings.tracking.excludedFolders.join(', ')) .setValue(this.plugin.settings.tracking.excludedFolders.join(', '))
@ -112,8 +112,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Ausgeschlossene Patterns') .setName('Excluded patterns')
.setDesc('Komma-getrennte Glob-Patterns (z.B. **/*.excalidraw.md).') .setDesc('Comma-separated glob patterns (e.g. **/*.excalidraw.md).')
.addText(t => t .addText(t => t
.setPlaceholder('**/*.excalidraw.md') .setPlaceholder('**/*.excalidraw.md')
.setValue(this.plugin.settings.tracking.excludedPatterns.join(', ')) .setValue(this.plugin.settings.tracking.excludedPatterns.join(', '))
@ -126,11 +126,11 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
// ----- Projections ----- // ----- Projections -----
containerEl.createEl('h2', { text: 'Projektionen' }); containerEl.createEl('h2', { text: 'Projections' });
new Setting(containerEl) new Setting(containerEl)
.setName('Projektionen aktiviert') .setName('Projections enabled')
.setDesc('Automatische Markdown-Reports aus Event-Daten.') .setDesc('Automatic Markdown reports from event data.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.projections.enabled) .setValue(this.plugin.settings.projections.enabled)
.onChange(async (v) => { .onChange(async (v) => {
@ -139,8 +139,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Output-Ordner') .setName('Output folder')
.setDesc('Ordner für generierte Projektions-Dateien.') .setDesc('Folder for generated projection files.')
.addText(t => t .addText(t => t
.setPlaceholder('Logfire') .setPlaceholder('Logfire')
.setValue(this.plugin.settings.projections.outputFolder) .setValue(this.plugin.settings.projections.outputFolder)
@ -150,8 +150,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Tagesprotokoll') .setName('Daily log')
.setDesc('Automatisches Tagesprotokoll generieren.') .setDesc('Generate automatic daily log.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.projections.dailyLog.enabled) .setValue(this.plugin.settings.projections.dailyLog.enabled)
.onChange(async (v) => { .onChange(async (v) => {
@ -169,8 +169,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Session-Protokoll') .setName('Session log')
.setDesc('Protokoll bei Session-Ende generieren.') .setDesc('Generate log on session end.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.projections.sessionLog.enabled) .setValue(this.plugin.settings.projections.sessionLog.enabled)
.onChange(async (v) => { .onChange(async (v) => {
@ -179,8 +179,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(containerEl) new Setting(containerEl)
.setName('Wochen-Digest') .setName('Weekly digest')
.setDesc('Wöchentliche Zusammenfassung generieren.') .setDesc('Generate weekly summary.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.projections.weeklyDigest.enabled) .setValue(this.plugin.settings.projections.weeklyDigest.enabled)
.onChange(async (v) => { .onChange(async (v) => {
@ -189,8 +189,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})) }))
.addDropdown(d => d .addDropdown(d => d
.addOptions({ .addOptions({
'0': 'Sonntag', '1': 'Montag', '2': 'Dienstag', '3': 'Mittwoch', '0': 'Sunday', '1': 'Monday', '2': 'Tuesday', '3': 'Wednesday',
'4': 'Donnerstag', '5': 'Freitag', '6': 'Samstag', '4': 'Thursday', '5': 'Friday', '6': 'Saturday',
}) })
.setValue(String(this.plugin.settings.projections.weeklyDigest.dayOfWeek)) .setValue(String(this.plugin.settings.projections.weeklyDigest.dayOfWeek))
.onChange(async (v) => { .onChange(async (v) => {
@ -200,12 +200,12 @@ export class LogfireSettingTab extends PluginSettingTab {
// ----- Advanced (collapsible) ----- // ----- Advanced (collapsible) -----
const advancedHeader = containerEl.createEl('details'); const advancedHeader = containerEl.createEl('details');
advancedHeader.createEl('summary', { text: 'Erweiterte Einstellungen' }) advancedHeader.createEl('summary', { text: 'Advanced Settings' })
.style.cursor = 'pointer'; .style.cursor = 'pointer';
const advEl = advancedHeader.createDiv(); const advEl = advancedHeader.createDiv();
advEl.createEl('p', { advEl.createEl('p', {
text: 'Diese Einstellungen beeinflussen Performance und Speicher. Die Standardwerte sind für die meisten Vaults optimal.', text: 'These settings affect performance and storage. The defaults are optimal for most vaults.',
cls: 'setting-item-description', cls: 'setting-item-description',
}); });
@ -213,8 +213,8 @@ export class LogfireSettingTab extends PluginSettingTab {
advEl.createEl('h3', { text: 'Performance' }); advEl.createEl('h3', { text: 'Performance' });
new Setting(advEl) new Setting(advEl)
.setName('Editor-Debounce (ms)') .setName('Editor debounce (ms)')
.setDesc('Wartezeit nach letztem Tastenanschlag vor editor:change Event.') .setDesc('Wait time after last keystroke before editor:change event.')
.addText(t => t .addText(t => t
.setValue(String(this.plugin.settings.advanced.debounceMs)) .setValue(String(this.plugin.settings.advanced.debounceMs))
.onChange(async (v) => { .onChange(async (v) => {
@ -226,8 +226,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(advEl) new Setting(advEl)
.setName('Flush-Intervall (ms)') .setName('Flush interval (ms)')
.setDesc('Wie oft gepufferte Events in die Datenbank geschrieben werden.') .setDesc('How often buffered events are written to the database.')
.addText(t => t .addText(t => t
.setValue(String(this.plugin.settings.advanced.flushIntervalMs)) .setValue(String(this.plugin.settings.advanced.flushIntervalMs))
.onChange(async (v) => { .onChange(async (v) => {
@ -239,8 +239,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(advEl) new Setting(advEl)
.setName('Flush-Schwellwert') .setName('Flush threshold')
.setDesc('Maximale Events im Puffer vor erzwungenem Schreiben.') .setDesc('Maximum events in buffer before forced write.')
.addText(t => t .addText(t => t
.setValue(String(this.plugin.settings.advanced.flushThreshold)) .setValue(String(this.plugin.settings.advanced.flushThreshold))
.onChange(async (v) => { .onChange(async (v) => {
@ -252,11 +252,11 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
// Retention // Retention
advEl.createEl('h3', { text: 'Aufbewahrung' }); advEl.createEl('h3', { text: 'Retention' });
new Setting(advEl) new Setting(advEl)
.setName('Raw-Events aufbewahren (Tage)') .setName('Keep raw events (days)')
.setDesc('Danach werden Events in Tages-Statistiken aggregiert und gelöscht.') .setDesc('After this, events are aggregated into daily stats and deleted.')
.addText(t => t .addText(t => t
.setValue(String(this.plugin.settings.advanced.retention.rawEventsDays)) .setValue(String(this.plugin.settings.advanced.retention.rawEventsDays))
.onChange(async (v) => { .onChange(async (v) => {
@ -268,8 +268,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(advEl) new Setting(advEl)
.setName('Tages-Statistiken aufbewahren (Tage)') .setName('Keep daily stats (days)')
.setDesc('Danach werden Tages-Statistiken in Monats-Statistiken aggregiert.') .setDesc('After this, daily stats are aggregated into monthly stats.')
.addText(t => t .addText(t => t
.setValue(String(this.plugin.settings.advanced.retention.dailyStatsDays)) .setValue(String(this.plugin.settings.advanced.retention.dailyStatsDays))
.onChange(async (v) => { .onChange(async (v) => {
@ -281,8 +281,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(advEl) new Setting(advEl)
.setName('Wartung beim Start') .setName('Maintenance on startup')
.setDesc('Automatisch Retention-Cleanup beim Plugin-Start ausführen.') .setDesc('Automatically run retention cleanup on plugin start.')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.advanced.retention.maintenanceOnStartup) .setValue(this.plugin.settings.advanced.retention.maintenanceOnStartup)
.onChange(async (v) => { .onChange(async (v) => {
@ -291,11 +291,11 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
// Database settings // Database settings
advEl.createEl('h3', { text: 'Datenbank' }); advEl.createEl('h3', { text: 'Database' });
new Setting(advEl) new Setting(advEl)
.setName('WAL-Modus') .setName('WAL mode')
.setDesc('Write-Ahead-Logging für bessere Parallelität (Neustart erforderlich).') .setDesc('Write-Ahead Logging for better concurrency (restart required).')
.addToggle(t => t .addToggle(t => t
.setValue(this.plugin.settings.advanced.database.walMode) .setValue(this.plugin.settings.advanced.database.walMode)
.onChange(async (v) => { .onChange(async (v) => {
@ -304,8 +304,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(advEl) new Setting(advEl)
.setName('Cache-Größe (MB)') .setName('Cache size (MB)')
.setDesc('SQLite Page-Cache (8256 MB). Mehr Cache = schnellere Queries.') .setDesc('SQLite page cache (8256 MB). More cache = faster queries.')
.addSlider(s => s .addSlider(s => s
.setLimits(8, 256, 8) .setLimits(8, 256, 8)
.setValue(this.plugin.settings.advanced.database.cacheSizeMb) .setValue(this.plugin.settings.advanced.database.cacheSizeMb)
@ -316,8 +316,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(advEl) new Setting(advEl)
.setName('MMap-Größe (MB)') .setName('MMap size (MB)')
.setDesc('Memory-Mapped I/O (01024 MB). 0 deaktiviert MMap.') .setDesc('Memory-Mapped I/O (01024 MB). 0 disables MMap.')
.addSlider(s => s .addSlider(s => s
.setLimits(0, 1024, 32) .setLimits(0, 1024, 32)
.setValue(this.plugin.settings.advanced.database.mmapSizeMb) .setValue(this.plugin.settings.advanced.database.mmapSizeMb)
@ -328,8 +328,8 @@ export class LogfireSettingTab extends PluginSettingTab {
})); }));
new Setting(advEl) new Setting(advEl)
.setName('Geschützte Event-Typen') .setName('Protected event types')
.setDesc('Event-Typen die nie gelöscht werden (komma-getrennt).') .setDesc('Event types that are never deleted (comma-separated).')
.addText(t => t .addText(t => t
.setPlaceholder('file:create, file:delete, file:rename') .setPlaceholder('file:create, file:delete, file:rename')
.setValue(this.plugin.settings.advanced.retention.neverDeleteTypes.join(', ')) .setValue(this.plugin.settings.advanced.retention.neverDeleteTypes.join(', '))
@ -349,10 +349,10 @@ export class LogfireSettingTab extends PluginSettingTab {
const oldest = this.plugin.db.getOldestEventTimestamp(); const oldest = this.plugin.db.getOldestEventTimestamp();
dbInfoEl.createEl('p', { dbInfoEl.createEl('p', {
text: `Events: ${eventCount.toLocaleString()} | Größe: ${formatBytes(dbSize)} | Ältestes: ${oldest ? new Date(oldest).toLocaleDateString() : 'k.A.'}`, text: `Events: ${eventCount.toLocaleString()} | Size: ${formatBytes(dbSize)} | Oldest: ${oldest ? new Date(oldest).toLocaleDateString() : 'N/A'}`,
}); });
} catch { } catch {
dbInfoEl.createEl('p', { text: 'Datenbank-Info nicht verfügbar.' }); dbInfoEl.createEl('p', { text: 'Database info unavailable.' });
} }
// Info section // Info section
@ -362,17 +362,17 @@ export class LogfireSettingTab extends PluginSettingTab {
}); });
new Setting(advEl) new Setting(advEl)
.setName('Datenbank-Aktionen') .setName('Database actions')
.addButton(b => b .addButton(b => b
.setButtonText('Wartung ausführen') .setButtonText('Run maintenance')
.onClick(() => { .onClick(() => {
try { try {
this.plugin.db.runMaintenance(this.plugin.settings.advanced.retention); this.plugin.db.runMaintenance(this.plugin.settings.advanced.retention);
new Notice('Logfire: Wartung abgeschlossen.'); new Notice('Logfire: Maintenance complete.');
this.display(); this.display();
} catch (err) { } catch (err) {
new Notice('Logfire: Wartung fehlgeschlagen.'); new Notice('Logfire: Maintenance failed.');
console.error('[Logfire] Wartung fehlgeschlagen:', err); console.error('[Logfire] Maintenance failed:', err);
} }
})); }));
} }

View file

@ -36,7 +36,7 @@ export class StatusBar {
private update(): void { private update(): void {
const paused = this.plugin.isPaused(); const paused = this.plugin.isPaused();
const indicator = paused ? '\u23F8' : '\u{1F534}'; const indicator = paused ? '\u23F8' : '\u{1F534}';
const state = paused ? 'Pausiert' : 'Aufnahme'; const state = paused ? 'Paused' : 'Recording';
const duration = this.formatDuration(this.plugin.sessionManager.sessionDurationMs); const duration = this.formatDuration(this.plugin.sessionManager.sessionDurationMs);
const words = this.wordsAdded > 0 ? ` | +${this.wordsAdded}w` : ''; const words = this.wordsAdded > 0 ? ` | +${this.wordsAdded}w` : '';

View file

@ -36,7 +36,7 @@ export function renderChart(
config: ChartConfig, config: ChartConfig,
): void { ): void {
if (rows.length === 0) { if (rows.length === 0) {
el.createEl('p', { text: 'Keine Daten.', cls: 'logfire-empty' }); el.createEl('p', { text: 'No data.', cls: 'logfire-empty' });
return; return;
} }

View file

@ -71,7 +71,7 @@ export class DashboardView extends ItemView {
const refreshBtn = actions.createEl('button', { const refreshBtn = actions.createEl('button', {
cls: 'logfire-dash-btn clickable-icon', cls: 'logfire-dash-btn clickable-icon',
attr: { 'aria-label': 'Aktualisieren' }, attr: { 'aria-label': 'Refresh' },
text: '\u21bb', text: '\u21bb',
}); });
refreshBtn.addEventListener('click', () => this.refreshAll()); refreshBtn.addEventListener('click', () => this.refreshAll());
@ -121,7 +121,7 @@ export class DashboardView extends ItemView {
this.contentContainer.empty(); this.contentContainer.empty();
this.contentContainer.createDiv({ this.contentContainer.createDiv({
cls: 'logfire-empty', cls: 'logfire-empty',
text: 'Kein Dashboard geladen.', text: 'No dashboard loaded.',
}); });
} }
@ -170,19 +170,19 @@ export class DashboardView extends ItemView {
} catch (err) { } catch (err) {
container.createDiv({ container.createDiv({
cls: 'logfire-error', cls: 'logfire-error',
text: `Fehler: ${err instanceof Error ? err.message : String(err)}`, text: `Error: ${err instanceof Error ? err.message : String(err)}`,
}); });
} }
} }
private renderQueryWidget(container: HTMLElement, widget: DashboardWidget): void { private renderQueryWidget(container: HTMLElement, widget: DashboardWidget): void {
if (!widget.sql) { if (!widget.sql) {
container.createDiv({ cls: 'logfire-empty', text: 'Keine Query konfiguriert.' }); container.createDiv({ cls: 'logfire-empty', text: 'No query configured.' });
return; return;
} }
const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[]; const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[];
if (rows.length === 0) { if (rows.length === 0) {
container.createDiv({ cls: 'logfire-empty', text: 'Keine Ergebnisse.' }); container.createDiv({ cls: 'logfire-empty', text: 'No results.' });
return; return;
} }
renderTable(container, rows); renderTable(container, rows);
@ -190,7 +190,7 @@ export class DashboardView extends ItemView {
private renderChartWidget(container: HTMLElement, widget: DashboardWidget): void { private renderChartWidget(container: HTMLElement, widget: DashboardWidget): void {
if (!widget.sql || !widget.chartConfig) { if (!widget.sql || !widget.chartConfig) {
container.createDiv({ cls: 'logfire-empty', text: 'Kein Chart konfiguriert.' }); container.createDiv({ cls: 'logfire-empty', text: 'No chart configured.' });
return; return;
} }
const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[]; const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[];
@ -199,7 +199,7 @@ export class DashboardView extends ItemView {
private renderStatWidget(container: HTMLElement, widget: DashboardWidget): void { private renderStatWidget(container: HTMLElement, widget: DashboardWidget): void {
if (!widget.sql) { if (!widget.sql) {
container.createDiv({ cls: 'logfire-empty', text: 'Keine Query konfiguriert.' }); container.createDiv({ cls: 'logfire-empty', text: 'No query configured.' });
return; return;
} }
const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[]; const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[];

View file

@ -154,7 +154,7 @@ export class KeyboardNavigator {
navigator.clipboard.writeText(td.textContent ?? ''); navigator.clipboard.writeText(td.textContent ?? '');
td.classList.add('logfire-vim-yanked'); td.classList.add('logfire-vim-yanked');
setTimeout(() => td.classList.remove('logfire-vim-yanked'), 600); setTimeout(() => td.classList.remove('logfire-vim-yanked'), 600);
new Notice('In Zwischenablage kopiert.'); new Notice('Copied to clipboard.');
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -225,7 +225,7 @@ export class KeyboardNavigator {
return; return;
} }
} }
new Notice('Nicht gefunden.'); new Notice('Not found.');
} }
} }