From 23df440b7981d80ab91c41957649b56365c66602 Mon Sep 17 00:00:00 2001 From: Nurfog Date: Wed, 9 Sep 2026 09:54:53 -0300 Subject: [PATCH] feat: sidebar IT con acciones reales (agregar/actualizar/eliminar + proxy deployer) --- src/app/actualizar/page.tsx | 152 ++++++++++++++++++++++++ src/app/api/deployer/[...slug]/route.ts | 79 ++++++++++++ src/app/eliminar/page.tsx | 145 ++++++++++++++++++++++ src/app/tablero/page.tsx | 22 +--- src/lib/sam.ts | 17 +++ 5 files changed, 398 insertions(+), 17 deletions(-) create mode 100644 src/app/actualizar/page.tsx create mode 100644 src/app/api/deployer/[...slug]/route.ts create mode 100644 src/app/eliminar/page.tsx diff --git a/src/app/actualizar/page.tsx b/src/app/actualizar/page.tsx new file mode 100644 index 0000000..da696c1 --- /dev/null +++ b/src/app/actualizar/page.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { useState } from "react"; +import SamShell from "@/components/SamShell"; +import { refreshModules, useModules } from "@/lib/store"; +import { + displayName, + envOf, + getModuleMeta, + itMenu, +} from "@/lib/sam"; + +// Actualizar: redeploy manual + reinstalación de webhook por módulo. +export default function Actualizar() { + const { data, error } = useModules(); + const modules = data?.modules ?? []; + const [busy, setBusy] = useState(null); + const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(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); + } + }; + + return ( + +
+ Actualizar módulos +
+ + {error &&
{error}
} + {msg && ( +
+ {msg.text} +
+ )} + +
+
+ + + + + + + + + + + {modules.map((m) => { + const env = envOf(m.branch); + return ( + + + + + + + ); + })} + {modules.length === 0 && ( + + + + )} + +
MóduloRamaÚltimo deployAcciones
+ + {displayName(m.name)}{" "} + + {env.label} + +
+ + {m.lastDeploy + ? `${(m.lastDeploy.sha || "").slice(0, 7)} · ${m.lastDeploy.ok ? "ok" : "falló"}` + : "sin deploys"} + +
+
+ {m.branch} + + + {m.lastDeploy?.time + ? new Date(m.lastDeploy.time).toLocaleString("es-CL") + : "—"} + + +
+ + +
+
+ No hay módulos integrados. +
+
+
+
+ ); +} diff --git a/src/app/api/deployer/[...slug]/route.ts b/src/app/api/deployer/[...slug]/route.ts new file mode 100644 index 0000000..913e060 --- /dev/null +++ b/src/app/api/deployer/[...slug]/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from "next/server"; + +const DEPLOYER_URL = process.env.DEPLOYER_URL || "http://127.0.0.1:3010"; +const DEPLOYER_TOKEN = process.env.DEPLOYER_TOKEN || ""; + +// Proxy genérico al deployer: /api/deployer/? +// Métodos: GET, POST, PUT, PATCH, DELETE. Permite exponer +// futuras acciones sin agregar rutas (deploy, webhook, backend...). +async function proxy(req: NextRequest, path: string[]) { + const headers: Record = { + "Content-Type": "application/json", + }; + if (DEPLOYER_TOKEN) headers["X-API-Token"] = DEPLOYER_TOKEN; + try { + const qs = req.nextUrl.search || ""; + const init: RequestInit = { + method: req.method, + headers, + cache: "no-store", + }; + if (req.method !== "GET" && req.method !== "DELETE") { + try { + init.body = JSON.stringify(await req.json()); + } catch { + init.body = undefined; + } + } + const res = await fetch( + `${DEPLOYER_URL}/api/${path.join("/")}${qs}`, + init + ); + const text = await res.text(); + try { + return NextResponse.json(JSON.parse(text), { status: res.status }); + } catch { + return new NextResponse(text, { status: res.status }); + } + } catch (e) { + return NextResponse.json( + { error: e instanceof Error ? e.message : "Error al conectar con deployer" }, + { status: 502 } + ); + } +} + +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ slug: string[] }> } +) { + return proxy(req, (await params).slug); +} + +export async function POST( + req: NextRequest, + { params }: { params: Promise<{ slug: string[] }> } +) { + return proxy(req, (await params).slug); +} + +export async function PUT( + req: NextRequest, + { params }: { params: Promise<{ slug: string[] }> } +) { + return proxy(req, (await params).slug); +} + +export async function PATCH( + req: NextRequest, + { params }: { params: Promise<{ slug: string[] }> } +) { + return proxy(req, (await params).slug); +} + +export async function DELETE( + req: NextRequest, + { params }: { params: Promise<{ slug: string[] }> } +) { + return proxy(req, (await params).slug); +} diff --git a/src/app/eliminar/page.tsx b/src/app/eliminar/page.tsx new file mode 100644 index 0000000..aab0a90 --- /dev/null +++ b/src/app/eliminar/page.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useState } from "react"; +import SamShell from "@/components/SamShell"; +import { refreshModules, useModules } from "@/lib/store"; +import { + displayName, + envOf, + getModuleMeta, + itMenu, +} from "@/lib/sam"; + +// Eliminar: baja de módulos integrados, con o sin código (purge). +export default function Eliminar() { + const { data, error } = useModules(); + const modules = data?.modules ?? []; + const [purge, setPurge] = useState(true); + const [busy, setBusy] = useState(null); + const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); + + const remove = async (name: string) => { + const extra = purge ? " y SE ELIMINARÁ SU CÓDIGO" : ""; + if ( + !window.confirm(`¿Eliminar el módulo "${name}"?${extra}\nEsta acción no se puede deshacer.`) + ) { + return; + } + setBusy(name); + setMsg(null); + try { + const res = await fetch( + `/api/deployer/modules/${name}?purge=${purge ? "true" : "false"}`, + { method: "DELETE", cache: "no-store" } + ); + const j = await res.json(); + if (!res.ok || !j.ok) throw new Error(j.error || `HTTP ${res.status}`); + setMsg({ + ok: true, + text: `Módulo "${name}" eliminado${purge ? " con código" : ""}.`, + }); + refreshModules(); + } catch (e) { + setMsg({ ok: false, text: e instanceof Error ? e.message : "Error" }); + } finally { + setBusy(null); + } + }; + + return ( + +
+ Eliminar módulos +
+ + {error &&
{error}
} + {msg && ( +
+ {msg.text} +
+ )} + +
+
+ setPurge(e.target.checked)} + /> + +
+
+ + + + + + + + + + {modules.map((m) => { + const env = envOf(m.branch); + return ( + + + + + + ); + })} + {modules.length === 0 && ( + + + + )} + +
MóduloSubdominioAcción
+ + {displayName(m.name)}{" "} + + {env.label} + + + + {m.subdomain}.devops.norteamericano.cl + + + +
+ No hay módulos integrados. +
+
+

+ La eliminación baja contenedores y limpia Caddy, DNS, webhook Gitea y + registro. Recuerda recargar el Caddy host si el deployer lo indica. +

+
+
+ ); +} diff --git a/src/app/tablero/page.tsx b/src/app/tablero/page.tsx index ad9010d..a4d89f5 100644 --- a/src/app/tablero/page.tsx +++ b/src/app/tablero/page.tsx @@ -3,32 +3,20 @@ import SamShell from "@/components/SamShell"; import TableroView from "@/components/TableroView"; import { useModules } from "@/lib/store"; -import { displayName, getModuleMeta } from "@/lib/sam"; +import { itMenu } from "@/lib/sam"; // App IT: sidebar con todas sus opciones (resumen, aplicaciones, -// tableros por módulo y gestión). Se sirve en /tablero y en -// it.sam.devops.norteamericano.cl +// tableros por módulo, agregar, actualizar, eliminar). +// Se sirve en /tablero y en it.sam.devops.norteamericano.cl export default function Tablero() { const { data } = useModules(); - const modules = data?.modules ?? []; - - const menu = [ - { label: "Resumen", icon: "bx-grid-alt", href: "#resumen" }, - { label: "Aplicaciones", icon: "bx-layer", href: "/" }, - ...modules.map((m) => ({ - label: displayName(m.name), - icon: getModuleMeta(m.name).icon, - href: `/m/${m.name}`, - })), - { label: "Nuevo Módulo", icon: "bx-plus", href: "/upload" }, - ]; return ( diff --git a/src/lib/sam.ts b/src/lib/sam.ts index 453e6d8..c1ed09d 100644 --- a/src/lib/sam.ts +++ b/src/lib/sam.ts @@ -260,3 +260,20 @@ export function envOf(branch: string) { ? { label: "Prod", cls: "bg-success" } : { label: "Dev", cls: "bg-warning text-dark" }; } + +// Menú del portal IT: acciones reales de gestión + tableros. +// Las futuras opciones se agregan como entradas de esta lista. +export function itMenu(modules: ModuleState[]): MenuItem[] { + return [ + { label: "Resumen", icon: "bx-grid-alt", href: "/tablero" }, + { label: "Aplicaciones", icon: "bx-layer", href: "/" }, + ...modules.map((m) => ({ + label: displayName(m.name), + icon: getModuleMeta(m.name).icon, + href: `/m/${m.name}`, + })), + { label: "Agregar", icon: "bx-plus", href: "/upload" }, + { label: "Actualizar", icon: "bx-refresh", href: "/actualizar" }, + { label: "Eliminar", icon: "bx-trash", href: "/eliminar" }, + ]; +}