AUDIT / src /components /DeployPanel.tsx
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
34.3 kB
/**
* 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<T>(path: string, opts?: RequestInit): Promise<T | null> {
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<T> : 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 <div style={{ width: 8, height: 8, borderRadius: "50%", background: "#6366f1", animation: "mem-tab-pulse 1.2s ease-in-out infinite" }} />;
return <div style={{ width: 8, height: 8, borderRadius: "50%", background: ok === null ? "#4a4a78" : ok ? "#10b981" : "#f87171", flexShrink: 0 }} />;
}
// ─── 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<GHWorkflowRun[]>([]);
const [loading, setLoading] = useState(false);
const [triggering, setTriggering] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [lastLoad, setLastLoad] = useState(0);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const statusTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const repairTaskSentRef = useRef<string | null>(null); // GAP-2: evita task duplicati per lo stesso run
// ── Stato target ─────────────────────────────────────────────────────────────
const [deployStatus, setDeployStatus] = useState<DeployStatus | null>(null);
const [statusLoading, setStatusLoading] = useState(false);
// ── Auto-repair ───────────────────────────────────────────────────────────────
const [autoRunning, setAutoRunning] = useState(false);
const [autoResult, setAutoResult] = useState<AutoResult | null>(null);
const [autoActions, setAutoActions] = useState<string[]>([]);
// ── Carica stato target ───────────────────────────────────────────────────────
const loadDeployStatus = useCallback(async () => {
setStatusLoading(true);
const res = await backendFetch<DeployStatus>("/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<AutoResult>("/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 (
<div role="dialog" aria-modal="true" aria-label="Deploy"
style={{ position: "fixed", inset: 0, zIndex: Z_INDEX.MODAL, display: "flex", justifyContent: "flex-end" }}>
<div onClick={onClose} style={{ flex: 1, background: "rgba(0,0,0,0.45)" }} />
<div style={{
width: "min(100vw, 420px)", height: "100%", overflowY: "auto",
background: "rgba(6,6,18,0.97)", borderLeft: "1px solid rgba(99,102,241,0.18)",
display: "flex", flexDirection: "column",
paddingBottom: "env(safe-area-inset-bottom, 0px)",
}}>
{/* ── Header ──────────────────────────────────────────────────────────── */}
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 16px", borderBottom: "1px solid rgba(99,102,241,0.12)", flexShrink: 0 }}>
<Rocket size={18} color="#6366f1" />
<span style={{ flex: 1, fontWeight: 700, fontSize: "0.95rem", color: "#d0d0f0" }}>Deploy</span>
<a href={LIVE_URL} target="_blank" rel="noopener noreferrer"
style={{ display: "flex", alignItems: "center", gap: 5, fontSize: "0.72rem", color: "#6366f1", textDecoration: "none", padding: "5px 10px", background: "rgba(99,102,241,0.09)", borderRadius: 8, border: "1px solid rgba(99,102,241,0.20)" }}>
<ExternalLink size={12} />Live
</a>
<button onClick={onClose} style={{ all: "unset", cursor: "pointer", color: "#4a4a78", padding: 6, touchAction: "manipulation" }}><X size={18} /></button>
</div>
<div style={{ flex: 1, overflowY: "auto", padding: "14px 16px", display: "flex", flexDirection: "column", gap: 12 }}>
{/* ── Stato target ────────────────────────────────────────────────────── */}
<div style={{ padding: "12px 14px", background: "rgba(255,255,255,0.02)", border: `1px solid ${allGreen ? "rgba(16,185,129,0.18)" : anyBroken ? "rgba(239,68,68,0.18)" : "rgba(99,102,241,0.12)"}`, borderRadius: 10 }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
{allGreen
? <CheckCircle size={13} color="#10b981" />
: anyBroken
? <AlertCircle size={13} color="#f87171" />
: <Clock size={13} color="#6366f1" />}
<span style={{ fontSize: "0.72rem", color: allGreen ? "#10b981" : anyBroken ? "#f87171" : "#4a4a78", fontWeight: 700, letterSpacing: "0.04em" }}>
{allGreen ? "TUTTI I SISTEMI OK" : anyBroken ? "PROBLEMI RILEVATI" : "STATO TARGET"}
</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
{checkedAgo && <span style={{ fontSize: "0.63rem", color: "#3a3a6a" }}>{checkedAgo}</span>}
<button onClick={() => void loadDeployStatus()} disabled={statusLoading}
style={{ all: "unset", cursor: statusLoading ? "not-allowed" : "pointer", color: "#6366f1", padding: 4, opacity: statusLoading ? 0.5 : 1, touchAction: "manipulation" }}>
<RefreshCw size={11} style={{ animation: statusLoading ? "spin 1s linear infinite" : "none" }} />
</button>
</div>
</div>
{/* Griglia 3 target */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 6, marginBottom: anyBroken ? 10 : 0 }}>
{/* CF Pages */}
<a href="https://agente-ai.pages.dev" target="_blank" rel="noopener noreferrer"
style={{ display: "flex", flexDirection: "column", gap: 4, padding: "9px 10px", background: "rgba(0,0,0,0.15)", border: `1px solid ${t?.cloudflare.ok ? "rgba(16,185,129,0.22)" : t ? "rgba(239,68,68,0.22)" : "rgba(99,102,241,0.10)"}`, borderRadius: 8, textDecoration: "none" }}>
<div style={{ display: "flex", alignItems: "center", gap: 5 }}>
<Globe size={11} color="#6366f1" />
<StatusDot ok={t ? t.cloudflare.ok : null} loading={statusLoading && !t} />
</div>
<span style={{ fontSize: "0.68rem", color: "#8080b0", fontWeight: 700 }}>CF Pages</span>
<span style={{ fontSize: "0.62rem", color: t?.cloudflare.ok ? "#10b981" : t?.cloudflare.error ? "#f87171" : "#4a4a78", lineHeight: 1.3, wordBreak: "break-word" }}>
{statusLoading && !t ? "…" : t ? (t.cloudflare.ok ? "live ✓" : t.cloudflare.error ?? t.cloudflare.status) : "—"}
</span>
{t?.cloudflare.last_deploy && <span style={{ fontSize: "0.60rem", color: "#3a3a6a" }}>{timeAgo(t.cloudflare.last_deploy)}</span>}
</a>
{/* Railway */}
<a href="https://ai-production-4c06.up.railway.app" target="_blank" rel="noopener noreferrer"
style={{ display: "flex", flexDirection: "column", gap: 4, padding: "9px 10px", background: "rgba(0,0,0,0.15)", border: `1px solid ${t?.railway.ok ? "rgba(16,185,129,0.22)" : t ? "rgba(239,68,68,0.22)" : "rgba(99,102,241,0.10)"}`, borderRadius: 8, textDecoration: "none" }}>
<div style={{ display: "flex", alignItems: "center", gap: 5 }}>
<Server size={11} color="#6366f1" />
<StatusDot ok={t ? t.railway.ok : null} loading={statusLoading && !t} />
</div>
<span style={{ fontSize: "0.68rem", color: "#8080b0", fontWeight: 700 }}>Railway</span>
<span style={{ fontSize: "0.62rem", color: t?.railway.ok ? "#10b981" : t?.railway.error ? "#f87171" : "#4a4a78", lineHeight: 1.3 }}>
{statusLoading && !t ? "…" : t ? (t.railway.ok ? `up ✓${t.railway.latency_ms != null ? ` · ${t.railway.latency_ms}ms` : ""}` : t.railway.error ?? t.railway.status) : "—"}
</span>
</a>
{/* HF Space */}
<a href="https://huggingface.co/spaces/Arjanit98/Terminal" target="_blank" rel="noopener noreferrer"
style={{ display: "flex", flexDirection: "column", gap: 4, padding: "9px 10px", background: "rgba(0,0,0,0.15)", border: `1px solid ${t?.huggingface.ok ? "rgba(16,185,129,0.22)" : t ? "rgba(239,68,68,0.22)" : "rgba(99,102,241,0.10)"}`, borderRadius: 8, textDecoration: "none" }}>
<div style={{ display: "flex", alignItems: "center", gap: 5 }}>
<Database size={11} color="#6366f1" />
<StatusDot ok={t ? t.huggingface.ok : null} loading={statusLoading && !t} />
</div>
<span style={{ fontSize: "0.68rem", color: "#8080b0", fontWeight: 700 }}>HF Space</span>
<span style={{ fontSize: "0.62rem", color: t?.huggingface.ok ? "#10b981" : t?.huggingface.error ? "#f87171" : "#4a4a78", lineHeight: 1.3 }}>
{statusLoading && !t ? "…" : t ? (t.huggingface.ok ? "running ✓" : t.huggingface.error ?? t.huggingface.status) : "—"}
</span>
</a>
</div>
{/* Pulsante auto-riparazione — compare solo se ci sono problemi */}
{anyBroken && !autoRunning && !autoResult && (
<button onClick={() => void runAutoRepair()} style={actionBtn("danger", false)}>
<Wrench size={14} />Ripara automaticamente
</button>
)}
{allGreen && !autoRunning && (
<div style={{ display: "flex", alignItems: "center", gap: 6, padding: "7px 10px", background: "rgba(16,185,129,0.08)", borderRadius: 8, border: "1px solid rgba(16,185,129,0.18)" }}>
<Zap size={12} color="#10b981" />
<span style={{ fontSize: "0.72rem", color: "#10b981" }}>Tutti i sistemi operativi — nessuna azione necessaria</span>
</div>
)}
</div>
{/* ── Risultato auto-riparazione ──────────────────────────────────────── */}
{(autoRunning || autoActions.length > 0) && (
<div style={{ padding: "12px 14px", background: "rgba(99,102,241,0.06)", border: `1px solid ${autoResult?.ok === false ? "rgba(239,68,68,0.22)" : "rgba(99,102,241,0.20)"}`, borderRadius: 10 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
<Wrench size={13} color={autoResult?.ok === false ? "#f87171" : "#6366f1"} style={{ animation: autoRunning ? "spin 1.5s linear infinite" : "none" }} />
<span style={{ fontSize: "0.75rem", fontWeight: 700, color: autoResult?.ok === false ? "#f87171" : "#818cf8" }}>
{autoRunning ? "Riparazione in corso…" : autoResult?.ok ? "Riparazione completata ✓" : "Riparazione parziale"}
</span>
{autoResult && (
<span style={{ fontSize: "0.65rem", color: "#4a4a78", marginLeft: "auto" }}>{autoResult.elapsed}s</span>
)}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{autoActions.map((action, i) => (
<div key={i} style={{ display: "flex", gap: 7, alignItems: "flex-start" }}>
<span style={{ fontSize: "0.7rem", color: action.startsWith("✅") ? "#10b981" : action.startsWith("❌") ? "#f87171" : action.startsWith("⚠") ? "#fbbf24" : "#6366f1", flexShrink: 0, marginTop: 1 }}>
{action.startsWith("✅") || action.startsWith("❌") || action.startsWith("⚠") || action.startsWith("ℹ") ? "" : "›"}
</span>
<span style={{ fontSize: "0.7rem", color: action.startsWith("✅") ? "#10b981" : action.startsWith("❌") ? "#f87171" : action.startsWith("⚠") ? "#fbbf24" : "#a0a0c0", lineHeight: 1.4 }}>{action}</span>
</div>
))}
</div>
{autoResult && !autoRunning && (
<button onClick={() => { setAutoResult(null); setAutoActions([]); }} style={{ ...actionBtn("secondary"), marginTop: 10, fontSize: "0.7rem", padding: "6px 12px" }}>
Chiudi
</button>
)}
</div>
)}
{/* ── Error / Success ──────────────────────────────────────────────────── */}
{error && (
<div style={{ padding: "9px 13px", background: "rgba(239,68,68,0.10)", border: "1px solid rgba(239,68,68,0.28)", borderRadius: 8, display: "flex", gap: 8, alignItems: "flex-start" }}>
<AlertCircle size={15} color="#f87171" style={{ flexShrink: 0, marginTop: 1 }} />
<span style={{ fontSize: "0.78rem", color: "#f87171", flex: 1, lineHeight: 1.5 }}>{error}</span>
<button onClick={() => setError(null)} style={{ all: "unset", cursor: "pointer", color: "#f87171" }}><X size={13} /></button>
</div>
)}
{success && !error && (
<div style={{ padding: "9px 13px", background: "rgba(16,185,129,0.10)", border: "1px solid rgba(16,185,129,0.25)", borderRadius: 8, display: "flex", gap: 8, alignItems: "center" }}>
<CheckCircle size={14} color="#10b981" />
<span style={{ fontSize: "0.78rem", color: "#10b981", flex: 1 }}>{success}</span>
<button onClick={() => setSuccess(null)} style={{ all: "unset", cursor: "pointer", color: "#10b981" }}><X size={13} /></button>
</div>
)}
{/* ── Run attivo ───────────────────────────────────────────────────────── */}
{latestRun && isActive && (
<div style={{ padding: "14px 16px", background: "rgba(99,102,241,0.08)", border: "1px solid rgba(99,102,241,0.25)", borderRadius: 12 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
<span style={{ fontSize: "1.1em" }}>{runIcon(latestRun)}</span>
<div style={{ flex: 1 }}>
<div style={{ fontSize: "0.85rem", fontWeight: 700, color: "#d0d0f0" }}>Deploy in corso</div>
<div style={{ fontSize: "0.7rem", color: "#6366f1" }}>{runLabel(latestRun)} — {timeAgo(latestRun.created_at)}</div>
</div>
<div style={{ width: 8, height: 8, borderRadius: "50%", background: "#6366f1", animation: "mem-tab-pulse 1.2s ease-in-out infinite" }} />
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{DEPLOY_STEPS.map((step, i) => (
<div key={step} style={{ display: "flex", alignItems: "center", gap: 7, opacity: i === 0 ? 1 : 0.45 }}>
<div style={{ width: 5, height: 5, borderRadius: "50%", background: i === 0 ? "#6366f1" : "rgba(99,102,241,0.2)", flexShrink: 0 }} />
<span style={{ fontSize: "0.71rem", color: i === 0 ? "#818cf8" : "#3a3a6a" }}>{step}</span>
</div>
))}
</div>
<a href={latestRun.html_url} target="_blank" rel="noopener noreferrer" style={{ display: "block", marginTop: 10, fontSize: "0.71rem", color: "#6366f1", textDecoration: "none" }}>Vedi su GitHub Actions →</a>
</div>
)}
{/* ── Ultimo deploy ────────────────────────────────────────────────────── */}
{latestRun && !isActive && (
<div style={{ padding: "11px 14px", background: latestRun.conclusion === "success" ? "rgba(16,185,129,0.07)" : "rgba(239,68,68,0.07)", border: `1px solid ${latestRun.conclusion === "success" ? "rgba(16,185,129,0.22)" : "rgba(239,68,68,0.22)"}`, borderRadius: 10 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
{latestRun.conclusion === "success" ? <CheckCircle size={15} color="#10b981" /> : <XCircle size={15} color="#f87171" />}
<div style={{ flex: 1 }}>
<div style={{ fontSize: "0.83rem", fontWeight: 700, color: latestRun.conclusion === "success" ? "#10b981" : "#f87171" }}>
{latestRun.conclusion === "success" ? "Ultimo deploy OK" : "Ultimo deploy fallito"}
</div>
<div style={{ fontSize: "0.7rem", color: "#4a4a78" }}>{timeAgo(latestRun.updated_at)}</div>
</div>
{latestRun.conclusion === "success" && (
<a href={LIVE_URL} target="_blank" rel="noopener noreferrer" style={{ fontSize: "0.71rem", color: "#10b981", textDecoration: "none", padding: "4px 10px", background: "rgba(16,185,129,0.12)", borderRadius: 8, border: "1px solid rgba(16,185,129,0.25)" }}>Vai al sito</a>
)}
</div>
{latestRun.conclusion !== "success" && (
<a href={latestRun.html_url} target="_blank" rel="noopener noreferrer" style={{ display: "block", marginTop: 7, fontSize: "0.71rem", color: "#f87171", textDecoration: "none" }}>Vedi logs su GitHub Actions →</a>
)}
</div>
)}
{/* ── Azioni manuali ───────────────────────────────────────────────────── */}
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<button onClick={() => void runAutoRepair()} disabled={autoRunning} style={actionBtn("primary", autoRunning)}>
{autoRunning ? <><RefreshCw size={14} style={{ animation: "spin 1s linear infinite" }} />Riparazione in corso…</> : <><Zap size={14} />Controlla e ripara tutto</>}
</button>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
<button onClick={() => void triggerDeploy()} disabled={triggering || !!isActive} style={actionBtn("secondary", triggering || !!isActive)}>
<Rocket size={13} />{triggering ? "Avvio…" : "Deploy CF"}
</button>
<button onClick={() => void triggerSmokeTest()} disabled={triggering} style={actionBtn("secondary", triggering)}>
<Play size={13} />Smoke Test
</button>
</div>
</div>
{/* ── Info pipeline ────────────────────────────────────────────────────── */}
<div style={{ padding: "10px 14px", background: "rgba(255,255,255,0.02)", border: "1px solid rgba(99,102,241,0.08)", borderRadius: 9 }}>
<p style={{ margin: "0 0 6px", fontSize: "0.70rem", color: "#3a3a6a", fontWeight: 700, letterSpacing: "0.06em" }}>PIPELINE CF PAGES</p>
{DEPLOY_STEPS.map(s => (
<div key={s} style={{ display: "flex", alignItems: "center", gap: 5, marginBottom: 3 }}>
<div style={{ width: 3, height: 3, borderRadius: "50%", background: "rgba(99,102,241,0.3)", flexShrink: 0 }} />
<span style={{ fontSize: "0.69rem", color: "#3a3a6a" }}>{s}</span>
</div>
))}
</div>
{/* ── Storico runs ──────────────────────────────────────────────────────── */}
<div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
<p style={{ margin: 0, fontSize: "0.70rem", color: "#3a3a6a", fontWeight: 700, letterSpacing: "0.06em" }}>STORICO DEPLOY</p>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
{lastLoad > 0 && <span style={{ fontSize: "0.63rem", color: "#3a3a6a" }}>{timeAgo(new Date(lastLoad).toISOString())}</span>}
<button onClick={() => void loadRuns()} disabled={loading}
style={{ all: "unset", cursor: loading ? "not-allowed" : "pointer", color: "#6366f1", padding: 4, opacity: loading ? 0.5 : 1, touchAction: "manipulation" }}>
<RefreshCw size={12} style={{ animation: loading ? "spin 1s linear infinite" : "none" }} />
</button>
</div>
</div>
{!getGHToken() && <p style={{ color: "#3a3a6a", fontSize: "0.78rem", textAlign: "center", padding: "12px 0" }}>Configura il GitHub token per vedere lo storico.</p>}
{getGHToken() && runs.length === 0 && !loading && <p style={{ color: "#3a3a6a", fontSize: "0.78rem", textAlign: "center", padding: "12px 0" }}>Nessun deploy trovato.</p>}
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
{runs.map(r => (
<a key={r.id} href={r.html_url} target="_blank" rel="noopener noreferrer"
style={{ display: "flex", alignItems: "center", gap: 8, padding: "9px 12px", background: "rgba(255,255,255,0.02)", border: "1px solid rgba(99,102,241,0.09)", borderRadius: 8, textDecoration: "none" }}>
<span style={{ fontSize: "0.88em", flexShrink: 0 }}>{runIcon(r)}</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: "0.74rem", color: "#c0c0e0", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{runLabel(r)} — #{r.id}</div>
<div style={{ fontSize: "0.66rem", color: "#3a3a6a", marginTop: 1 }}>{timeAgo(r.created_at)}</div>
</div>
<ExternalLink size={11} color="#4a4a78" />
</a>
))}
</div>
</div>
</div>
</div>
</div>
);
});
export default DeployPanel;