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:
parent
878b144ccc
commit
3c8c22ee07
23 changed files with 377 additions and 270 deletions
107
CLAUDE.md
Normal file
107
CLAUDE.md
Normal 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.
|
||||
|
|
@ -92,7 +92,7 @@ export class ContentCollector {
|
|||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Logfire] ContentCollector-Fehler:', err);
|
||||
console.error('[Logfire] ContentCollector error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export class EventBus {
|
|||
try {
|
||||
this.db.insertEvents(batch);
|
||||
} catch (err) {
|
||||
console.error('[Logfire] Flush fehlgeschlagen:', err);
|
||||
console.error('[Logfire] Flush failed:', err);
|
||||
this.buffer.unshift(...batch);
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ export class EventBus {
|
|||
try {
|
||||
cb(event);
|
||||
} catch (err) {
|
||||
console.error('[Logfire] Subscriber-Fehler:', err);
|
||||
console.error('[Logfire] Subscriber error:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
66
src/main.ts
66
src/main.ts
|
|
@ -49,7 +49,7 @@ export default class LogfirePlugin extends Plugin {
|
|||
private paused = false;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
console.log('[Logfire] Lade Plugin...');
|
||||
console.log('[Logfire] Loading plugin...');
|
||||
|
||||
await this.loadSettings();
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ export default class LogfirePlugin extends Plugin {
|
|||
this.statusBar = new StatusBar(this);
|
||||
this.statusBar.start();
|
||||
|
||||
// Query: Code-Block-Prozessoren
|
||||
// Query: Code-block processors
|
||||
registerLogfireBlock(this.db, (lang, handler) => {
|
||||
this.registerMarkdownCodeBlockProcessor(lang, handler);
|
||||
});
|
||||
|
|
@ -137,7 +137,7 @@ export default class LogfirePlugin extends Plugin {
|
|||
});
|
||||
|
||||
// Ribbon icons
|
||||
this.addRibbonIcon('activity', 'Logfire: Event-Stream', () => {
|
||||
this.addRibbonIcon('activity', 'Logfire: Event Stream', () => {
|
||||
this.activateEventStream();
|
||||
});
|
||||
this.addRibbonIcon('layout-dashboard', 'Logfire: Dashboard', () => {
|
||||
|
|
@ -163,7 +163,7 @@ export default class LogfirePlugin extends Plugin {
|
|||
try {
|
||||
this.db.runMaintenance(this.settings.advanced.retention);
|
||||
} 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.start();
|
||||
|
||||
// Autocomplete (nach Virtual Tables)
|
||||
// Autocomplete (after virtual tables)
|
||||
this.autocomplete = new SqlAutocomplete(this.db);
|
||||
|
||||
// Keyboard Navigator
|
||||
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> {
|
||||
console.log('[Logfire] Entlade Plugin...');
|
||||
console.log('[Logfire] Unloading plugin...');
|
||||
|
||||
cleanupAllRefreshTimers();
|
||||
this.projectionEngine?.destroy();
|
||||
|
|
@ -206,7 +206,7 @@ export default class LogfirePlugin extends Plugin {
|
|||
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 {
|
||||
this.addCommand({
|
||||
id: 'show-event-stream',
|
||||
name: 'Event-Stream anzeigen',
|
||||
name: 'Show event stream',
|
||||
callback: () => this.activateEventStream(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'toggle-tracking',
|
||||
name: 'Tracking pausieren/fortsetzen',
|
||||
name: 'Toggle tracking',
|
||||
callback: () => {
|
||||
if (this.paused) {
|
||||
this.resume();
|
||||
new Notice('Logfire: Tracking fortgesetzt.');
|
||||
new Notice('Logfire: Tracking resumed.');
|
||||
} else {
|
||||
this.pause();
|
||||
new Notice('Logfire: Tracking pausiert.');
|
||||
new Notice('Logfire: Tracking paused.');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'rescan-vault',
|
||||
name: 'Vault erneut scannen',
|
||||
name: 'Rescan vault',
|
||||
callback: () => this.runInitialScan(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'run-maintenance',
|
||||
name: 'Wartung ausführen',
|
||||
name: 'Run maintenance',
|
||||
callback: () => {
|
||||
this.db.runMaintenance(this.settings.advanced.retention);
|
||||
new Notice('Logfire: Wartung abgeschlossen.');
|
||||
new Notice('Logfire: Maintenance complete.');
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'refresh-virtual-tables',
|
||||
name: 'Virtual Tables neu aufbauen',
|
||||
name: 'Rebuild virtual tables',
|
||||
callback: () => {
|
||||
this.virtualTables?.rebuild();
|
||||
new Notice('Logfire: Virtual Tables aktualisiert.');
|
||||
new Notice('Logfire: Virtual tables updated.');
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'show-dashboard',
|
||||
name: 'Dashboard anzeigen',
|
||||
name: 'Show dashboard',
|
||||
callback: () => this.activateDashboard(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'open-query',
|
||||
name: 'Query-Editor \u00f6ffnen',
|
||||
name: 'Open query editor',
|
||||
callback: () => {
|
||||
new QueryModal(this.app, this.db, this.historyManager, undefined, this.autocomplete).open();
|
||||
},
|
||||
|
|
@ -362,13 +362,13 @@ export default class LogfirePlugin extends Plugin {
|
|||
|
||||
this.addCommand({
|
||||
id: 'show-schema',
|
||||
name: 'Schema-Browser anzeigen',
|
||||
name: 'Show schema browser',
|
||||
callback: () => this.activateSchema(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'show-templates',
|
||||
name: 'Query-Templates anzeigen',
|
||||
name: 'Show query templates',
|
||||
callback: () => {
|
||||
new TemplatePickerModal(this, this.templateManager, (sql) => {
|
||||
new QueryModal(this.app, this.db, this.historyManager, sql, this.autocomplete).open();
|
||||
|
|
@ -378,7 +378,7 @@ export default class LogfirePlugin extends Plugin {
|
|||
|
||||
this.addCommand({
|
||||
id: 'save-favorite',
|
||||
name: 'Aktuelle Query als Favorit speichern',
|
||||
name: 'Save current query as favorite',
|
||||
callback: () => {
|
||||
new SaveFavoriteModal(this, this.favoritesManager, '').open();
|
||||
},
|
||||
|
|
@ -402,7 +402,7 @@ export default class LogfirePlugin extends Plugin {
|
|||
|
||||
this.addCommand({
|
||||
id: 'run-projection',
|
||||
name: 'Projektion manuell ausführen',
|
||||
name: 'Run projection',
|
||||
callback: () => {
|
||||
new ProjectionPickerModal(this.app, this.projectionEngine).open();
|
||||
},
|
||||
|
|
@ -410,7 +410,7 @@ export default class LogfirePlugin extends Plugin {
|
|||
|
||||
this.addCommand({
|
||||
id: 'run-all-projections',
|
||||
name: 'Alle Projektionen ausführen',
|
||||
name: 'Run all projections',
|
||||
callback: () => {
|
||||
this.projectionEngine.runAllProjections();
|
||||
},
|
||||
|
|
@ -418,33 +418,33 @@ export default class LogfirePlugin extends Plugin {
|
|||
|
||||
this.addCommand({
|
||||
id: 'export-csv',
|
||||
name: 'Letzte Query als CSV exportieren',
|
||||
name: 'Export last query as CSV',
|
||||
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({
|
||||
id: 'export-json',
|
||||
name: 'Letzte Query als JSON exportieren',
|
||||
name: 'Export last query as JSON',
|
||||
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({
|
||||
id: 'toggle-vim-nav',
|
||||
name: 'Vim-Navigation umschalten',
|
||||
name: 'Toggle Vim navigation',
|
||||
callback: () => {
|
||||
if (this.keyboardNav?.isActive()) {
|
||||
this.keyboardNav.detach();
|
||||
new Notice('Logfire: Vim-Navigation deaktiviert.');
|
||||
new Notice('Logfire: Vim navigation disabled.');
|
||||
} else {
|
||||
const active = document.querySelector('.logfire-qm-results, .logfire-dash-widget-content');
|
||||
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 {
|
||||
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 {
|
||||
const adapter = this.app.vault.adapter;
|
||||
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();
|
||||
return `${basePath}/${this.app.vault.configDir}/plugins/logfire/logfire.db`;
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ export interface FavoriteCategory {
|
|||
const STORAGE_KEY = 'logfire-favorites';
|
||||
|
||||
const DEFAULT_CATEGORIES: FavoriteCategory[] = [
|
||||
{ id: 'allgemein', name: 'Allgemein', color: '#4C78A8' },
|
||||
{ id: 'analyse', name: 'Analyse', color: '#F58518' },
|
||||
{ id: 'wartung', name: 'Wartung', color: '#E45756' },
|
||||
{ id: 'general', name: 'General', color: '#4C78A8' },
|
||||
{ id: 'analysis', name: 'Analysis', color: '#F58518' },
|
||||
{ id: 'maintenance', name: 'Maintenance', color: '#E45756' },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -75,7 +75,7 @@ export class FavoritesManager {
|
|||
// 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 = {
|
||||
id: `fav-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name,
|
||||
|
|
@ -154,7 +154,7 @@ export class SaveFavoriteModal extends Modal {
|
|||
private manager: FavoritesManager;
|
||||
private sql: string;
|
||||
private name = '';
|
||||
private category = 'allgemein';
|
||||
private category = 'general';
|
||||
private tags: string[] = [];
|
||||
|
||||
constructor(plugin: LogfirePlugin, manager: FavoritesManager, sql: string) {
|
||||
|
|
@ -167,41 +167,41 @@ export class SaveFavoriteModal extends Modal {
|
|||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
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 =>
|
||||
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);
|
||||
dd.setValue(this.category);
|
||||
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 => {
|
||||
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', {
|
||||
cls: 'logfire-sql-preview',
|
||||
text: this.sql.length > 200 ? this.sql.slice(0, 200) + '...' : this.sql,
|
||||
});
|
||||
|
||||
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());
|
||||
const cancelBtn = btns.createEl('button', { text: 'Abbrechen' });
|
||||
const cancelBtn = btns.createEl('button', { text: 'Cancel' });
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
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);
|
||||
new Notice(`Favorit "${this.name}" gespeichert.`);
|
||||
new Notice(`Favorite "${this.name}" saved.`);
|
||||
this.close();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class HistoryManager {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
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()) {
|
||||
if (entry.sql === sql) {
|
||||
entry.executedAt = new Date().toISOString();
|
||||
|
|
@ -75,7 +75,7 @@ export class HistoryManager {
|
|||
};
|
||||
this.entries.set(entry.id, entry);
|
||||
|
||||
// Limit: aelteste nicht-favorisierte loeschen
|
||||
// Limit: delete oldest non-favorited entries
|
||||
if (this.entries.size > MAX_ENTRIES) {
|
||||
const sorted = this.getAll();
|
||||
for (const e of sorted.slice(MAX_ENTRIES)) {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export class TemplateManager {
|
|||
for (const t of arr) this.templates.set(t.id, t);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// Immer Built-in Templates sicherstellen
|
||||
// Ensure built-in templates are always present
|
||||
for (const t of BUILTIN_TEMPLATES) {
|
||||
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[] = [
|
||||
{
|
||||
id: 'builtin-events-today',
|
||||
name: 'Events heute',
|
||||
description: 'Alle Events seit Mitternacht',
|
||||
sql: `SELECT type, source, datetime(timestamp/1000, 'unixepoch', 'localtime') as zeit
|
||||
name: 'Events Today',
|
||||
description: 'All events since midnight',
|
||||
sql: `SELECT type, source, datetime(timestamp/1000, 'unixepoch', 'localtime') as time
|
||||
FROM events
|
||||
WHERE timestamp > (strftime('%s', 'now', 'start of day') * 1000)
|
||||
ORDER BY timestamp DESC
|
||||
|
|
@ -141,9 +141,9 @@ LIMIT {{limit:100}}`,
|
|||
},
|
||||
{
|
||||
id: 'builtin-active-files',
|
||||
name: 'Aktivste Dateien',
|
||||
description: 'Dateien mit den meisten Events',
|
||||
sql: `SELECT source as datei, COUNT(*) as events
|
||||
name: 'Most Active Files',
|
||||
description: 'Files with the most events',
|
||||
sql: `SELECT source as file, COUNT(*) as events
|
||||
FROM events
|
||||
WHERE source IS NOT NULL
|
||||
GROUP BY source
|
||||
|
|
@ -155,14 +155,14 @@ LIMIT {{limit:20}}`,
|
|||
{
|
||||
id: 'builtin-session-overview',
|
||||
name: 'Sessions',
|
||||
description: 'Letzte Sessions mit Dauer',
|
||||
description: 'Recent sessions with duration',
|
||||
sql: `SELECT
|
||||
id,
|
||||
datetime(start_time/1000, 'unixepoch', 'localtime') as start,
|
||||
CASE WHEN end_time IS NOT NULL
|
||||
THEN ROUND((end_time - start_time) / 60000.0, 1) || ' min'
|
||||
ELSE 'aktiv'
|
||||
END as dauer
|
||||
ELSE 'active'
|
||||
END as duration
|
||||
FROM sessions
|
||||
ORDER BY start_time DESC
|
||||
LIMIT {{limit:10}}`,
|
||||
|
|
@ -171,23 +171,23 @@ LIMIT {{limit:10}}`,
|
|||
},
|
||||
{
|
||||
id: 'builtin-daily-stats',
|
||||
name: 'Tagesstatistik',
|
||||
description: 'Aggregierte Statistiken pro Tag',
|
||||
sql: `SELECT date as tag, SUM(events_count) as events,
|
||||
SUM(words_added) as woerter_hinzu, SUM(words_removed) as woerter_entfernt
|
||||
name: 'Daily Statistics',
|
||||
description: 'Aggregated statistics per day',
|
||||
sql: `SELECT date as day, SUM(events_count) as events,
|
||||
SUM(words_added) as words_added, SUM(words_removed) as words_removed
|
||||
FROM daily_stats
|
||||
GROUP BY date
|
||||
ORDER BY date DESC
|
||||
LIMIT {{limit:14}}`,
|
||||
parameters: [{ name: 'limit', label: 'Tage', defaultValue: '14', type: 'number' }],
|
||||
parameters: [{ name: 'limit', label: 'Days', defaultValue: '14', type: 'number' }],
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'builtin-recent-files',
|
||||
name: 'Zuletzt geaenderte Dateien',
|
||||
description: 'Dateien nach Aenderungsdatum',
|
||||
name: 'Recently Modified Files',
|
||||
description: 'Files by modification date',
|
||||
sql: `SELECT name, extension,
|
||||
datetime(modified/1000, 'unixepoch', 'localtime') as geaendert,
|
||||
datetime(modified/1000, 'unixepoch', 'localtime') as modified_at,
|
||||
folder
|
||||
FROM _files
|
||||
WHERE extension = 'md'
|
||||
|
|
@ -198,22 +198,22 @@ LIMIT {{limit:20}}`,
|
|||
},
|
||||
{
|
||||
id: 'builtin-tag-stats',
|
||||
name: 'Tag-Statistiken',
|
||||
description: 'Notizen pro Tag zaehlen',
|
||||
sql: `SELECT tag, COUNT(DISTINCT path) as notizen
|
||||
name: 'Tag Statistics',
|
||||
description: 'Count notes per tag',
|
||||
sql: `SELECT tag, COUNT(DISTINCT path) as notes
|
||||
FROM _tags
|
||||
GROUP BY tag
|
||||
ORDER BY notizen DESC
|
||||
ORDER BY notes DESC
|
||||
LIMIT {{limit:30}}`,
|
||||
parameters: [{ name: 'limit', label: 'Limit', defaultValue: '30', type: 'number' }],
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'builtin-orphan-notes',
|
||||
name: 'Verwaiste Notizen',
|
||||
description: 'Notizen ohne eingehende Links',
|
||||
name: 'Orphan Notes',
|
||||
description: 'Notes without incoming links',
|
||||
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
|
||||
WHERE f.extension = 'md'
|
||||
AND f.path NOT IN (SELECT DISTINCT to_path FROM _links)
|
||||
|
|
@ -223,9 +223,9 @@ ORDER BY f.modified DESC`,
|
|||
},
|
||||
{
|
||||
id: 'builtin-broken-links',
|
||||
name: 'Defekte Links',
|
||||
description: 'Links zu nicht-existierenden Notizen',
|
||||
sql: `SELECT l.from_path as von, l.to_path as nach, l.display_text
|
||||
name: 'Broken Links',
|
||||
description: 'Links to non-existent notes',
|
||||
sql: `SELECT l.from_path as from_file, l.to_path as to_file, l.display_text
|
||||
FROM _links l
|
||||
WHERE l.to_path NOT IN (SELECT path FROM _files)
|
||||
AND l.link_type = 'link'`,
|
||||
|
|
@ -234,12 +234,12 @@ WHERE l.to_path NOT IN (SELECT path FROM _files)
|
|||
},
|
||||
{
|
||||
id: 'builtin-event-types',
|
||||
name: 'Event-Typen',
|
||||
description: 'Verteilung der Event-Typen',
|
||||
sql: `SELECT type as typ, category as kategorie, COUNT(*) as anzahl
|
||||
name: 'Event Types',
|
||||
description: 'Distribution of event types',
|
||||
sql: `SELECT type, category, COUNT(*) as count
|
||||
FROM events
|
||||
GROUP BY type, category
|
||||
ORDER BY anzahl DESC`,
|
||||
ORDER BY count DESC`,
|
||||
parameters: [],
|
||||
builtIn: true,
|
||||
},
|
||||
|
|
@ -262,7 +262,7 @@ export class TemplatePickerModal extends Modal {
|
|||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
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' });
|
||||
|
||||
|
|
@ -285,7 +285,7 @@ export class TemplatePickerModal extends Modal {
|
|||
});
|
||||
|
||||
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', () => {
|
||||
if (tpl.parameters.length > 0) {
|
||||
this.close();
|
||||
|
|
@ -299,11 +299,11 @@ export class TemplatePickerModal extends Modal {
|
|||
});
|
||||
|
||||
if (!tpl.builtIn) {
|
||||
const delBtn = actions.createEl('button', { text: 'Entfernen' });
|
||||
const delBtn = actions.createEl('button', { text: 'Remove' });
|
||||
delBtn.addEventListener('click', () => {
|
||||
this.manager.delete(tpl.id);
|
||||
item.remove();
|
||||
new Notice('Template entfernt.');
|
||||
new Notice('Template removed.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -334,7 +334,7 @@ class ParameterModal extends Modal {
|
|||
|
||||
onOpen(): void {
|
||||
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) {
|
||||
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 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(); });
|
||||
const cancelBtn = btns.createEl('button', { text: 'Abbrechen' });
|
||||
const cancelBtn = btns.createEl('button', { text: 'Cancel' });
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { toMarkdownTable } from '../viz/table-renderer';
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
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`;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { ProjectionTemplate } from '../../types';
|
|||
|
||||
export const dailyLogTemplate: ProjectionTemplate = {
|
||||
id: 'builtin:daily-log',
|
||||
name: 'Tagesprotokoll',
|
||||
description: 'Sessions, aktive Dateien, Events und Zeitleiste des Tages.',
|
||||
name: 'Daily Log',
|
||||
description: 'Sessions, active files, events, and timeline of the day.',
|
||||
enabled: false,
|
||||
trigger: { type: 'manual' },
|
||||
output: {
|
||||
|
|
@ -30,8 +30,8 @@ export const dailyLogTemplate: ProjectionTemplate = {
|
|||
name: 'table',
|
||||
columns: [
|
||||
{ header: 'Session', value: 'session' },
|
||||
{ header: 'Typ', value: 'type' },
|
||||
{ header: 'Zeit', value: 'timestamp' },
|
||||
{ header: 'Type', value: 'type' },
|
||||
{ header: 'Time', value: 'timestamp' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -39,7 +39,7 @@ export const dailyLogTemplate: ProjectionTemplate = {
|
|||
},
|
||||
{
|
||||
id: 'active-files',
|
||||
heading: 'Aktive Dateien',
|
||||
heading: 'Active Files',
|
||||
type: 'table',
|
||||
query: {
|
||||
timeRange: { type: 'relative', value: 'today' },
|
||||
|
|
@ -53,10 +53,10 @@ export const dailyLogTemplate: ProjectionTemplate = {
|
|||
builtin: {
|
||||
name: 'table',
|
||||
columns: [
|
||||
{ header: 'Datei', value: 'group' },
|
||||
{ header: 'File', value: 'group' },
|
||||
{ header: 'Events', value: 'count' },
|
||||
{ header: 'Woerter+', value: 'words_added' },
|
||||
{ header: 'Woerter-', value: 'words_removed' },
|
||||
{ header: 'Words+', value: 'words_added' },
|
||||
{ header: 'Words-', value: 'words_removed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -64,7 +64,7 @@ export const dailyLogTemplate: ProjectionTemplate = {
|
|||
},
|
||||
{
|
||||
id: 'event-summary',
|
||||
heading: 'Event-Uebersicht',
|
||||
heading: 'Event Summary',
|
||||
type: 'table',
|
||||
query: {
|
||||
timeRange: { type: 'relative', value: 'today' },
|
||||
|
|
@ -77,8 +77,8 @@ export const dailyLogTemplate: ProjectionTemplate = {
|
|||
builtin: {
|
||||
name: 'table',
|
||||
columns: [
|
||||
{ header: 'Typ', value: 'group' },
|
||||
{ header: 'Anzahl', value: 'count' },
|
||||
{ header: 'Type', value: 'group' },
|
||||
{ header: 'Count', value: 'count' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -86,7 +86,7 @@ export const dailyLogTemplate: ProjectionTemplate = {
|
|||
},
|
||||
{
|
||||
id: 'timeline',
|
||||
heading: 'Zeitleiste',
|
||||
heading: 'Timeline',
|
||||
type: 'timeline',
|
||||
query: {
|
||||
timeRange: { type: 'relative', value: 'today' },
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { ProjectionTemplate } from '../../types';
|
|||
|
||||
export const sessionLogTemplate: ProjectionTemplate = {
|
||||
id: 'builtin:session-log',
|
||||
name: 'Session-Protokoll',
|
||||
description: 'Einzelne Session: Dauer, bearbeitete Dateien, ausgefuehrte Befehle.',
|
||||
name: 'Session Log',
|
||||
description: 'Single session: duration, edited files, executed commands.',
|
||||
enabled: false,
|
||||
trigger: { type: 'on-session-end' },
|
||||
output: {
|
||||
|
|
@ -19,7 +19,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
|
|||
sections: [
|
||||
{
|
||||
id: 'overview',
|
||||
heading: 'Session-Uebersicht',
|
||||
heading: 'Session Overview',
|
||||
type: 'summary',
|
||||
query: {
|
||||
timeRange: { type: 'session' },
|
||||
|
|
@ -29,7 +29,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
|
|||
builtin: {
|
||||
name: 'summary',
|
||||
metrics: [
|
||||
{ label: 'Events gesamt', aggregate: 'count' },
|
||||
{ label: 'Total events', aggregate: 'count' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -37,7 +37,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
|
|||
},
|
||||
{
|
||||
id: 'files',
|
||||
heading: 'Bearbeitete Dateien',
|
||||
heading: 'Edited Files',
|
||||
type: 'table',
|
||||
query: {
|
||||
timeRange: { type: 'session' },
|
||||
|
|
@ -51,10 +51,10 @@ export const sessionLogTemplate: ProjectionTemplate = {
|
|||
builtin: {
|
||||
name: 'table',
|
||||
columns: [
|
||||
{ header: 'Datei', value: 'group' },
|
||||
{ header: 'File', value: 'group' },
|
||||
{ header: 'Events', value: 'count' },
|
||||
{ header: 'Woerter+', value: 'words_added' },
|
||||
{ header: 'Woerter-', value: 'words_removed' },
|
||||
{ header: 'Words+', value: 'words_added' },
|
||||
{ header: 'Words-', value: 'words_removed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -62,7 +62,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
|
|||
},
|
||||
{
|
||||
id: 'commands',
|
||||
heading: 'Ausgefuehrte Befehle',
|
||||
heading: 'Executed Commands',
|
||||
type: 'table',
|
||||
query: {
|
||||
timeRange: { type: 'session' },
|
||||
|
|
@ -74,8 +74,8 @@ export const sessionLogTemplate: ProjectionTemplate = {
|
|||
builtin: {
|
||||
name: 'table',
|
||||
columns: [
|
||||
{ header: 'Zeit', value: 'timestamp' },
|
||||
{ header: 'Befehl', value: 'source' },
|
||||
{ header: 'Time', value: 'timestamp' },
|
||||
{ header: 'Command', value: 'source' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { ProjectionTemplate } from '../../types';
|
|||
|
||||
export const weeklyDigestTemplate: ProjectionTemplate = {
|
||||
id: 'builtin:weekly-digest',
|
||||
name: 'Wochen-Digest',
|
||||
description: 'Wochenuebersicht: Tages-Summary, Top-Dateien, Schreib-Statistiken, Heatmap.',
|
||||
name: 'Weekly Digest',
|
||||
description: 'Weekly overview: daily summary, top files, writing statistics, heatmap.',
|
||||
enabled: false,
|
||||
trigger: { type: 'manual' },
|
||||
output: {
|
||||
|
|
@ -18,7 +18,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
|
|||
sections: [
|
||||
{
|
||||
id: 'daily-summary',
|
||||
heading: 'Tages-Uebersicht',
|
||||
heading: 'Daily Overview',
|
||||
type: 'table',
|
||||
query: {
|
||||
timeRange: { type: 'relative', value: 'last-7-days' },
|
||||
|
|
@ -31,10 +31,10 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
|
|||
builtin: {
|
||||
name: 'table',
|
||||
columns: [
|
||||
{ header: 'Tag', value: 'group' },
|
||||
{ header: 'Day', value: 'group' },
|
||||
{ header: 'Events', value: 'count' },
|
||||
{ header: 'Woerter+', value: 'words_added' },
|
||||
{ header: 'Woerter-', value: 'words_removed' },
|
||||
{ header: 'Words+', value: 'words_added' },
|
||||
{ header: 'Words-', value: 'words_removed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -42,7 +42,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
|
|||
},
|
||||
{
|
||||
id: 'top-files',
|
||||
heading: 'Top-Dateien',
|
||||
heading: 'Top Files',
|
||||
type: 'table',
|
||||
query: {
|
||||
timeRange: { type: 'relative', value: 'last-7-days' },
|
||||
|
|
@ -56,9 +56,9 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
|
|||
builtin: {
|
||||
name: 'table',
|
||||
columns: [
|
||||
{ header: 'Datei', value: 'group' },
|
||||
{ header: 'File', value: 'group' },
|
||||
{ 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',
|
||||
heading: 'Schreib-Statistiken',
|
||||
heading: 'Writing Statistics',
|
||||
type: 'summary',
|
||||
query: {
|
||||
timeRange: { type: 'relative', value: 'last-7-days' },
|
||||
|
|
@ -77,9 +77,9 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
|
|||
builtin: {
|
||||
name: 'summary',
|
||||
metrics: [
|
||||
{ label: 'Tage aktiv', aggregate: 'count' },
|
||||
{ label: 'Woerter geschrieben', aggregate: 'sum', field: 'words_added' },
|
||||
{ label: 'Woerter geloescht', aggregate: 'sum', field: 'words_removed' },
|
||||
{ label: 'Days active', aggregate: 'count' },
|
||||
{ label: 'Words written', aggregate: 'sum', field: 'words_added' },
|
||||
{ label: 'Words deleted', aggregate: 'sum', field: 'words_removed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -87,7 +87,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
|
|||
},
|
||||
{
|
||||
id: 'activity-heatmap',
|
||||
heading: 'Aktivitaets-Heatmap',
|
||||
heading: 'Activity Heatmap',
|
||||
type: 'chart-data',
|
||||
query: {
|
||||
timeRange: { type: 'relative', value: 'last-7-days' },
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export class ProjectionEngine {
|
|||
start(): void {
|
||||
if (!this.settings.projections.enabled) return;
|
||||
|
||||
// Session-End-Listener fuer session-log
|
||||
// Session-end listener for session-log
|
||||
if (this.settings.projections.sessionLog.enabled) {
|
||||
this.sessionEndUnsub = this.eventBus.onEvent('system:session-end', (event) => {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ export class ProjectionEngine {
|
|||
if (template) this.runProjection(template);
|
||||
}
|
||||
|
||||
// Weekly Digest (dayOfWeek: 0=So, 1=Mo, ...)
|
||||
// Weekly Digest (dayOfWeek: 0=Sun, 1=Mon, ...)
|
||||
if (
|
||||
this.settings.projections.weeklyDigest.enabled &&
|
||||
now.getDay() === this.settings.projections.weeklyDigest.dayOfWeek &&
|
||||
|
|
@ -115,7 +115,7 @@ export class ProjectionEngine {
|
|||
|
||||
const queryConfig = { ...section.query };
|
||||
|
||||
// Session-Kontext einsetzen
|
||||
// Inject session context
|
||||
if (queryConfig.timeRange.type === 'session' && context?.sessionId) {
|
||||
queryConfig.timeRange = { ...queryConfig.timeRange, sessionId: context.sessionId };
|
||||
}
|
||||
|
|
@ -133,10 +133,10 @@ export class ProjectionEngine {
|
|||
});
|
||||
const filePath = `${outputFolder}/${fileName}`;
|
||||
|
||||
// Ordner erstellen falls noetig
|
||||
// Create folder if needed
|
||||
await this.ensureFolder(outputFolder);
|
||||
|
||||
// Datei schreiben
|
||||
// Write file
|
||||
const existing = this.app.vault.getAbstractFileByPath(filePath);
|
||||
if (existing) {
|
||||
if (template.output.mode === 'append') {
|
||||
|
|
@ -151,8 +151,8 @@ export class ProjectionEngine {
|
|||
|
||||
return filePath;
|
||||
} catch (err) {
|
||||
console.error('[Logfire] Projektion fehlgeschlagen:', err);
|
||||
new Notice(`Logfire: Projektion "${template.name}" fehlgeschlagen.`);
|
||||
console.error('[Logfire] Projection failed:', err);
|
||||
new Notice(`Logfire: Projection "${template.name}" failed.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -166,7 +166,7 @@ export class ProjectionEngine {
|
|||
if (result) count++;
|
||||
}
|
||||
}
|
||||
new Notice(`Logfire: ${count} Projektion(en) ausgefuehrt.`);
|
||||
new Notice(`Logfire: ${count} projection(s) executed.`);
|
||||
}
|
||||
|
||||
private async ensureFolder(path: string): Promise<void> {
|
||||
|
|
@ -198,7 +198,7 @@ export class ProjectionPickerModal extends Modal {
|
|||
contentEl.empty();
|
||||
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 templates = this.engine.getRegistry().getAll();
|
||||
|
|
@ -221,11 +221,11 @@ export class ProjectionPickerModal extends Modal {
|
|||
});
|
||||
|
||||
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 () => {
|
||||
const path = await this.engine.runProjection(template);
|
||||
if (path) {
|
||||
new Notice(`Projektion geschrieben: ${path}`);
|
||||
new Notice(`Projection written: ${path}`);
|
||||
}
|
||||
this.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export class SqlAutocomplete {
|
|||
) as { name: string }[];
|
||||
this.columnCache.set(table, rows.map(r => r.name));
|
||||
} 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 'group-by':
|
||||
case 'order-by': {
|
||||
// Spalten aus aktiven Tabellen
|
||||
// Columns from active tables
|
||||
const tables = extractTables(textBefore);
|
||||
for (const table of tables) {
|
||||
const cols = this.columnCache.get(table) ?? [];
|
||||
|
|
|
|||
|
|
@ -76,14 +76,14 @@ export function registerLogfireSqlBlock(
|
|||
const { sql, refresh } = parseSqlBlock(source);
|
||||
|
||||
if (!sql) {
|
||||
renderError(el, new Error('Leere Query.'));
|
||||
renderError(el, new Error('Empty query.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety: only SELECT/WITH
|
||||
const firstWord = sql.split(/\s+/)[0].toUpperCase();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ export function registerLogfireSqlBlock(
|
|||
try {
|
||||
const rows = db.queryReadOnly(sql) as Record<string, unknown>[];
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ export function registerLogfireSqlBlock(
|
|||
try {
|
||||
const freshRows = db.queryReadOnly(sql) as Record<string, unknown>[];
|
||||
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;
|
||||
}
|
||||
if (chartConfig) {
|
||||
|
|
@ -139,7 +139,7 @@ export function registerLogfireDashboardBlock(
|
|||
registerFn('logfire-dashboard', (source, el, ctx) => {
|
||||
const dashboard = parseDashboardBlock(source);
|
||||
if (!dashboard) {
|
||||
renderError(el, new Error('Ungültige Dashboard-Definition.'));
|
||||
renderError(el, new Error('Invalid dashboard definition.'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ export function registerLogfireDashboardBlock(
|
|||
} else if (widget.sql) {
|
||||
const rows = db.queryReadOnly(widget.sql) as Record<string, unknown>[];
|
||||
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) {
|
||||
renderChart(content, rows, widget.chartConfig);
|
||||
} else if (widget.type === 'stat') {
|
||||
|
|
@ -182,7 +182,7 @@ export function registerLogfireDashboardBlock(
|
|||
} catch (err) {
|
||||
content.createDiv({
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,17 +35,17 @@ export class QueryModal extends Modal {
|
|||
// Mode toggle
|
||||
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.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'
|
||||
? 'events today\nstats this-week group by file\nfiles modified yesterday'
|
||||
: 'SELECT * FROM events\nWHERE type = \'file:create\'\nORDER BY timestamp DESC\nLIMIT 50';
|
||||
});
|
||||
|
||||
const helpSpan = toolbar.createEl('span', {
|
||||
text: 'Ctrl+Enter: Ausf\u00fchren',
|
||||
text: 'Ctrl+Enter: Execute',
|
||||
cls: 'logfire-qm-hint',
|
||||
});
|
||||
|
||||
|
|
@ -87,13 +87,13 @@ export class QueryModal extends Modal {
|
|||
// 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());
|
||||
|
||||
const copyBtn = buttonRow.createEl('button', { text: 'Als Markdown kopieren' });
|
||||
const copyBtn = buttonRow.createEl('button', { text: 'Copy as Markdown' });
|
||||
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());
|
||||
|
||||
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' });
|
||||
jsonBtn.addEventListener('click', () => this.exportJson());
|
||||
|
||||
const clearBtn = buttonRow.createEl('button', { text: 'Leeren' });
|
||||
const clearBtn = buttonRow.createEl('button', { text: 'Clear' });
|
||||
clearBtn.addEventListener('click', () => {
|
||||
this.editorEl.value = '';
|
||||
this.resultEl.empty();
|
||||
|
|
@ -113,10 +113,10 @@ export class QueryModal extends Modal {
|
|||
// Results
|
||||
this.resultEl = contentEl.createDiv({ cls: 'logfire-qm-results' });
|
||||
|
||||
// Initial SQL setzen
|
||||
// Set initial SQL
|
||||
if (this.initialSql) {
|
||||
this.mode = 'sql';
|
||||
this.modeToggle.textContent = 'Modus: SQL';
|
||||
this.modeToggle.textContent = 'Mode: SQL';
|
||||
this.editorEl.value = this.initialSql;
|
||||
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();
|
||||
if (firstWord !== 'SELECT' && firstWord !== 'WITH') {
|
||||
this.resultEl.createEl('pre', {
|
||||
text: 'Nur SELECT- und WITH-Queries sind erlaubt.',
|
||||
text: 'Only SELECT and WITH queries are allowed.',
|
||||
cls: 'logfire-error',
|
||||
});
|
||||
return;
|
||||
|
|
@ -170,7 +170,7 @@ export class QueryModal extends Modal {
|
|||
this.renderResults(rows);
|
||||
} catch (err) {
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
|
@ -180,7 +180,7 @@ export class QueryModal extends Modal {
|
|||
this.resultEl.empty();
|
||||
|
||||
if (rows.length === 0) {
|
||||
this.resultEl.createEl('p', { text: 'Keine Ergebnisse.' });
|
||||
this.resultEl.createEl('p', { text: 'No results.' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ export class QueryModal extends Modal {
|
|||
|
||||
if (rows.length > 200) {
|
||||
this.resultEl.createEl('p', {
|
||||
text: `${rows.length} Ergebnisse, 200 angezeigt.`,
|
||||
text: `${rows.length} results, 200 shown.`,
|
||||
cls: 'logfire-qm-truncated',
|
||||
});
|
||||
}
|
||||
|
|
@ -197,22 +197,22 @@ export class QueryModal extends Modal {
|
|||
|
||||
private copyAsMarkdown(): void {
|
||||
if (this.lastRows.length === 0) {
|
||||
new Notice('Keine Ergebnisse zum Kopieren.');
|
||||
new Notice('No results to copy.');
|
||||
return;
|
||||
}
|
||||
const md = toMarkdownTable(this.lastKeys, this.lastRows);
|
||||
navigator.clipboard.writeText(md);
|
||||
new Notice('In Zwischenablage kopiert.');
|
||||
new Notice('Copied to clipboard.');
|
||||
}
|
||||
|
||||
private insertInNote(): void {
|
||||
if (this.lastRows.length === 0) {
|
||||
new Notice('Keine Ergebnisse zum Einf\u00fcgen.');
|
||||
new Notice('No results to insert.');
|
||||
return;
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!view) {
|
||||
new Notice('Kein aktiver Editor zum Einf\u00fcgen.');
|
||||
new Notice('No active editor to insert into.');
|
||||
return;
|
||||
}
|
||||
const md = toMarkdownTable(this.lastKeys, this.lastRows);
|
||||
|
|
@ -226,7 +226,7 @@ export class QueryModal extends Modal {
|
|||
|
||||
private exportCsv(): void {
|
||||
if (this.lastRows.length === 0) {
|
||||
new Notice('Keine Ergebnisse zum Exportieren.');
|
||||
new Notice('No results to export.');
|
||||
return;
|
||||
}
|
||||
const bom = '\uFEFF';
|
||||
|
|
@ -235,17 +235,17 @@ export class QueryModal extends Modal {
|
|||
this.lastKeys.map(k => csvEscape(row[k])).join(',')
|
||||
).join('\n');
|
||||
downloadBlob(bom + header + '\n' + body, 'logfire-export.csv', 'text/csv;charset=utf-8');
|
||||
new Notice('CSV exportiert.');
|
||||
new Notice('CSV exported.');
|
||||
}
|
||||
|
||||
private exportJson(): void {
|
||||
if (this.lastRows.length === 0) {
|
||||
new Notice('Keine Ergebnisse zum Exportieren.');
|
||||
new Notice('No results to export.');
|
||||
return;
|
||||
}
|
||||
const json = JSON.stringify(this.lastRows, null, 2);
|
||||
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 after = text.substring(cursorPos);
|
||||
|
||||
// Letztes Wort ersetzen
|
||||
// Replace last word
|
||||
const lastWordMatch = before.match(/[\w._:-]+$/);
|
||||
const replaceStart = lastWordMatch ? cursorPos - lastWordMatch[0].length : cursorPos;
|
||||
const newBefore = text.substring(0, replaceStart) + suggestion.text;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class EventStreamView extends ItemView {
|
|||
|
||||
const searchInput = toolbar.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Nach Quelle filtern...',
|
||||
placeholder: 'Filter by source...',
|
||||
cls: 'logfire-stream-search',
|
||||
});
|
||||
searchInput.addEventListener('input', () => {
|
||||
|
|
@ -54,11 +54,11 @@ export class EventStreamView extends ItemView {
|
|||
const pauseBtn = btnGroup.createEl('button', { text: 'Pause' });
|
||||
pauseBtn.addEventListener('click', () => {
|
||||
this.viewPaused = !this.viewPaused;
|
||||
pauseBtn.textContent = this.viewPaused ? 'Weiter' : 'Pause';
|
||||
pauseBtn.textContent = this.viewPaused ? 'Resume' : 'Pause';
|
||||
pauseBtn.toggleClass('is-active', this.viewPaused);
|
||||
});
|
||||
|
||||
const clearBtn = btnGroup.createEl('button', { text: 'Leeren' });
|
||||
const clearBtn = btnGroup.createEl('button', { text: 'Clear' });
|
||||
clearBtn.addEventListener('click', () => {
|
||||
this.entries = [];
|
||||
this.renderList();
|
||||
|
|
@ -132,9 +132,9 @@ export class EventStreamView extends ItemView {
|
|||
const p = event.payload;
|
||||
switch (event.type) {
|
||||
case 'content:words-changed':
|
||||
return `+${p.wordsAdded ?? 0}/-${p.wordsRemoved ?? 0} Wörter`;
|
||||
return `+${p.wordsAdded ?? 0}/-${p.wordsRemoved ?? 0} words`;
|
||||
case 'editor:change':
|
||||
return `+${p.insertedChars ?? 0}/-${p.deletedChars ?? 0} Zeichen`;
|
||||
return `+${p.insertedChars ?? 0}/-${p.deletedChars ?? 0} chars`;
|
||||
case 'nav:file-close':
|
||||
return typeof p.duration === 'number' ? `${Math.round(p.duration / 1000)}s` : '';
|
||||
case 'file:rename':
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@ export class InitialScanModal extends Modal {
|
|||
const files = this.app.vault.getMarkdownFiles()
|
||||
.filter(f => this.shouldTrack(f.path));
|
||||
|
||||
contentEl.createEl('h2', { text: 'Logfire – Vault-Scan' });
|
||||
contentEl.createEl('h2', { text: 'Logfire – Vault Scan' });
|
||||
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' });
|
||||
|
|
@ -62,15 +62,15 @@ export class InitialScanModal extends Modal {
|
|||
buttonContainer.style.gap = '8px';
|
||||
buttonContainer.style.marginTop = '16px';
|
||||
|
||||
const cancelBtn = buttonContainer.createEl('button', { text: 'Abbrechen' });
|
||||
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel' });
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
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.disabled = true;
|
||||
cancelBtn.textContent = 'Stoppen';
|
||||
cancelBtn.textContent = 'Stop';
|
||||
|
||||
let scanned = 0;
|
||||
let totalWords = 0;
|
||||
|
|
@ -103,9 +103,9 @@ export class InitialScanModal extends Modal {
|
|||
|
||||
progressBar.value = scanned;
|
||||
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) {
|
||||
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
|
||||
? `Scan gestoppt. ${scanned} von ${files.length} Dateien gescannt.`
|
||||
: `Scan abgeschlossen! ${scanned} Dateien gescannt.`;
|
||||
? `Scan stopped. ${scanned} of ${files.length} files scanned.`
|
||||
: `Scan complete! ${scanned} files scanned.`;
|
||||
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', () => {
|
||||
this.close();
|
||||
this.resolve(true);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export class SchemaView extends ItemView {
|
|||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'Schema-Browser';
|
||||
return 'Schema Browser';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
|
|
@ -63,7 +63,7 @@ export class SchemaView extends ItemView {
|
|||
|
||||
const refreshBtn = header.createEl('button', {
|
||||
cls: 'logfire-dash-btn clickable-icon',
|
||||
attr: { 'aria-label': 'Aktualisieren' },
|
||||
attr: { 'aria-label': 'Refresh' },
|
||||
text: '\u21bb',
|
||||
});
|
||||
refreshBtn.addEventListener('click', () => this.refresh());
|
||||
|
|
@ -77,7 +77,7 @@ export class SchemaView extends ItemView {
|
|||
|
||||
const tables = this.introspect();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -144,11 +144,11 @@ export class SchemaView extends ItemView {
|
|||
this.refresh();
|
||||
});
|
||||
|
||||
// Context menu: SQL kopieren
|
||||
// Context menu: copy SQL
|
||||
header.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
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;
|
||||
|
|
@ -166,7 +166,7 @@ export class SchemaView extends ItemView {
|
|||
|
||||
// Indexes
|
||||
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) {
|
||||
const row = details.createDiv({ cls: 'logfire-schema-idx' });
|
||||
row.createSpan({ text: `${idx.unique ? 'UNIQUE ' : ''}${idx.name}` });
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
containerEl.empty();
|
||||
|
||||
// ----- General -----
|
||||
containerEl.createEl('h2', { text: 'Allgemein' });
|
||||
containerEl.createEl('h2', { text: 'General' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Tracking aktiviert')
|
||||
.setDesc('Hauptschalter für alle Event-Collector.')
|
||||
.setName('Tracking enabled')
|
||||
.setDesc('Master switch for all event collectors.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.general.enabled)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -24,8 +24,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Log-Ordner')
|
||||
.setDesc('Ordner für Markdown-Projektionen. Wird automatisch vom Tracking ausgeschlossen.')
|
||||
.setName('Log folder')
|
||||
.setDesc('Folder for Markdown projections. Automatically excluded from tracking.')
|
||||
.addText(t => t
|
||||
.setPlaceholder('Logfire')
|
||||
.setValue(this.plugin.settings.general.logFolder)
|
||||
|
|
@ -35,8 +35,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Beim Start pausieren')
|
||||
.setDesc('Startet im pausierten Zustand — keine Events bis manuell fortgesetzt.')
|
||||
.setName('Pause on startup')
|
||||
.setDesc('Starts in paused state — no events until manually resumed.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.general.pauseOnStartup)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -48,8 +48,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
containerEl.createEl('h2', { text: 'Tracking' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Datei-Events')
|
||||
.setDesc('Erstellen, Löschen, Umbenennen, Verschieben und Ändern von Dateien.')
|
||||
.setName('File events')
|
||||
.setDesc('Create, delete, rename, move, and modify files.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.tracking.fileEvents)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -58,8 +58,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Content-Analyse')
|
||||
.setDesc('Semantische Diffs: Wortzählung, Links, Tags, Überschriften, Frontmatter.')
|
||||
.setName('Content analysis')
|
||||
.setDesc('Semantic diffs: word count, links, tags, headings, frontmatter.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.tracking.contentAnalysis)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -69,7 +69,7 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
|
||||
new Setting(containerEl)
|
||||
.setName('Navigation')
|
||||
.setDesc('Aktive Datei-Wechsel, Datei öffnen/schließen mit Dauer.')
|
||||
.setDesc('Active file switches, file open/close with duration.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.tracking.navigation)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -78,8 +78,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Editor-Änderungen')
|
||||
.setDesc('Tastenanschläge (debounced und aggregiert).')
|
||||
.setName('Editor changes')
|
||||
.setDesc('Keystrokes (debounced and aggregated).')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.tracking.editorChanges)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -88,8 +88,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Kommando-Tracking')
|
||||
.setDesc('Ausführung von Befehlen über Palette und Hotkeys.')
|
||||
.setName('Command tracking')
|
||||
.setDesc('Execution of commands via palette and hotkeys.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.tracking.commandTracking)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -98,8 +98,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Ausgeschlossene Ordner')
|
||||
.setDesc('Komma-getrennte Liste von Ordnern, die nicht getrackt werden.')
|
||||
.setName('Excluded folders')
|
||||
.setDesc('Comma-separated list of folders to exclude from tracking.')
|
||||
.addText(t => t
|
||||
.setPlaceholder('.obsidian, templates')
|
||||
.setValue(this.plugin.settings.tracking.excludedFolders.join(', '))
|
||||
|
|
@ -112,8 +112,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Ausgeschlossene Patterns')
|
||||
.setDesc('Komma-getrennte Glob-Patterns (z.B. **/*.excalidraw.md).')
|
||||
.setName('Excluded patterns')
|
||||
.setDesc('Comma-separated glob patterns (e.g. **/*.excalidraw.md).')
|
||||
.addText(t => t
|
||||
.setPlaceholder('**/*.excalidraw.md')
|
||||
.setValue(this.plugin.settings.tracking.excludedPatterns.join(', '))
|
||||
|
|
@ -126,11 +126,11 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// ----- Projections -----
|
||||
containerEl.createEl('h2', { text: 'Projektionen' });
|
||||
containerEl.createEl('h2', { text: 'Projections' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Projektionen aktiviert')
|
||||
.setDesc('Automatische Markdown-Reports aus Event-Daten.')
|
||||
.setName('Projections enabled')
|
||||
.setDesc('Automatic Markdown reports from event data.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.projections.enabled)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -139,8 +139,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Output-Ordner')
|
||||
.setDesc('Ordner für generierte Projektions-Dateien.')
|
||||
.setName('Output folder')
|
||||
.setDesc('Folder for generated projection files.')
|
||||
.addText(t => t
|
||||
.setPlaceholder('Logfire')
|
||||
.setValue(this.plugin.settings.projections.outputFolder)
|
||||
|
|
@ -150,8 +150,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Tagesprotokoll')
|
||||
.setDesc('Automatisches Tagesprotokoll generieren.')
|
||||
.setName('Daily log')
|
||||
.setDesc('Generate automatic daily log.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.projections.dailyLog.enabled)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -169,8 +169,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Session-Protokoll')
|
||||
.setDesc('Protokoll bei Session-Ende generieren.')
|
||||
.setName('Session log')
|
||||
.setDesc('Generate log on session end.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.projections.sessionLog.enabled)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -179,8 +179,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Wochen-Digest')
|
||||
.setDesc('Wöchentliche Zusammenfassung generieren.')
|
||||
.setName('Weekly digest')
|
||||
.setDesc('Generate weekly summary.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.projections.weeklyDigest.enabled)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -189,8 +189,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}))
|
||||
.addDropdown(d => d
|
||||
.addOptions({
|
||||
'0': 'Sonntag', '1': 'Montag', '2': 'Dienstag', '3': 'Mittwoch',
|
||||
'4': 'Donnerstag', '5': 'Freitag', '6': 'Samstag',
|
||||
'0': 'Sunday', '1': 'Monday', '2': 'Tuesday', '3': 'Wednesday',
|
||||
'4': 'Thursday', '5': 'Friday', '6': 'Saturday',
|
||||
})
|
||||
.setValue(String(this.plugin.settings.projections.weeklyDigest.dayOfWeek))
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -200,12 +200,12 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
|
||||
// ----- Advanced (collapsible) -----
|
||||
const advancedHeader = containerEl.createEl('details');
|
||||
advancedHeader.createEl('summary', { text: 'Erweiterte Einstellungen' })
|
||||
advancedHeader.createEl('summary', { text: 'Advanced Settings' })
|
||||
.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.',
|
||||
text: 'These settings affect performance and storage. The defaults are optimal for most vaults.',
|
||||
cls: 'setting-item-description',
|
||||
});
|
||||
|
||||
|
|
@ -213,8 +213,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
advEl.createEl('h3', { text: 'Performance' });
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('Editor-Debounce (ms)')
|
||||
.setDesc('Wartezeit nach letztem Tastenanschlag vor editor:change Event.')
|
||||
.setName('Editor debounce (ms)')
|
||||
.setDesc('Wait time after last keystroke before editor:change event.')
|
||||
.addText(t => t
|
||||
.setValue(String(this.plugin.settings.advanced.debounceMs))
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -226,8 +226,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('Flush-Intervall (ms)')
|
||||
.setDesc('Wie oft gepufferte Events in die Datenbank geschrieben werden.')
|
||||
.setName('Flush interval (ms)')
|
||||
.setDesc('How often buffered events are written to the database.')
|
||||
.addText(t => t
|
||||
.setValue(String(this.plugin.settings.advanced.flushIntervalMs))
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -239,8 +239,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('Flush-Schwellwert')
|
||||
.setDesc('Maximale Events im Puffer vor erzwungenem Schreiben.')
|
||||
.setName('Flush threshold')
|
||||
.setDesc('Maximum events in buffer before forced write.')
|
||||
.addText(t => t
|
||||
.setValue(String(this.plugin.settings.advanced.flushThreshold))
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -252,11 +252,11 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// Retention
|
||||
advEl.createEl('h3', { text: 'Aufbewahrung' });
|
||||
advEl.createEl('h3', { text: 'Retention' });
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('Raw-Events aufbewahren (Tage)')
|
||||
.setDesc('Danach werden Events in Tages-Statistiken aggregiert und gelöscht.')
|
||||
.setName('Keep raw events (days)')
|
||||
.setDesc('After this, events are aggregated into daily stats and deleted.')
|
||||
.addText(t => t
|
||||
.setValue(String(this.plugin.settings.advanced.retention.rawEventsDays))
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -268,8 +268,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('Tages-Statistiken aufbewahren (Tage)')
|
||||
.setDesc('Danach werden Tages-Statistiken in Monats-Statistiken aggregiert.')
|
||||
.setName('Keep daily stats (days)')
|
||||
.setDesc('After this, daily stats are aggregated into monthly stats.')
|
||||
.addText(t => t
|
||||
.setValue(String(this.plugin.settings.advanced.retention.dailyStatsDays))
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -281,8 +281,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('Wartung beim Start')
|
||||
.setDesc('Automatisch Retention-Cleanup beim Plugin-Start ausführen.')
|
||||
.setName('Maintenance on startup')
|
||||
.setDesc('Automatically run retention cleanup on plugin start.')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.advanced.retention.maintenanceOnStartup)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -291,11 +291,11 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// Database settings
|
||||
advEl.createEl('h3', { text: 'Datenbank' });
|
||||
advEl.createEl('h3', { text: 'Database' });
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('WAL-Modus')
|
||||
.setDesc('Write-Ahead-Logging für bessere Parallelität (Neustart erforderlich).')
|
||||
.setName('WAL mode')
|
||||
.setDesc('Write-Ahead Logging for better concurrency (restart required).')
|
||||
.addToggle(t => t
|
||||
.setValue(this.plugin.settings.advanced.database.walMode)
|
||||
.onChange(async (v) => {
|
||||
|
|
@ -304,8 +304,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('Cache-Größe (MB)')
|
||||
.setDesc('SQLite Page-Cache (8–256 MB). Mehr Cache = schnellere Queries.')
|
||||
.setName('Cache size (MB)')
|
||||
.setDesc('SQLite page cache (8–256 MB). More cache = faster queries.')
|
||||
.addSlider(s => s
|
||||
.setLimits(8, 256, 8)
|
||||
.setValue(this.plugin.settings.advanced.database.cacheSizeMb)
|
||||
|
|
@ -316,8 +316,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('MMap-Größe (MB)')
|
||||
.setDesc('Memory-Mapped I/O (0–1024 MB). 0 deaktiviert MMap.')
|
||||
.setName('MMap size (MB)')
|
||||
.setDesc('Memory-Mapped I/O (0–1024 MB). 0 disables MMap.')
|
||||
.addSlider(s => s
|
||||
.setLimits(0, 1024, 32)
|
||||
.setValue(this.plugin.settings.advanced.database.mmapSizeMb)
|
||||
|
|
@ -328,8 +328,8 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('Geschützte Event-Typen')
|
||||
.setDesc('Event-Typen die nie gelöscht werden (komma-getrennt).')
|
||||
.setName('Protected event types')
|
||||
.setDesc('Event types that are never deleted (comma-separated).')
|
||||
.addText(t => t
|
||||
.setPlaceholder('file:create, file:delete, file:rename')
|
||||
.setValue(this.plugin.settings.advanced.retention.neverDeleteTypes.join(', '))
|
||||
|
|
@ -349,10 +349,10 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
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.'}`,
|
||||
text: `Events: ${eventCount.toLocaleString()} | Size: ${formatBytes(dbSize)} | Oldest: ${oldest ? new Date(oldest).toLocaleDateString() : 'N/A'}`,
|
||||
});
|
||||
} catch {
|
||||
dbInfoEl.createEl('p', { text: 'Datenbank-Info nicht verfügbar.' });
|
||||
dbInfoEl.createEl('p', { text: 'Database info unavailable.' });
|
||||
}
|
||||
|
||||
// Info section
|
||||
|
|
@ -362,17 +362,17 @@ export class LogfireSettingTab extends PluginSettingTab {
|
|||
});
|
||||
|
||||
new Setting(advEl)
|
||||
.setName('Datenbank-Aktionen')
|
||||
.setName('Database actions')
|
||||
.addButton(b => b
|
||||
.setButtonText('Wartung ausführen')
|
||||
.setButtonText('Run maintenance')
|
||||
.onClick(() => {
|
||||
try {
|
||||
this.plugin.db.runMaintenance(this.plugin.settings.advanced.retention);
|
||||
new Notice('Logfire: Wartung abgeschlossen.');
|
||||
new Notice('Logfire: Maintenance complete.');
|
||||
this.display();
|
||||
} catch (err) {
|
||||
new Notice('Logfire: Wartung fehlgeschlagen.');
|
||||
console.error('[Logfire] Wartung fehlgeschlagen:', err);
|
||||
new Notice('Logfire: Maintenance failed.');
|
||||
console.error('[Logfire] Maintenance failed:', err);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class StatusBar {
|
|||
private update(): void {
|
||||
const paused = this.plugin.isPaused();
|
||||
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 words = this.wordsAdded > 0 ? ` | +${this.wordsAdded}w` : '';
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export function renderChart(
|
|||
config: ChartConfig,
|
||||
): void {
|
||||
if (rows.length === 0) {
|
||||
el.createEl('p', { text: 'Keine Daten.', cls: 'logfire-empty' });
|
||||
el.createEl('p', { text: 'No data.', cls: 'logfire-empty' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export class DashboardView extends ItemView {
|
|||
|
||||
const refreshBtn = actions.createEl('button', {
|
||||
cls: 'logfire-dash-btn clickable-icon',
|
||||
attr: { 'aria-label': 'Aktualisieren' },
|
||||
attr: { 'aria-label': 'Refresh' },
|
||||
text: '\u21bb',
|
||||
});
|
||||
refreshBtn.addEventListener('click', () => this.refreshAll());
|
||||
|
|
@ -121,7 +121,7 @@ export class DashboardView extends ItemView {
|
|||
this.contentContainer.empty();
|
||||
this.contentContainer.createDiv({
|
||||
cls: 'logfire-empty',
|
||||
text: 'Kein Dashboard geladen.',
|
||||
text: 'No dashboard loaded.',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -170,19 +170,19 @@ export class DashboardView extends ItemView {
|
|||
} catch (err) {
|
||||
container.createDiv({
|
||||
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 {
|
||||
if (!widget.sql) {
|
||||
container.createDiv({ cls: 'logfire-empty', text: 'Keine Query konfiguriert.' });
|
||||
container.createDiv({ cls: 'logfire-empty', text: 'No query configured.' });
|
||||
return;
|
||||
}
|
||||
const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[];
|
||||
if (rows.length === 0) {
|
||||
container.createDiv({ cls: 'logfire-empty', text: 'Keine Ergebnisse.' });
|
||||
container.createDiv({ cls: 'logfire-empty', text: 'No results.' });
|
||||
return;
|
||||
}
|
||||
renderTable(container, rows);
|
||||
|
|
@ -190,7 +190,7 @@ export class DashboardView extends ItemView {
|
|||
|
||||
private renderChartWidget(container: HTMLElement, widget: DashboardWidget): void {
|
||||
if (!widget.sql || !widget.chartConfig) {
|
||||
container.createDiv({ cls: 'logfire-empty', text: 'Kein Chart konfiguriert.' });
|
||||
container.createDiv({ cls: 'logfire-empty', text: 'No chart configured.' });
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
if (!widget.sql) {
|
||||
container.createDiv({ cls: 'logfire-empty', text: 'Keine Query konfiguriert.' });
|
||||
container.createDiv({ cls: 'logfire-empty', text: 'No query configured.' });
|
||||
return;
|
||||
}
|
||||
const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[];
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ export class KeyboardNavigator {
|
|||
navigator.clipboard.writeText(td.textContent ?? '');
|
||||
td.classList.add('logfire-vim-yanked');
|
||||
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;
|
||||
}
|
||||
}
|
||||
new Notice('Nicht gefunden.');
|
||||
new Notice('Not found.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue