Skip to content

Plataforma técnica · Sevastopol

Sevastopol (Frontend)

Sevastopol Islands

Sevastopol es la interfaz visual del ecosistema Nostromo. Renderiza un shell SSR con Astro e hidrata interactividad selectiva con islands de SolidJS. No habla nunca directo con la base de datos: toda lectura y escritura pasa por el Orchestrator vía un proxy Backend-for-Frontend que vive en src/pages/api/.

Esta página cubre el stack, la arquitectura SSR + islands, la organización de componentes (Atomic Design) y el contrato con el backend. Las páginas hermanas profundizan en cada capa: islands, hooks, middleware y UI components.


CapaTecnologíaRol en el frontend
SSR + routingAstro 5.17Renderiza el shell, define rutas y monta el proxy BFF
InteractividadSolidJS 1.9Islands reactivas con signals de grano fino, sin VDOM
EstilosTailwind CSS 3.4Sistema utility-first y dark mode
TiposTypeScript 5.8End-to-end, incluye contratos con la API del Orchestrator
Adaptador SSR@astrojs/nodeOutput server, modo standalone
E2EPlaywrightPruebas de navegación y flujos de login

Bibliotecas de dominio frecuentes: rut.js (validación de RUT), date-fns (fechas), jspdf + jspdf-autotable (reportes PDF), xlsx (export Excel), plotly.js (charts), marked + dompurify (markdown).

El setup local del monorepo (con Orchestrator y Mother corriendo en paralelo) vive en Setup Local. Sevastopol no se instala como repo separado: forma parte del workspace pnpm de ChrisTkm/Accounting.


Sevastopol no es solo una herramienta administrativa; es una experiencia Premium.

  • Glassmorphism: Uso extensivo de transparencias y blurs para dar profundidad.
  • Dark Mode First: Diseñado nativamente para interfaces oscuras, reduciendo fatiga visual.
  • Micro-interacciones: Feedback visual inmediato en hovers, clicks y transiciones.
  • Tipografía: Uso de familias tipográficas modernas (Inter/Roboto) para máxima legibilidad.
  1. Dynamic Design: La interfaz debe sentirse “viva”. Evitar componentes estáticos aburridos.
  2. Visual Excellence: No aceptar diseños “MVP” o básicos. Cada pantalla debe tener un acabado profesional.
  3. Responsividad: Fluidez total entre resoluciones de escritorio y tablet.

Sevastopol combina SSR de Astro con islands de SolidJS: el shell se sirve como HTML mínimo y solo se hidrata JavaScript donde existe interactividad real.

src/pages/ define cinco entradas .astro de alto nivel, cada una con una responsabilidad acotada. El shell de cada zona renderiza el menú lateral, monta un contenedor vacío y deja que view-router.ts cargue la island correspondiente bajo demanda.

flowchart LR
  subgraph PAGES["src/pages/ (shells SSR)"]
    idx["index.astro<br/>Login"]
    dash["dashboard.astro<br/>Operación contable"]
    cmd["command/<br/>Command Center"]
    set["settings.astro<br/>Configuración"]
    man["manual-cuentas.astro<br/>Plan de cuentas"]
    docs["docs.astro<br/>Embebe Jean d'Arc"]
  end

  subgraph SHARED["src/components/"]
    sb["organisms/Sidebar.astro<br/>Menú SSR"]
    vr["islands/view-router.ts<br/>Bus de eventos + import()"]
  end

  subgraph ISL["src/components/islands/ (por dominio)"]
    pay["payroll/"]
    adm["admin/"]
    acc["accounting/"]
    dec["declaraciones/"]
    fin["financieros/"]
    rep["reportes/"]
    otr["…"]
  end

  dash -- monta --> sb
  cmd -- monta --> sb
  set -- monta --> sb
  man -- monta --> sb

  dash -- carga --> vr
  cmd -- carga --> vr

  vr -- "import() on demand" --> pay
  vr -- "import() on demand" --> adm
  vr -- "import() on demand" --> acc
  vr -- "import() on demand" --> dec
  vr -- "import() on demand" --> fin
  vr -- "import() on demand" --> rep

Cada shell carga el <Sidebar /> SSR (sin hidratación) y un contenedor vacío que view-router.ts rellena al recibir el evento sidebar:navigate. Las islands viven agrupadas por dominio bajo src/components/islands/.

Las islas SolidJS se cargan de forma diferida y se hidratan selectivamente: solo los componentes interactivos se convierten en JavaScript vivo, el resto permanece como HTML estático. El bundle inicial se mantiene chico y cada vista pesa solo cuando el usuario navega a ella.

flowchart LR
  subgraph B["Browser · Carga inicial"]
    shell["Shell .astro (SSR)<br/>HTML mínimo"]
    sidebar["Sidebar.astro<br/>SSR, sin hidratación"]
    mount["div #command-view<br/>contenedor vacío"]
    click_evt["click data-view →<br/>evento sidebar:navigate"]

    shell --> sidebar
    shell --> mount
    sidebar --> click_evt
  end

  subgraph VR["view-router.ts"]
    listener["addEventListener<br/>sidebar:navigate"]
    unmount_fn["unmount()<br/>dispone island anterior"]
    registry["views registry<br/>(import dinámico por clave)"]
    render_fn["render()<br/>de solid-js/web"]
  end

  click_evt --> listener --> unmount_fn --> registry --> render_fn
  render_fn -- inyecta en --> mount

  subgraph CH["Chunks code-split (lazy)"]
    isls["islands/<dominio>/<Island>.tsx"]
  end

  registry --> isls --> render_fn

  subgraph BE["Backend"]
    bff["src/pages/api/ (BFF proxy)"]
    orch["Orchestrator · /api/*"]
  end

  isls -->|"authenticatedFetch"| bff -->|"forward con cookie sid"| orch

Flujo del ciclo de navegación: clic en sidebar → evento sidebar:navigateview-router desmonta la island anterior, importa la nueva on-demand y la monta en #command-view → la island consulta el Orchestrator a través del proxy BFF local.


Los componentes siguen Atomic Design con un piso adicional para templates de página. La regla es estricta: la lógica de estado y los fetch viven solo en islands/; el resto es presentación.

  • Directorycomponents/
    • Directoryatoms/ — primitivos sin lógica de negocio
      • Button.astro, Icon.astro, SidebarButton.astro
      • DataTable.tsx, Modal.tsx, Pagination.tsx, SearchBox.tsx, Fields.tsx
      • PlotlyChart.tsx, ChartContainer.tsx, ExportButtons.tsx, TableAction.tsx
      • LoadingSpinner.tsx, ToastProvider.tsx, TenantPicker.tsx
      • MarkdownEditor.tsx, MarkdownView.tsx
    • Directorymolecules/ — combinaciones con propósito visual
      • FormGroup.astro, InputField.astro, SidebarGroup.astro, SocialLinks.astro
      • Breadcrumb.tsx, FilterBar.tsx, PageHeader.tsx
      • StatCard.tsx, StatsGrid.tsx
      • StatusBadge.tsx, BoolBadge.tsx, TypeBadge.tsx, PeriodicidadBadge.tsx, RegimeBadge.tsx
      • ParameterFormModal.tsx
    • Directoryorganisms/ — secciones SSR no interactivas
      • LoginForm.astro
      • Sidebar.astro
    • Directorytemplates/ — andamios de página reutilizables
      • IslandBase.tsx — wrapper estándar de toda island (header, stats, contenido)
      • WorkspaceTheme.tsx — provider de tema dark/light por workspace
    • Directoryislands/ — interactividad por dominio
      • view-router.ts
      • accounting/, admin/, cicloContable/, command/, contabilidad/
      • core/, dashboard/, declaraciones/, dev/, financieros/
      • manual/, payroll/, registry/, reportes/, settings/

Todas las islas siguen una estructura consistente:

export default function EmployeesViewIsland() {
// 1. Signals de estado
const [data, setData] = createSignal<Employee[]>([]);
const [loading, setLoading] = createSignal(true);
// 2. Estado derivado (memos)
const filtered = createMemo(() => /* ... */);
// 3. Effects para carga de datos
createEffect(() => { /* fetch data */ });
// 4. Manejadores de eventos
const handleCreate = async () => { /* ... */ };
// 5. Renderizar usando plantilla IslandBase
return (
<IslandBase
mode="standard"
title="Empleados"
stats={[/* ... */]}
>
<DataTable headers={headers} data={filtered()} />
</IslandBase>
);
}

Sevastopol usa el sistema de reactividad de grano fino de SolidJS para gestión de estado. A diferencia de React, SolidJS no usa un DOM virtual ni re-renderiza componentes; en cambio, las actualizaciones son quirúrgicas.

PrimitivaPropósitoEjemplo
createSignal<T>()Estado mutableconst [count, setCount] = createSignal(0)
createMemo<T>()Estado derivado (cacheado)const doubled = createMemo(() => count() * 2)
createEffect()Efectos secundarioscreateEffect(() => console.log(count()))
function MyIsland() {
// Datos crudos desde API
const [items, setItems] = createSignal<Item[]>([]);
// Estado UI
const [searchTerm, setSearchTerm] = createSignal("");
const [loading, setLoading] = createSignal(true);
// Datos derivados (se actualiza automáticamente cuando cambian dependencias)
const filtered = createMemo(() => {
const term = searchTerm().toLowerCase();
return items().filter(item =>
item.name.toLowerCase().includes(term)
);
});
// Effect de carga de datos
createEffect(async () => {
setLoading(true);
const data = await fetchItems();
setItems(data);
setLoading(false);
});
return <DataTable data={filtered()} />;
}

Los signals de SolidJS son funciones: llámalas para leer (count()) y pasa un valor para escribir (setCount(5)).


Sevastopol nunca habla directo con PostgreSQL ni con el Orchestrator desde el navegador. Toda llamada cruza un proxy local Backend-for-Frontend en src/pages/api/, que reenvía al Orchestrator adjuntando la cookie de sesión sid.

Esto deja la superficie pública del frontend acotada a su propio dominio y centraliza la rotación de credenciales y headers de seguridad en el middleware de Sevastopol.

flowchart LR
  subgraph ISL["Island (SolidJS)"]
    ce["createEffect()<br/>data load"]
    af["authenticatedFetch()<br/>@/lib/authFetch"]
    ce --> af
  end

  subgraph SEV["Sevastopol · src/pages/api/"]
    bff["/api/* proxy<br/>(BFF endpoint .ts)"]
    mw["middleware.ts<br/>headers seguridad + sesión"]
    af --> mw --> bff
  end

  subgraph ORCH["Orchestrator"]
    api["/api/* express routes"]
    bff --> |"forward + cookie sid"| api
  end

  api -->|"JSON snake_case"| bff
  bff -->|"Response"| af -->|"setData()"| ce

Patrón canónico dentro de una island:

import { authenticatedFetch } from '@/lib/authFetch';
createEffect(async () => {
const tenantId = activeTenant();
if (!tenantId) return;
setLoading(true);
const res = await authenticatedFetch(`/api/employees?tenant_id=${tenantId}`);
if (res.ok) setEmployees(await res.json());
setLoading(false);
});

authenticatedFetch (en src/lib/authFetch.ts) adjunta la cookie sid y normaliza 401/403; los errores HTTP de negocio se propagan a la island, que decide cómo mostrarlos al usuario.


Las islands se agrupan por dominio de negocio dentro de src/components/islands/. La regla de oro: cada dominio tiene su propio ViewIsland raíz, y las sub-islands consumen datos vía hooks compartidos (useActiveTenant, useActivePeriod, useTenants).

accounting/, cicloContable/, contabilidad/, declaraciones/, manual/ — registro de operaciones, ciclo contable, plan de cuentas, F29 y declaraciones juradas.