feat: tablero por modulo funcional (estado+carga+funciones en sidebar)

This commit is contained in:
Nurfog committed 2026-09-09 09:44:58 -03:00
1 parent a209133e43
commit e20e9d6657
1 file changed
+130 -89
+130 -89
View File
@@ -1,67 +1,77 @@
"use client"; "use client";
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
import { useParams } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import SamShell from "@/components/SamShell"; import SamShell from "@/components/SamShell";
import { useModules, useStats } from "@/lib/store";
import { useParams } from "next/navigation";
import { import {
displayName, displayName,
envOf,
getModuleFunctions, getModuleFunctions,
getStatusInfo,
moduleUrl, moduleUrl,
type ApiResponse,
type ModuleState,
} from "@/lib/sam"; } from "@/lib/sam";
import {
memUsed,
pct,
shortUptime,
statFor,
} from "@/lib/stats";
const API_URL = const API_URL =
process.env.NEXT_PUBLIC_API_URL || "https://api.devops.norteamericano.cl"; process.env.NEXT_PUBLIC_API_URL || "https://api.devops.norteamericano.cl";
function subscribeHash(cb: () => void) { function Bar({ value, color }: { value: number; color: string }) {
window.addEventListener("hashchange", cb); return (
return () => window.removeEventListener("hashchange", cb); <div className="progress" style={{ height: 8 }}>
} <div
className="progress-bar"
function snapshotHash() { role="progressbar"
return window.location.hash.slice(1); style={{ width: `${value}%`, backgroundColor: color }}
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={100}
/>
</div>
);
} }
// Tablero funcional del módulo: estado + carga de trabajo +
// funciones API en el sidebar (plantilla SamShell).
export default function ModuleBoard() { export default function ModuleBoard() {
const params = useParams(); const params = useParams();
const name = Array.isArray(params.name) ? params.name[0] : params.name ?? ""; const name = Array.isArray(params.name) ? params.name[0] : params.name ?? "";
const [data, setData] = useState<ApiResponse | null>(null); const { data } = useModules();
const [loading, setLoading] = useState(true); const stats = useStats();
const hash = const loading = data === null;
useSyncExternalStore(subscribeHash, snapshotHash, () => "") || null;
useEffect(() => { const mod = data?.modules.find(
fetch("/api/modules", { cache: "no-store" }) (m) => m.name.toLowerCase() === name.toLowerCase()
.then((r) => r.json())
.then((j) => setData(j))
.catch(() => setData(null))
.finally(() => setLoading(false));
}, []);
const mod: ModuleState | undefined = useMemo(
() => data?.modules.find((m) => m.name.toLowerCase() === name.toLowerCase()),
[data, name]
); );
const functions = getModuleFunctions(name);
const functions = useMemo(() => getModuleFunctions(name), [name]); const stat = mod ? statFor(stats, `${mod.name}-frontend`) : undefined;
const activePath = hash || functions[0]?.path || null; const status = mod ? getStatusInfo(mod) : null;
const activeFn = functions.find((f) => f.path === activePath) ?? functions[0]; const env = mod ? envOf(mod.branch) : null;
const cpu = pct(stat?.cpu);
const mem = pct(stat?.memPerc);
const title = mod ? displayName(mod.name) : displayName(name || "Módulo"); const title = mod ? displayName(mod.name) : displayName(name || "Módulo");
const menu = functions.map((f) => ({ const menu = [
label: f.label, { label: "Estado", icon: "bx-grid-alt", href: "#estado" },
icon: f.icon, ...functions.map((f) => ({
href: `#${f.path}`, label: f.label,
})); icon: f.icon,
href: `/m/${name}#${f.path}`,
})),
{ label: "Aplicaciones", icon: "bx-layer", href: "/" },
];
return ( return (
<SamShell <SamShell
brand={title} brand={title}
subtitle="Tablero del módulo" subtitle="Tablero del módulo"
menu={menu} menu={menu}
activeHref={activeFn ? `#${activeFn.path}` : undefined} activeHref="#estado"
topUser="SAM" topUser="SAM"
topSede={mod ? `${mod.subdomain}.devops.norteamericano.cl` : undefined} topSede={mod ? `${mod.subdomain}.devops.norteamericano.cl` : undefined}
backHref="/" backHref="/"
@@ -82,78 +92,109 @@ export default function ModuleBoard() {
</div> </div>
)} )}
{!loading && mod && activeFn && ( {!loading && mod && status && env && (
<> <>
<div className="alert alert-light section-header mb-3 py-2 px-3 d-flex justify-content-between align-items-center flex-wrap gap-2"> <div className="row row-cards" id="estado">
<span>
<i className={`bx ${activeFn.icon} me-2`} />
{activeFn.label}
<small className="text-muted ms-2 d-block d-md-inline">
{activeFn.desc}
</small>
</span>
<span className="d-flex gap-2">
<span className="badge bg-secondary font-monospace">
GET {activeFn.path}
</span>
<a
href={`${API_URL}/${activeFn.path}`}
target="_blank"
rel="noopener noreferrer"
className="btn btn-sm btn-outline-primary"
>
<i className="bx bx-link-external me-1" />
API
</a>
<a
href={moduleUrl(mod)}
target="_blank"
rel="noopener noreferrer"
className="btn btn-sm btn-primary"
>
<i className="bx bx-window-open me-1" />
Abrir aplicación
</a>
</span>
</div>
<div className="row row-cards">
<div className="col-12"> <div className="col-12">
<div className="app-card p-4"> <div className="app-card p-4">
<h6 className="fw-bold mb-3"> <div className="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<i className="bx bx-list-ul me-2" /> <h6 className="fw-bold mb-0">
Funciones disponibles de la API <i className="bx bx-pulse me-2" />
</h6> Estado y carga de trabajo
</h6>
<span className="d-flex gap-2 align-items-center">
<span className={`status-dot ${status.cls}`} />
<strong>{status.label}</strong>
<span className={`badge ${env.cls} badge-respaldo`}>
{env.label}
</span>
</span>
</div>
<div className="row g-3 mb-3">
<div className="col-md-3 col-6">
<small className="text-muted d-block">Uptime</small>
<strong>{shortUptime(stat?.status ?? mod.container)}</strong>
</div>
<div className="col-md-3 col-6">
<small className="text-muted d-block">Puerto</small>
<strong>:{mod.port}</strong>
</div>
<div className="col-md-3 col-6">
<small className="text-muted d-block">Rama</small>
<strong>{mod.branch}</strong>
</div>
<div className="col-md-3 col-6">
<small className="text-muted d-block">Red</small>
<small className="fw-semibold">{stat?.netIO ?? "—"}</small>
</div>
</div>
<div className="mb-2">
<div className="d-flex justify-content-between">
<small className="text-muted">CPU</small>
<small className="fw-semibold">{stat?.cpu ?? "—"}</small>
</div>
<Bar value={cpu} color="#0d6efd" />
</div>
<div className="mb-3">
<div className="d-flex justify-content-between">
<small className="text-muted">MEM {memUsed(stat)}</small>
<small className="fw-semibold">{stat?.memPerc ?? "—"}</small>
</div>
<Bar value={mem} color="#6f42c1" />
</div>
<div className="d-flex gap-2 flex-wrap">
<a
href={moduleUrl(mod)}
target="_blank"
rel="noopener noreferrer"
className="btn btn-sm btn-primary"
>
<i className="bx bx-window-open me-1" />
Abrir aplicación
</a>
<a
href={`${API_URL}/api/${mod.name}`}
target="_blank"
rel="noopener noreferrer"
className="btn btn-sm btn-outline-primary"
>
<i className="bx bx-link-external me-1" />
Ver en API
</a>
</div>
</div>
</div>
</div>
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
<i className="bx bx-list-ul me-1" /> Funciones disponibles de la API
</div>
<div className="row row-cards">
<div className="col-12">
<div className="app-card p-3">
<div className="list-group"> <div className="list-group">
{functions.map((f) => ( {functions.map((f) => (
<button <a
key={f.path} key={f.path}
onClick={() => { href={`${API_URL}/${f.path}`}
window.location.hash = f.path; target="_blank"
}} rel="noopener noreferrer"
className={`list-group-item list-group-item-action d-flex justify-content-between align-items-center${ className="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
f.path === activeFn.path ? " active" : ""
}`}
> >
<span> <span>
<i className={`bx ${f.icon} me-2`} /> <i className={`bx ${f.icon} me-2`} />
{f.label} {f.label}
<small className="text-muted d-block">{f.desc}</small> <small className="text-muted d-block">{f.desc}</small>
</span> </span>
<code className="small">{f.path}</code> <code className="small">GET {f.path}</code>
</button> </a>
))} ))}
</div> </div>
<p className="text-muted small mt-3 mb-0">
Las funciones se consumen desde {API_URL}. Usa «Abrir
aplicación» para operar en {moduleUrl(mod)}.
</p>
</div> </div>
</div> </div>
</div> </div>
</> </>
)} )}
</SamShell> </SamShell>
); );
} }