AUDIT / src /lib /autoFix.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
23.5 kB
/**
* 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<FixResult> {
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<FixResult> {
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<FixResult> {
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<FixResult> {
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<FixResult> {
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<FixResult> {
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<FixResult> {
window.location.reload();
return { ok: true, message: "Ricaricando la pagina..." };
}
async function resetProviderState(): Promise<FixResult> {
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<FixResult> {
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<FixResult> {
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(/<script[^>]+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<FixResult> {
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<FixResult> {
try {
const total = localStorage.length;
const saved: Record<string, string> = {};
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<FixResult> {
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<HealthCheck[]> {
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(/<script[^>]+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<string, unknown>).memory as Record<string, number> | 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;
}