From b9732810f9423941d8a6cffd1ccbda4cb2588395 Mon Sep 17 00:00:00 2001 From: tolvitty Date: Thu, 12 Feb 2026 11:24:08 +0100 Subject: [PATCH] Templates: Built-in und Custom Query-Templates mit Parametern TemplateManager mit 9 Built-in Templates fuer Logfire-Schema (Events, Sessions, Stats, Dateien, Tags, verwaiste Notizen, defekte Links). Parameter-Substitution via {{name:default}}. Co-Authored-By: Claude Opus 4.6 --- src/management/templates.ts | 356 ++++++++++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 src/management/templates.ts diff --git a/src/management/templates.ts b/src/management/templates.ts new file mode 100644 index 0000000..af9aa29 --- /dev/null +++ b/src/management/templates.ts @@ -0,0 +1,356 @@ +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(); } +}