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:
parent
4547e0606e
commit
4b8f1fc814
1 changed files with 126 additions and 0 deletions
126
src/management/history.ts
Normal file
126
src/management/history.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue