feat: sidebar de aplicaciones + landing IT por hostname (it.sam)
This commit is contained in:
1 parent
3949224d94
commit
af638d4154
4 files changed
+355
-336
No files matched your search
+73
-79
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useModules, refreshModules } from "@/lib/store";
|
import SamShell from "@/components/SamShell";
|
||||||
|
import TableroView from "@/components/TableroView";
|
||||||
|
import { useModules, refreshModules, useSiteSection } from "@/lib/store";
|
||||||
import {
|
import {
|
||||||
displayName,
|
displayName,
|
||||||
envOf,
|
envOf,
|
||||||
@@ -11,16 +13,36 @@ import {
|
|||||||
REFERENCE_APPS,
|
REFERENCE_APPS,
|
||||||
sectionOf,
|
sectionOf,
|
||||||
SECTIONS,
|
SECTIONS,
|
||||||
type ApiResponse,
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -31,42 +53,6 @@ export default function Home() {
|
|||||||
bySection[s].push(m);
|
bySection[s].push(m);
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderRefCard = (app: RefApp) => (
|
|
||||||
<div className="col-md-6" key={`ref-${app.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 ${app.icon} bx-sm bx-border text-light ${app.bg}`}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
href={app.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
|
||||||
>
|
|
||||||
{app.title}
|
|
||||||
</a>
|
|
||||||
<div className="d-flex align-items-center gap-2">
|
|
||||||
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
|
||||||
sam.norteamericano.cl
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="text-end pe-3" style={{ width: 40 }}>
|
|
||||||
<i className="bx bx-chevron-right text-muted" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderModuleCard = (m: ModuleState) => {
|
const renderModuleCard = (m: ModuleState) => {
|
||||||
const meta = getModuleMeta(m.name);
|
const meta = getModuleMeta(m.name);
|
||||||
const status = getStatusInfo(m);
|
const status = getStatusInfo(m);
|
||||||
@@ -121,42 +107,55 @@ export default function Home() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const renderRefCard = (app: {
|
||||||
<>
|
name: string;
|
||||||
<div className="sidenav">
|
title: string;
|
||||||
<div className="row text-light login-main-text">
|
icon: string;
|
||||||
<div className="col-12 text-center">
|
bg: string;
|
||||||
<div
|
url: string;
|
||||||
className="mx-auto d-flex align-items-center justify-content-center bg-white rounded-3 overflow-hidden"
|
}) => (
|
||||||
style={{ width: 80, height: 80 }}
|
<div className="col-md-6" key={`ref-${app.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 ${app.icon} bx-sm bx-border text-light ${app.bg}`}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href={app.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
||||||
>
|
>
|
||||||
<img src="/logo-sam.png" alt="SAM" style={{ width: 64, height: 64, objectFit: "contain" }} />
|
{app.title}
|
||||||
</div>
|
</a>
|
||||||
</div>
|
<div className="d-flex align-items-center gap-2">
|
||||||
<div className="col-12 text-center mt-3">
|
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||||
<h1 className="fw-bold fs-2">Sistema SAM</h1>
|
sam.norteamericano.cl
|
||||||
</div>
|
</small>
|
||||||
<div className="col-12 text-center">
|
</div>
|
||||||
<h4 className="fw-semibold opacity-75">Aplicaciones</h4>
|
</td>
|
||||||
</div>
|
<td className="text-end pe-3" style={{ width: 40 }}>
|
||||||
<div className="col-12 text-center mt-4 d-flex flex-column gap-2 align-items-center">
|
<i className="bx bx-chevron-right text-muted" />
|
||||||
<Link href="/tablero" className="btn btn-light btn-sm px-4">
|
</td>
|
||||||
<i className="bx bx-pie-chart-alt-2 me-1" /> Tablero
|
</tr>
|
||||||
</Link>
|
</tbody>
|
||||||
<Link href="/upload" className="btn btn-outline-light btn-sm px-4">
|
</table>
|
||||||
<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>
|
||||||
|
);
|
||||||
|
|
||||||
<div className="main">
|
return (
|
||||||
<div className="row" id="modulos">
|
<SamShell
|
||||||
<div className="col-lg-1" />
|
brand="SAM"
|
||||||
<div className="col-lg-10 mt-4">
|
subtitle="Sistema de Administración de Matrículas"
|
||||||
|
menu={sectionMenu}
|
||||||
|
topUser="SAM"
|
||||||
|
>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="text-center py-5">
|
<div className="text-center py-5">
|
||||||
<div className="spinner-border text-primary" role="status" />
|
<div className="spinner-border text-primary" role="status" />
|
||||||
@@ -200,7 +199,7 @@ export default function Home() {
|
|||||||
const isReview = section.id === "otras-apps";
|
const isReview = section.id === "otras-apps";
|
||||||
if (!isReview && list.length === 0) return null;
|
if (!isReview && list.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className="col-12" key={section.id}>
|
<div className="col-12" key={section.id} id={section.id}>
|
||||||
<div className="row row-cards">
|
<div className="row row-cards">
|
||||||
<div className="col-12">
|
<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">
|
<div className="alert alert-light section-header mb-3 py-2 px-3 d-flex justify-content-between align-items-center">
|
||||||
@@ -222,9 +221,7 @@ export default function Home() {
|
|||||||
{section.id === "sam" &&
|
{section.id === "sam" &&
|
||||||
REFERENCE_APPS.filter(
|
REFERENCE_APPS.filter(
|
||||||
(a) =>
|
(a) =>
|
||||||
!list.some(
|
!list.some((m) => m.name.toLowerCase() === a.name)
|
||||||
(m) => m.name.toLowerCase() === a.name
|
|
||||||
)
|
|
||||||
).map(renderRefCard)}
|
).map(renderRefCard)}
|
||||||
|
|
||||||
{isReview && backend && (
|
{isReview && backend && (
|
||||||
@@ -296,9 +293,6 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SamShell>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+6
-257
@@ -1,275 +1,24 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import SamShell from "@/components/SamShell";
|
import SamShell from "@/components/SamShell";
|
||||||
import { useModules, useStats } from "@/lib/store";
|
import TableroView from "@/components/TableroView";
|
||||||
import {
|
|
||||||
displayName,
|
|
||||||
envOf,
|
|
||||||
getModuleMeta,
|
|
||||||
moduleUrl,
|
|
||||||
sectionOf,
|
|
||||||
SECTIONS,
|
|
||||||
type ApiResponse,
|
|
||||||
type ModuleState,
|
|
||||||
} from "@/lib/sam";
|
|
||||||
import {
|
|
||||||
healthOf,
|
|
||||||
memUsed,
|
|
||||||
pct,
|
|
||||||
shortUptime,
|
|
||||||
statFor,
|
|
||||||
type ContainerStat,
|
|
||||||
} from "@/lib/stats";
|
|
||||||
|
|
||||||
function Donut({ ok, starting, down }: { ok: number; starting: number; down: number }) {
|
|
||||||
const total = ok + starting + down;
|
|
||||||
if (total === 0)
|
|
||||||
return <p className="text-muted mb-0">Sin módulos integrados.</p>;
|
|
||||||
const R = 54;
|
|
||||||
const C = 2 * Math.PI * R;
|
|
||||||
const segs = [
|
|
||||||
{ v: ok, color: "#198754" },
|
|
||||||
{ v: starting, color: "#ffc107" },
|
|
||||||
{ v: down, color: "#dc3545" },
|
|
||||||
];
|
|
||||||
let acc = 0;
|
|
||||||
return (
|
|
||||||
<div className="d-flex align-items-center gap-4 flex-wrap">
|
|
||||||
<svg width="140" height="140" viewBox="0 0 140 140">
|
|
||||||
<circle cx="70" cy="70" r={R} fill="none" stroke="#e9ecef" strokeWidth="16" />
|
|
||||||
{segs.map((s, i) => {
|
|
||||||
if (s.v === 0) return null;
|
|
||||||
const frac = s.v / total;
|
|
||||||
const el = (
|
|
||||||
<circle
|
|
||||||
key={i}
|
|
||||||
cx="70"
|
|
||||||
cy="70"
|
|
||||||
r={R}
|
|
||||||
fill="none"
|
|
||||||
stroke={s.color}
|
|
||||||
strokeWidth="16"
|
|
||||||
strokeDasharray={`${frac * C} ${C}`}
|
|
||||||
strokeDashoffset={-acc * C}
|
|
||||||
transform="rotate(-90 70 70)"
|
|
||||||
strokeLinecap="butt"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
acc += frac;
|
|
||||||
return el;
|
|
||||||
})}
|
|
||||||
<text x="70" y="66" textAnchor="middle" fontSize="26" fontWeight="700" fill="#00285d">
|
|
||||||
{total}
|
|
||||||
</text>
|
|
||||||
<text x="70" y="86" textAnchor="middle" fontSize="12" fill="#6c757d">
|
|
||||||
módulos
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
<ul className="list-unstyled mb-0">
|
|
||||||
<li className="mb-1">
|
|
||||||
<span className="status-dot status-healthy me-2" />
|
|
||||||
Activos <strong>{ok}</strong>
|
|
||||||
</li>
|
|
||||||
<li className="mb-1">
|
|
||||||
<span className="status-dot status-starting me-2" />
|
|
||||||
Iniciando <strong>{starting}</strong>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span className="status-dot status-unhealthy me-2" />
|
|
||||||
Detenidos <strong>{down}</strong>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Bar({ value, color }: { value: number; color: string }) {
|
|
||||||
return (
|
|
||||||
<div className="progress" style={{ height: 8 }}>
|
|
||||||
<div
|
|
||||||
className="progress-bar"
|
|
||||||
role="progressbar"
|
|
||||||
style={{ width: `${value}%`, backgroundColor: color }}
|
|
||||||
aria-valuenow={value}
|
|
||||||
aria-valuemin={0}
|
|
||||||
aria-valuemax={100}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AppCard({ m, stat }: { m: ModuleState; stat?: ContainerStat }) {
|
|
||||||
const meta = getModuleMeta(m.name);
|
|
||||||
const env = envOf(m.branch);
|
|
||||||
const cpu = pct(stat?.cpu);
|
|
||||||
const mem = pct(stat?.memPerc);
|
|
||||||
return (
|
|
||||||
<div className="col-md-6 col-xl-4">
|
|
||||||
<div className="app-card p-3 h-100">
|
|
||||||
<div className="d-flex align-items-center gap-2 mb-2">
|
|
||||||
<i className={`bx ${meta.icon} bx-border text-light ${meta.bg}`} />
|
|
||||||
<div>
|
|
||||||
<strong>{displayName(m.name)}</strong>
|
|
||||||
<div>
|
|
||||||
<small className="text-muted">
|
|
||||||
{shortUptime(stat?.status ?? m.container)} · :{m.port}
|
|
||||||
</small>{" "}
|
|
||||||
<span className={`badge ${env.cls} badge-respaldo`}>{env.label}</span>
|
|
||||||
</div>
|
|
||||||
</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-2">
|
|
||||||
<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 mt-2">
|
|
||||||
<Link href={`/m/${m.name}`} className="btn btn-sm btn-outline-primary flex-fill">
|
|
||||||
<i className="bx bx-grid-alt me-1" /> Tablero
|
|
||||||
</Link>
|
|
||||||
<a
|
|
||||||
href={moduleUrl(m)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="btn btn-sm btn-outline-secondary"
|
|
||||||
>
|
|
||||||
<i className="bx bx-window-open" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Tablero() {
|
|
||||||
const { data } = useModules();
|
|
||||||
const stats = useStats();
|
|
||||||
const loading = data === null;
|
|
||||||
|
|
||||||
const modules = 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;
|
|
||||||
const prod = modules.filter(
|
|
||||||
(m) => m.branch.toLowerCase() === "main" || m.branch.toLowerCase() === "master"
|
|
||||||
).length;
|
|
||||||
|
|
||||||
const menu = [
|
const menu = [
|
||||||
{ label: "Resumen", icon: "bx-grid-alt", href: "#resumen" },
|
{ label: "Resumen", icon: "bx-grid-alt", href: "#resumen" },
|
||||||
...modules.map((m) => ({
|
{ label: "Aplicaciones", icon: "bx-layer", href: "/" },
|
||||||
label: displayName(m.name),
|
|
||||||
icon: getModuleMeta(m.name).icon,
|
|
||||||
href: `/m/${m.name}`,
|
|
||||||
})),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const core = ["sam-api", "sam-deployer", "sam-postgres", "sam-redis"]
|
export default function Tablero() {
|
||||||
.map((n) => statFor(stats, n))
|
|
||||||
.filter((s): s is ContainerStat => Boolean(s));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SamShell
|
<SamShell
|
||||||
brand="Tablero"
|
brand="IT"
|
||||||
subtitle="Gestión de aplicaciones"
|
subtitle="Tablero de gestión"
|
||||||
menu={menu}
|
menu={menu}
|
||||||
activeHref="#resumen"
|
activeHref="#resumen"
|
||||||
topUser="SAM"
|
topUser="SAM"
|
||||||
backHref="/"
|
backHref="/"
|
||||||
>
|
>
|
||||||
{loading && (
|
<TableroView />
|
||||||
<div className="text-center py-5">
|
|
||||||
<div className="spinner-border text-primary" role="status" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && (
|
|
||||||
<>
|
|
||||||
<div className="row row-cards" id="resumen">
|
|
||||||
<div className="col-12">
|
|
||||||
<div className="app-card p-4">
|
|
||||||
<h6 className="fw-bold mb-3">
|
|
||||||
<i className="bx bx-pie-chart-alt-2 me-2" />
|
|
||||||
Estado de todos los módulos
|
|
||||||
</h6>
|
|
||||||
<div className="d-flex gap-5 flex-wrap align-items-center">
|
|
||||||
<Donut ok={ok} starting={starting} down={down} />
|
|
||||||
<div>
|
|
||||||
<div className="mb-1">
|
|
||||||
<small className="text-muted">Producción (main)</small>
|
|
||||||
<div className="fw-bold fs-5">{prod}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<small className="text-muted">Desarrollo (otras ramas)</small>
|
|
||||||
<div className="fw-bold fs-5">{modules.length - prod}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{SECTIONS.map((section) => {
|
|
||||||
const list = modules.filter(
|
|
||||||
(m) => sectionOf(m.name, m.category) === section.id
|
|
||||||
);
|
|
||||||
if (list.length === 0) return null;
|
|
||||||
return (
|
|
||||||
<div key={section.id}>
|
|
||||||
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
|
|
||||||
<i className={`bx ${section.icon} me-1`} /> {section.title}
|
|
||||||
</div>
|
|
||||||
<div className="row row-cards">
|
|
||||||
{list.map((m) => (
|
|
||||||
<AppCard key={m.name} m={m} stat={statFor(stats, `${m.name}-frontend`)} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{core.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
|
|
||||||
<i className="bx bx-server me-1" /> Servicios base
|
|
||||||
</div>
|
|
||||||
<div className="row row-cards">
|
|
||||||
{core.map((s) => (
|
|
||||||
<div className="col-md-6 col-xl-3" key={s.name}>
|
|
||||||
<div className="app-card p-3">
|
|
||||||
<strong className="small">{s.name}</strong>
|
|
||||||
<div className="small text-muted mb-2">
|
|
||||||
{shortUptime(s.status)} · {s.pids} proc
|
|
||||||
</div>
|
|
||||||
<div className="d-flex justify-content-between">
|
|
||||||
<small className="text-muted">CPU {s.cpu}</small>
|
|
||||||
<small className="text-muted">MEM {s.memPerc}</small>
|
|
||||||
</div>
|
|
||||||
<Bar value={pct(s.cpu)} color="#0d6efd" />
|
|
||||||
<div className="mt-1">
|
|
||||||
<Bar value={pct(s.memPerc)} color="#6f42c1" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p className="text-muted small mt-4 mb-4 text-center">
|
|
||||||
Auto-actualizado cada 30s · Carga de trabajo en tiempo real (docker stats)
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</SamShell>
|
</SamShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import SamShell from "@/components/SamShell";
|
||||||
|
import { useModules, useStats } from "@/lib/store";
|
||||||
|
import {
|
||||||
|
displayName,
|
||||||
|
envOf,
|
||||||
|
getModuleMeta,
|
||||||
|
moduleUrl,
|
||||||
|
sectionOf,
|
||||||
|
SECTIONS,
|
||||||
|
} from "@/lib/sam";
|
||||||
|
import {
|
||||||
|
healthOf,
|
||||||
|
memUsed,
|
||||||
|
pct,
|
||||||
|
shortUptime,
|
||||||
|
statFor,
|
||||||
|
type ContainerStat,
|
||||||
|
} from "@/lib/stats";
|
||||||
|
import type { ModuleState } from "@/lib/sam";
|
||||||
|
|
||||||
|
function Donut({ ok, starting, down }: { ok: number; starting: number; down: number }) {
|
||||||
|
const total = ok + starting + down;
|
||||||
|
if (total === 0)
|
||||||
|
return <p className="text-muted mb-0">Sin módulos integrados.</p>;
|
||||||
|
const R = 54;
|
||||||
|
const C = 2 * Math.PI * R;
|
||||||
|
const segs = [
|
||||||
|
{ v: ok, color: "#198754" },
|
||||||
|
{ v: starting, color: "#ffc107" },
|
||||||
|
{ v: down, color: "#dc3545" },
|
||||||
|
];
|
||||||
|
let acc = 0;
|
||||||
|
return (
|
||||||
|
<div className="d-flex align-items-center gap-4 flex-wrap">
|
||||||
|
<svg width="140" height="140" viewBox="0 0 140 140">
|
||||||
|
<circle cx="70" cy="70" r={R} fill="none" stroke="#e9ecef" strokeWidth="16" />
|
||||||
|
{segs.map((s, i) => {
|
||||||
|
if (s.v === 0) return null;
|
||||||
|
const frac = s.v / total;
|
||||||
|
const el = (
|
||||||
|
<circle
|
||||||
|
key={i}
|
||||||
|
cx="70"
|
||||||
|
cy="70"
|
||||||
|
r={R}
|
||||||
|
fill="none"
|
||||||
|
stroke={s.color}
|
||||||
|
strokeWidth="16"
|
||||||
|
strokeDasharray={`${frac * C} ${C}`}
|
||||||
|
strokeDashoffset={-acc * C}
|
||||||
|
transform="rotate(-90 70 70)"
|
||||||
|
strokeLinecap="butt"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
acc += frac;
|
||||||
|
return el;
|
||||||
|
})}
|
||||||
|
<text x="70" y="66" textAnchor="middle" fontSize="26" fontWeight="700" fill="#00285d">
|
||||||
|
{total}
|
||||||
|
</text>
|
||||||
|
<text x="70" y="86" textAnchor="middle" fontSize="12" fill="#6c757d">
|
||||||
|
módulos
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
<ul className="list-unstyled mb-0">
|
||||||
|
<li className="mb-1">
|
||||||
|
<span className="status-dot status-healthy me-2" />
|
||||||
|
Activos <strong>{ok}</strong>
|
||||||
|
</li>
|
||||||
|
<li className="mb-1">
|
||||||
|
<span className="status-dot status-starting me-2" />
|
||||||
|
Iniciando <strong>{starting}</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="status-dot status-unhealthy me-2" />
|
||||||
|
Detenidos <strong>{down}</strong>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Bar({ value, color }: { value: number; color: string }) {
|
||||||
|
return (
|
||||||
|
<div className="progress" style={{ height: 8 }}>
|
||||||
|
<div
|
||||||
|
className="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
style={{ width: `${value}%`, backgroundColor: color }}
|
||||||
|
aria-valuenow={value}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppCard({ m, stat }: { m: ModuleState; stat?: ContainerStat }) {
|
||||||
|
const meta = getModuleMeta(m.name);
|
||||||
|
const env = envOf(m.branch);
|
||||||
|
const cpu = pct(stat?.cpu);
|
||||||
|
const mem = pct(stat?.memPerc);
|
||||||
|
return (
|
||||||
|
<div className="col-md-6 col-xl-4">
|
||||||
|
<div className="app-card p-3 h-100">
|
||||||
|
<div className="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<i className={`bx ${meta.icon} bx-border text-light ${meta.bg}`} />
|
||||||
|
<div>
|
||||||
|
<strong>{displayName(m.name)}</strong>
|
||||||
|
<div>
|
||||||
|
<small className="text-muted">
|
||||||
|
{shortUptime(stat?.status ?? m.container)} · :{m.port}
|
||||||
|
</small>{" "}
|
||||||
|
<span className={`badge ${env.cls} badge-respaldo`}>{env.label}</span>
|
||||||
|
</div>
|
||||||
|
</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-2">
|
||||||
|
<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 mt-2">
|
||||||
|
<Link href={`/m/${m.name}`} className="btn btn-sm btn-outline-primary flex-fill">
|
||||||
|
<i className="bx bx-grid-alt me-1" /> Tablero
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href={moduleUrl(m)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn btn-sm btn-outline-secondary"
|
||||||
|
>
|
||||||
|
<i className="bx bx-window-open" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vista del tablero (contenido): tarjeta grande de estado global +
|
||||||
|
// estado y carga por aplicación. Se usa en /tablero y como
|
||||||
|
// landing de it.sam.devops.norteamericano.cl
|
||||||
|
export default function TableroView() {
|
||||||
|
const { data } = useModules();
|
||||||
|
const stats = useStats();
|
||||||
|
const loading = data === null;
|
||||||
|
|
||||||
|
const modules = 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;
|
||||||
|
const prod = modules.filter(
|
||||||
|
(m) => m.branch.toLowerCase() === "main" || m.branch.toLowerCase() === "master"
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const core = ["sam-api", "sam-deployer", "sam-postgres", "sam-redis"]
|
||||||
|
.map((n) => statFor(stats, n))
|
||||||
|
.filter((s): s is ContainerStat => Boolean(s));
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-5">
|
||||||
|
<div className="spinner-border text-primary" role="status" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="row row-cards" id="resumen">
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="app-card p-4">
|
||||||
|
<h6 className="fw-bold mb-3">
|
||||||
|
<i className="bx bx-pie-chart-alt-2 me-2" />
|
||||||
|
Estado de todos los módulos
|
||||||
|
</h6>
|
||||||
|
<div className="d-flex gap-5 flex-wrap align-items-center">
|
||||||
|
<Donut ok={ok} starting={starting} down={down} />
|
||||||
|
<div>
|
||||||
|
<div className="mb-1">
|
||||||
|
<small className="text-muted">Producción (main)</small>
|
||||||
|
<div className="fw-bold fs-5">{prod}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<small className="text-muted">Desarrollo (otras ramas)</small>
|
||||||
|
<div className="fw-bold fs-5">{modules.length - prod}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{SECTIONS.map((section) => {
|
||||||
|
const list = modules.filter(
|
||||||
|
(m) => sectionOf(m.name, m.category) === section.id
|
||||||
|
);
|
||||||
|
if (list.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div key={section.id}>
|
||||||
|
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
|
||||||
|
<i className={`bx ${section.icon} me-1`} /> {section.title}
|
||||||
|
</div>
|
||||||
|
<div className="row row-cards">
|
||||||
|
{list.map((m) => (
|
||||||
|
<AppCard key={m.name} m={m} stat={statFor(stats, `${m.name}-frontend`)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{core.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="alert alert-light section-header mt-4 mb-3 py-2 px-3">
|
||||||
|
<i className="bx bx-server me-1" /> Servicios base
|
||||||
|
</div>
|
||||||
|
<div className="row row-cards">
|
||||||
|
{core.map((s) => (
|
||||||
|
<div className="col-md-6 col-xl-3" key={s.name}>
|
||||||
|
<div className="app-card p-3">
|
||||||
|
<strong className="small">{s.name}</strong>
|
||||||
|
<div className="small text-muted mb-2">
|
||||||
|
{shortUptime(s.status)} · {s.pids} proc
|
||||||
|
</div>
|
||||||
|
<div className="d-flex justify-content-between">
|
||||||
|
<small className="text-muted">CPU {s.cpu}</small>
|
||||||
|
<small className="text-muted">MEM {s.memPerc}</small>
|
||||||
|
</div>
|
||||||
|
<Bar value={pct(s.cpu)} color="#0d6efd" />
|
||||||
|
<div className="mt-1">
|
||||||
|
<Bar value={pct(s.memPerc)} color="#6f42c1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-muted small mt-4 mb-4 text-center">
|
||||||
|
Auto-actualizado cada 30s · Carga de trabajo en tiempo real (docker stats)
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -106,3 +106,20 @@ export function refreshModules() {
|
|||||||
export function refreshStats() {
|
export function refreshStats() {
|
||||||
statsStore.load();
|
statsStore.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sección del sitio según hostname: it.sam.devops... -> "it".
|
||||||
|
// useSyncExternalStore evita mismatch de hidratación SSR/cliente.
|
||||||
|
function siteSnapshot(): string {
|
||||||
|
const h = window.location.hostname.toLowerCase();
|
||||||
|
const first = h.split(".")[0];
|
||||||
|
if (first === "it" && h.endsWith("sam.devops.norteamericano.cl")) return "it";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeNoop() {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSiteSection(): string {
|
||||||
|
return useSyncExternalStore(subscribeNoop, siteSnapshot, () => "");
|
||||||
|
}
|
||||||
Reference in new issue
Block a user