diff --git a/src/app/page.tsx b/src/app/page.tsx index 23b388a..6887c6e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,9 @@ "use client"; 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 { displayName, envOf, @@ -11,16 +13,36 @@ import { REFERENCE_APPS, sectionOf, SECTIONS, - type ApiResponse, type ModuleState, - type RefApp, } 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() { const { data, error } = useModules(); + const site = useSiteSection(); const loading = data === null && error === null; const fetchModules = refreshModules; + // it.sam.devops... aterriza directo en el tablero + if (site === "it") { + return ( + + + + ); + } + const modules = data?.modules ?? []; const backend = data?.backend; @@ -31,42 +53,6 @@ export default function Home() { bySection[s].push(m); }); - const renderRefCard = (app: RefApp) => ( -
-
- - - - - - - - -
- - - - {app.title} - -
- - sam.norteamericano.cl - -
-
- -
-
-
- ); - const renderModuleCard = (m: ModuleState) => { const meta = getModuleMeta(m.name); const status = getStatusInfo(m); @@ -121,184 +107,192 @@ export default function Home() { ); }; - return ( - <> -
-
-
-
- SAM -
-
-
-

Sistema SAM

-
-
-

Aplicaciones

-
-
- - Tablero - - - Nuevo Módulo - -
-
- Portal de integración -
-
-
- -
-
-
-
- {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 - -
-
-
- - )} -
-
- ); - })} - -
- - 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 + +
+
+
+ + )} +
+
+ ); + })} + +
+ + Auto-actualizado cada 15s ·{" "} + { e.preventDefault(); fetchModules(); }}> + actualizar ahora + + +
+
+ )} +
); } 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 ( -
- - - {segs.map((s, i) => { - if (s.v === 0) return null; - const frac = s.v / total; - const el = ( - - ); - acc += frac; - return el; - })} - - {total} - - - módulos - - -
    -
  • - - 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 ?? "—"} -
- -
-
- - Tablero - - - - -
-
-
- ); -} +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 ( +
+ + + {segs.map((s, i) => { + if (s.v === 0) return null; + const frac = s.v / total; + const el = ( + + ); + acc += frac; + return el; + })} + + {total} + + + módulos + + +
    +
  • + + 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 ?? "—"} +
+ +
+
+ + Tablero + + + + +
+
+
+ ); +} + +// 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, () => ""); +}