/** * autoFix.ts — Motore di auto-fix diagnostico * * Mappa ogni tipo di errore (DiagType) a una lista di azioni correttive * che possono essere applicate con un solo tap. * * Ogni azione: * - Ha un label, descrizione e indicatore di pericolosità * - Esegue la correzione in modo safe (try/catch, non blocca la UI) * - Ritorna un FixResult con esito e dettaglio leggibile * - Viene tracciata nello storico (in-memoria + sessionStorage) * * Fix disponibili: * clearAgentStorage — rimuove chiavi agente da localStorage * compactDatabase — pruna messaggi/eventi/telemetry vecchi * pruneAggressivo — libera spazio aggressivamente (quota critica) * resetServiceWorker — ri-registra il service worker * testNetwork — verifica connettività al sito live * clearDiagLog — svuota il log diagnostico * reloadApp — ricarica la pagina * resetProviderState — resetta lo stato di exhausted dei provider * clearTasksStuck — cancella task bloccati da > 10 minuti * clearCorruptedLS — svuota completamente localStorage (emergenza) */ import type { DiagType } from "./autoDiagnostics"; import { makeTimedSignal } from "./agentLoop/networkConstants"; // iOS-safe timeout // ─── Tipi (estratti in autoFixTypes.ts — S550 — re-esportati per backward compat) ────────── import type { FixResult, FixHistoryEntry, FixAction, HealthCheck } from "./autoFixTypes"; export type { FixResult, FixHistoryEntry, FixAction, HealthCheck } from "./autoFixTypes"; // ─── Storico fix ────────────────────────────────────────────────────────────── const HISTORY_KEY = "agente:fix-history"; const _history: FixHistoryEntry[] = []; function _loadHistory(): void { try { const raw = sessionStorage.getItem(HISTORY_KEY); if (raw) _history.push(...(JSON.parse(raw) as FixHistoryEntry[])); } catch { /* */ } } function _saveHistory(): void { try { sessionStorage.setItem(HISTORY_KEY, JSON.stringify(_history.slice(-50))); } catch { /* */ } } _loadHistory(); function _record(fixId: string, fixLabel: string, result: FixResult): void { _history.push({ at: Date.now(), fixId, fixLabel, result }); if (_history.length > 100) _history.splice(0, _history.length - 100); _saveHistory(); } export function getFixHistory(): FixHistoryEntry[] { return [..._history].reverse(); } export function clearFixHistory(): void { _history.length = 0; _saveHistory(); } // ─── Implementazioni dei fix ────────────────────────────────────────────────── async function clearAgentStorage(): Promise { try { const keys = Object.keys(localStorage).filter(k => k.startsWith("agente") || k.startsWith("groq-") || k.startsWith("together-") || k.startsWith("openrouter-") || k.startsWith("hf-") || k.startsWith("gemini-") ); const tokenKeys = keys.filter(k => k.includes("token") || k.includes("key") || k.includes("api") ); const safeKeys = keys.filter(k => !tokenKeys.includes(k)); safeKeys.forEach(k => localStorage.removeItem(k)); return { ok: true, message: `Rimosso ${safeKeys.length} chiavi localStorage (token preservati)`, detail: safeKeys.slice(0, 5).join(", ") + (safeKeys.length > 5 ? "..." : ""), }; } catch(e) { return { ok: false, message: "Errore pulizia localStorage", detail: String(e) }; } } async function compactDatabase(): Promise { try { const { messagesDb, tasksDb, telemetryDb } = await import("./vfsDb"); const [msgs, tasks, tel] = await Promise.all([ messagesDb.pruneOlderThan(7), tasksDb.pruneOlderThan(3), telemetryDb.pruneOlderThan(7), ]); return { ok: true, message: `Compattato: ${msgs + tasks + tel} record eliminati`, detail: `messaggi >7gg: ${msgs} | task >3gg: ${tasks} | telemetry >7gg: ${tel}`, }; } catch(e) { return { ok: false, message: "Errore compattazione DB", detail: String(e) }; } } async function pruneAggressivo(): Promise { try { const { messagesDb, tasksDb, telemetryDb, vfsDb } = await import("./vfsDb"); const [msgs, tasks, tel] = await Promise.all([ messagesDb.pruneOlderThan(1), tasksDb.pruneOlderThan(1), telemetryDb.pruneOlderThan(1), ]); await vfsDb.diagnosticsLog.clear(); await vfsDb.runtimeEvents.where("at").below(Date.now() - 86_400_000).delete(); return { ok: true, message: `Liberato spazio: ${msgs + tasks + tel} record eliminati (aggressivo)`, detail: `msgs: ${msgs} | tasks: ${tasks} | tel: ${tel} | diag: svuotato`, }; } catch(e) { return { ok: false, message: "Errore pruning aggressivo", detail: String(e) }; } } async function resetServiceWorker(): Promise { try { if (!("serviceWorker" in navigator)) { return { ok: false, message: "Service Worker non supportato su questo browser" }; } const regs = await navigator.serviceWorker.getRegistrations(); for (const r of regs) await r.unregister(); setTimeout(() => window.location.reload(), 1500); return { ok: true, message: `${regs.length} Service Worker rimossi — ricarico in 1.5s`, detail: regs.map(r => r.scope).join(", "), }; } catch(e) { return { ok: false, message: "Errore reset SW", detail: String(e) }; } } async function testNetwork(): Promise { const url = "https://ai-agente-ai.vercel.app/"; try { const start = Date.now(); const r = await fetch(url, { method: "HEAD", cache: "no-store", signal: makeTimedSignal(8000) }); const ms = Date.now() - start; return { ok: r.ok, message: r.ok ? `Sito raggiungibile in ${ms}ms` : `HTTP ${r.status} — sito con problemi`, detail: `${url} → ${r.status} ${r.statusText}`, }; } catch(e) { const msg = e instanceof Error ? e.message : String(e); return { ok: false, message: "Sito non raggiungibile", detail: msg }; } } async function clearDiagLog(): Promise { try { const { autoDiag } = await import("./autoDiagnostics"); autoDiag.clear(); return { ok: true, message: "Log diagnostico svuotato" }; } catch(e) { return { ok: false, message: "Errore pulizia diag log", detail: String(e) }; } } async function reloadApp(): Promise { window.location.reload(); return { ok: true, message: "Ricaricando la pagina..." }; } async function resetProviderState(): Promise { try { const agente = Object.keys(localStorage).filter(k => k.includes("exhausted") || k.includes("fallback") || k.includes("provider-state") ); agente.forEach(k => localStorage.removeItem(k)); return { ok: true, message: `Stato provider resettato (${agente.length} chiavi rimossi)`, detail: agente.join(", ") || "nessuna chiave provider-state trovata", }; } catch(e) { return { ok: false, message: "Errore reset provider", detail: String(e) }; } } async function clearTasksStuck(): Promise { try { const { vfsDb } = await import("./vfsDb"); const tenMinsAgo = Date.now() - 10 * 60 * 1000; const stuckKeys = await vfsDb.tasks .where("status").equals("running") .filter(t => t.startedAt < tenMinsAgo) .primaryKeys(); await vfsDb.tasks.bulkDelete(stuckKeys as string[]); return { ok: true, message: `${stuckKeys.length} task bloccati rimossi`, }; } catch(e) { return { ok: false, message: "Errore rimozione task bloccati", detail: String(e) }; } } async function verifyVercelBuild(): Promise { const url = "https://ai-agente-ai.vercel.app/"; try { const r = await fetch(url, { cache: "no-store", signal: makeTimedSignal(9000) }); const html = await r.text(); const hasBundle = /assets\/(index|main)[^"]*\.js/.test(html); const scriptCount = (html.match(/]+src/g) ?? []).length; const ok = r.ok && hasBundle; return { ok, message: ok ? `Build ok — ${scriptCount} script caricati` : `Build incompleto — bundle JS non trovato nella pagina`, detail: `HTTP ${r.status} | bundle: ${hasBundle} | script tags: ${scriptCount}`, }; } catch(e) { return { ok: false, message: "Impossibile verificare il build Vercel", detail: e instanceof Error ? e.message : String(e) }; } } async function clearModuleCache(): Promise { try { if (!("caches" in window)) { return { ok: false, message: "Cache API non disponibile su questo browser" }; } const keys = await caches.keys(); const toClear = keys.filter(k => /vite|module|workbox|precache/i.test(k)); for (const k of toClear) await caches.delete(k); return { ok: true, message: `Cache moduli rimossa (${toClear.length} cache svuotate)`, detail: toClear.join(", ") || "nessuna cache Vite/Workbox trovata", }; } catch(e) { return { ok: false, message: "Errore pulizia cache moduli", detail: e instanceof Error ? e.message : String(e) }; } } async function clearCorruptedLS(): Promise { try { const total = localStorage.length; const saved: Record = {}; const preserve = ["groq-token", "together-token", "openrouter-token", "hf-token", "gemini-token", "openai-token"]; preserve.forEach(k => { const v = localStorage.getItem(k); if (v) saved[k] = v; }); localStorage.clear(); Object.entries(saved).forEach(([k, v]) => localStorage.setItem(k, v)); return { ok: true, message: `localStorage svuotato completamente (${total} chiavi, token preservati)`, detail: `Token preservati: ${Object.keys(saved).join(", ") || "nessuno"}`, }; } catch(e) { return { ok: false, message: "Errore pulizia localStorage", detail: String(e) }; } } // ─── Registro azioni ────────────────────────────────────────────────────────── export const FIX_ACTIONS: FixAction[] = [ { id: "test-network", label: "Verifica connessione", description: "Testa la raggiungibilità del sito live su Vercel", applyTo: ["network-error", "promise-rejection", "js-error"], patterns: [/fetch|network|failed to fetch|connection|cors/i], dangerous: false, run: testNetwork, }, { id: "compact-db", label: "Compatta database", description: "Elimina messaggi, task e telemetry più vecchi di 7 giorni", applyTo: ["console-error", "js-error"], patterns: [/quota|storage|indexeddb|dexie/i], dangerous: false, run: compactDatabase, }, { id: "prune-aggressivo", label: "Libera spazio (aggressivo)", description: "Elimina tutto il possibile: dati >1 giorno, diag log, eventi runtime", applyTo: ["console-error", "js-error"], patterns: [/quota|storage|full|exceeded/i], dangerous: true, run: pruneAggressivo, }, { id: "clear-agent-storage", label: "Svuota cache locale", description: "Rimuove chiavi agente da localStorage (token preservati)", applyTo: ["react-crash", "js-error", "console-error"], dangerous: false, run: clearAgentStorage, }, { id: "reset-provider", label: "Reset provider", description: "Pulisce lo stato di fallback/exhausted dei provider AI", applyTo: ["promise-rejection", "js-error"], patterns: [/provider|groq|together|openrouter|exhausted|fallback/i], dangerous: false, run: resetProviderState, }, { id: "clear-stuck-tasks", label: "Rimuovi task bloccati", description: "Cancella i task in stato 'running' da più di 10 minuti", applyTo: ["promise-rejection", "js-error"], patterns: [/task|timeout|abort|stuck/i], dangerous: false, run: clearTasksStuck, }, { id: "reset-service-worker", label: "Reset Service Worker", description: "Disinstalla i SW registrati e ricarica la pagina", applyTo: ["js-error", "network-error"], patterns: [/service.worker|sw\.js|cache|install/i], dangerous: true, run: resetServiceWorker, }, { id: "clear-diag-log", label: "Svuota log diagnostico", description: "Rimuove tutte le voci dal log diagnostico", applyTo: "all", dangerous: false, run: clearDiagLog, }, { id: "clear-ls-full", label: "Reset localStorage completo", description: "EMERGENZA: svuota tutto localStorage (token preservati)", applyTo: ["react-crash", "js-error"], patterns: [/localstorage|storage|corrupt|invalid/i], dangerous: true, run: clearCorruptedLS, }, { id: "verify-vercel-build", label: "Verifica build Vercel", description: "Controlla che il bundle JS del sito live sia stato compilato correttamente", applyTo: ["console-error", "js-error", "promise-rejection"], patterns: [/TS\d{4}|is not assignable|does not exist on type|cannot find|unexpected token|SyntaxError|chunk|module/i], dangerous: false, run: verifyVercelBuild, }, { id: "clear-module-cache", label: "Svuota cache moduli JS", description: "Rimuove cache Vite/Workbox del browser — risolve errori di caricamento moduli", applyTo: ["js-error", "console-error", "network-error"], patterns: [/TS\d{4}|module|import|chunk|loading|failed to fetch|unexpected token|cache/i], dangerous: false, run: clearModuleCache, }, { id: "reload-app", label: "Ricarica app", description: "Ricarica la pagina (equivale a F5)", applyTo: "all", dangerous: false, run: reloadApp, }, ]; // ─── Util: trova fix applicabili a un errore ────────────────────────────────── export function getApplicableFixes(type: DiagType, message: string): FixAction[] { return FIX_ACTIONS.filter(f => { const typeMatch = f.applyTo === "all" || f.applyTo.includes(type); if (!typeMatch) return false; if (f.patterns && f.patterns.length > 0) { return f.patterns.some(p => p.test(message)); } return true; }); } // ─── Esegui fix e registra ──────────────────────────────────────────────────── export async function runFix(action: FixAction): Promise { let result: FixResult; try { result = await action.run(); } catch(e) { result = { ok: false, message: "Errore imprevisto durante il fix", detail: e instanceof Error ? e.message : String(e), }; } _record(action.id, action.label, result); return result; } // ─── Health check completo del sistema ──────────────────────────────────────── // HealthCheck estratto in autoFixTypes.ts — S550 export async function runHealthChecks(): Promise { const results: HealthCheck[] = []; // 1. IndexedDB try { const { vfsDb } = await import("./vfsDb"); const testKey = `__healthcheck__${Date.now()}`; await vfsDb.agentContext.put({ key: testKey, value: "ok", updatedAt: Date.now() }); const r = await vfsDb.agentContext.get(testKey); await vfsDb.agentContext.delete(testKey); results.push({ id: "indexeddb", label: "IndexedDB (Dexie)", ok: r?.value === "ok", value: r?.value === "ok" ? "lettura/scrittura ok" : "errore round-trip" }); } catch(e) { results.push({ id: "indexeddb", label: "IndexedDB (Dexie)", ok: false, value: String(e).slice(0, 60), fixId: "compact-db" }); } // 2. localStorage try { const testKey = `__lscheck__${Date.now()}`; localStorage.setItem(testKey, "1"); const v = localStorage.getItem(testKey); localStorage.removeItem(testKey); const lsKeys = localStorage.length; const lsSize = Object.keys(localStorage).reduce((n, k) => n + k.length + (localStorage.getItem(k)?.length ?? 0), 0); const lsKb = Math.round(lsSize / 1024); const ok = v === "1" && lsKb < 3000; results.push({ id: "localstorage", label: "localStorage", ok: ok ? true : "warn", value: `${lsKb} KB (${lsKeys} chiavi)${lsKb >= 3000 ? " ⚠ vicino al limite" : ""}`, fixId: lsKb >= 3000 ? "clear-agent-storage" : undefined, }); } catch { results.push({ id: "localstorage", label: "localStorage", ok: false, value: "non disponibile o corrotto", fixId: "clear-ls-full" }); } // 3. Storage quota try { if (navigator.storage?.estimate) { const { usage = 0, quota = 0 } = await navigator.storage.estimate(); const usedMB = Math.round(usage / 1024 / 1024); const quotaMB = Math.round(quota / 1024 / 1024); const pct = quota > 0 ? Math.round((usage / quota) * 100) : 0; results.push({ id: "quota", label: "Storage quota", ok: pct < 80 ? true : pct < 95 ? "warn" : false, value: `${usedMB} / ${quotaMB} MB (${pct}%)`, fixId: pct >= 80 ? (pct >= 95 ? "prune-aggressivo" : "compact-db") : undefined, }); } else { results.push({ id: "quota", label: "Storage quota", ok: "warn", value: "stima non disponibile (Safari < 15.4)" }); } } catch(e) { results.push({ id: "quota", label: "Storage quota", ok: "warn", value: String(e).slice(0, 50) }); } // 4. Service Worker try { if ("serviceWorker" in navigator) { const regs = await navigator.serviceWorker.getRegistrations(); const active = regs.filter(r => r.active).length; results.push({ id: "sw", label: "Service Worker", ok: active > 0 ? true : "warn", value: active > 0 ? `${active} attivo/i` : "nessuno attivo", fixId: active === 0 ? "reset-service-worker" : undefined, }); } else { results.push({ id: "sw", label: "Service Worker", ok: "warn", value: "non supportato" }); } } catch(e) { results.push({ id: "sw", label: "Service Worker", ok: "warn", value: String(e).slice(0, 50) }); } // 5. Connettività sito live try { const start = Date.now(); const r = await fetch("https://ai-agente-ai.vercel.app/", { method: "HEAD", cache: "no-store", signal: makeTimedSignal(6000), }); const ms = Date.now() - start; results.push({ id: "vercel", label: "Sito live (Vercel)", ok: r.ok ? true : false, value: r.ok ? `HTTP ${r.status} in ${ms}ms` : `HTTP ${r.status} — verifica deploy`, }); } catch(e) { const msg = e instanceof Error ? e.message : String(e); results.push({ id: "vercel", label: "Sito live (Vercel)", ok: false, value: `Non raggiungibile: ${msg.slice(0, 60)}`, fixId: "test-network" }); } // 6. Token provider configurati try { const { getAllConfiguredTokens } = await import("./apiKeys"); const tokens = getAllConfiguredTokens(); const configured = Object.entries(tokens).filter(([, ok]) => ok); results.push({ id: "providers", label: "Provider AI configurati", ok: configured.length > 0 ? true : false, value: configured.length > 0 ? configured.map(([n]) => n).join(", ") : "nessun token configurato — vai in Impostazioni", }); } catch { results.push({ id: "providers", label: "Provider AI", ok: "warn", value: "impossibile verificare" }); } // 7. Vercel build (verifica che il bundle JS sia presente nell'HTML) try { const r = await fetch("https://ai-agente-ai.vercel.app/", { method: "GET", cache: "no-store", signal: makeTimedSignal(8000), }); const html = await r.text(); const hasBundle = /assets\/(index|main)[^"]*\.js/.test(html); const scriptCount = (html.match(/]+src/g) ?? []).length; results.push({ id: "build", label: "Build Vercel (bundle JS)", ok: r.ok && hasBundle ? true : false, value: r.ok && hasBundle ? `ok — ${scriptCount} script` : `HTTP ${r.status} — bundle non trovato (build fallito?)`, fixId: !hasBundle ? "verify-vercel-build" : undefined, }); } catch(e) { const msg = e instanceof Error ? e.message.slice(0, 50) : String(e).slice(0, 50); results.push({ id: "build", label: "Build Vercel", ok: false, value: `Non verificabile: ${msg}`, fixId: "verify-vercel-build" }); } // 8. JS heap (Chrome only) try { const mem = (performance as unknown as Record).memory as Record | undefined; if (mem?.usedJSHeapSize && mem.jsHeapSizeLimit) { const used = Math.round(mem.usedJSHeapSize / 1024 / 1024); const limit = Math.round(mem.jsHeapSizeLimit / 1024 / 1024); const pct = Math.round((mem.usedJSHeapSize / mem.jsHeapSizeLimit) * 100); results.push({ id: "heap", label: "JS Heap (Chrome)", ok: pct < 70 ? true : pct < 90 ? "warn" : false, value: `${used} / ${limit} MB (${pct}%)`, fixId: pct >= 70 ? "compact-db" : undefined, }); } } catch { /* heap non disponibile — non aggiungere */ } // 9. Backend exec-engine (HF Space) — latenza + sessioni attive try { const _backendUrl = ( typeof import.meta !== "undefined" ? (import.meta.env?.VITE_EXEC_BACKEND_URL ?? "") : "" ).replace(/\/$/, ""); if (_backendUrl) { const _t0 = Date.now(); const _r = await fetch(`${_backendUrl}/health`, { cache: "no-store", signal: makeTimedSignal(8_000), }); const _ms = Date.now() - _t0; if (_r.ok) { const _d = await _r.json() as { status?: string; active_sessions?: number; cpu_pct?: number; mem_pct?: number; disk_free_mb?: number }; const _warn = (_d.cpu_pct ?? 0) > 85 || (_d.mem_pct ?? 0) > 85 || (_d.disk_free_mb ?? 9999) < 200; const _val = [ `${_ms}ms`, _d.active_sessions != null ? `${_d.active_sessions} sessioni` : "", _d.cpu_pct != null ? `CPU ${_d.cpu_pct}%` : "", _d.mem_pct != null ? `RAM ${_d.mem_pct}%` : "", _d.disk_free_mb != null ? `disk ${_d.disk_free_mb}MB` : "", ].filter(Boolean).join(" | "); results.push({ id: "backend", label: "Backend (exec-engine)", ok: _warn ? "warn" : true, value: _val || `HTTP ${_r.status}`, fixId: _warn ? "restart-backend-process" : undefined, }); } else { results.push({ id: "backend", label: "Backend (exec-engine)", ok: false, value: `HTTP ${_r.status}`, fixId: "restart-backend-process" }); } } } catch(_e) { const _msg = _e instanceof Error ? _e.message.slice(0, 60) : String(_e).slice(0, 60); results.push({ id: "backend", label: "Backend (exec-engine)", ok: false, value: `non raggiungibile: ${_msg}`, fixId: "restart-backend-process" }); } return results; }