Query-History: automatisches Tracking aller Queries

HistoryManager speichert ausgefuehrte Queries mit Metriken
(Laufzeit, Zeilenanzahl), Deduplizierung, max 200 Eintraege,
Favoriten-Schutz vor Loeschung, Suche.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Oelfke 2026-02-12 11:23:54 +01:00
parent 4547e0606e
commit 4b8f1fc814

126
src/management/history.ts Normal file
View file

@ -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<string, HistoryEntry>();
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();
}
}