obsidian-logfire/src/projection/projection-engine.ts
tolvitty 3c8c22ee07 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>
2026-02-12 12:17:24 +01:00

238 lines
7.9 KiB
TypeScript

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<typeof setInterval> | 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 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');
for (const t of templates) {
this.runProjection(t, { sessionId: event.session });
}
});
}
// Scheduler for daily + weekly (checks every 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=Sun, 1=Mon, ...)
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<string | null> {
try {
const sections: string[] = [];
// Frontmatter
const fmData: Record<string, string> = {};
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 };
// Inject session context
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<string, unknown>[];
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}`;
// Create folder if needed
await this.ensureFolder(outputFolder);
// Write file
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] Projection failed:', err);
new Notice(`Logfire: Projection "${template.name}" failed.`);
return null;
}
}
async runAllProjections(): Promise<void> {
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} projection(s) executed.`);
}
private async ensureFolder(path: string): Promise<void> {
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: 'Run Projection' });
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: 'Execute', cls: 'mod-cta' });
runBtn.addEventListener('click', async () => {
const path = await this.engine.runProjection(template);
if (path) {
new Notice(`Projection written: ${path}`);
}
this.close();
});
}
}
onClose(): void {
this.contentEl.empty();
}
}