Cortex Panel — runtime frontend¶
Cómo el shell React consume manifests y dashboards CUS por HTTP. Para declarar UI desde Python, usa la guía Plugins.
Ver ADR 003, ADR 018 y contexto del proyecto.
Arquitectura npm¶
| Paquete | Rol |
|---|---|
@cortex/panel-core | Headless: tipos, PanelProvider, nav, routing, PanelApiClient |
@cortex/panel-shadcn | Skin shadcn/ui + Tailwind v4: shell, widgets, DashboardRenderer |
@cortex/web | createPanelShellApp — rutas, provider, API client |
framework/web-shadcn | Demo Vite → :5175 |
flowchart TB
subgraph demo [Demo Vite]
SH[framework/web-shadcn]
end
subgraph web ["@cortex/web"]
W["createPanelShellApp"]
end
subgraph skin ["@cortex/panel-shadcn"]
SK[widgets + PanelShell]
end
subgraph core ["@cortex/panel-core"]
C["PanelProvider navUtils PanelApiClient"]
end
SH --> W --> SK --> C Arranque local¶
npm install # raíz — workspaces
npm run dev:web-shadcn # http://localhost:5175
npm run build:web-shadcn
Vite hace proxy de /api → http://localhost:8000. La API debe estar en marcha (uv run cortex-api).
Flujo runtime¶
- En bootstrap, el plugin registra UI (
register_resourcesoregister_dashboards). - El shell carga rutas con
GET /api/v1/panels/routes. ModuleScreenresuelve pantalla →GET /api/v1/ui/dashboards/{dashboardId}.WidgetRendererinstancia el componente segúnwidget.type.
sequenceDiagram
participant API as FastAPI
participant Shell as PanelProvider
participant M as ModuleScreen
participant W as Widgets
Shell->>API: GET /api/v1/panels/{panelId}
M->>API: GET manifest + dashboard
M->>W: layout + configs
W->>API: fetchPath datos y forms Componentes clave¶
| Componente | Responsabilidad |
|---|---|
PanelProvider | Contexto API, registry, refresh entre widgets |
PanelShell | Sidebar, header, slots de render hooks |
ModuleScreen | Pantalla activa → dashboard |
DashboardRenderer | Grid de widgets |
WidgetRenderer | Lookup registry.get(type) |
Jerarquía de títulos¶
El shell shadcn aplica un contrato de un título visible por pantalla (ver ADR 018):
| Capa | Responsabilidad |
|---|---|
PanelShell | h1 = screens[].title de la pantalla activa |
ModuleScreen | h2 solo si theme.shellOwnsPageTitle es false |
DashboardRenderer | h3 solo si layout.hideTitle es false |
Widgets (data-table, form, infolist) | CardTitle solo si config.title está definido |
Flags en PanelConfiguration.theme:
| Flag | Default (shadcn) | Efecto |
|---|---|---|
shellOwnsPageTitle | true | El header del shell muestra el título; ModuleScreen omite h2 |
showPanelSubtitle | true | Muestra panel.title como subtítulo gris sobre el h1 |
En Python, emit_dashboard(..., hide_title=True) suprime el título del dashboard; el título vive en screens[].title. ResourceBuilder lo aplica por defecto en list/create/edit/view.
flowchart TB
subgraph titles [Jerarquía de títulos]
Shell["PanelShell h1 screen.title"]
Module["ModuleScreen h2 opcional"]
Dash["DashboardRenderer h3 si no hideTitle"]
Widget["Widget CardTitle si config.title"]
end
Shell --> Module --> Dash --> Widget Resolución de pantallas¶
ModuleScreen y PanelShell usan matchModuleScreen de @cortex/panel-core:
- Calculan la ruta relativa con
relativeModulePath(pathname, moduleUrl). - Evalúan todas las entradas de
manifest.screenscondashboardId. - Eligen la pantalla con mayor especificidad (
scoreScreenPath): segmentos literales antes que parámetros (:id).
Ejemplo: la ruta relativa create coincide con path: "create" y con path: ":id", pero gana la literal.
import { matchModuleScreen, relativeModulePath } from '@cortex/panel-core';
const relative = relativeModulePath('/operations/clients/create', '/operations/clients');
const { screen, params } = matchModuleScreen(manifest, relative);
// screen.path === 'create'
Si una acción CREATE navega a create, el manifest debe declarar una pantalla con path: "create" (o usar ResourceBuilder, que la genera automáticamente).
PanelApiClient¶
Rutas habituales (packages/cortex-panel-core/src/panelApiClient.ts):
| Método | Endpoint |
|---|---|
listPanelRoutes() | GET /api/v1/panels/routes |
getPanel(panelId) | GET /api/v1/panels/{panelId} |
getModuleManifest(...) | GET /api/v1/panels/{panelId}/modules/{moduleId}/manifest |
getDashboard(id) | GET /api/v1/ui/dashboards/{id} |
getForm(formId) | GET /api/v1/forms/{formId} |
fetchPath(path) | Cualquier ruta de datos del plugin |
Widget registry¶
import { createDefaultRegistry } from '@cortex/panel-shadcn';
const registry = createDefaultRegistry();
registry.register('mi-widget', MiWidget);
Catálogo de tipos: Widgets (plugins).
Shell mínimo¶
import { createPanelShellApp } from '@cortex/web';
import { PanelShell, createDefaultRegistry } from '@cortex/panel-shadcn';
export const { AppPanelProvider, AppPanelRoutes } = createPanelShellApp({
PanelShell,
createDefaultRegistry,
});
Multi-módulo en un panel¶
GET /api/v1/panels/operations devuelve modules[], navGroups[] y navigation[] — sidebar agrupado con orden por navSort. Varios plugins contribuyen módulos sin compartir código React.
flowchart TB
subgraph panelOperativo [Panel panel]
Shell[PanelShell sidebar único]
B[cortex_plugin_booking]
C[cortex_plugin_clients]
P[cortex_plugin_pricing]
end
Shell --> B
Shell --> C
Shell --> P
B --> API_B["/api/v1/booking/"]
C --> API_C["/api/v1/clients/"]
P --> API_P["/api/v1/pricing/"] Panel Control¶
Módulo embebido cortex_framework.control — rutas /control/, API /api/v1/control/plugins. No es plugin pip.
Validación CUS¶
cortex_framework.panel.validate — schemas en framework/cortex_framework/panel/schemas/.
Siguiente lectura¶
Arquitectura 4 capas (Magento → HIVE)¶
El panel separa controller (headless), template (view por skin), styles y statics para intercambiar presentación sin recompilar el bundle npm. Ver ADR 026 y ADR 027.
| Capa Magento | HIVE | Ubicación |
|---|---|---|
| Controller (Block) | Hooks/controllers headless | packages/cortex-panel-core/src/widgets/{widget}/ |
Template (.phtml) | Views React por skin | public/skins/{skin}/templates/registry.js |
| Styles | CSS por skin | public/skins/{skin}/css/theme.css |
| Statics | Assets por skin | public/skins/{skin}/assets/ |
| Layout XML | register_layouts + merge API | framework/cortex_framework/layout/ |
Carga de templates en runtime¶
PanelShellleepanel.configuration.theme.skin.loadSkinTemplates(registry, skin)importa el entry ESM del skin y llamaregisterSkinTemplates(registry).WidgetRegistry.resolve(type, skin)elige la view (data-table@operations, etc.).- Build de skins:
npm run build:skins(assets +registry.jspor skin).
sequenceDiagram
participant Shell as PanelShell
participant Loader as SkinTemplateLoader
participant Registry as WidgetRegistry
participant API as API CUS
Shell->>API: GET /panels/operations
API-->>Shell: theme.skin=operations
Shell->>Loader: loadSkinTemplates(operations)
Loader->>Registry: registerSkinTemplates
Shell->>API: GET dashboard JSON
Shell->>Registry: resolve type + skin Layouts composables¶
Los plugins aportan fragmentos con register_layouts. El loader fusiona con merge_layouts y expone:
| Endpoint | Uso |
|---|---|
GET /api/v1/ui/layouts/{layoutId}?panelId= | Nodos del layout fusionado |
GET /api/v1/ui/dashboards/{id}?panelId=&layoutHandle= | Dashboard con widgets del layout aplicados |
ModuleScreen pasa screen.layoutHandle al cargar el dashboard (p. ej. panel-home en operations).
Shell declarativo¶
PanelConfiguration.shellLayout define slots del chrome (shell.sidebar.footer, toolbar móvil). GET /api/v1/info devuelve shell.defaultRoute y rutas de auth; RootRedirect en el demo usa esa respuesta (sin hardcode /operations).
Catálogo de widget types¶
GET /api/v1/ui/widget-types lista tipos core (__framework__) y de dominio (plugin owner). En desarrollo, DashboardRenderer advierte tipos desconocidos vía warnUnknownDashboardWidgetTypes.
Publicación¶
scripts/publish-cloudsmith.sh publica wheels Python (core, framework y cada cortex-plugin-*), paquetes npm (@cortex/panel-core, @cortex/panel-shadcn, @cortex/web, skins) y deja los bundles de template en framework/web-shadcn/public/skins/.
Código fuente de plugins: repos GitHub cortex-ia-com-co/cortex-plugin-* integrados en el monorepo vía submódulos. Ver Repositorios y submódulos.