feat: secciones configurables del indice (apps/system, manifiesto MODULE_SECTION)

This commit is contained in:
Nurfog committed 2026-09-08 16:21:23 -03:00
1 parent 5c6d6bc46e
commit 3787152ca0
2 files changed
+175 -119

No files matched your search

+141 -119
View File
@@ -7,7 +7,10 @@ import {
getModuleMeta, getModuleMeta,
getStatusInfo, getStatusInfo,
moduleUrl, moduleUrl,
sectionOf,
SECTIONS,
type ApiResponse, type ApiResponse,
type ModuleState,
} from "@/lib/sam"; } from "@/lib/sam";
// Store externo: evita setState en efectos (regla react-hooks) // Store externo: evita setState en efectos (regla react-hooks)
@@ -69,6 +72,63 @@ export default function Home() {
const modules = data?.modules ?? []; const modules = data?.modules ?? [];
const backend = data?.backend; const backend = data?.backend;
const bySection: Record<string, ModuleState[]> = {};
modules.forEach((m) => {
const s = sectionOf(m.name);
if (!bySection[s]) bySection[s] = [];
bySection[s].push(m);
});
const renderModuleCard = (m: ModuleState) => {
const meta = getModuleMeta(m.name);
const status = getStatusInfo(m);
const url = moduleUrl(m);
const name = displayName(m.name);
return (
<div className="col-md-6" key={m.name}>
<div className="app-card p-2">
<table className="table table-borderless mb-0 align-middle">
<tbody>
<tr>
<td className="ps-3" style={{ width: 56 }}>
<i
className={`bx ${meta.icon} bx-sm bx-border text-light ${meta.bg}`}
/>
</td>
<td>
<Link
href={`/m/${m.name}`}
className="text-decoration-none text-dark d-block py-2 stretched-link"
>
{name}
</Link>
<div className="d-flex align-items-center gap-2">
<span className={`status-dot ${status.cls}`} />
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
{status.label} · :{m.port} · {m.branch}
</small>
</div>
</td>
<td className="text-end pe-3" style={{ width: 40 }}>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
title={`Abrir ${url}`}
className="position-relative"
style={{ zIndex: 2 }}
>
<i className="bx bx-window-open text-muted" />
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
};
return ( return (
<> <>
<div className="sidenav"> <div className="sidenav">
@@ -126,18 +186,7 @@ export default function Home() {
{!loading && !error && ( {!loading && !error && (
<div className="row row-cards"> <div className="row row-cards">
<div className="col-12"> {modules.length === 0 && (
<div className="alert alert-light section-header mb-3 py-2 px-3 d-flex justify-content-between align-items-center">
<span>
<i className="bx bx-grid-alt me-1" /> Selección de Aplicación
</span>
<span className="badge bg-primary rounded-pill">
{modules.length} módulo{modules.length !== 1 ? "s" : ""}
</span>
</div>
</div>
{modules.length === 0 ? (
<div className="col-12"> <div className="col-12">
<div className="app-card p-4 text-center"> <div className="app-card p-4 text-center">
<i className="bx bx-inbox fs-1 text-muted" /> <i className="bx bx-inbox fs-1 text-muted" />
@@ -149,117 +198,90 @@ export default function Home() {
</Link> </Link>
</div> </div>
</div> </div>
) : (
modules.map((m) => {
const meta = getModuleMeta(m.name);
const status = getStatusInfo(m);
const url = moduleUrl(m);
const name = displayName(m.name);
return (
<div className="col-md-6" key={m.name}>
<div className="app-card p-2">
<table className="table table-borderless mb-0 align-middle">
<tbody>
<tr>
<td className="ps-3" style={{ width: 56 }}>
<i
className={`bx ${meta.icon} bx-sm bx-border text-light ${meta.bg}`}
/>
</td>
<td>
<Link
href={`/m/${m.name}`}
className="text-decoration-none text-dark d-block py-2 stretched-link"
>
{name}
</Link>
<div className="d-flex align-items-center gap-2">
<span className={`status-dot ${status.cls}`} />
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
{status.label} · :{m.port} · {m.branch}
</small>
</div>
</td>
<td className="text-end pe-3" style={{ width: 40 }}>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
title={`Abrir ${url}`}
className="position-relative"
style={{ zIndex: 2 }}
>
<i className="bx bx-window-open text-muted" />
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
})
)} )}
{backend && ( {SECTIONS.map((section) => {
<> const list = bySection[section.id] ?? [];
<div className="col-12 mt-4 mb-2"> const isSystem = section.id === "system";
<div className="alert alert-light section-header mb-3 py-2 px-3"> if (!isSystem && list.length === 0) return null;
<i className="bx bx-server me-1" /> Backend & Sistema return (
<div className="col-12" key={section.id}>
<div className="row row-cards">
<div className="col-12">
<div className="alert alert-light section-header mb-3 py-2 px-3 d-flex justify-content-between align-items-center">
<span>
<i className={`bx ${section.icon} me-1`} />{" "}
{section.title}
</span>
{list.length > 0 && (
<span className="badge bg-primary rounded-pill">
{list.length} módulo
{list.length !== 1 ? "s" : ""}
</span>
)}
</div>
</div>
{list.map(renderModuleCard)}
{isSystem && backend && (
<>
<div className="col-md-6">
<div className="app-card p-2">
<table className="table table-borderless mb-0 align-middle">
<tbody>
<tr>
<td className="ps-3" style={{ width: 56 }}>
<i className="bx bx-code-alt bx-sm bx-border text-light bg-dark" />
</td>
<td>
<a
href="https://api.devops.norteamericano.cl/health"
target="_blank"
rel="noopener noreferrer"
className="text-decoration-none text-dark d-block py-2 stretched-link"
>
API Backend
</a>
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
{backend.container ? "Activo" : "Detenido"} · {backend.branch}
</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div className="col-md-6">
<div className="app-card p-2">
<table className="table table-borderless mb-0 align-middle">
<tbody>
<tr>
<td className="ps-3" style={{ width: 56 }}>
<i className="bx bx-cog bx-sm bx-border text-light bg-secondary" />
</td>
<td>
<Link
href="/upload"
className="text-decoration-none text-dark d-block py-2 stretched-link"
>
Gestionar Módulos
</Link>
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
Subir / actualizar proyectos
</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</>
)}
</div> </div>
</div> </div>
<div className="col-md-6"> );
<div className="app-card p-2"> })}
<table className="table table-borderless mb-0 align-middle">
<tbody>
<tr>
<td className="ps-3" style={{ width: 56 }}>
<i className="bx bx-code-alt bx-sm bx-border text-light bg-dark" />
</td>
<td>
<a
href="https://api.devops.norteamericano.cl/health"
target="_blank"
rel="noopener noreferrer"
className="text-decoration-none text-dark d-block py-2 stretched-link"
>
API Backend
</a>
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
{backend.container ? "Activo" : "Detenido"} · {backend.branch}
</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div className="col-md-6">
<div className="app-card p-2">
<table className="table table-borderless mb-0 align-middle">
<tbody>
<tr>
<td className="ps-3" style={{ width: 56 }}>
<i className="bx bx-cog bx-sm bx-border text-light bg-secondary" />
</td>
<td>
<Link
href="/upload"
className="text-decoration-none text-dark d-block py-2 stretched-link"
>
Gestionar Módulos
</Link>
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
Subir / actualizar proyectos
</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</>
)}
<div className="col-12 mt-4 mb-4 text-center"> <div className="col-12 mt-4 mb-4 text-center">
<small className="text-muted"> <small className="text-muted">
+34
View File
@@ -167,3 +167,37 @@ export function displayName(name: string) {
export function moduleUrl(m: ModuleState) { export function moduleUrl(m: ModuleState) {
return `https://${m.subdomain}.devops.norteamericano.cl`; return `https://${m.subdomain}.devops.norteamericano.cl`;
} }
// ============================================================
// Secciones del índice: dónde aparece cada módulo.
// - Todo módulo nuevo cae en "apps" (Aplicaciones) automáticamente.
// - Para mover un módulo a otra sección, agrégalo a MODULE_SECTION.
// - Para crear una sección futura: agrega una entrada en SECTIONS
// (id, título, icono) y la regla en MODULE_SECTION o en
// sectionOf(). El índice la renderiza sola si tiene módulos.
// ============================================================
export interface Section {
id: string;
title: string;
icon: string;
}
export const SECTIONS: Section[] = [
{ id: "apps", title: "Selección de Aplicación", icon: "bx-grid-alt" },
{ id: "system", title: "Backend & Sistema", icon: "bx-server" },
];
const MODULE_SECTION: Record<string, string> = {
// Ejemplos:
// contabilidad: "system",
// pagos: "system",
};
export function sectionOf(name: string): string {
const key = name.toLowerCase();
if (MODULE_SECTION[key]) return MODULE_SECTION[key];
const known = SECTIONS.some((s) => s.id === key);
if (known) return key;
return "apps";
}