Plataforma técnica · Sevastopol
Sevastopol (Frontend)
Sevastopol Islands
Propósito y Alcance
Section titled “Propósito y Alcance”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.
Stack tecnológico
Section titled “Stack tecnológico”| Capa | Tecnología | Rol en el frontend |
|---|---|---|
| SSR + routing | Astro 5.17 | Renderiza el shell, define rutas y monta el proxy BFF |
| Interactividad | SolidJS 1.9 | Islands reactivas con signals de grano fino, sin VDOM |
| Estilos | Tailwind CSS 3.4 | Sistema utility-first y dark mode |
| Tipos | TypeScript 5.8 | End-to-end, incluye contratos con la API del Orchestrator |
| Adaptador SSR | @astrojs/node | Output server, modo standalone |
| E2E | Playwright | Pruebas 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.
Principios de Diseño
Section titled “Principios de Diseño”Sevastopol no es solo una herramienta administrativa; es una experiencia Premium.
Estética y UX
Section titled “Estética y UX”- 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.
Reglas Críticas
Section titled “Reglas Críticas”- Dynamic Design: La interfaz debe sentirse “viva”. Evitar componentes estáticos aburridos.
- Visual Excellence: No aceptar diseños “MVP” o básicos. Cada pantalla debe tener un acabado profesional.
- Responsividad: Fluidez total entre resoluciones de escritorio y tablet.
Visión General de la Arquitectura
Section titled “Visión General de la Arquitectura”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.
Shells SSR y zonas de la app
Section titled “Shells SSR y zonas de la app”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/.
Arquitectura de Islas
Section titled “Arquitectura de Islas”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:navigate → view-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.
Organización de Componentes
Section titled “Organización de Componentes”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/
Patrón de Estructura de Isla
Section titled “Patrón de Estructura de Isla”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> );}Gestión de Estado con SolidJS
Section titled “Gestión de Estado con SolidJS”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.
Primitivas Principales
Section titled “Primitivas Principales”| Primitiva | Propósito | Ejemplo |
|---|---|---|
createSignal<T>() | Estado mutable | const [count, setCount] = createSignal(0) |
createMemo<T>() | Estado derivado (cacheado) | const doubled = createMemo(() => count() * 2) |
createEffect() | Efectos secundarios | createEffect(() => console.log(count())) |
Patrón Típico de Estado de Isla
Section titled “Patrón Típico de Estado de Isla”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)).
Integración API · Patrón BFF
Section titled “Integración API · Patrón BFF”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.
Dominios cubiertos por islands
Section titled “Dominios cubiertos por islands”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.
payroll/ — empleados, contratos, liquidaciones, AFP, Isapre, honorarios, vacaciones, finiquitos.
financieros/, reportes/ — estados financieros, análisis, exportes PDF/Excel.
admin/, command/, core/, dashboard/, registry/, settings/, dev/ — administración de tenants, sesiones, dashboard ejecutivo, monitor de agentes y configuración.