/** * DBBrowserPanel.tsx — S746: Integrated DB browser in-app (Gap 6 IDE Analysis) * * Esplora le table Dexie (IndexedDB) dal device: * - Conteggio record per table + preview ultimi 5 * - Export tabella come JSON (download) * - Clear tabella (con conferma) * - Quota storage totale (navigator.storage.estimate) * * Sprint: S746 — Gap 6 IDE Analysis (Integrated DB — database browser in-app) */ import { useState, useEffect, useCallback, memo } from "react"; import type * as React from "react"; import { X, Database, RefreshCw, Download, Trash2, ChevronDown, ChevronRight, AlertCircle } from "lucide-react"; import { Z_INDEX } from "@/lib/zindex"; import { vfsDb } from "@/lib/vfsDb"; import type { Table } from "dexie"; interface DBBrowserPanelProps { onClose: () => void; } interface TableInfo { name: string; label: string; count: number | null; preview: unknown[]; loading: boolean; open: boolean; error?: string; } // ─── Table definitions ──────────────────────────────────────────────────────── const TABLES: Array<{ name: string; label: string; key: keyof typeof vfsDb }> = [ { name: "files", label: "📁 File VFS", key: "files" }, { name: "messages", label: "💬 Messaggi", key: "messages" }, { name: "conversations",label: "🗂 Conversazioni", key: "conversations" }, { name: "tasks", label: "⚙️ Task agente", key: "tasks" }, { name: "snapshots", label: "📸 Snapshot", key: "snapshots" }, { name: "agentMemory", label: "🧠 Memoria agente", key: "agentMemory" }, { name: "agentContext", label: "📋 Contesto agente", key: "agentContext" }, { name: "skillUsage", label: "🔧 Skill usage", key: "skillUsage" }, { name: "telemetry", label: "📊 Telemetria", key: "telemetry" }, { name: "diagnosticsLog",label: "🔴 Diagnostics log", key: "diagnosticsLog" }, { name: "runtimeEvents",label: "⚡ Runtime events", key: "runtimeEvents" }, { name: "providerUsage",label: "🔑 Provider usage", key: "providerUsage" }, { name: "sessions", label: "📂 Sessioni", key: "sessions" }, { name: "fileBackups", label: "💾 File backups", key: "fileBackups" }, ]; // ─── Typed table accessor (avoids `as any` on dynamic Dexie table lookup) ──── type VfsTable = Table; function getTable(name: string): VfsTable | undefined { const entry = TABLES.find(t => t.name === name); return entry ? (vfsDb[entry.key] as unknown as VfsTable) : undefined; } // ─── Helpers ────────────────────────────────────────────────────────────────── function formatBytes(b: number): string { if (b < 1024) return `${b} B`; if (b < 1_048_576) return `${(b / 1024).toFixed(1)} KB`; return `${(b / 1_048_576).toFixed(2)} MB`; } function previewValue(v: unknown, depth = 0): string { if (v === null || v === undefined) return String(v); if (typeof v === "string") return v.length > 80 ? v.slice(0, 80) + "…" : v; if (typeof v === "number" || typeof v === "boolean") return String(v); if (Array.isArray(v)) return `[${v.length} items]`; if (typeof v === "object" && depth < 2) { const entries = Object.entries(v as Record).slice(0, 3); return "{" + entries.map(([k, val]) => `${k}: ${previewValue(val, depth + 1)}`).join(", ") + (Object.keys(v as object).length > 3 ? ", …" : "") + "}"; } return "[object]"; } async function exportTable(tableName: string): Promise { const table = getTable(tableName); if (!table) return; const data = await table.toArray(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${tableName}-${new Date().toISOString().slice(0,10)}.json`; a.click(); URL.revokeObjectURL(url); } // ─── Stili ──────────────────────────────────────────────────────────────────── const iconBtn = (color = "#6366f1"): React.CSSProperties => ({ all: "unset" as const, cursor: "pointer", color, padding: "4px 6px", borderRadius: 8, display: "flex", alignItems: "center", touchAction: "manipulation", transition: "background 0.15s", }); // ─── Componente principale ───────────────────────────────────────────────────── const DBBrowserPanel = memo(function DBBrowserPanel({ onClose }: DBBrowserPanelProps) { const [tables, setTables] = useState( TABLES.map(t => ({ name: t.name, label: t.label, count: null, preview: [], loading: false, open: false })) ); const [totalCounts, setTotalCounts] = useState>({}); const [quota, setQuota] = useState<{ usedMB: number; quotaMB: number; pct: number } | null>(null); const [loadingAll, setLoadingAll] = useState(false); const [confirmClear, setConfirmClear] = useState(null); // ── Carica conteggi ────────────────────────────────────────────────────── const loadCounts = useCallback(async () => { setLoadingAll(true); const counts: Record = {}; await Promise.all(TABLES.map(async t => { try { const table = getTable(t.name); if (table) counts[t.name] = await table.count(); } catch { counts[t.name] = -1; } })); setTotalCounts(counts); setTables(prev => prev.map(t => ({ ...t, count: counts[t.name] ?? null }))); setLoadingAll(false); // Quota storage if (navigator.storage?.estimate) { try { const { usage = 0, quota: q = 0 } = await navigator.storage.estimate(); setQuota({ usedMB: usage / 1_048_576, quotaMB: q / 1_048_576, pct: q > 0 ? (usage / q) * 100 : 0 }); } catch { /**/ } } }, []); useEffect(() => { void loadCounts(); }, [loadCounts]); // ── Toggle table expansion ───────────────────────────────────────────── const toggleTable = useCallback(async (tableName: string) => { setTables(prev => prev.map(t => { if (t.name !== tableName) return t; if (t.open) return { ...t, open: false }; return { ...t, open: true, loading: true }; })); try { const table = getTable(tableName); const preview = table ? await table.orderBy(":id").reverse().limit(5).toArray() : []; setTables(prev => prev.map(t => t.name === tableName ? { ...t, loading: false, preview } : t)); } catch (e) { setTables(prev => prev.map(t => t.name === tableName ? { ...t, loading: false, error: (e as Error).message } : t)); } }, []); // ── Clear table ────────────────────────────────────────────────────────── const clearTable = useCallback(async (tableName: string) => { try { const table = getTable(tableName); if (table) await table.clear(); setConfirmClear(null); await loadCounts(); } catch (e) { console.error("Clear failed:", e); } }, [loadCounts]); const totalRecords = Object.values(totalCounts).reduce((s, n) => s + (n > 0 ? n : 0), 0); return (
{/* Header */}
Database (IndexedDB) {totalRecords.toLocaleString()} record
{/* Storage quota */} {quota && (
Storage quota 80 ? "#f87171" : "#4a4a78" }}> {formatBytes(quota.usedMB * 1_048_576)} / {formatBytes(quota.quotaMB * 1_048_576)} ({quota.pct.toFixed(1)}%)
80 ? "#f87171" : quota.pct > 60 ? "#fbbf24" : "#6366f1", borderRadius: 2, transition: "width 0.3s" }} />
)} {/* Confirm clear modal */} {confirmClear && (
Eliminare tutti i record di "{confirmClear}"?
)} {/* Tables list */}
{tables.map(t => (
{/* Table header */}
void toggleTable(t.name)} > {t.open ? : } {t.label} {t.count === null ? "…" : t.count === -1 ? "err" : t.count.toLocaleString()}
{/* Expanded records */} {t.open && (
{t.loading &&

⏳ Caricamento…

} {t.error &&

{t.error}

} {!t.loading && t.preview.length === 0 &&

Tabella vuota.

} {t.preview.map((row, i) => (
{typeof row === "object" && row !== null ? Object.entries(row as Record).slice(0, 5).map(([k, v]) => (
{k} {previewValue(v)}
)) : {previewValue(row)} }
))} {t.count !== null && t.count > 5 && (

+ {(t.count - 5).toLocaleString()} altri record — usa Export per vedere tutto

)}
)}
))}
); }); export default DBBrowserPanel;