Paquete compartido — sisapps-ui

SisApps UI Shell

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.

Tipo: Python package (pip editable)
·
Apps integradas: 4
·
Estado: Producción
·
Ruta: c:\apps\jmfernandez\sisapps-ui
4
Apps integradas
1
Paquete único para todas
pip -e
Cambios instantáneos en dev
cookie
Tema compartido entre puertos

Problema resuelto

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.

🔥 Antes — Código duplicado

  • Mismo base.html en 3 repos distintos
  • Mismo CSS copiado (~600 líneas cada uno)
  • Mismo JS de tema, admin dropdown y confirm dialog
  • Filtros Jinja datefmt y timesince definidos 3 veces
  • Cambio de estilo = tocar 3 apps por separado
  • Sin navegación entre apps

✅ Ahora — Fuente única

  • Un paquete instalado en todas las apps
  • shell.css centralizado con todos los estilos comunes
  • shell.js único: tema, sidebar, admin dropdown, confirm
  • Filtros registrados automáticamente por el paquete
  • Cambio de estilo = tocar 1 fichero
  • Sidebar lateral con navegación entre todas las apps

Estructura del paquete

Flask Blueprint con templates y static propios, instalado con pip install -e.

📁 Ficheros del paquete

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

🔌 init_app() — integración en Flask

# 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 tras la migración

{# 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.

Apps integradas

Registro centralizado en registry.py. Toda la UI lee la lista de este registro para construir la sidebar y las tarjetas del hub.

🏠
SisApps Hub
Portal de entrada — landing con tarjetas y stats en vivo a todas las apps
:8080
🔔
SIS Alerts
Plataforma centralizada de alertas operacionales
:8082
🔒
Security Monitor
Monitor eventos seguridad M365 con Graph Security API
:8083
HyperV Monitor
Monitor de clusters Hyper-V y espacio en CSVs
:8086

🖼 Iconos SVG en el registro

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.

Resolución de URLs entre apps

Cada app sirve su propia URL en los enlaces de la sidebar. El sistema sigue una cascada de prioridad para determinar la URL correcta.

1
Override por app — app-config.json › sisapps.urls.{id}
URL específica definida en la configuración de esa app. Casos excepcionales.
2
Fichero compartido — sisapps-urls.json en la raíz del repo
Fichero con las URLs del App Proxy de producción. Se mantiene fuera de git (.gitignore). Todas las apps lo leen automáticamente.
3
Fallback — http://localhost:{puerto}
Para desarrollo local. Cada app usa su puerto predeterminado del registro.

📄 Ejemplo de sisapps-urls.json (producción)

{
  "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.

Tema y sidebar entre apps

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

🌟 Cookie de tema — 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).

☰ Cookie de sidebar — 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.

Permisos compartidos entre apps

Cada app gestiona su propia base de usuarios, pero el paquete proporciona mecanismos comunes para unificar el control de acceso.

👥 SISAPPS_ADMINS — Lista compartida

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]"]
  }
}

✅ auto_admin — Todos los SSO son admins

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.

Instalación en producción

El paquete se instala una sola vez en el servidor y todas las apps lo comparten mediante el entorno Python de cada una.

🚀 Primer deploy (manual)

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

🔄 Actualizaciones via deploy.ps1

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

📌 Blocks disponibles en shell_base.html

BlockQuien lo rellenaDefault
titlePágina individualNombre de la app
extra_headPágina individualVacío
app_cssbase.html de la appLink al global.css de la app
nav_linksShell — automáticoGenerado desde sisapps_nav_links
admin_dropdownShell — automáticoGenerado desde sisapps_admin_links
user_infobase.html de la appVacío (cada app tiene su propio avatar)
contentPágina individualVacío
after_contentbase.html de la appVacío (sisalerts pone confirmDialog aquí)
extra_scriptsPágina individualVacío