diff --git a/src/main.ts b/src/main.ts index fbbdcba..cf2ba86 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,6 +21,7 @@ import { HistoryManager } from './management/history'; import { FavoritesManager, SaveFavoriteModal } from './management/favorites'; import { TemplateManager, TemplatePickerModal } from './management/templates'; import { SchemaView, SCHEMA_VIEW_TYPE } from './ui/schema-view'; +import { ProjectionEngine, ProjectionPickerModal } from './projection/projection-engine'; export default class LogfirePlugin extends Plugin { settings!: LogfireSettings; @@ -31,6 +32,7 @@ export default class LogfirePlugin extends Plugin { historyManager!: HistoryManager; favoritesManager!: FavoritesManager; templateManager!: TemplateManager; + projectionEngine!: ProjectionEngine; private fileCollector!: FileCollector; private contentCollector!: ContentCollector; @@ -164,6 +166,10 @@ export default class LogfirePlugin extends Plugin { // Virtual Tables this.virtualTables = new VirtualTableManager(this.app, this.db); this.virtualTables.initialize(); + + // Projection Engine + this.projectionEngine = new ProjectionEngine(this.app, this.db, this.eventBus, this.settings); + this.projectionEngine.start(); }); console.log('[Logfire] Plugin geladen. Session:', this.sessionManager.currentSessionId); @@ -173,6 +179,7 @@ export default class LogfirePlugin extends Plugin { console.log('[Logfire] Entlade Plugin...'); cleanupAllRefreshTimers(); + this.projectionEngine?.destroy(); this.virtualTables?.destroy(); this.statusBar?.destroy(); this.stopTracking(); @@ -382,6 +389,22 @@ export default class LogfirePlugin extends Plugin { }); }, }); + + this.addCommand({ + id: 'run-projection', + name: 'Projektion manuell ausführen', + callback: () => { + new ProjectionPickerModal(this.app, this.projectionEngine).open(); + }, + }); + + this.addCommand({ + id: 'run-all-projections', + name: 'Alle Projektionen ausführen', + callback: () => { + this.projectionEngine.runAllProjections(); + }, + }); } // --------------------------------------------------------------------------- diff --git a/src/projection/formatters.ts b/src/projection/formatters.ts new file mode 100644 index 0000000..85f9eed --- /dev/null +++ b/src/projection/formatters.ts @@ -0,0 +1,180 @@ +import { SectionConfig, BuiltinFormat } from '../types'; +import { toMarkdownTable } from '../viz/table-renderer'; + +// --------------------------------------------------------------------------- +// Section → Markdown +// --------------------------------------------------------------------------- + +export function formatSection(rows: Record[], section: SectionConfig): string { + if (rows.length === 0) return `### ${section.heading}\n\n*Keine Daten.*\n`; + + const heading = `### ${section.heading}\n\n`; + + if (section.format.type === 'custom' && section.format.customTemplate) { + return heading + applyCustomTemplate(rows, section.format.customTemplate) + '\n'; + } + + const fmt = section.format.builtin; + if (!fmt) return heading + formatAsTable(rows) + '\n'; + + switch (fmt.name) { + case 'timeline': + return heading + formatAsTimeline(rows, fmt.showTimestamp, fmt.showPayload) + '\n'; + case 'table': + return heading + formatAsTableWithColumns(rows, fmt.columns) + '\n'; + case 'summary': + return heading + formatAsSummary(rows, fmt.metrics) + '\n'; + case 'metric': + return heading + formatAsMetric(rows, fmt) + '\n'; + case 'heatmap': + return heading + formatAsHeatmap(rows, fmt.labelField, fmt.valueField) + '\n'; + default: + return heading + formatAsTable(rows) + '\n'; + } +} + +// --------------------------------------------------------------------------- +// Timeline +// --------------------------------------------------------------------------- + +function formatAsTimeline(rows: Record[], showTs: boolean, showPayload: boolean): string { + const lines: string[] = []; + for (const row of rows) { + const parts: string[] = []; + if (showTs && row.timestamp != null) { + const ts = typeof row.timestamp === 'number' + ? new Date(row.timestamp).toLocaleTimeString() + : String(row.timestamp); + parts.push(`**${ts}**`); + } + parts.push(String(row.type ?? '')); + if (row.source) parts.push(`\`${row.source}\``); + if (showPayload && row.payload) { + const p = typeof row.payload === 'string' ? row.payload : JSON.stringify(row.payload); + if (p !== '{}') parts.push(`— ${p}`); + } + lines.push(`- ${parts.join(' ')}`); + } + return lines.join('\n'); +} + +// --------------------------------------------------------------------------- +// Table +// --------------------------------------------------------------------------- + +function formatAsTable(rows: Record[]): string { + const keys = Object.keys(rows[0]); + return toMarkdownTable(keys, rows); +} + +function formatAsTableWithColumns( + rows: Record[], + columns: { header: string; value: string; align?: string }[], +): string { + const headers = columns.map(c => c.header); + const mappedRows = rows.map(row => { + const mapped: Record = {}; + for (const col of columns) { + mapped[col.header] = row[col.value] ?? ''; + } + return mapped; + }); + return toMarkdownTable(headers, mappedRows); +} + +// --------------------------------------------------------------------------- +// Summary +// --------------------------------------------------------------------------- + +function formatAsSummary( + rows: Record[], + metrics: { label: string; aggregate: string; field?: string }[], +): string { + const lines: string[] = []; + for (const m of metrics) { + const values = rows.map(r => Number(r[m.field ?? 'count'] ?? 0)); + let result: number; + switch (m.aggregate) { + case 'sum': result = values.reduce((a, b) => a + b, 0); break; + case 'avg': result = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; break; + case 'min': result = values.length > 0 ? Math.min(...values) : 0; break; + case 'max': result = values.length > 0 ? Math.max(...values) : 0; break; + default: result = rows.length; + } + lines.push(`- **${m.label}**: ${result.toLocaleString()}`); + } + return lines.join('\n'); +} + +// --------------------------------------------------------------------------- +// Metric +// --------------------------------------------------------------------------- + +function formatAsMetric( + rows: Record[], + fmt: { aggregate: string; field?: string }, +): string { + const values = rows.map(r => Number(r[fmt.field ?? Object.keys(r).pop()!] ?? 0)); + let result: number; + switch (fmt.aggregate) { + case 'sum': result = values.reduce((a, b) => a + b, 0); break; + case 'avg': result = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; break; + case 'min': result = values.length > 0 ? Math.min(...values) : 0; break; + case 'max': result = values.length > 0 ? Math.max(...values) : 0; break; + default: result = rows.length; + } + return `**${result.toLocaleString()}**`; +} + +// --------------------------------------------------------------------------- +// Heatmap +// --------------------------------------------------------------------------- + +function formatAsHeatmap(rows: Record[], labelField: string, valueField: string): string { + const maxVal = Math.max(...rows.map(r => Number(r[valueField] ?? 0)), 1); + const lines: string[] = []; + for (const row of rows) { + const label = String(row[labelField] ?? ''); + const val = Number(row[valueField] ?? 0); + const barLen = Math.round((val / maxVal) * 30); + const bar = '\u2588'.repeat(barLen); + lines.push(`\`${label.padEnd(12)}\` ${bar} ${val}`); + } + return lines.join('\n'); +} + +// --------------------------------------------------------------------------- +// Custom template +// --------------------------------------------------------------------------- + +function applyCustomTemplate(rows: Record[], template: string): string { + return template.replace(/\{\{rows\}\}/g, JSON.stringify(rows, null, 2)); +} + +// --------------------------------------------------------------------------- +// Frontmatter +// --------------------------------------------------------------------------- + +export function buildFrontmatter(data: Record): string { + if (Object.keys(data).length === 0) return ''; + const lines = ['---']; + for (const [key, value] of Object.entries(data)) { + lines.push(`${key}: ${value}`); + } + lines.push('---', ''); + return lines.join('\n'); +} + +// --------------------------------------------------------------------------- +// Date placeholders +// --------------------------------------------------------------------------- + +export function resolvePlaceholders(pattern: string, context: { date?: Date; sessionId?: string }): string { + const d = context.date ?? new Date(); + return pattern + .replace(/\{\{date\}\}/g, d.toISOString().substring(0, 10)) + .replace(/\{\{year\}\}/g, String(d.getFullYear())) + .replace(/\{\{month\}\}/g, String(d.getMonth() + 1).padStart(2, '0')) + .replace(/\{\{day\}\}/g, String(d.getDate()).padStart(2, '0')) + .replace(/\{\{sessionId\}\}/g, context.sessionId ?? ''); +} diff --git a/src/projection/presets/daily-log.ts b/src/projection/presets/daily-log.ts new file mode 100644 index 0000000..7b933b8 --- /dev/null +++ b/src/projection/presets/daily-log.ts @@ -0,0 +1,102 @@ +import { ProjectionTemplate } from '../../types'; + +export const dailyLogTemplate: ProjectionTemplate = { + id: 'builtin:daily-log', + name: 'Tagesprotokoll', + description: 'Sessions, aktive Dateien, Events und Zeitleiste des Tages.', + enabled: false, + trigger: { type: 'manual' }, + output: { + folder: '', + filePattern: '{{date}}-daily-log.md', + mode: 'overwrite', + frontmatter: { + type: 'logfire/daily-log', + date: '{{date}}', + }, + }, + sections: [ + { + id: 'sessions', + heading: 'Sessions', + type: 'table', + query: { + timeRange: { type: 'relative', value: 'today' }, + eventTypes: ['system:session-start', 'system:session-end'], + }, + format: { + type: 'builtin', + builtin: { + name: 'table', + columns: [ + { header: 'Session', value: 'session' }, + { header: 'Typ', value: 'type' }, + { header: 'Zeit', value: 'timestamp' }, + ], + }, + }, + enabled: true, + }, + { + id: 'active-files', + heading: 'Aktive Dateien', + type: 'table', + query: { + timeRange: { type: 'relative', value: 'today' }, + groupBy: 'file', + orderBy: 'count', + orderDirection: 'desc', + limit: 20, + }, + format: { + type: 'builtin', + builtin: { + name: 'table', + columns: [ + { header: 'Datei', value: 'group' }, + { header: 'Events', value: 'count' }, + { header: 'Woerter+', value: 'words_added' }, + { header: 'Woerter-', value: 'words_removed' }, + ], + }, + }, + enabled: true, + }, + { + id: 'event-summary', + heading: 'Event-Uebersicht', + type: 'table', + query: { + timeRange: { type: 'relative', value: 'today' }, + groupBy: 'type', + orderBy: 'count', + orderDirection: 'desc', + }, + format: { + type: 'builtin', + builtin: { + name: 'table', + columns: [ + { header: 'Typ', value: 'group' }, + { header: 'Anzahl', value: 'count' }, + ], + }, + }, + enabled: true, + }, + { + id: 'timeline', + heading: 'Zeitleiste', + type: 'timeline', + query: { + timeRange: { type: 'relative', value: 'today' }, + limit: 100, + }, + format: { + type: 'builtin', + builtin: { name: 'timeline', showTimestamp: true, showPayload: false }, + }, + enabled: true, + }, + ], +}; diff --git a/src/projection/presets/session-log.ts b/src/projection/presets/session-log.ts new file mode 100644 index 0000000..db76218 --- /dev/null +++ b/src/projection/presets/session-log.ts @@ -0,0 +1,85 @@ +import { ProjectionTemplate } from '../../types'; + +export const sessionLogTemplate: ProjectionTemplate = { + id: 'builtin:session-log', + name: 'Session-Protokoll', + description: 'Einzelne Session: Dauer, bearbeitete Dateien, ausgefuehrte Befehle.', + enabled: false, + trigger: { type: 'on-session-end' }, + output: { + folder: '', + filePattern: '{{date}}-session-{{sessionId}}.md', + mode: 'overwrite', + frontmatter: { + type: 'logfire/session-log', + date: '{{date}}', + session: '{{sessionId}}', + }, + }, + sections: [ + { + id: 'overview', + heading: 'Session-Uebersicht', + type: 'summary', + query: { + timeRange: { type: 'session' }, + }, + format: { + type: 'builtin', + builtin: { + name: 'summary', + metrics: [ + { label: 'Events gesamt', aggregate: 'count' }, + ], + }, + }, + enabled: true, + }, + { + id: 'files', + heading: 'Bearbeitete Dateien', + type: 'table', + query: { + timeRange: { type: 'session' }, + groupBy: 'file', + orderBy: 'count', + orderDirection: 'desc', + limit: 20, + }, + format: { + type: 'builtin', + builtin: { + name: 'table', + columns: [ + { header: 'Datei', value: 'group' }, + { header: 'Events', value: 'count' }, + { header: 'Woerter+', value: 'words_added' }, + { header: 'Woerter-', value: 'words_removed' }, + ], + }, + }, + enabled: true, + }, + { + id: 'commands', + heading: 'Ausgefuehrte Befehle', + type: 'table', + query: { + timeRange: { type: 'session' }, + eventTypes: ['plugin:command-executed'], + limit: 50, + }, + format: { + type: 'builtin', + builtin: { + name: 'table', + columns: [ + { header: 'Zeit', value: 'timestamp' }, + { header: 'Befehl', value: 'source' }, + ], + }, + }, + enabled: true, + }, + ], +}; diff --git a/src/projection/presets/weekly-digest.ts b/src/projection/presets/weekly-digest.ts new file mode 100644 index 0000000..9fdac2a --- /dev/null +++ b/src/projection/presets/weekly-digest.ts @@ -0,0 +1,105 @@ +import { ProjectionTemplate } from '../../types'; + +export const weeklyDigestTemplate: ProjectionTemplate = { + id: 'builtin:weekly-digest', + name: 'Wochen-Digest', + description: 'Wochenuebersicht: Tages-Summary, Top-Dateien, Schreib-Statistiken, Heatmap.', + enabled: false, + trigger: { type: 'manual' }, + output: { + folder: '', + filePattern: '{{date}}-weekly-digest.md', + mode: 'overwrite', + frontmatter: { + type: 'logfire/weekly-digest', + date: '{{date}}', + }, + }, + sections: [ + { + id: 'daily-summary', + heading: 'Tages-Uebersicht', + type: 'table', + query: { + timeRange: { type: 'relative', value: 'last-7-days' }, + groupBy: 'day', + orderBy: 'timestamp', + orderDirection: 'asc', + }, + format: { + type: 'builtin', + builtin: { + name: 'table', + columns: [ + { header: 'Tag', value: 'group' }, + { header: 'Events', value: 'count' }, + { header: 'Woerter+', value: 'words_added' }, + { header: 'Woerter-', value: 'words_removed' }, + ], + }, + }, + enabled: true, + }, + { + id: 'top-files', + heading: 'Top-Dateien', + type: 'table', + query: { + timeRange: { type: 'relative', value: 'last-7-days' }, + groupBy: 'file', + orderBy: 'count', + orderDirection: 'desc', + limit: 15, + }, + format: { + type: 'builtin', + builtin: { + name: 'table', + columns: [ + { header: 'Datei', value: 'group' }, + { header: 'Events', value: 'count' }, + { header: 'Woerter+', value: 'words_added' }, + ], + }, + }, + enabled: true, + }, + { + id: 'write-stats', + heading: 'Schreib-Statistiken', + type: 'summary', + query: { + timeRange: { type: 'relative', value: 'last-7-days' }, + groupBy: 'day', + }, + format: { + type: 'builtin', + 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' }, + ], + }, + }, + enabled: true, + }, + { + id: 'activity-heatmap', + heading: 'Aktivitaets-Heatmap', + type: 'chart-data', + query: { + timeRange: { type: 'relative', value: 'last-7-days' }, + groupBy: 'hour', + orderBy: 'timestamp', + orderDirection: 'asc', + }, + format: { + type: 'builtin', + builtin: { name: 'heatmap', labelField: 'group', valueField: 'count' }, + }, + enabled: true, + }, + ], +}; diff --git a/src/projection/projection-engine.ts b/src/projection/projection-engine.ts new file mode 100644 index 0000000..01a866a --- /dev/null +++ b/src/projection/projection-engine.ts @@ -0,0 +1,238 @@ +import { App, Modal, Notice } from 'obsidian'; +import { ProjectionTemplate, LogfireSettings } from '../types'; +import { DatabaseManager } from '../core/database'; +import { EventBus } from '../core/event-bus'; +import { buildQuery } from '../core/query-builder'; +import { ProjectionTemplateRegistry } from './template-registry'; +import { formatSection, buildFrontmatter, resolvePlaceholders } from './formatters'; + +export class ProjectionEngine { + private registry: ProjectionTemplateRegistry; + private schedulerTimer: ReturnType | null = null; + private sessionEndUnsub: (() => void) | null = null; + private lastDailyRun = ''; + + constructor( + private app: App, + private db: DatabaseManager, + private eventBus: EventBus, + private settings: LogfireSettings, + ) { + this.registry = new ProjectionTemplateRegistry(settings.projections.templates); + } + + getRegistry(): ProjectionTemplateRegistry { + return this.registry; + } + + // --------------------------------------------------------------------------- + // Lifecycle + // --------------------------------------------------------------------------- + + start(): void { + if (!this.settings.projections.enabled) return; + + // Session-End-Listener fuer 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'); + for (const t of templates) { + this.runProjection(t, { sessionId: event.session }); + } + }); + } + + // Scheduler fuer daily + weekly (prueft alle 60s) + this.schedulerTimer = setInterval(() => this.checkSchedule(), 60_000); + } + + destroy(): void { + if (this.schedulerTimer !== null) { + clearInterval(this.schedulerTimer); + this.schedulerTimer = null; + } + this.sessionEndUnsub?.(); + this.sessionEndUnsub = null; + } + + // --------------------------------------------------------------------------- + // Schedule check + // --------------------------------------------------------------------------- + + private checkSchedule(): void { + const now = new Date(); + const todayStr = now.toISOString().substring(0, 10); + const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; + + // Daily Log + if ( + this.settings.projections.dailyLog.enabled && + currentTime === this.settings.projections.dailyLog.time && + this.lastDailyRun !== todayStr + ) { + this.lastDailyRun = todayStr; + const template = this.registry.getById('builtin:daily-log'); + if (template) this.runProjection(template); + } + + // Weekly Digest (dayOfWeek: 0=So, 1=Mo, ...) + if ( + this.settings.projections.weeklyDigest.enabled && + now.getDay() === this.settings.projections.weeklyDigest.dayOfWeek && + currentTime === '08:00' && + this.lastDailyRun !== `week-${todayStr}` + ) { + this.lastDailyRun = `week-${todayStr}`; + const template = this.registry.getById('builtin:weekly-digest'); + if (template) this.runProjection(template); + } + } + + // --------------------------------------------------------------------------- + // Run projection + // --------------------------------------------------------------------------- + + async runProjection( + template: ProjectionTemplate, + context?: { sessionId?: string }, + ): Promise { + try { + const sections: string[] = []; + + // Frontmatter + const fmData: Record = {}; + for (const [key, value] of Object.entries(template.output.frontmatter)) { + fmData[key] = resolvePlaceholders(value, { date: new Date(), sessionId: context?.sessionId }); + } + sections.push(buildFrontmatter(fmData)); + + // Title + sections.push(`# ${template.name}\n`); + + // Sections + for (const section of template.sections) { + if (!section.enabled) continue; + + const queryConfig = { ...section.query }; + + // Session-Kontext einsetzen + if (queryConfig.timeRange.type === 'session' && context?.sessionId) { + queryConfig.timeRange = { ...queryConfig.timeRange, sessionId: context.sessionId }; + } + + const { sql, params } = buildQuery(queryConfig); + const rows = this.db.queryReadOnly(sql, params) as Record[]; + sections.push(formatSection(rows, section)); + } + + const content = sections.join('\n'); + const outputFolder = template.output.folder || this.settings.projections.outputFolder; + const fileName = resolvePlaceholders(template.output.filePattern, { + date: new Date(), + sessionId: context?.sessionId, + }); + const filePath = `${outputFolder}/${fileName}`; + + // Ordner erstellen falls noetig + await this.ensureFolder(outputFolder); + + // Datei schreiben + const existing = this.app.vault.getAbstractFileByPath(filePath); + if (existing) { + if (template.output.mode === 'append') { + const current = await this.app.vault.read(existing as any); + await this.app.vault.modify(existing as any, current + '\n' + content); + } else { + await this.app.vault.modify(existing as any, content); + } + } else { + await this.app.vault.create(filePath, content); + } + + return filePath; + } catch (err) { + console.error('[Logfire] Projektion fehlgeschlagen:', err); + new Notice(`Logfire: Projektion "${template.name}" fehlgeschlagen.`); + return null; + } + } + + async runAllProjections(): Promise { + const templates = this.registry.getEnabled(); + let count = 0; + for (const t of templates) { + if (t.trigger.type !== 'on-session-end') { + const result = await this.runProjection(t); + if (result) count++; + } + } + new Notice(`Logfire: ${count} Projektion(en) ausgefuehrt.`); + } + + private async ensureFolder(path: string): Promise { + const parts = path.split('/'); + let current = ''; + for (const part of parts) { + current = current ? `${current}/${part}` : part; + if (!this.app.vault.getAbstractFileByPath(current)) { + await this.app.vault.createFolder(current); + } + } + } +} + +// --------------------------------------------------------------------------- +// Projection Picker Modal +// --------------------------------------------------------------------------- + +export class ProjectionPickerModal extends Modal { + constructor( + app: App, + private engine: ProjectionEngine, + ) { + super(app); + } + + onOpen(): void { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('logfire-projection-picker'); + + contentEl.createEl('h2', { text: 'Projektion ausfuehren' }); + + const list = contentEl.createDiv({ cls: 'logfire-template-list' }); + const templates = this.engine.getRegistry().getAll(); + + for (const template of templates) { + const item = list.createDiv({ cls: 'logfire-template-item' }); + + const header = item.createDiv({ cls: 'logfire-template-header' }); + header.createEl('span', { text: template.name, cls: 'logfire-template-name' }); + + if (template.id.startsWith('builtin:')) { + header.createEl('span', { text: 'BUILTIN', cls: 'logfire-template-badge' }); + } + + item.createEl('p', { text: template.description, cls: 'logfire-template-desc' }); + + const triggerInfo = item.createEl('p', { + text: `Trigger: ${template.trigger.type} | Output: ${template.output.filePattern}`, + cls: 'logfire-template-desc', + }); + + const actions = item.createDiv({ cls: 'logfire-template-actions' }); + const runBtn = actions.createEl('button', { text: 'Ausfuehren', cls: 'mod-cta' }); + runBtn.addEventListener('click', async () => { + const path = await this.engine.runProjection(template); + if (path) { + new Notice(`Projektion geschrieben: ${path}`); + } + this.close(); + }); + } + } + + onClose(): void { + this.contentEl.empty(); + } +} diff --git a/src/projection/template-registry.ts b/src/projection/template-registry.ts new file mode 100644 index 0000000..8f0a9a5 --- /dev/null +++ b/src/projection/template-registry.ts @@ -0,0 +1,54 @@ +import { ProjectionTemplate } from '../types'; +import { dailyLogTemplate } from './presets/daily-log'; +import { sessionLogTemplate } from './presets/session-log'; +import { weeklyDigestTemplate } from './presets/weekly-digest'; + +const BUILTIN_TEMPLATES: ProjectionTemplate[] = [ + dailyLogTemplate, + sessionLogTemplate, + weeklyDigestTemplate, +]; + +export class ProjectionTemplateRegistry { + private customTemplates: ProjectionTemplate[] = []; + + constructor(customTemplates: ProjectionTemplate[] = []) { + this.customTemplates = customTemplates; + } + + getAll(): ProjectionTemplate[] { + return [...BUILTIN_TEMPLATES, ...this.customTemplates]; + } + + getBuiltin(): ProjectionTemplate[] { + return BUILTIN_TEMPLATES; + } + + getCustom(): ProjectionTemplate[] { + return this.customTemplates; + } + + getById(id: string): ProjectionTemplate | undefined { + return this.getAll().find(t => t.id === id); + } + + getEnabled(): ProjectionTemplate[] { + return this.getAll().filter(t => t.enabled); + } + + getByTrigger(type: ProjectionTemplate['trigger']['type']): ProjectionTemplate[] { + return this.getEnabled().filter(t => t.trigger.type === type); + } + + addCustom(template: ProjectionTemplate): void { + this.customTemplates.push(template); + } + + removeCustom(id: string): void { + this.customTemplates = this.customTemplates.filter(t => t.id !== id); + } + + updateCustomTemplates(templates: ProjectionTemplate[]): void { + this.customTemplates = templates; + } +} diff --git a/src/types.ts b/src/types.ts index 9801d7d..375aa8a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -180,7 +180,8 @@ export type BuiltinFormat = | { name: 'timeline'; showTimestamp: boolean; showPayload: boolean } | { name: 'table'; columns: ColumnDef[] } | { name: 'summary'; metrics: MetricDef[] } - | { name: 'metric'; aggregate: 'count' | 'sum' | 'avg' | 'min' | 'max'; field?: string }; + | { name: 'metric'; aggregate: 'count' | 'sum' | 'avg' | 'min' | 'max'; field?: string } + | { name: 'heatmap'; labelField: string; valueField: string }; export interface ColumnDef { header: string; @@ -219,6 +220,11 @@ export interface LogfireSettings { }; projections: { + enabled: boolean; + outputFolder: string; + dailyLog: { enabled: boolean; time: string }; + sessionLog: { enabled: boolean }; + weeklyDigest: { enabled: boolean; dayOfWeek: number }; templates: ProjectionTemplate[]; }; @@ -265,6 +271,11 @@ export const DEFAULT_SETTINGS: LogfireSettings = { }, projections: { + enabled: false, + outputFolder: 'Logfire', + dailyLog: { enabled: false, time: '23:00' }, + sessionLog: { enabled: false }, + weeklyDigest: { enabled: false, dayOfWeek: 0 }, templates: [], }, diff --git a/src/ui/settings-tab.ts b/src/ui/settings-tab.ts index b97ccf3..d1a7022 100644 --- a/src/ui/settings-tab.ts +++ b/src/ui/settings-tab.ts @@ -125,6 +125,79 @@ export class LogfireSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); + // ----- Projections ----- + containerEl.createEl('h2', { text: 'Projektionen' }); + + new Setting(containerEl) + .setName('Projektionen aktiviert') + .setDesc('Automatische Markdown-Reports aus Event-Daten.') + .addToggle(t => t + .setValue(this.plugin.settings.projections.enabled) + .onChange(async (v) => { + this.plugin.settings.projections.enabled = v; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Output-Ordner') + .setDesc('Ordner für generierte Projektions-Dateien.') + .addText(t => t + .setPlaceholder('Logfire') + .setValue(this.plugin.settings.projections.outputFolder) + .onChange(async (v) => { + this.plugin.settings.projections.outputFolder = v || 'Logfire'; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Tagesprotokoll') + .setDesc('Automatisches Tagesprotokoll generieren.') + .addToggle(t => t + .setValue(this.plugin.settings.projections.dailyLog.enabled) + .onChange(async (v) => { + this.plugin.settings.projections.dailyLog.enabled = v; + await this.plugin.saveSettings(); + })) + .addText(t => t + .setPlaceholder('23:00') + .setValue(this.plugin.settings.projections.dailyLog.time) + .onChange(async (v) => { + if (/^\d{2}:\d{2}$/.test(v)) { + this.plugin.settings.projections.dailyLog.time = v; + await this.plugin.saveSettings(); + } + })); + + new Setting(containerEl) + .setName('Session-Protokoll') + .setDesc('Protokoll bei Session-Ende generieren.') + .addToggle(t => t + .setValue(this.plugin.settings.projections.sessionLog.enabled) + .onChange(async (v) => { + this.plugin.settings.projections.sessionLog.enabled = v; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Wochen-Digest') + .setDesc('Wöchentliche Zusammenfassung generieren.') + .addToggle(t => t + .setValue(this.plugin.settings.projections.weeklyDigest.enabled) + .onChange(async (v) => { + this.plugin.settings.projections.weeklyDigest.enabled = v; + await this.plugin.saveSettings(); + })) + .addDropdown(d => d + .addOptions({ + '0': 'Sonntag', '1': 'Montag', '2': 'Dienstag', '3': 'Mittwoch', + '4': 'Donnerstag', '5': 'Freitag', '6': 'Samstag', + }) + .setValue(String(this.plugin.settings.projections.weeklyDigest.dayOfWeek)) + .onChange(async (v) => { + this.plugin.settings.projections.weeklyDigest.dayOfWeek = parseInt(v, 10); + await this.plugin.saveSettings(); + })); + // ----- Advanced (collapsible) ----- const advancedHeader = containerEl.createEl('details'); advancedHeader.createEl('summary', { text: 'Erweiterte Einstellungen' }) diff --git a/styles.css b/styles.css index bdae9eb..241b06d 100644 --- a/styles.css +++ b/styles.css @@ -1197,3 +1197,16 @@ max-height: 120px; overflow: auto; } + +/* ═══════════════════════════════════════════════════════════════════════════ + Projection Picker Modal + ═══════════════════════════════════════════════════════════════════════════ */ + +.logfire-projection-picker h2 { + font-family: var(--font-monospace); + font-size: 16px; + font-weight: 700; + letter-spacing: -0.01em; + margin-bottom: 10px; + color: var(--text-normal); +}