diff --git a/src/management/history.ts b/src/management/history.ts new file mode 100644 index 0000000..5e0554a --- /dev/null +++ b/src/management/history.ts @@ -0,0 +1,126 @@ +// --------------------------------------------------------------------------- +// Query History — automatisches Tracking aller ausgefuehrten Queries +// --------------------------------------------------------------------------- + +export interface HistoryEntry { + id: string; + sql: string; + executedAt: string; + executionTimeMs?: number; + rowCount?: number; + isFavorite: boolean; + name?: string; +} + +const STORAGE_KEY = 'logfire-query-history'; +const MAX_ENTRIES = 200; + +export class HistoryManager { + private entries = new Map(); + private listeners = new Set<() => void>(); + + constructor() { + this.load(); + } + + // --------------------------------------------------------------------------- + // Persistence + // --------------------------------------------------------------------------- + + private load(): void { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return; + try { + const arr: HistoryEntry[] = JSON.parse(raw); + for (const e of arr) this.entries.set(e.id, e); + } catch { /* ignore */ } + } + + private save(): void { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify(Array.from(this.entries.values())), + ); + for (const fn of this.listeners) fn(); + } + + onChange(fn: () => void): () => void { + this.listeners.add(fn); + return () => this.listeners.delete(fn); + } + + // --------------------------------------------------------------------------- + // CRUD + // --------------------------------------------------------------------------- + + addEntry(sql: string, executionTimeMs?: number, rowCount?: number): HistoryEntry { + // Deduplizieren: gleiche SQL aktualisiert vorhandenen Eintrag + for (const entry of this.entries.values()) { + if (entry.sql === sql) { + entry.executedAt = new Date().toISOString(); + entry.executionTimeMs = executionTimeMs; + entry.rowCount = rowCount; + this.save(); + return entry; + } + } + + const entry: HistoryEntry = { + id: `h-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + sql, + executedAt: new Date().toISOString(), + executionTimeMs, + rowCount, + isFavorite: false, + }; + this.entries.set(entry.id, entry); + + // Limit: aelteste nicht-favorisierte loeschen + if (this.entries.size > MAX_ENTRIES) { + const sorted = this.getAll(); + for (const e of sorted.slice(MAX_ENTRIES)) { + if (!e.isFavorite) this.entries.delete(e.id); + } + } + + this.save(); + return entry; + } + + getAll(searchTerm?: string): HistoryEntry[] { + let results = Array.from(this.entries.values()); + + if (searchTerm) { + const term = searchTerm.toLowerCase(); + results = results.filter( + e => e.sql.toLowerCase().includes(term) || e.name?.toLowerCase().includes(term), + ); + } + + return results.sort( + (a, b) => new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime(), + ); + } + + toggleFavorite(id: string): void { + const e = this.entries.get(id); + if (e) { e.isFavorite = !e.isFavorite; this.save(); } + } + + setName(id: string, name: string): void { + const e = this.entries.get(id); + if (e) { e.name = name || undefined; this.save(); } + } + + delete(id: string): void { + this.entries.delete(id); + this.save(); + } + + clear(): void { + for (const [id, entry] of this.entries) { + if (!entry.isFavorite) this.entries.delete(id); + } + this.save(); + } +}