252 lines
8.8 KiB
TypeScript
252 lines
8.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import SamShell from "@/components/SamShell";
|
|
import { refreshModules, useModules, useMe } from "@/lib/store"
|
|
import {
|
|
assignColorsForModules,
|
|
displayName,
|
|
envOf,
|
|
getModuleMeta,
|
|
isValidHexColor,
|
|
itMenu,
|
|
} from "@/lib/sam";
|
|
|
|
// Actualizar: redeploy manual + reinstalación de webhook por módulo.
|
|
export default function Actualizar() {
|
|
const { data, error } = useModules();
|
|
const me = useMe();
|
|
const modules = data?.modules ?? [];
|
|
const [busy, setBusy] = useState<string | null>(null);
|
|
const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null);
|
|
|
|
const toggleVisible = async (name: string, current: boolean) => {
|
|
setBusy(`visible:${name}`);
|
|
try {
|
|
const res = await fetch(`/api/deployer/modules/${name}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ visible: !current }),
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
refreshModules();
|
|
} catch (e) {
|
|
setMsg({ ok: false, text: e instanceof Error ? e.message : "Error" });
|
|
} finally {
|
|
setBusy(null);
|
|
}
|
|
};
|
|
|
|
const call = async (name: string, action: "deploy" | "webhook") => { setBusy(`${action}:${name}`);
|
|
setMsg(null);
|
|
try {
|
|
const res = await fetch(
|
|
action === "deploy"
|
|
? `/api/deployer/modules/${name}/deploy`
|
|
: `/api/deployer/modules/${name}/webhook`,
|
|
{ method: "POST", cache: "no-store" }
|
|
);
|
|
const j = await res.json();
|
|
if (!res.ok) throw new Error(j.error || `HTTP ${res.status}`);
|
|
setMsg({
|
|
ok: true,
|
|
text:
|
|
action === "deploy"
|
|
? `"${name}" actualizado (sha ${(j.sha || "").slice(0, 7)})`
|
|
: `"${name}": webhook ${j.created ? `creado #${j.created}` : j.updated ? `actualizado #${j.updated}` : "ok"}`,
|
|
});
|
|
refreshModules();
|
|
} catch (e) {
|
|
setMsg({
|
|
ok: false,
|
|
text: e instanceof Error ? e.message : "Error",
|
|
});
|
|
} finally {
|
|
setBusy(null);
|
|
}
|
|
};
|
|
|
|
// Rellena el color de los módulos ya importados que aún no lo tienen
|
|
// guardado: lo genera de forma autoritaria y lo persiste en el deployer.
|
|
const missingColor = modules.filter((m) => !isValidHexColor(m.color));
|
|
const backfillColors = async () => {
|
|
if (missingColor.length === 0) return;
|
|
setBusy("color:backfill");
|
|
setMsg(null);
|
|
try {
|
|
const assigned = assignColorsForModules(modules);
|
|
let ok = 0;
|
|
const failed: string[] = [];
|
|
for (const m of missingColor) {
|
|
try {
|
|
const res = await fetch(
|
|
`/api/deployer/modules/${encodeURIComponent(m.name)}`,
|
|
{
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ color: assigned[m.name] }),
|
|
cache: "no-store",
|
|
}
|
|
);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
ok++;
|
|
} catch {
|
|
failed.push(m.name);
|
|
}
|
|
}
|
|
refreshModules();
|
|
setMsg({
|
|
ok: failed.length === 0,
|
|
text:
|
|
failed.length === 0
|
|
? `Colores generados y guardados en ${ok} módulo${ok !== 1 ? "s" : ""}.`
|
|
: `Guardados ${ok}, fallaron: ${failed.join(", ")}.`,
|
|
});
|
|
} finally {
|
|
setBusy(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<SamShell
|
|
brand="IT"
|
|
subtitle="Actualizar módulos"
|
|
menu={itMenu(modules)}
|
|
activeHref="/actualizar"
|
|
topUser={me?.email || "SAM"}
|
|
allowLogout
|
|
backHref="/"
|
|
>
|
|
<div className="alert alert-light section-header mb-3 py-2 px-3">
|
|
<i className="bx bx-refresh me-1" /> Actualizar módulos
|
|
</div>
|
|
|
|
{error && <div className="alert alert-danger">{error}</div>}
|
|
{msg && (
|
|
<div className={`alert ${msg.ok ? "alert-success" : "alert-danger"}`}>
|
|
{msg.text}
|
|
</div>
|
|
)}
|
|
|
|
<div className="app-card p-3">
|
|
{missingColor.length > 0 && (
|
|
<div className="alert alert-info py-2 d-flex align-items-center justify-content-between flex-wrap gap-2">
|
|
<span className="small">
|
|
<i className="bx bx-palette me-1" />
|
|
Hay {missingColor.length} módulo{missingColor.length !== 1 ? "s" : ""} sin
|
|
color guardado ({missingColor.map((m) => m.name).join(", ")}).
|
|
</span>
|
|
<button
|
|
className="btn btn-sm btn-primary"
|
|
disabled={busy !== null}
|
|
onClick={backfillColors}
|
|
>
|
|
{busy === "color:backfill" ? (
|
|
<span className="spinner-border spinner-border-sm" />
|
|
) : (
|
|
<>
|
|
<i className="bx bx-palette me-1" />
|
|
Generar colores
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
<div className="table-responsive">
|
|
<table className="table table-hover align-middle mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Módulo</th>
|
|
<th>Rama</th>
|
|
<th>Último deploy</th>
|
|
<th className="text-end">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{modules.map((m) => {
|
|
const env = envOf(m.branch);
|
|
return (
|
|
<tr key={m.name}>
|
|
<td>
|
|
<i className={`bx ${getModuleMeta(m.name).icon} me-2`} />
|
|
<strong>{displayName(m.name)}</strong>{" "}
|
|
<span className={`badge ${env.cls} badge-respaldo`}>
|
|
{env.label}
|
|
</span>{" "}
|
|
{m.kind === "backend" && (
|
|
<span className="badge bg-dark badge-respaldo">backend</span>
|
|
)}{" "}
|
|
{m.visible === false && (
|
|
<span className="badge bg-secondary badge-respaldo">oculto</span>
|
|
)}
|
|
<div>
|
|
<small className="text-muted">
|
|
{m.lastDeploy
|
|
? `${(m.lastDeploy.sha || "").slice(0, 7)} · ${m.lastDeploy.ok ? "ok" : "falló"}`
|
|
: "sin deploys"}
|
|
</small>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<code className="small">{m.branch}</code>
|
|
</td>
|
|
<td>
|
|
<small className="text-muted">
|
|
{m.lastDeploy?.time
|
|
? new Date(m.lastDeploy.time).toLocaleString("es-CL")
|
|
: "—"}
|
|
</small>
|
|
</td>
|
|
<td className="text-end">
|
|
<div className="d-flex gap-2 justify-content-end">
|
|
<button
|
|
className="btn btn-sm btn-primary"
|
|
disabled={busy !== null}
|
|
onClick={() => call(m.name, "deploy")}
|
|
>
|
|
{busy === `deploy:${m.name}` ? (
|
|
<span className="spinner-border spinner-border-sm" />
|
|
) : (
|
|
<>
|
|
<i className="bx bx-refresh me-1" />
|
|
Actualizar
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
className="btn btn-sm btn-outline-secondary"
|
|
disabled={busy !== null}
|
|
onClick={() => call(m.name, "webhook")}
|
|
title="Reinstalar webhook Gitea"
|
|
>
|
|
<i className="bx bx-link" />
|
|
</button>
|
|
<button
|
|
className="btn btn-sm btn-outline-secondary"
|
|
disabled={busy !== null}
|
|
onClick={() => toggleVisible(m.name, m.visible !== false)}
|
|
title={m.visible === false ? "Mostrar en portal" : "Ocultar del portal"}
|
|
>
|
|
<i className={`bx ${m.visible === false ? "bx-eye" : "bx-eye-off"}`} />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
{modules.length === 0 && (
|
|
<tr>
|
|
<td colSpan={4} className="text-center text-muted py-4">
|
|
No hay módulos integrados.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</SamShell>
|
|
);
|
|
}
|