diff --git a/src/app/actualizar/page.tsx b/src/app/actualizar/page.tsx index da696c1..7bbda1d 100644 --- a/src/app/actualizar/page.tsx +++ b/src/app/actualizar/page.tsx @@ -17,8 +17,25 @@ export default function Actualizar() { 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}`); + 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( @@ -88,7 +105,13 @@ export default function Actualizar() { {displayName(m.name)}{" "} {env.label} - + {" "} + {m.kind === "backend" && ( + backend + )}{" "} + {m.visible === false && ( + oculto + )}
{m.lastDeploy @@ -131,6 +154,14 @@ export default function Actualizar() { > +
diff --git a/src/app/eliminar/page.tsx b/src/app/eliminar/page.tsx index aab0a90..c0b501c 100644 --- a/src/app/eliminar/page.tsx +++ b/src/app/eliminar/page.tsx @@ -18,6 +18,24 @@ export default function Eliminar() { const [busy, setBusy] = useState(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 remove = async (name: string) => { const extra = purge ? " y SE ELIMINARÁ SU CÓDIGO" : ""; if ( @@ -99,7 +117,13 @@ export default function Eliminar() { {displayName(m.name)}{" "} {env.label} - + {" "} + {m.kind === "backend" && ( + backend + )}{" "} + {m.visible === false && ( + oculto + )} @@ -107,6 +131,15 @@ export default function Eliminar() { +
+ +
); diff --git a/src/app/m/[name]/page.tsx b/src/app/m/[name]/page.tsx index 9991975..583614b 100644 --- a/src/app/m/[name]/page.tsx +++ b/src/app/m/[name]/page.tsx @@ -15,7 +15,7 @@ import { memUsed, pct, shortUptime, - statFor, + statForModule, } from "@/lib/stats"; const API_URL = @@ -49,7 +49,7 @@ export default function ModuleBoard() { (m) => m.name.toLowerCase() === name.toLowerCase() ); const functions = getModuleFunctions(name); - const stat = mod ? statFor(stats, `${mod.name}-frontend`) : undefined; + const stat = mod ? statForModule(stats, mod.name) : undefined; const status = mod ? getStatusInfo(mod) : null; const env = mod ? envOf(mod.branch) : null; const cpu = pct(stat?.cpu); diff --git a/src/app/page.tsx b/src/app/page.tsx index 7061c6a..4c53036 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -11,6 +11,7 @@ import { moduleUrl, sectionOf, SECTIONS, + visibleModules, type ModuleState, } from "@/lib/sam"; @@ -19,7 +20,7 @@ export default function Home() { const loading = data === null && error === null; const fetchModules = refreshModules; - const modules = data?.modules ?? []; + const modules = visibleModules(data?.modules ?? []); const bySection: Record = {}; modules.forEach((m) => { diff --git a/src/app/upload/page.tsx b/src/app/upload/page.tsx index 5fb2e73..9045180 100644 --- a/src/app/upload/page.tsx +++ b/src/app/upload/page.tsx @@ -14,6 +14,9 @@ export default function UploadPage() { port: "", subdomain: "", category: "otras-apps", + kind: "frontend", + visible: true, + targetPort: "", }); const [loading, setLoading] = useState(false); const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null); @@ -21,7 +24,13 @@ export default function UploadPage() { const handleChange = ( e: React.ChangeEvent ) => { - setForm({ ...form, [e.target.name]: e.target.value }); + const { name, value } = e.target; + setForm((f) => ({ ...f, [name]: value })); + }; + + const handleCheck = (e: React.ChangeEvent) => { + const { name, checked } = e.target; + setForm((f) => ({ ...f, [name]: checked })); }; const handleSubmit = async (e: React.FormEvent) => { @@ -34,9 +43,12 @@ export default function UploadPage() { repoUrl: form.repoUrl.trim(), branch: form.branch.trim() || "main", category: form.category, + kind: form.kind, + visible: form.visible, }; if (form.port) payload.port = parseInt(form.port, 10); if (form.subdomain) payload.subdomain = form.subdomain.trim(); + if (form.targetPort) payload.targetPort = parseInt(form.targetPort, 10); try { const res = await fetch("/api/modules", { @@ -50,7 +62,7 @@ export default function UploadPage() { ok: true, message: `Módulo "${payload.name}" registrado. Deploy: ${data.webhook ? "webhook creado" : "sin webhook"}${data.deployLog ? " · " + data.deployLog.slice(0, 80) : ""}`, }); - setForm({ name: "", repoUrl: "", branch: "main", port: "", subdomain: "", category: "otras-apps" }); + setForm({ name: "", repoUrl: "", branch: "main", port: "", subdomain: "", category: "otras-apps", kind: "frontend", visible: true, targetPort: "" }); } else { setResult({ ok: false, message: data.error || `Error HTTP ${res.status}` }); } @@ -155,45 +167,100 @@ export default function UploadPage() { />
https://{form.subdomain || form.name || "subdominio"}.devops.norteamericano.cl
-
- - -
Se asigna automáticamente si se deja vacío.
-
+
+ + +
Se asigna automáticamente si se deja vacío.
+
+
+ + +
Puerto que expone el contenedor.
+
-
- - -
- Define en qué sección del índice SAM aparece el módulo. -
-
+
+ + +
+ Define en qué sección del índice SAM aparece el módulo. +
+
+ +
+ + +
+ El backend queda interno salvo subdominio explícito. +
+
+ +
+
+ + +
+
diff --git a/src/components/TableroView.tsx b/src/components/TableroView.tsx index 82577aa..6ba8472 100644 --- a/src/components/TableroView.tsx +++ b/src/components/TableroView.tsx @@ -11,6 +11,7 @@ import { moduleUrl, sectionOf, SECTIONS, + visibleModules, } from "@/lib/sam"; import { healthOf, @@ -18,6 +19,7 @@ import { pct, shortUptime, statFor, + statForModule, type ContainerStat, } from "@/lib/stats"; import type { ModuleState } from "@/lib/sam"; @@ -166,7 +168,7 @@ export default function TableroView() { const stats = useStats(); const loading = data === null; - const modules = data?.modules ?? []; + const modules = visibleModules(data?.modules ?? []); const ok = modules.filter((m) => healthOf(m) === "ok").length; const starting = modules.filter((m) => healthOf(m) === "starting").length; const down = modules.length - ok - starting; @@ -224,7 +226,7 @@ export default function TableroView() {
{list.map((m) => ( - + ))}
diff --git a/src/lib/sam.ts b/src/lib/sam.ts index 53d9f93..b775dab 100644 --- a/src/lib/sam.ts +++ b/src/lib/sam.ts @@ -20,10 +20,23 @@ export interface ModuleState { port: number; active: boolean; category?: string; + kind?: string; + visible?: boolean; + targetPort?: number; lastDeploy?: LastDeploy; container?: string | null; } +// Solo módulos visibles en índice, tableros y sidebars. +// (Gestión —actualizar/eliminar— muestra todos con su badge.) +export function visibleModules(modules: ModuleState[]): ModuleState[] { + return modules.filter((m) => m.visible !== false); +} + +export function isBackend(m: ModuleState): boolean { + return (m.kind || "frontend") === "backend"; +} + export interface ApiResponse { backend: { repoUrl: string; @@ -280,10 +293,11 @@ export function envOf(branch: string) { // 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[] { + const vis = visibleModules(modules); return [ { label: "Resumen", icon: "bx-grid-alt", href: "/tablero" }, { label: "Aplicaciones", icon: "bx-layer", href: "/" }, - ...modules.map((m) => ({ + ...vis.map((m) => ({ label: displayName(m.name), icon: getModuleMeta(m.name).icon, href: boardHref(m).href, diff --git a/src/lib/stats.ts b/src/lib/stats.ts index 3bd6a72..3a16a68 100644 --- a/src/lib/stats.ts +++ b/src/lib/stats.ts @@ -35,6 +35,16 @@ export function statFor( return stats.find((s) => s.name === containerName); } +export function statForModule( + stats: ContainerStat[], + moduleName: string +): ContainerStat | undefined { + return ( + statFor(stats, `${moduleName}-frontend`) || + statFor(stats, `${moduleName}-backend`) + ); +} + export type Health = "ok" | "starting" | "down"; export function healthOf(m: ModuleState): Health {