Paquete pip local que unifica la interfaz de todas las apps del equipo de sistemas. Sidebar tipo Microsoft 365 Admin Center, tema compartido entre apps y registro centralizado de aplicaciones.
Tres apps con HTML, CSS y JS duplicados exactamente. Sin este paquete, cualquier cambio en la UI había que replicarlo a mano en cada app.
base.html en 3 repos distintosdatefmt y timesince definidos 3 vecesshell.css centralizado con todos los estilos comunesshell.js único: tema, sidebar, admin dropdown, confirmFlask Blueprint con templates y static propios, instalado con pip install -e.
sisapps-ui/
├── pyproject.toml
└── sisapps_ui/
├── __init__.py # init_app() — punto de entrada, blueprint, context_processor
├── registry.py # DEFAULT_APPS — lista de apps conocidas con metadatos
├── filters.py # Filtros Jinja: datefmt, timesince
├── static/sisapps/
│ ├── shell.css # CSS común: variables, navbar, cards, forms, sidebar...
│ └── shell.js # JS: tema, sidebar collapse, admin dropdown, confirmDialog
└── templates/sisapps/
└── shell_base.html # Template base: sidebar + navbar + blocks
# En app.py de cada app: from sisapps_ui import init_app as init_shell init_shell(app, app_id='sisalerts', app_name='SIS Alerts', app_icon='🔔', nav_links=[ {'label': 'Dashboard', 'href': '/'}, {'label': 'Alertas', 'href': '/alerts'}, ], admin_links=[ {'label': 'Buzones', 'href': '/admin/mailboxes'}, ], )
Registra el blueprint, inyecta context processors y registra los filtros Jinja comunes.
{# base.html de cada app — solo lo específico #} {% extends "sisapps/shell_base.html" %} {% block app_css %} <link rel="stylesheet" href="{{ url_for('static', filename='global.css') }}"> {% endblock %} {% block user_info %} {# avatar/iniciales específico de la app #} {% endblock %}
global.css queda reducido a variables --primary y estilos exclusivos de la app.
Registro centralizado en registry.py. Toda la UI lee la lista de este registro para construir la sidebar y las tarjetas del hub.
Cada entrada en registry.py incluye un campo icon_svg con SVG inline
(además del icon emoji como fallback). Los templates lo renderizan con {{ app.icon_svg | safe }}.
# registry.py — extracto { 'id': 'sisalerts', 'name': 'SIS Alerts', 'icon': '🔔', # fallback emoji 'icon_svg': '<svg width="20" height="20" ...>...</svg>', 'port': 8082, ... }
La sidebar usa los SVGs de 20×20px. El hub usa 24×24px. La navbar muestra el icono de la app actual en 18×18px.
El título de la app en la navbar es un enlace <a href="/"> con icono + nombre.
La marca SisApps en la parte superior de la sidebar es un enlace a la URL del hub.
Las entradas de la sidebar muestran solo icono + nombre, sin subtítulo de descripción.
Cada app sirve su propia URL en los enlaces de la sidebar. El sistema sigue una cascada de prioridad para determinar la URL correcta.
app-config.json › sisapps.urls.{id}sisapps-urls.json en la raíz del repo.gitignore). Todas las apps lo leen automáticamente.http://localhost:{puerto}{
"sisapps-hub": "https://sisapps.empresa.com",
"sisalerts": "https://sisalerts.empresa.com",
"security-monitor": "https://secmon.empresa.com",
"hypervmonitor": "https://hypervmon.empresa.com"
}
El paquete localiza este fichero subiendo desde su propio directorio (sisapps_ui/__file__ → raiz del repo).
Solo hay que mantener este fichero; todas las apps lo usan sin reinicio.
localStorage es por origen (scheme+host+puerto), por lo que no se comparte entre apps en diferentes puertos.
La solución usa cookies, que ignoran el puerto (RFC 6265).
sisapps_theme
Almacena dark o light. Al navegar de una app a otra, el tema se mantiene
porque la cookie se comparte en el mismo hostname independientemente del puerto.
// shell.js — toggle de tema function toggleTheme() { const theme = doc.body.dataset.theme === 'dark' ? 'light' : 'dark'; doc.body.dataset.theme = theme; setCookie('sisapps_theme', theme, 365); }
Migra automáticamente desde los keys antiguos de localStorage (sisalerts-theme, secmon-theme, hvmon-theme).
sisapps_sidebar
Almacena collapsed o expanded. Si el usuario colapsa la sidebar en una app,
llega colapsada a las demás.
// Sidebar: 48px icono / 220px expandida .sisapps-sidebar { width: 220px; transition: width 0.22s ease; } .sisapps-sidebar.collapsed { width: 48px; }
En móvil (<768px) la sidebar se oculta y se activa con el hamburger de la navbar.
Cada app gestiona su propia base de usuarios, pero el paquete proporciona mecanismos comunes para unificar el control de acceso.
Lista de UPNs en app-config.json › sisapps.admins que son admin en todas las apps automáticamente.
init_app() la carga y la pone en app.config['SISAPPS_ADMINS'].
// app-config.json { "sisapps": { "admins": ["[email protected]"] } }
Para equipos pequeños donde el SSO ya filtra quién tiene acceso: cualquier usuario que pase el App Proxy se convierte en admin automáticamente.
// app-config.json { "sso": { "auto_admin": true } }
El SSO de Entra ID Application Proxy inyecta X-MS-CLIENT-PRINCIPAL-NAME y actua como barrera de acceso.
El paquete se instala una sola vez en el servidor y todas las apps lo comparten mediante el entorno Python de cada una.
# En el servidor, en el venv de cada app: pip install -e C:\apps\jmfernandez\sisapps-ui # Verificar: python -c "import sisapps_ui; print('OK')"
Con -e (editable) los cambios en el paquete se reflejan sin reinstalar. Solo hay que hacer restart del servicio.
# deploy.ps1 de cada app incluye: git -C C:\apps\jmfernandez pull pip install -e C:\apps\jmfernandez\sisapps-ui # ... restart WinSW service
El paquete está en sparse checkout del repo en el servidor. Un git pull actualiza el paquete y todas las apps que lo usan.
| Block | Quien lo rellena | Default |
|---|---|---|
title | Página individual | Nombre de la app |
extra_head | Página individual | Vacío |
app_css | base.html de la app | Link al global.css de la app |
nav_links | Shell — automático | Generado desde sisapps_nav_links |
admin_dropdown | Shell — automático | Generado desde sisapps_admin_links |
user_info | base.html de la app | Vacío (cada app tiene su propio avatar) |
content | Página individual | Vacío |
after_content | base.html de la app | Vacío (sisalerts pone confirmDialog aquí) |
extra_scripts | Página individual | Vacío |