/** * DeployPanel.tsx — S744 + S-DEPLOY-UNIFY + S-AUTO * * S744: Trigger workflow_dispatch su cloudflare-deploy.yml, polling stato, * progress step-by-step, URL live, storico deploy. * S-DEPLOY-UNIFY: Griglia "Stato target" — CF Pages, Railway, HF Space. * S-AUTO: Auto-riparazione — diagnostica all'apertura + pulsante "Ripara automaticamente". * POST /api/deploy/auto gestisce tutte le situazioni senza intervento manuale. */ import { useState, useEffect, useCallback, useRef, memo } from "react"; import type * as React from "react"; import { X, Rocket, CheckCircle, XCircle, Clock, RefreshCw, ExternalLink, AlertCircle, Play, Server, Globe, Database, Wrench, Zap, } from "lucide-react"; import { Z_INDEX } from "@/lib/zindex"; import { getGHToken, getRepoConfig, ghTriggerWorkflow, ghGetWorkflowRuns } from "@/lib/github"; import { BACKEND_BASE, _BACKEND_TOKEN } from "@/lib/agentLoop/toolExecutor"; import type { GHWorkflowRun } from "@/lib/github"; import { useTaskStore } from "@/store/taskStore"; // GAP-2: auto-task su build failure interface DeployPanelProps { onClose: () => void; } const LIVE_URL = "https://agente-ai.pages.dev/"; const POLL_MS = 6_000; const STATUS_REFRESH_MS = 60_000; // ─── Tipi /api/deploy/status ────────────────────────────────────────────────── interface CFStatus { ok: boolean; status: string; url: string; last_deploy: string | null; deployment_id: string | null; error: string | null; } interface RailwayStatus { ok: boolean; status: string; url: string; latency_ms: number | null; error: string | null; } interface HFStatus { ok: boolean; status: string; url: string; sdk: string | null; error: string | null; } interface DeployTargets { cloudflare: CFStatus; railway: RailwayStatus; huggingface: HFStatus; } interface DeployStatus { targets: DeployTargets; checked_at: string; } // ─── Tipi /api/deploy/auto ──────────────────────────────────────────────────── interface AutoResult { ok: boolean; fixed: string[]; broken: string[]; actions: string[]; dry_run: boolean; message: string; elapsed: number; } // ─── Helpers ────────────────────────────────────────────────────────────────── function runIcon(run: GHWorkflowRun): string { if (run.status === "queued") return "🟡"; if (run.status === "in_progress") return "🔵"; if (run.conclusion === "success") return "✅"; if (run.conclusion === "failure") return "❌"; if (run.conclusion === "cancelled") return "⚪"; return "⏳"; } function runLabel(run: GHWorkflowRun): string { if (run.status === "queued") return "In coda…"; if (run.status === "in_progress") return "In esecuzione…"; if (run.conclusion === "success") return "Completato"; if (run.conclusion === "failure") return "Fallito"; if (run.conclusion === "cancelled") return "Annullato"; return run.status; } function timeAgo(iso: string): string { const ms = Date.now() - new Date(iso).getTime(); if (ms < 60_000) return `${Math.round(ms / 1000)}s fa`; if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m fa`; if (ms < 86_400_000) return `${Math.round(ms / 3_600_000)}h fa`; return `${Math.floor(ms / 86_400_000)}g fa`; } function backendFetch(path: string, opts?: RequestInit): Promise { if (!BACKEND_BASE) return Promise.resolve(null); return fetch(BACKEND_BASE + path, { ...opts, headers: { "Content-Type": "application/json", ...(_BACKEND_TOKEN ? { "X-Internal-Token": _BACKEND_TOKEN } : {}), ...(opts?.headers ?? {}), }, }).then(r => r.ok ? r.json() as Promise : null).catch(() => null); } // ─── Stili ──────────────────────────────────────────────────────────────────── const actionBtn = (variant: "primary" | "secondary" | "success" | "danger" = "primary", disabled = false): React.CSSProperties => ({ all: "unset" as const, cursor: disabled ? "not-allowed" : "pointer", padding: "9px 18px", borderRadius: 10, fontSize: "0.83rem", fontWeight: 700, display: "flex", alignItems: "center", gap: 6, justifyContent: "center", opacity: disabled ? 0.4 : 1, background: variant === "primary" ? "rgba(99,102,241,0.22)" : variant === "success" ? "rgba(16,185,129,0.18)" : variant === "danger" ? "rgba(239,68,68,0.15)" : "rgba(255,255,255,0.06)", color: variant === "primary" ? "#818cf8" : variant === "success" ? "#10b981" : variant === "danger" ? "#f87171" : "#a0a0c0", border: `1px solid ${ variant === "primary" ? "rgba(99,102,241,0.38)" : variant === "success" ? "rgba(16,185,129,0.35)" : variant === "danger" ? "rgba(239,68,68,0.30)" : "rgba(255,255,255,0.10)" }`, touchAction: "manipulation", transition: "background 0.15s", }); const DEPLOY_STEPS = [ "Quality gate", "Check imports", "Check console.log", "Check circular imports", "Type check", "Unit tests", "Build", "Benchmark (≥95%)", "Deploy to Cloudflare", ]; function StatusDot({ ok, loading }: { ok: boolean | null; loading?: boolean }) { if (loading) return
; return
; } // ─── Componente principale ───────────────────────────────────────────────────── const DeployPanel = memo(function DeployPanel({ onClose }: DeployPanelProps) { const cfg = getRepoConfig(); const owner = cfg.owner || "Baida98"; const repo = cfg.repo || "AI"; const [runs, setRuns] = useState([]); const [loading, setLoading] = useState(false); const [triggering, setTriggering] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [lastLoad, setLastLoad] = useState(0); const pollRef = useRef | null>(null); const statusTimerRef = useRef | null>(null); const repairTaskSentRef = useRef(null); // GAP-2: evita task duplicati per lo stesso run // ── Stato target ───────────────────────────────────────────────────────────── const [deployStatus, setDeployStatus] = useState(null); const [statusLoading, setStatusLoading] = useState(false); // ── Auto-repair ─────────────────────────────────────────────────────────────── const [autoRunning, setAutoRunning] = useState(false); const [autoResult, setAutoResult] = useState(null); const [autoActions, setAutoActions] = useState([]); // ── Carica stato target ─────────────────────────────────────────────────────── const loadDeployStatus = useCallback(async () => { setStatusLoading(true); const res = await backendFetch("/api/deploy/status"); if (res) setDeployStatus(res); setStatusLoading(false); }, []); // ── Carica workflow runs ────────────────────────────────────────────────────── const loadRuns = useCallback(async (silent = false) => { if (!getGHToken()) return; if (!silent) setLoading(true); try { const r = await ghGetWorkflowRuns(owner, repo, "cloudflare-deploy.yml", 8); setRuns(r); setLastLoad(Date.now()); } catch (e) { if (!silent) setError((e as Error).message); } finally { if (!silent) setLoading(false); } }, [owner, repo]); // ── Boot ────────────────────────────────────────────────────────────────────── useEffect(() => { void loadRuns(); void loadDeployStatus(); statusTimerRef.current = setInterval(() => void loadDeployStatus(), STATUS_REFRESH_MS); return () => { if (pollRef.current) clearInterval(pollRef.current); if (statusTimerRef.current) clearInterval(statusTimerRef.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // latestRun dichiarato qui per essere disponibile nel useEffect GAP-2 const latestRun = runs[0] ?? null; // ── GAP-2: auto-task quando deploy fallisce — chiude il loop di autonomia ────── useEffect(() => { if (!latestRun) return; const isFailed = latestRun.status !== 'queued' && latestRun.status !== 'in_progress' && latestRun.conclusion === 'failure'; if (!isFailed) return; if (repairTaskSentRef.current === String(latestRun.id)) return; // già inviato repairTaskSentRef.current = String(latestRun.id); useTaskStore.getState().startTask( `🔧 Deploy #${latestRun.id} fallito su GitHub Actions. Analizza i log CF Pages con cf_pages_status, individua gli errori TypeScript/build, applica i fix nei file coinvolti, fai git_push e monitora con deploy_and_verify. Logs: ${latestRun.html_url}` ); }, [latestRun]); // ── Polling auto durante run attivo ────────────────────────────────────────── useEffect(() => { const hasActive = runs.some(r => r.status === "queued" || r.status === "in_progress"); if (hasActive) { if (!pollRef.current) pollRef.current = setInterval(() => void loadRuns(true), POLL_MS); } else { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } } }, [runs, loadRuns]); // ── Auto-riparazione ────────────────────────────────────────────────────────── const runAutoRepair = useCallback(async () => { setAutoRunning(true); setAutoResult(null); setAutoActions(["⏳ Diagnosi in corso…"]); setError(null); setSuccess(null); const result = await backendFetch("/api/deploy/auto", { method: "POST", body: JSON.stringify({ targets: ["cloudflare", "railway", "huggingface"], dry_run: false }), }); if (result) { setAutoResult(result); setAutoActions(result.actions); if (result.ok) setSuccess(`Auto-riparazione completata: ${result.message} (${result.elapsed}s)`); else setError(`Alcuni target non risolti: ${result.broken.join(", ")}`); // Aggiorna lo stato target dopo la riparazione setTimeout(() => void loadDeployStatus(), 3_000); } else { setAutoActions(["❌ Backend non raggiungibile — verifica Railway"]); setError("Backend non raggiungibile. Assicurati che Railway sia attivo."); } setAutoRunning(false); }, [loadDeployStatus]); // ── Trigger deploy manuale ──────────────────────────────────────────────────── const triggerDeploy = useCallback(async () => { if (!getGHToken()) { setError("Configura il GitHub token (Impostazioni → GitHub)."); return; } setTriggering(true); setError(null); setSuccess(null); setAutoResult(null); try { const ok = await ghTriggerWorkflow(owner, repo, "cloudflare-deploy.yml", "main"); if (ok) { setSuccess("Deploy avviato! Polling ogni 6s…"); setTimeout(() => void loadRuns(), 3_000); } else setError("Workflow dispatch fallito. Verifica i permessi del token (scope: workflow)."); } catch (e) { setError(`Errore: ${(e as Error).message}`); } finally { setTriggering(false); } }, [owner, repo, loadRuns]); const triggerSmokeTest = useCallback(async () => { if (!getGHToken()) { setError("Configura il GitHub token."); return; } setTriggering(true); setError(null); try { await ghTriggerWorkflow(owner, repo, "smoke-test.yml", "main"); setSuccess("Smoke test avviato su GitHub Actions."); } catch (e) { setError(`Errore: ${(e as Error).message}`); } finally { setTriggering(false); } }, [owner, repo]); // ── Dati derivati ───────────────────────────────────────────────────────────── const isActive = latestRun && (latestRun.status === "queued" || latestRun.status === "in_progress"); const t = deployStatus?.targets; const checkedAgo = deployStatus ? timeAgo(deployStatus.checked_at) : null; const anyBroken = t ? (!t.cloudflare.ok || !t.railway.ok || !t.huggingface.ok) : false; const allGreen = t ? (t.cloudflare.ok && t.railway.ok && t.huggingface.ok) : false; return (
{/* ── Header ──────────────────────────────────────────────────────────── */}
Deploy Live
{/* ── Stato target ────────────────────────────────────────────────────── */}
{allGreen ? : anyBroken ? : } {allGreen ? "TUTTI I SISTEMI OK" : anyBroken ? "PROBLEMI RILEVATI" : "STATO TARGET"}
{checkedAgo && {checkedAgo}}
{/* Griglia 3 target */} {/* Pulsante auto-riparazione — compare solo se ci sono problemi */} {anyBroken && !autoRunning && !autoResult && ( )} {allGreen && !autoRunning && (
Tutti i sistemi operativi — nessuna azione necessaria
)}
{/* ── Risultato auto-riparazione ──────────────────────────────────────── */} {(autoRunning || autoActions.length > 0) && (
{autoRunning ? "Riparazione in corso…" : autoResult?.ok ? "Riparazione completata ✓" : "Riparazione parziale"} {autoResult && ( {autoResult.elapsed}s )}
{autoActions.map((action, i) => (
{action.startsWith("✅") || action.startsWith("❌") || action.startsWith("⚠") || action.startsWith("ℹ") ? "" : "›"} {action}
))}
{autoResult && !autoRunning && ( )}
)} {/* ── Error / Success ──────────────────────────────────────────────────── */} {error && (
{error}
)} {success && !error && (
{success}
)} {/* ── Run attivo ───────────────────────────────────────────────────────── */} {latestRun && isActive && (
{runIcon(latestRun)}
Deploy in corso
{runLabel(latestRun)} — {timeAgo(latestRun.created_at)}
{DEPLOY_STEPS.map((step, i) => (
{step}
))}
Vedi su GitHub Actions →
)} {/* ── Ultimo deploy ────────────────────────────────────────────────────── */} {latestRun && !isActive && (
{latestRun.conclusion === "success" ? : }
{latestRun.conclusion === "success" ? "Ultimo deploy OK" : "Ultimo deploy fallito"}
{timeAgo(latestRun.updated_at)}
{latestRun.conclusion === "success" && ( Vai al sito )}
{latestRun.conclusion !== "success" && ( Vedi logs su GitHub Actions → )}
)} {/* ── Azioni manuali ───────────────────────────────────────────────────── */}
{/* ── Info pipeline ────────────────────────────────────────────────────── */}

PIPELINE CF PAGES

{DEPLOY_STEPS.map(s => (
{s}
))}
{/* ── Storico runs ──────────────────────────────────────────────────────── */}

STORICO DEPLOY

{lastLoad > 0 && {timeAgo(new Date(lastLoad).toISOString())}}
{!getGHToken() &&

Configura il GitHub token per vedere lo storico.

} {getGHToken() && runs.length === 0 && !loading &&

Nessun deploy trovato.

}
); }); export default DeployPanel;