Skip to content
GitHub

Sevastopol (Frontend)

Este documento proporciona una visión general de la aplicación frontend Sevastopol, que sirve como interfaz de usuario para el sistema de contabilidad Nostromo. Sevastopol está construido usando Astro para renderizado del lado del servidor y enrutamiento, con SolidJS para componentes reactivos del lado del cliente. Esta página cubre la arquitectura general, el stack tecnológico y los patrones de alto nivel utilizados en todo el frontend.


Sevastopol utiliza un stack de desarrollo web moderno optimizado para rendimiento y experiencia del desarrollador:

TecnologíaVersiónPropósito
Astro5.12.3Framework SSR, enrutamiento, shell de página
SolidJS1.8.5+Islands reactivas, lógica de componentes
TailwindCSS3.xEstilos utility-first
TypeScript5.8.3Seguridad de tipos
Vite6.3.5+Herramienta de build, servidor dev, proxy API

Las bibliotecas adicionales incluyen:

  • jsPDF + jspdf-autotable para generación de PDF
  • xlsx para exportación a Excel
  • rut.js para validación de RUT chileno
  • @playwright/test para pruebas E2E

Para levantar el entorno de desarrollo local de Sevastopol:

  • Node.js: v18+ (LTS recomendado)
  • Gestor de paquetes: npm (incluido con Node) o pnpm
  1. Clonar el repositorio:

    Terminal window
    git clone https://github.com/ChrisTkm/Sevastopol.git
    cd Sevastopol
  2. Instalar dependencias:

    Terminal window
    npm install
    # o si usas pnpm
    pnpm install
  3. Iniciar servidor de desarrollo:

    Terminal window
    npm run dev

    El servidor estará disponible en http://localhost:4321.


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 implementa una arquitectura híbrida de SSR + Islands, combinando los beneficios de rendimiento del renderizado del lado del servidor con interactividad selectiva del lado del cliente.

Astro renderiza el HTML inicial con JavaScript mínimo. La carcasa de la aplicación se renderiza del lado del servidor, proporcionando un First Contentful Paint instantáneo:

Diagrama: Estructura de Archivos y Flujo SSR

Section titled “Diagrama: Estructura de Archivos y Flujo SSR”
flowchart LR
  %% --- PAGES ---
  subgraph P[pages/]
    cmd["command.astro<br/>Carcasa principal app"]
    idx["index.astro<br/>Formulario de login"]
  end

  %% --- SRC ---
  subgraph SRC["sevastopol/src/"]
    vr["view-router.ts Listener<br/>eventos + import dinámico"]
    sb["organisms/Sidebar.astro<br/>Menú navegación SSR"]

    subgraph ISL["components/islands/"]
      subgraph PAY["payroll/"]
        pay["PayrollViewIsland.tsx"]
        emp["EmployeesViewIsland.tsx"]
        con["ContractsViewIsland.tsx"]
      end

      subgraph ADM["admin/"]
        ten["TenantsViewIsland.tsx"]
        ses["SessionsViewIsland.tsx"]
      end
    end
  end

  %% --- OTROS ---
  cfg["config/menu.config.ts<br/>menu: MenuItem[]"]
  layout["layouts/Layout.astro<br/>HTML base, fuentes, init tema"]

  %% --- FLUJOS ---
  cmd -- inicializa --> vr
  cmd -- renderiza --> sb

  vr -- "import() bajo demanda" --> pay
  vr -- "import() bajo demanda" --> emp
  vr -- "import() bajo demanda" --> con
  vr -- "import() bajo demanda" --> ten
  vr -- "import() bajo demanda" --> ses

  sb -- lee --> cfg

La página command.astro sirve como la carcasa de la aplicación, conteniendo:

  • Componente <Sidebar /> (SSR, sin hidratación)
  • Contenedor <div id="command-view"> (objetivo para renderizado de islas)
  • Etiqueta <script type="module"> importando view-router.ts

Las islas SolidJS se cargan de forma diferida y se hidratan selectivamente—solo los componentes interactivos se convierten en JavaScript vivo, mientras que el resto permanece como HTML estático. Esto reduce drásticamente el tamaño del bundle inicial.

Diagrama: Arquitectura de Islas - Mapeo de Entidades de Código

flowchart LR
  %% =========================
  %% Browser (SSR + navegación)
  %% =========================
  subgraph B["Browser · Carga inicial"]
    cmd["command.astro (SSR)<br/>~15KB HTML"]
    sidebar["Sidebar.astro (estático)<br/>sin hidratación JS"]
    mount["div#command-view<br/>contenedor vacío"]
    click_evt["click data-view<br/>emite evento sidebar:navigate"]

    cmd --> sidebar
    cmd --> mount
    sidebar --> click_evt
  end

  %% =========================
  %% Router (import dinámico + render)
  %% =========================
  subgraph VR["view-router.ts"]
    listener["window.addEventListener<br/>evento sidebar:navigate"]
    unmount_fn["unmount()<br/>dispone isla anterior"]
    registry["views: Record<br/>57 entradas"]

    importPay["await import<br/>./payroll/PayrollViewIsland.tsx"]
    importEmp["await import<br/>./payroll/EmployeesViewIsland.tsx"]
    importCon["await import<br/>./payroll/ContractsViewIsland.tsx"]
    importTen["await import<br/>./admin/TenantsViewIsland.tsx"]

    render_fn["render()<br/>desde solid-js/web"]
  end

  click_evt --> listener --> unmount_fn --> registry
  registry --> importPay --> render_fn
  registry --> importEmp --> render_fn
  registry --> importCon --> render_fn
  registry --> importTen --> render_fn

  render_fn -- inyecta en --> mount

  %% =========================
  %% Chunks (code-split)
  %% =========================
  subgraph CH["Island Chunks · Code-split"]
    pay["PayrollViewIsland.tsx<br/>~65KB chunk"]
    emp["EmployeesViewIsland.tsx<br/>~45KB chunk"]
    con["ContractsViewIsland.tsx<br/>~40KB chunk"]
    ten["TenantsViewIsland.tsx<br/>~35KB chunk"]
  end

  importPay --> pay
  importEmp --> emp
  importCon --> con
  importTen --> ten

  %% =========================
  %% Backend
  %% =========================
  subgraph BE["Orchestrator Backend"]
    api["/api/* endpoints<br/>localhost:8000"]
  end

  pay -->|"authenticatedFetch /api/remuneraciones/payroll"| api
  emp -->|"authenticatedFetch /api/employees"| api
  ten -->|"authenticatedFetch /api/admin/tenants"| api
  con -->|"authenticatedFetch /api/payroll/contracts"| api

Patrón clave: Usuario hace clic en barra lateral → evento sidebar:navigate → router importa isla → render() monta en #command-view → isla obtiene datos de Orchestrator.


Los componentes siguen la metodología de Diseño Atómico:

  • Directorycomponents/
    • Directoryatoms/
      • Bloques de construcción básicos
      • Button.tsx
      • Input.tsx
      • Select.tsx
      • Modal.tsx
      • SearchBox.tsx
      • Pagination.tsx
      • SidebarButton.astro
    • Directorymolecules/
      • Combinaciones simples
      • SidebarGroup.astro
      • SocialLinks.astro
      • FilterBar.tsx
    • Directoryorganisms/
      • Secciones UI complejas
      • Sidebar.astro
      • DataTable.tsx
      • IslandBase.tsx
    • Directoryislands/
      • Características interactivas
      • view-router.ts
      • Directoryadmin/
        • TenantsViewIsland.tsx
        • SessionsViewIsland.tsx
        • PlanContableViewIsland.tsx
      • Directorypayroll/
        • PayrollViewIsland.tsx
        • EmployeesViewIsland.tsx
        • ContractsViewIsland.tsx

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).

Fuentes: Inferido de patrones SolidJS, no se proporcionó archivo específico


Las islas se comunican con Orchestrator vía authenticatedFetch() de @/lib/api. Esta utilidad adjunta la cookie de sesión sid y maneja errores 401/403.

Diagrama: Flujo de Petición API - Frontend a Backend

flowchart LR
  %% ================
  %% Componente Island
  %% ================
  subgraph ISL["Componente Island"]
    ce["createEffect()<br/>Carga de datos"]
    fetch["authenticatedFetch()<br/>@/lib/api"]
    set["setData(employees)"]

    ce --> fetch
    fetch --> set
  end

  %% ==========
  %% Request URL
  %% ==========
  req["GET /api/employees?<br/>tenant_id=6000431"]

  fetch --> req

  %% =============
  %% Vite Dev Proxy
  %% =============
  subgraph VITE["Vite Dev Proxy"]
    proxy["Proxy /api/* → localhost:8000"]
  end

  req --> proxy

  %% ===================
  %% Orchestrator (app.ts)
  %% ===================
  subgraph ORCH["Orchestrator · app.ts"]
    mw_auth["authenticatedToken<br/>middleware"]
    mw_authz["authorizeRoute<br/>middleware"]
    route["Manejador ruta Express<br/>/api/employees"]
  end

  proxy --> mw_auth --> mw_authz --> route

  %% =========
  %% Database
  %% =========
  subgraph DB["Database"]
    pool["getTenantPool()<br/>Pool específico tenant"]
    pg["PostgreSQL<br/>nostromo_6000431"]
  end

  route --> pool --> pg
  pg -->|"200 OK + data<br/>JSON snake_case"| route
  route -->|"200 OK + data"| proxy -->|"200 OK + data"| fetch

Patrón típico de carga de datos en isla:

createEffect(async () => {
const tenantId = selectedTenant();
if (!tenantId) return;
setLoading(true);
const res = await authenticatedFetch(`/api/employees?tenant_id=${tenantId}`);
if (res.ok) {
const data = await res.json();
setEmployees(data);
}
setLoading(false);
});

El servidor dev de Vite hace proxy de /api/* a localhost:8000 (Orchestrator). En producción, ambos servicios corren detrás del mismo dominio, eliminando problemas de CORS.


Gestiona la configuración global y multi-tenant. - TenantManagement: Creación y edición de tenants. - UserRoles: Asignación de permisos y usuarios.