feat: indice estilo lanzador sam.norteamericano.cl, sidebar solo en IT + rewrite it.sam

This commit is contained in:
Nurfog committed 2026-09-08 17:10:44 -03:00
1 parent af638d4154
commit 347d8510b4
3 files changed
+205 -171

No files matched your search

+168 -165
View File
@@ -1,9 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import SamShell from "@/components/SamShell"; import { useModules, refreshModules } from "@/lib/store";
import TableroView from "@/components/TableroView";
import { useModules, refreshModules, useSiteSection } from "@/lib/store";
import { import {
displayName, displayName,
envOf, envOf,
@@ -14,35 +12,14 @@ import {
sectionOf, sectionOf,
SECTIONS, SECTIONS,
type ModuleState, type ModuleState,
type RefApp,
} from "@/lib/sam"; } from "@/lib/sam";
const sectionMenu = [
...SECTIONS.map((s) => ({ label: s.title, icon: s.icon, href: `/#${s.id}` })),
{ label: "Tablero IT", icon: "bx-pie-chart-alt-2", href: "/tablero" },
];
export default function Home() { export default function Home() {
const { data, error } = useModules(); const { data, error } = useModules();
const site = useSiteSection();
const loading = data === null && error === null; const loading = data === null && error === null;
const fetchModules = refreshModules; const fetchModules = refreshModules;
// it.sam.devops... aterriza directo en el tablero
if (site === "it") {
return (
<SamShell
brand="IT"
subtitle="Tablero de gestión"
menu={sectionMenu}
activeHref="/tablero"
topUser="SAM"
backHref="/"
>
<TableroView />
</SamShell>
);
}
const modules = data?.modules ?? []; const modules = data?.modules ?? [];
const backend = data?.backend; const backend = data?.backend;
@@ -107,13 +84,7 @@ export default function Home() {
); );
}; };
const renderRefCard = (app: { const renderRefCard = (app: RefApp) => (
name: string;
title: string;
icon: string;
bg: string;
url: string;
}) => (
<div className="col-md-6" key={`ref-${app.name}`}> <div className="col-md-6" key={`ref-${app.name}`}>
<div className="app-card p-2"> <div className="app-card p-2">
<table className="table table-borderless mb-0 align-middle"> <table className="table table-borderless mb-0 align-middle">
@@ -150,149 +121,181 @@ export default function Home() {
); );
return ( return (
<SamShell <>
brand="SAM" <div className="sidenav">
subtitle="Sistema de Administración de Matrículas" <div className="row text-light login-main-text">
menu={sectionMenu} <div className="col-12 text-center">
topUser="SAM" <div
> className="mx-auto d-flex align-items-center justify-content-center bg-white rounded-3 overflow-hidden"
{loading && ( style={{ width: 80, height: 80 }}
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status" />
<p className="mt-2 text-muted">Cargando módulos...</p>
</div>
)}
{error && (
<div className="alert alert-danger d-flex align-items-center">
<i className="bx bx-error-circle me-2 fs-5" />
<div>
<strong>Error:</strong> {error}
<button
onClick={fetchModules}
className="btn btn-sm btn-outline-danger ms-3"
> >
Reintentar <img src="/logo-sam.png" alt="SAM" style={{ width: 64, height: 64, objectFit: "contain" }} />
</button> </div>
</div>
<div className="col-12 text-center mt-3">
<h1 className="fw-bold fs-2">Sistema SAM</h1>
</div>
<div className="col-12 text-center">
<h4 className="fw-semibold opacity-75">Aplicaciones</h4>
</div>
<div className="col-12 text-center mt-4 d-flex flex-column gap-2 align-items-center">
<Link href="/tablero" className="btn btn-light btn-sm px-4">
<i className="bx bx-pie-chart-alt-2 me-1" /> Tablero IT
</Link>
<Link href="/upload" className="btn btn-outline-light btn-sm px-4">
<i className="bx bx-plus me-1" /> Nuevo Módulo
</Link>
</div>
<div className="col-12 text-center mt-3">
<small className="opacity-50">Portal de integración</small>
</div> </div>
</div> </div>
)} </div>
{!loading && !error && ( <div className="main">
<div className="row row-cards"> <div className="row" id="modulos">
{modules.length === 0 && ( <div className="col-lg-1" />
<div className="col-12"> <div className="col-lg-10 mt-4">
<div className="app-card p-4 text-center"> {loading && (
<i className="bx bx-inbox fs-1 text-muted" /> <div className="text-center py-5">
<p className="text-muted mt-2 mb-3"> <div className="spinner-border text-primary" role="status" />
No hay módulos integrados. ¡Sube tu primer proyecto! <p className="mt-2 text-muted">Cargando módulos...</p>
</p>
<Link href="/upload" className="btn btn-primary btn-sm">
<i className="bx bx-upload me-1" /> Subir Proyecto
</Link>
</div> </div>
</div> )}
)}
{SECTIONS.map((section) => { {error && (
const list = bySection[section.id] ?? []; <div className="alert alert-danger d-flex align-items-center">
const isReview = section.id === "otras-apps"; <i className="bx bx-error-circle me-2 fs-5" />
if (!isReview && list.length === 0) return null; <div>
return ( <strong>Error:</strong> {error}
<div className="col-12" key={section.id} id={section.id}> <button
<div className="row row-cards"> onClick={fetchModules}
<div className="col-12"> className="btn btn-sm btn-outline-danger ms-3"
<div className="alert alert-light section-header mb-3 py-2 px-3 d-flex justify-content-between align-items-center"> >
<span> Reintentar
<i className={`bx ${section.icon} me-1`} />{" "} </button>
{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)}
{section.id === "sam" &&
REFERENCE_APPS.filter(
(a) =>
!list.some((m) => m.name.toLowerCase() === a.name)
).map(renderRefCard)}
{isReview && 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-12 mt-4 mb-4 text-center"> {!loading && !error && (
<small className="text-muted"> <div className="row row-cards">
Auto-actualizado cada 15s ·{" "} {modules.length === 0 && (
<a href="#" onClick={(e) => { e.preventDefault(); fetchModules(); }}> <div className="col-12">
actualizar ahora <div className="app-card p-4 text-center">
</a> <i className="bx bx-inbox fs-1 text-muted" />
</small> <p className="text-muted mt-2 mb-3">
No hay módulos integrados. ¡Sube tu primer proyecto!
</p>
<Link href="/upload" className="btn btn-primary btn-sm">
<i className="bx bx-upload me-1" /> Subir Proyecto
</Link>
</div>
</div>
)}
{SECTIONS.map((section) => {
const list = bySection[section.id] ?? [];
const isReview = section.id === "otras-apps";
if (!isReview && list.length === 0) return null;
return (
<div className="col-12" key={section.id} id={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)}
{section.id === "sam" &&
REFERENCE_APPS.filter(
(a) =>
!list.some((m) => m.name.toLowerCase() === a.name)
).map(renderRefCard)}
{isReview && 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 className="col-12 mt-4 mb-4 text-center">
<small className="text-muted">
Auto-actualizado cada 15s ·{" "}
<a href="#" onClick={(e) => { e.preventDefault(); fetchModules(); }}>
actualizar ahora
</a>
</small>
</div>
</div>
)}
</div> </div>
</div> </div>
)} </div>
</SamShell> </>
); );
} }
+20 -6
View File
@@ -2,17 +2,31 @@
import SamShell from "@/components/SamShell"; import SamShell from "@/components/SamShell";
import TableroView from "@/components/TableroView"; import TableroView from "@/components/TableroView";
import { useModules } from "@/lib/store";
import { displayName, getModuleMeta } from "@/lib/sam";
const menu = [ // App IT: sidebar con todas sus opciones (resumen, aplicaciones,
{ label: "Resumen", icon: "bx-grid-alt", href: "#resumen" }, // tableros por módulo y gestión). Se sirve en /tablero y en
{ label: "Aplicaciones", icon: "bx-layer", href: "/" }, // it.sam.devops.norteamericano.cl
];
export default function Tablero() { 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 ( return (
<SamShell <SamShell
brand="IT" brand="IT"
subtitle="Tablero de gestión" subtitle="Gestión de aplicaciones"
menu={menu} menu={menu}
activeHref="#resumen" activeHref="#resumen"
topUser="SAM" topUser="SAM"
+17
View File
@@ -0,0 +1,17 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// it.sam.devops... muestra el tablero IT manteniendo la URL.
export function middleware(req: NextRequest) {
const host = (req.headers.get("host") || "").toLowerCase();
if (host.startsWith("it.sam.") && req.nextUrl.pathname === "/") {
const url = req.nextUrl.clone();
url.pathname = "/tablero";
return NextResponse.rewrite(url);
}
return NextResponse.next();
}
export const config = {
matcher: ["/"],
};