← Volver al índice
💻 Gestión de Activos IT

Asset Manager

Sistema centralizado para la gestión y aprobación de material IT asignado a usuarios. Aprobaciones auditables via Microsoft Approvals + Purview, portal web con SSO y sincronización desde listas SharePoint.

🐍 Python 3 + Flask + Waitress
🗄️ SQLite WAL
🌐 Puerto 8087
Power Automate + Approvals
En planificación
N
Listas SharePoint configurables
30m
Intervalo de sincronización
7d
Caducidad aprobación antes de escalar
1
Aprobación consolidada por usuario

Qué resuelve este proyecto

Las asignaciones de material IT viven en múltiples listas de SharePoint. No hay proceso automatizado que confirme con el usuario lo que tiene asignado, ni registro auditable de esa confirmación.

✅ Aprobación auditable con Microsoft Approvals

Power Automate + conector de Approvals. Aprobaciones gestionadas en la infraestructura Microsoft, visibles desde Purview y el centro de aprobaciones M365. No manipulables por la aplicación local.

🖥️ Portal web con SSO

Interfaz web con Entra ID App Proxy donde cada usuario consulta en cualquier momento el material asignado, agrupado por categoría, con el estado de su última aprobación.

🔔 Notificación de desasignación

Cuando se detecta que un activo ha sido desasignado, el usuario recibe un aviso consolidado con todo el material que ha dejado de tener asignado ese día. No requiere aprobación, solo notificación. Mecanismo a decidir (ver sección dedicada).

🔄 Sincronización automática

El scheduler sincroniza las listas de activos y la lista de log de aprobaciones desde SharePoint a SQLite local. Páginas instantáneas, sin latencia Graph API por carga.

🔮 Escalabilidad

Admin UI para asignar material desde la web, ciclo de vida de activos (devolución, baja), reporting y write-back a SharePoint. La base de datos local facilita todas estas extensiones.

Las dos decisiones clave

Decisión 1 — Almacenamiento de activos
SQLite local con sync periódico desde SharePoint
Las listas SP son el source of truth. Python sincroniza una copia local que permite carga de página instantánea, historial y sirve de base para las extensiones futuras. Coherente con todas las sisapps del monorepo.
Decisión 2 — Mecanismo de aprobación
Microsoft Approvals vía Power Automate — NO aprobación custom en la app
Las aprobaciones residen en la infraestructura Microsoft. Son auditables desde Purview y el centro de aprobaciones M365. Manipular el registro requiere permisos de administrador de tenant. PA gestiona recordatorios y caducidad de forma nativa, sin código personalizado.

✅ Microsoft Approvals + PA (elegido)

  • Auditable desde Purview sin configuración extra
  • Registro no manipulable por la app (infraestructura Microsoft)
  • Recordatorios y caducidad gestionados por PA de forma nativa
  • Escalación al manager configurable en el propio flujo PA
  • UX de aprobación ya conocida por los usuarios (Teams Approvals)

✗ Aprobación custom en la app (descartado)

  • Registro en SQLite local: manipulable, no válido para compliance
  • No auditable desde Purview
  • Requiere token custom, páginas sin SSO y scheduler de recordatorios
  • Toda la lógica de caducidad y escalación a implementar manualmente

Flujo de datos completo

La clave del diseño: el sync acumula novedades durante el día en un buffer local. Un job independiente dispara PA una única vez por usuario con todo lo acumulado — aunque los activos lleguen de distintas listas en distintos ciclos de sync.

⚙️ Job 1 — Sync (cada 30 min)

Lee listas SP, detecta cambios. Acumula asignaciones en pending_approvals y desasignaciones en pending_unassignments. Nunca llama a PA directamente.

Merge si ya existe fila para hoy. Sin historial SP — Python tiene su propia copia en SQLite.

⚡ Job 2 — Trigger asignaciones (1×/día)

Lee pending_approvals con status=pending. Por cada usuario: POST al flujo PA de aprobaciones con payload consolidado.

Marca como triggered. Una única aprobación PA por usuario por día.

🔔 Job 3 — Trigger desasignaciones (1×/día)

Lee pending_unassignments con status=pending. Por cada usuario: envía notificación consolidada.

Mecanismo a decidir PA ligero (auditable) o notificación directa Python (más simple).

1
Sync job (cada 30 min) — sharepoint_sync.sync_all_lists() lee todas las listas de activos SP vía Graph API. UPSERT en assets.
2
Detección de cambios — comparando el valor actual de SP contra assets.assigned_to_upn en SQLite (Python tiene el historial propio, sin necesidad de consultar el historial de versiones de SP):
  • assigned_to_upn vacío → existía en DB → desasignación del usuario que estaba guardado
  • assigned_to_upn nuevo o cambiado → asignación al nuevo usuario
  • Item desaparece del SP + DB tenía usuario → desasignación del último usuario conocido
3
Acumulación en buffers (separados por tipo):
— Asignaciones nuevas → UPSERT en pending_approvals (user_upn, date, assets_json). Merge si ya existe fila para hoy.
— Desasignaciones → UPSERT en pending_unassignments (user_upn, date, assets_json). Merge si ya existe.
Sin llamada a PA todavía en ninguno de los dos casos.
4
Trigger job (1×/día a hora configurable) — lee pending_approvals con status=pending. Para cada usuario construye el payload completo: assets_new (del buffer) + assets_prev (todos los activos activos del usuario no incluidos como nuevos). Una única aprobación por usuario, con todo lo acumulado en el día.
5
HTTP trigger → Power Automate — POST al endpoint del flujo PA. Asíncrono: PA responde con 202 Accepted y ejecuta el flujo en la nube. Python guarda el trigger en pa_triggers y marca pending_approvals como triggered.
6
Microsoft Approvals — PA crea la aprobación asignada al usuario con tabla HTML del material (nuevos destacados, previos listados). Gestiona recordatorios automáticos y caducidad a los 7 días. Disponible en Teams Approvals, email y app móvil.
7
Resultado → SP Approval Log — PA escribe el resultado (aprobado / rechazado / caducado + escalación al manager) en la lista SP: Approval Log. Registro inmutable en SharePoint, auditable desde Purview.
8
Sync approval log — el sync job también lee SP: Approval Log y hace UPSERT en approval_log en SQLite. El portal web muestra el estado de aprobación leyendo esta copia local sin latencia.

Diseño del flujo PA

Un único flujo, disparado por HTTP desde Python. PA gestiona toda la lógica de aprobación, recordatorios, caducidad y escalación de forma nativa.

HTTP Request
(desde Python)
Trigger. Payload con user_upn + assets[]
Crear Approval
(Microsoft Approvals)
Asignada al usuario. Caduca en 7 días
Esperar respuesta
(+ recordatorios)
PA envía recordatorios automáticos
¿Resp.?
Aprobado / Rechazado / Caducado
Registrar
Aprobado
→ SP Approval Log
Registrar
Rechazado
→ SP Approval Log
Escalar
al Manager
M365 Users + Teams msg → SP Log

Payload HTTP trigger → PA

// POST {pa_flow_url} // API Key: x-functions-key o header custom { "user_upn": "[email protected]", "user_name": "Juan García", "portal_url": "https://portal.idealista.com/asset-manager", "assets_new": [ { "name": "Dell Latitude 5540", "tag": "SN123456", "category": "Portátil" } ], "assets_prev": [ { "name": "iPhone 14", "tag": "IMEI-999", "category": "Móvil" } ] }

Configuración del flujo PA

TriggerWhen an HTTP request is received
Approval typeApprove/Reject – First to respond
Assigned to@{triggerBody()?['user_upn']}
Due dateaddDays(utcNow(), 7)
Timeout7 días (caducidad nativa PA)
Item linkURL del portal web
Manager lookupConector M365 Users: Get manager
Escalation notificationPost message in a chat or channel (Teams)
Lista SP: Approval Log — creada específicamente para este proyecto
PA escribe en ella al cerrar cada aprobación. Columnas: UserUPN, Outcome (Approved/Rejected/Expired), RespondedBy, RespondedAt, Comments, ApprovalId (GUID de PA), AssetSnapshot (JSON). Python sincroniza esta lista igual que el resto, generando el mirror local en approval_log.

Layout del proyecto

La eliminación del motor de aprobaciones custom simplifica significativamente el código Python. En morado, la nueva capa de integración con Power Automate.

asset-manager/ Puerto 8087 ├── CLAUDE.md → @docs/context.md ├── run_webapp.py Waitress + scheduler daemon ├── requirements.txt ├── deploy.ps1 git pull + pip install + restart ├── asset-manager-service.xml WinSW service ├── config/ │ └── app-config.json ├── db/ │ ├── database.py get_db(), close_db(), _migrate() │ └── schema.sql ├── services/ │ ├── scheduler.py Sync assets + sync approval log │ ├── sharepoint_sync.py Graph API → SQLite (assets + approval_log) │ └── pa_trigger.py HTTP trigger al flujo PA con payload consolidado ├── webapp/ │ ├── app.py Flask factory + sisapps-ui init │ ├── auth.py Re-export sisapps_ui.auth │ ├── routes/ │ │ ├── dashboard.py GET / → mis activos + estado aprobación │ │ ├── admin.py /admin/* → admin-required │ │ └── api.py JSON endpoints │ ├── static/ │ │ ├── global.css │ │ └── dashboard.js │ └── templates/ │ ├── base.html Extiende sisapps/shell_base.html │ ├── dashboard.html Activos + estado aprobación │ ├── admin_overview.html Aprobaciones + filtros │ ├── admin_assets.html │ └── admin_sync.html └── docs/ └── context.md

Eliminados respecto al diseño inicial: approval_engine.py (motor custom), ruta /approval/<token> (página sin SSO), tablas approval_batches y approval_items. Simplificación significativa del código Python.

Esquema SQLite

SQLite con WAL mode, conexiones thread-local. La tabla approval_log es un mirror de la lista SP que PA actualiza — no se escribe desde Python, solo se lee desde SharePoint.

TablaDescripciónCampos clave
settings Key-value persistente de la app key, value
sp_lists Listas SP configuradas (activos + approval log), con estado de último sync list_id, category, list_type, last_sync_at, last_sync_error
assets Un registro por item de cualquier lista de activos SP. UPSERT por (sp_list_id, sp_item_id) assigned_to_upn, asset_name, asset_tag, asset_details (JSON), first_seen_at, removed_at
pending_approvals Buffer acumulador diario de asignaciones nuevas. El sync job acumula activos nuevos de cada usuario. El trigger job los consume una vez al día. Garantiza una sola aprobación PA por usuario por día aunque el sync corra varias veces. user_upn, date, assets_json (JSON acumulado), status (pending/triggered), created_at, triggered_at
pending_unassignments Buffer acumulador diario de desasignaciones. Mismo patrón que pending_approvals pero para activos retirados. Consumido por el trigger job de notificación (mecanismo a decidir: PA o directo desde Python). user_upn, date, assets_json (JSON acumulado), status (pending/triggered), created_at, triggered_at
approval_log Mirror de SP "Approval Log". PA escribe en SP; Python solo sincroniza. Registro no modificable desde la app. sp_item_id, user_upn, outcome, responded_by, responded_at, comments, approval_id, asset_snapshot (JSON)
pa_triggers Log de cada llamada HTTP a PA: usuario, timestamp, assets incluidos, respuesta HTTP. user_upn, triggered_at, asset_ids (JSON), pa_response_status
sync_log Log de cada ejecución del sync: estado, items procesados, errores started_at, status, items_new, items_removed, error_message
audit_log Eventos internos de la app: sync, pa_trigger enviado. Las aprobaciones en sí están en SP / Purview. timestamp, actor_upn, action, details (JSON)
Detección de "nuevo hoy" y control de duplicados
Asset nuevo: first_seen_at = timestamp del sync actual. Reasignación: cambió assigned_to_upn. Antes de disparar PA, se comprueba pa_triggers: si ya hay un trigger de las últimas 24h para ese usuario, se añaden los nuevos activos al snapshot pero no se crea una nueva aprobación.

Estructura de app-config.json

// config/app-config.json { "port": 8087, "db_path": "../db/asset-manager.db", "sharepoint": { "site_id": "idealista.sharepoint.com,<SITE-GUID>,<WEB-GUID>", "sync_interval_minutes": 30, "approval_log_list_id": "GUID-APPROVAL-LOG", // lista que PA escribe "lists": [ { "list_id": "GUID-PORTATILES", "display_name": "Portatiles", "category": "Portatiles", "column_mapping": { "assigned_to_upn": "AsignadoA_Email", "asset_name": "Modelo", "asset_tag": "NumeroSerie", "assignment_date": "FechaAsignacion" }, "extra_columns": ["RAM", "Disco", "SO"] } ] }, "power_automate": { "flow_url": "https://prod-XX.westeurope.logic.azure.com/workflows/.../triggers/...", "api_key_header": "x-functions-key", // o header custom configurado en el flujo "api_key": "KEY_HERE", "trigger_window_hours": 24, // no repetir trigger si ya se lanzó en 24h "portal_url": "https://portal.idealista.com/asset-manager" } }

Endpoints web

Sin páginas de aprobación custom — toda la aprobación ocurre en Teams / M365 Approvals. El portal web muestra el resultado sincronizado desde SharePoint.

RutaAuthDescripción
GET / SSO Dashboard del usuario: material asignado agrupado por categoría. Badge de estado de aprobación (aprobado / pendiente / rechazado) junto a cada grupo.
GET /admin/overview Admin Aprobaciones de todos los usuarios: filtros por outcome, usuario y fecha. Link al centro de aprobaciones M365 para ver el detalle completo.
GET /admin/assets Admin Todos los activos de todas las listas. Búsqueda por usuario, tag de inventario o categoría.
GET /admin/sync Admin Estado de sincronización (listas de activos + approval log), historial sync_log y trigger manual.
POST /admin/sync/trigger Admin Lanza un sync inmediato fuera del intervalo programado.
GET /api/my-assets SSO JSON con activos del usuario actual y último estado de aprobación.

Graph API & App Registration

Nueva App Registration con certificado (Windows cert store). Permisos mínimos necesarios — el manager lookup y las notificaciones las gestiona el flujo de PA con sus propias credenciales.

Permisos App Registration (Python)

PermisoTipoPara qué
Sites.Read.AllApplicationLeer listas SP (activos + approval log)
User.Read.AllApplicationResolver nombres y avatares SSO

Quitados respecto al plan inicial: Mail.Send (lo gestiona PA). Futuro write-back: Sites.ReadWrite.All.

Llamadas Graph desde Python

# Leer items de lista de activos SP GET /sites/{site_id}/lists/{list_id}/items ?$expand=fields&$top=200 # seguir @odata.nextLink # Descubrir columnas internas (una vez) GET /sites/{site_id}/lists/{list_id}/columns # Leer lista Approval Log (sync) GET /sites/{site_id}/lists/{approval_log_id}/items ?$expand=fields&$top=200 # Avatares SSO GET /users/{upn}/photo/$value

Credenciales del flujo Power Automate

El flujo PA usa su propia conexión (cuenta de servicio o App Registration separada) para el conector de Approvals, el conector de M365 Users (manager lookup), el conector de Teams y la escritura en la lista SP Approval Log. No comparte credenciales con la app Python.

Conector PAPara qué
Microsoft ApprovalsCrear y gestionar la aprobación
Office 365 UsersGet manager (para escalación al caducar)
Microsoft TeamsNotificación de escalación al manager
SharePointCreate item en lista Approval Log

Jobs del scheduler (Python)

Significativamente más simple que el diseño anterior. Recordatorios, caducidad y escalación los gestiona PA. Python solo sincroniza datos y dispara el trigger.

JobFrecuenciaAcción
Sync assets SP → SQLite Cada 30 min configurable Leer listas de activos, UPSERT en assets. Si hay nuevos activos → UPSERT en pending_approvals (merge si ya existe fila para hoy). No llama a PA.
Sync approval log SP → SQLite Cada 30 min junto al anterior Leer lista SP: Approval Log, UPSERT en approval_log. Permite mostrar el estado en el portal sin latencia Graph API.
Trigger PA — asignaciones 1× al día a hora configurable Lee pending_approvals con status=pending. Por cada usuario: POST al flujo PA con payload consolidado (activos nuevos + previos). Marca triggered. Garantiza una única aprobación por usuario por día.
Trigger — desasignaciones a decidir 1× al día (misma hora) Lee pending_unassignments con status=pending. Por cada usuario: envía notificación con los activos desasignados ese día. Mecanismo pendiente de decisión (ver sección dedicada).
Purga Diario Limpiar audit_log, pa_triggers, pending_approvals y pending_unassignments con más de 6 meses.
Recordatorios, caducidad y escalación: gestionados por PA
No hay jobs de scheduler en Python para estas tareas. PA gestiona el ciclo de vida completo en la nube, de forma auditable y sin dependencia de que el servicio Windows esté levantado.

Notificación de desasignaciones

Las desasignaciones no requieren aprobación, solo notificación informativa al usuario. Python detecta el evento comparando SP con su copia local en SQLite — sin necesidad de consultar el historial de versiones de SP. Hay dos opciones para enviar el aviso.

Cómo Python detecta la desasignación sin historial SP
La tabla assets guarda siempre el último assigned_to_upn conocido. Cuando el sync lee SP y encuentra el campo vacío (o el item ha desaparecido), Python ya sabe quién era el usuario anterior porque lo tiene en su propia DB. No hace falta ninguna llamada extra a Graph.

Opción A — Flujo PA dedicado (simple, sin Approvals)

Python dispara un segundo flujo PA ligero, sin conector de Approvals. El flujo solo envía email + Teams al usuario con la lista de material desasignado y escribe una entrada en SP Approval Log.

Registro auditable en SP / Purview (misma lista Approval Log)
Coherente con el canal de asignaciones (todo via PA)
Flujo muy simple: trigger → send email/Teams → create SP item
Requiere crear y mantener un segundo flujo PA
// Payload Python → PA (flujo notificación) { "user_upn": "[email protected]", "user_name": "Juan García", "assets_removed": [ { "name": "Dell Latitude 5540", "tag": "SN123" } ] }

Opción B — Notificación directa desde Python

Python envía directamente: email via Graph API sendMail y/o Teams via SisAlerts ingest. Sin PA de por medio. El evento se registra únicamente en audit_log de SQLite.

Sin dependencias externas adicionales, todo en Python
Más rápido de implementar, sin flujo PA que configurar
Control total del formato y contenido del mensaje
Registro solo en SQLite local, no en SP/Purview
Si hay requisito de trazabilidad, la Opción A es preferible
# Python directo via SisAlerts POST {sisalerts_url}/api/ingest X-Api-Key: {key} { "subject": "Material IT desasignado", "priority": "low", "body": "Se ha desasignado: ..." }
Recomendación
Si las desasignaciones tienen el mismo requisito de trazabilidad que las asignaciones → Opción A. Si son meramente informativas y no necesitan registro en Purview → Opción B, más simple. Ambas usan el mismo buffer pending_unassignments y la misma lógica de trigger diario, por lo que cambiar de una a otra en el futuro no implica refactorizar el scheduler.

Fases de implementación

Fase 1 — Fundación Día 1
Estructura del proyecto, db/schema.sql, db/database.py, config/app-config.json, run_webapp.py, deploy.ps1, WinSW XML.
Fase 2 — Shell web Día 1
webapp/app.py con init_shell() de sisapps-ui, base.html, auth.py, dashboard con estado vacío. Verificar SSO con dev bypass.
Fase 3 — Sync SharePoint Día 2
services/sharepoint_sync.py: cliente Graph API con paginación y column_mapping. Sync de listas de activos + lista Approval Log. Scheduler con ambos jobs.
Fase 4 — Dashboard de activos Día 2
dashboard.html: activos agrupados por categoría + badge con estado de aprobación (leyendo approval_log).
Fase 5 — Flujo Power Automate Día 3
Crear flujo PA en el tenant: HTTP trigger → crear Approval → esperar respuesta (7 días) → rama aprobado/rechazado/caducado → crear item en SP Approval Log. Configurar cuenta de servicio y conexiones de los conectores.
Fase 6 — PA trigger desde Python Día 3
services/pa_trigger.py: construye el payload consolidado (nuevos + previos) y hace POST al HTTP trigger del flujo. Guarda el trigger en pa_triggers. Probar end-to-end con usuario de prueba.
Fase 7 — Admin Día 4
Overview de aprobaciones con filtros y link al centro de aprobaciones M365, browser de activos, estado de sync con trigger manual. Registro en sisapps_ui/registry.py.
Fase 8 — Deploy a producción Día 5
Crear App Registration, configurar certificado, validar SSO con App Proxy, instalar WinSW service. Publicar el flujo PA y validar ciclo completo con datos reales.

Posibles dificultades

RiesgoProbabilidadMitigación
Nombres internos SP ≠ display names Alta Llamar a GET /lists/{id}/columns para descubrir los nombres OData internos antes de configurar el column_mapping.
Seguridad del HTTP trigger PA Alta El endpoint HTTP trigger debe protegerse. Opciones: (1) SAS key nativa de Logic Apps en la URL, (2) IP restriction en el flujo (solo la IP del servidor Windows), (3) header de API key verificado en la primera acción del flujo.
Identidad de usuario inconsistente en SP Media Las listas pueden tener email, UPN o solo nombre. Normalizar a lowercase. Fallback a Graph API /users?$filter=mail eq '...' si hace falta.
Límites de PA y throttling Media Power Automate tiene límites de ejecuciones por día según licencia. Con volumen bajo (decenas de aprobaciones/día) no debería ser problema. Monitorizar desde el centro de administración PA.
Paginación en listas SP grandes Media Seguir @odata.nextLink en el sync client para listas con más de 200 items.
Syncs concurrentes Baja Lock de sync con threading.Lock(). Siguiente tick salta si ya hay sync en curso.
Approval Log desincronizado Baja El portal muestra el estado del último sync. Máximo 30 min de retraso. Aceptable para este caso de uso.

Extensiones previstas (Fase 2)

🏗️ Admin UI de asignaciones

Crear y editar asignaciones desde el portal web. Se escribe en SQLite local y un job sincroniza a SharePoint via PATCH /items/{id}/fields. Requiere Sites.ReadWrite.All. Al asignar, Python dispara automáticamente el flujo PA.

🔄 Ciclo de vida de activos

Estados: assigned → returned → retired → in_repair. Transiciones registradas en audit_log. Al devolver un activo, se actualiza SP y se cancela cualquier aprobación pendiente en PA.

📊 Reporting

Activos por departamento, tasa de respuesta a aprobaciones, tiempo medio hasta aprobación, activos sin confirmar. Los datos ya están en SQLite local desde el mirror de SP Approval Log.

Asset Manager · Equipo de Sistemas · 2026 · En planificación