import { Modal, Setting, Notice } from 'obsidian'; import type LogfirePlugin from '../main'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface QueryTemplate { id: string; name: string; description: string; sql: string; parameters: TemplateParameter[]; builtIn: boolean; } export interface TemplateParameter { name: string; label: string; defaultValue: string; type: 'text' | 'number' | 'date'; } const STORAGE_KEY = 'logfire-templates'; // --------------------------------------------------------------------------- // TemplateManager // --------------------------------------------------------------------------- export class TemplateManager { private templates = new Map(); constructor() { this.load(); } private load(): void { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { try { const arr: QueryTemplate[] = JSON.parse(raw); for (const t of arr) this.templates.set(t.id, t); } catch { /* ignore */ } } // Immer Built-in Templates sicherstellen for (const t of BUILTIN_TEMPLATES) { if (!this.templates.has(t.id)) this.templates.set(t.id, t); } this.save(); } private save(): void { localStorage.setItem( STORAGE_KEY, JSON.stringify(Array.from(this.templates.values())), ); } getAll(): QueryTemplate[] { return Array.from(this.templates.values()); } get(id: string): QueryTemplate | undefined { return this.templates.get(id); } addCustom(name: string, description: string, sql: string): QueryTemplate { const t: QueryTemplate = { id: `tpl-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, name, description, sql, parameters: parseParameters(sql), builtIn: false, }; this.templates.set(t.id, t); this.save(); return t; } delete(id: string): void { const t = this.templates.get(id); if (t && !t.builtIn) { this.templates.delete(id); this.save(); } } // --------------------------------------------------------------------------- // Parameter handling // --------------------------------------------------------------------------- static applyParameters(sql: string, values: Record): string { return sql.replace(/\{\{(\w+)(?::([^}]*))?\}\}/g, (_match, name, defaultVal) => { const v = values[name]; if (v !== undefined && v !== '') return v; return defaultVal ?? ''; }); } } // --------------------------------------------------------------------------- // Parameter detection // --------------------------------------------------------------------------- function parseParameters(sql: string): TemplateParameter[] { const params: TemplateParameter[] = []; const seen = new Set(); const re = /\{\{(\w+)(?::([^}]*))?\}\}/g; let m: RegExpExecArray | null; while ((m = re.exec(sql)) !== null) { if (seen.has(m[1])) continue; seen.add(m[1]); params.push({ name: m[1], label: m[1].charAt(0).toUpperCase() + m[1].slice(1).replace(/_/g, ' '), defaultValue: m[2] ?? '', type: 'text', }); } return params; } // --------------------------------------------------------------------------- // Built-in Templates (angepasst an 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 FROM events WHERE timestamp > (strftime('%s', 'now', 'start of day') * 1000) ORDER BY timestamp DESC LIMIT {{limit:100}}`, parameters: [{ name: 'limit', label: 'Limit', defaultValue: '100', type: 'number' }], builtIn: true, }, { id: 'builtin-active-files', name: 'Aktivste Dateien', description: 'Dateien mit den meisten Events', sql: `SELECT source as datei, COUNT(*) as events FROM events WHERE source IS NOT NULL GROUP BY source ORDER BY events DESC LIMIT {{limit:20}}`, parameters: [{ name: 'limit', label: 'Limit', defaultValue: '20', type: 'number' }], builtIn: true, }, { id: 'builtin-session-overview', name: 'Sessions', description: 'Letzte Sessions mit Dauer', 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 FROM sessions ORDER BY start_time DESC LIMIT {{limit:10}}`, parameters: [{ name: 'limit', label: 'Limit', defaultValue: '10', type: 'number' }], builtIn: true, }, { 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 FROM daily_stats GROUP BY date ORDER BY date DESC LIMIT {{limit:14}}`, parameters: [{ name: 'limit', label: 'Tage', defaultValue: '14', type: 'number' }], builtIn: true, }, { id: 'builtin-recent-files', name: 'Zuletzt geaenderte Dateien', description: 'Dateien nach Aenderungsdatum', sql: `SELECT name, extension, datetime(modified/1000, 'unixepoch', 'localtime') as geaendert, folder FROM _files WHERE extension = 'md' ORDER BY modified DESC LIMIT {{limit:20}}`, parameters: [{ name: 'limit', label: 'Limit', defaultValue: '20', type: 'number' }], builtIn: true, }, { id: 'builtin-tag-stats', name: 'Tag-Statistiken', description: 'Notizen pro Tag zaehlen', sql: `SELECT tag, COUNT(DISTINCT path) as notizen FROM _tags GROUP BY tag ORDER BY notizen 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', sql: `SELECT f.path, f.name, datetime(f.modified/1000, 'unixepoch', 'localtime') as geaendert FROM _files f WHERE f.extension = 'md' AND f.path NOT IN (SELECT DISTINCT to_path FROM _links) ORDER BY f.modified DESC`, parameters: [], builtIn: true, }, { 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 FROM _links l WHERE l.to_path NOT IN (SELECT path FROM _files) AND l.link_type = 'link'`, parameters: [], builtIn: true, }, { id: 'builtin-event-types', name: 'Event-Typen', description: 'Verteilung der Event-Typen', sql: `SELECT type as typ, category as kategorie, COUNT(*) as anzahl FROM events GROUP BY type, category ORDER BY anzahl DESC`, parameters: [], builtIn: true, }, ]; // --------------------------------------------------------------------------- // TemplatePickerModal // --------------------------------------------------------------------------- export class TemplatePickerModal extends Modal { private manager: TemplateManager; private onSelect: (sql: string) => void; constructor(plugin: LogfirePlugin, manager: TemplateManager, onSelect: (sql: string) => void) { super(plugin.app); this.manager = manager; this.onSelect = onSelect; } onOpen(): void { const { contentEl } = this; contentEl.addClass('logfire-template-picker'); contentEl.createEl('h2', { text: 'Query-Templates' }); const list = contentEl.createDiv({ cls: 'logfire-template-list' }); for (const tpl of this.manager.getAll()) { const item = list.createDiv({ cls: 'logfire-template-item' }); const header = item.createDiv({ cls: 'logfire-template-header' }); header.createSpan({ cls: 'logfire-template-name', text: tpl.name }); if (tpl.builtIn) { header.createSpan({ cls: 'logfire-template-badge', text: 'Built-in' }); } if (tpl.description) { item.createDiv({ cls: 'logfire-template-desc', text: tpl.description }); } item.createEl('pre', { cls: 'logfire-template-preview', text: tpl.sql.length > 120 ? tpl.sql.slice(0, 120) + '...' : tpl.sql, }); const actions = item.createDiv({ cls: 'logfire-template-actions' }); const useBtn = actions.createEl('button', { text: 'Verwenden', cls: 'mod-cta' }); useBtn.addEventListener('click', () => { if (tpl.parameters.length > 0) { this.close(); new ParameterModal(this.app, tpl, values => { this.onSelect(TemplateManager.applyParameters(tpl.sql, values)); }).open(); } else { this.onSelect(tpl.sql); this.close(); } }); if (!tpl.builtIn) { const delBtn = actions.createEl('button', { text: 'Entfernen' }); delBtn.addEventListener('click', () => { this.manager.delete(tpl.id); item.remove(); new Notice('Template entfernt.'); }); } } } onClose(): void { this.contentEl.empty(); } } // --------------------------------------------------------------------------- // ParameterModal // --------------------------------------------------------------------------- class ParameterModal extends Modal { private template: QueryTemplate; private onSubmit: (values: Record) => void; private values: Record = {}; constructor( app: import('obsidian').App, template: QueryTemplate, onSubmit: (values: Record) => void, ) { super(app); this.template = template; this.onSubmit = onSubmit; for (const p of template.parameters) this.values[p.name] = p.defaultValue; } onOpen(): void { const { contentEl } = this; contentEl.createEl('h3', { text: `Parameter: ${this.template.name}` }); for (const param of this.template.parameters) { new Setting(contentEl).setName(param.label).addText(text => { text.setValue(param.defaultValue).setPlaceholder(param.defaultValue) .onChange(v => { this.values[param.name] = v; }); if (param.type === 'number') text.inputEl.type = 'number'; if (param.type === 'date') text.inputEl.type = 'date'; }); } const btns = contentEl.createDiv({ cls: 'logfire-modal-buttons' }); const runBtn = btns.createEl('button', { text: 'Ausfuehren', cls: 'mod-cta' }); runBtn.addEventListener('click', () => { this.onSubmit(this.values); this.close(); }); const cancelBtn = btns.createEl('button', { text: 'Abbrechen' }); cancelBtn.addEventListener('click', () => this.close()); } onClose(): void { this.contentEl.empty(); } }