- {modules.length === 0 && (
-
-
-
-
- No hay módulos integrados. ¡Sube tu primer proyecto!
-
-
-
Subir Proyecto
-
-
-
- )}
-
- {SECTIONS.map((section) => {
- const list = bySection[section.id] ?? [];
- const isReview = section.id === "otras-apps";
- if (!isReview && list.length === 0) return null;
- return (
-
-
-
-
-
- {" "}
- {section.title}
-
- {list.length > 0 && (
-
- {list.length} módulo
- {list.length !== 1 ? "s" : ""}
-
- )}
-
-
-
- {list.map(renderModuleCard)}
-
- {section.id === "sam" &&
- REFERENCE_APPS.filter(
- (a) =>
- !list.some(
- (m) => m.name.toLowerCase() === a.name
- )
- ).map(renderRefCard)}
-
- {isReview && backend && (
- <>
-
-
-
-
-
- |
-
- |
-
-
- API Backend
-
-
- {backend.container ? "Activo" : "Detenido"} · {backend.branch}
-
- |
-
-
-
-
-
-
-
-
-
-
- |
-
- |
-
-
- Gestionar Módulos
-
-
- Subir / actualizar proyectos
-
- |
-
-
-
-
-
- >
- )}
-
-
- );
- })}
-
-
-
- Auto-actualizado cada 15s ·{" "}
- { e.preventDefault(); fetchModules(); }}>
- actualizar ahora
-
+ const renderRefCard = (app: {
+ name: string;
+ title: string;
+ icon: string;
+ bg: string;
+ url: string;
+ }) => (
+
+
+
+
+
+ |
+
+ |
+
+
+ {app.title}
+
+
+
+ sam.norteamericano.cl
-
- )}
+ |
+
+
+ |
+
+
+
+
+
+ );
+
+ return (
+
+ {loading && (
+
+
+
Cargando módulos...
+
+ )}
+
+ {error && (
+
+
+
+ Error: {error}
+
-
- >
+ )}
+
+ {!loading && !error && (
+
+ {modules.length === 0 && (
+
+
+
+
+ No hay módulos integrados. ¡Sube tu primer proyecto!
+
+
+
Subir Proyecto
+
+
+
+ )}
+
+ {SECTIONS.map((section) => {
+ const list = bySection[section.id] ?? [];
+ const isReview = section.id === "otras-apps";
+ if (!isReview && list.length === 0) return null;
+ return (
+
+
+
+
+
+ {" "}
+ {section.title}
+
+ {list.length > 0 && (
+
+ {list.length} módulo
+ {list.length !== 1 ? "s" : ""}
+
+ )}
+
+
+
+ {list.map(renderModuleCard)}
+
+ {section.id === "sam" &&
+ REFERENCE_APPS.filter(
+ (a) =>
+ !list.some((m) => m.name.toLowerCase() === a.name)
+ ).map(renderRefCard)}
+
+ {isReview && backend && (
+ <>
+
+
+
+
+
+ |
+
+ |
+
+
+ API Backend
+
+
+ {backend.container ? "Activo" : "Detenido"} · {backend.branch}
+
+ |
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+ Gestionar Módulos
+
+
+ Subir / actualizar proyectos
+
+ |
+
+
+
+
+
+ >
+ )}
+
+
+ );
+ })}
+
+
+
+ )}
+
);
}
diff --git a/src/app/tablero/page.tsx b/src/app/tablero/page.tsx
index 02ef63c..604f627 100644
--- a/src/app/tablero/page.tsx
+++ b/src/app/tablero/page.tsx
@@ -1,275 +1,24 @@
"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,
- type ApiResponse,
- type ModuleState,
-} from "@/lib/sam";
-import {
- healthOf,
- memUsed,
- pct,
- shortUptime,
- statFor,
- type ContainerStat,
-} from "@/lib/stats";
+import TableroView from "@/components/TableroView";
-function Donut({ ok, starting, down }: { ok: number; starting: number; down: number }) {
- const total = ok + starting + down;
- if (total === 0)
- return
Sin módulos integrados.
;
- 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 (
-
-
-
- -
-
- Activos {ok}
-
- -
-
- Iniciando {starting}
-
- -
-
- Detenidos {down}
-
-
-
- );
-}
-
-function Bar({ value, color }: { value: number; color: string }) {
- return (
-
- );
-}
-
-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 (
-
-
-
-
-
-
{displayName(m.name)}
-
-
- {shortUptime(stat?.status ?? m.container)} · :{m.port}
- {" "}
- {env.label}
-
-
-
-
-
- CPU
- {stat?.cpu ?? "—"}
-
-
-
-
-
- MEM {memUsed(stat)}
- {stat?.memPerc ?? "—"}
-
-
-
-
-
-
- );
-}
+const menu = [
+ { label: "Resumen", icon: "bx-grid-alt", href: "#resumen" },
+ { label: "Aplicaciones", icon: "bx-layer", href: "/" },
+];
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 = [
- { label: "Resumen", icon: "bx-grid-alt", href: "#resumen" },
- ...modules.map((m) => ({
- label: displayName(m.name),
- icon: getModuleMeta(m.name).icon,
- href: `/m/${m.name}`,
- })),
- ];
-
- const core = ["sam-api", "sam-deployer", "sam-postgres", "sam-redis"]
- .map((n) => statFor(stats, n))
- .filter((s): s is ContainerStat => Boolean(s));
-
return (
- {loading && (
-
- )}
-
- {!loading && (
- <>
-
-
-
-
-
- Estado de todos los módulos
-
-
-
-
-
-
Producción (main)
-
{prod}
-
-
-
Desarrollo (otras ramas)
-
{modules.length - prod}
-
-
-
-
-
-
-
- {SECTIONS.map((section) => {
- const list = modules.filter(
- (m) => sectionOf(m.name, m.category) === section.id
- );
- if (list.length === 0) return null;
- return (
-
-
- {section.title}
-
-
- {list.map((m) => (
-
- ))}
-
-
- );
- })}
-
- {core.length > 0 && (
- <>
-
- Servicios base
-
-
- {core.map((s) => (
-
-
-
{s.name}
-
- {shortUptime(s.status)} · {s.pids} proc
-
-
- CPU {s.cpu}
- MEM {s.memPerc}
-
-
-
-
-
-
-
- ))}
-
- >
- )}
-
-
- Auto-actualizado cada 30s · Carga de trabajo en tiempo real (docker stats)
-
- >
- )}
+
);
}
diff --git a/src/components/TableroView.tsx b/src/components/TableroView.tsx
new file mode 100644
index 0000000..17f538e
--- /dev/null
+++ b/src/components/TableroView.tsx
@@ -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
Sin módulos integrados.
;
+ 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 (
+
+
+
+ -
+
+ Activos {ok}
+
+ -
+
+ Iniciando {starting}
+
+ -
+
+ Detenidos {down}
+
+
+
+ );
+}
+
+function Bar({ value, color }: { value: number; color: string }) {
+ return (
+
+ );
+}
+
+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 (
+
+
+
+
+
+
{displayName(m.name)}
+
+
+ {shortUptime(stat?.status ?? m.container)} · :{m.port}
+ {" "}
+ {env.label}
+
+
+
+
+
+ CPU
+ {stat?.cpu ?? "—"}
+
+
+
+
+
+ MEM {memUsed(stat)}
+ {stat?.memPerc ?? "—"}
+
+
+
+
+
+
+ );
+}
+
+// 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 (
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+ Estado de todos los módulos
+
+
+
+
+
+
Producción (main)
+
{prod}
+
+
+
Desarrollo (otras ramas)
+
{modules.length - prod}
+
+
+
+
+
+
+
+ {SECTIONS.map((section) => {
+ const list = modules.filter(
+ (m) => sectionOf(m.name, m.category) === section.id
+ );
+ if (list.length === 0) return null;
+ return (
+
+
+ {section.title}
+
+
+ {list.map((m) => (
+
+ ))}
+
+
+ );
+ })}
+
+ {core.length > 0 && (
+ <>
+
+ Servicios base
+
+
+ {core.map((s) => (
+
+
+
{s.name}
+
+ {shortUptime(s.status)} · {s.pids} proc
+
+
+ CPU {s.cpu}
+ MEM {s.memPerc}
+
+
+
+
+
+
+
+ ))}
+
+ >
+ )}
+
+
+ Auto-actualizado cada 30s · Carga de trabajo en tiempo real (docker stats)
+
+ >
+ );
+}
diff --git a/src/lib/store.ts b/src/lib/store.ts
index a49b4e2..9e0a83b 100644
--- a/src/lib/store.ts
+++ b/src/lib/store.ts
@@ -106,3 +106,20 @@ export function refreshModules() {
export function refreshStats() {
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, () => "");
+}