Pantallas del sistema¶
El frontend de OrpycaMCP (frontend/, SvelteKit 2 + Svelte 4 SSR, E22) es la única
capa de presentación oficial del sistema. Ninguna pantalla accede a datos
directamente: cada vista tiene su propia subcarpeta api/ que actúa como
Backend-for-Frontend (BFF) — recibe la acción del navegador, adjunta el JWT
(guardado en cookie httpOnly, nunca visible al JS del cliente) y reenvía la petición
al api-gateway. Ver el detalle de esta arquitectura en
Arquitectura, sección "Flujo de autenticación".
La navegación y las acciones visibles en cada pantalla reflejan el RBAC real del
usuario (PERM_RADI, USUA_PERM_EXPEDIENTE, USUA_PERM_ADMIN, PERM_RADI_SALIDA,
PERM_FIRMA, PERM_DLQ_ADMIN, …), pero esa restricción es solo defensa en
profundidad en la UI — el backend siempre revalida el permiso y el nivel de
clearance (RF-SEG-08) por request.
Qué de esto está verificado desde el navegador¶
Este documento es un inventario funcional: dice qué hace cada pantalla, no qué se ha
comprobado. La distinción importa, porque hasta agosto de 2026 la garantía del proyecto era de
unidad y de contrato, no de uso: las 36 pantallas de (app)/ tenían cero cobertura de
navegador.
En agosto de 2026 se ejecutó el recorrido completo —bloques B0 a B13, con los actos reales
incluidos los irreversibles, contra tenant_demo—. El procedimiento, los casos y los puntos
ciegos declarados están en el plan de pruebas; el detalle de cada defecto
encontrado y de cómo se cerró, en el changelog.
El patrón dominante de aquella revisión merece leerse antes que la tabla que sigue: el sistema ya tenía el dato bueno y la pantalla mostraba otro. Ninguno de esos defectos era de cálculo ni de diseño, y ninguno lo detectaba un test de un solo lado — por eso hacía falta pulsar los botones.
El barrido negativo con un usuario de permisos reducidos (B13) confirmó lo que más importaba:
los controles de acceso no viven solo en el navegador. Todo lo que la interfaz esconde se
invocó además por el proxy BFF, y el backend respondió 403 en todos los casos de escritura. La decisión
de producto que dejó abierta —lecturas de TRD y PINAR sin permiso, con la interfaz escondiendo
esas pantallas— se resolvió el 2026-08-28, y por partes: el vocabulario de clasificación
queda abierto a cualquier autenticado del tenant (lo consume /radicar), el circuito de
versiones del instrumento pasa a exigir USUA_PERM_TRD y PINAR entero USUA_PERM_ADMIN — ver el
plan de pruebas.
Pantallas públicas ((public)/)¶
| Ruta | Propósito |
|---|---|
(public)/ |
Landing pública del producto: qué es OrpycaMCP, marco normativo (Ley 594/2000, Acuerdo AGN 060/2001), capacidades, comparativa con Orfeo y licencia AGPL v3. Si locals.user está poblado (sesión activa), su +page.server.js redirige 302 a /dashboard — un usuario autenticado nunca la ve |
Trámites ciudadanos ((public)/, sin cuenta)¶
Dos pantallas para quien no es usuario del sistema. Exigir registrarse para presentar un derecho de petición sería poner una barrera donde la ley no la pone (Ley 1755/2015), así que no hay sesión, ni cuenta, ni identificación obligatoria.
No sustituyen a la ventanilla de mostrador: /radicar sigue siendo la pantalla del
funcionario de correspondencia que radica lo que llega en persona, por correo o por mensajería.
Son dos puertas al mismo trámite.
| Ruta | Propósito |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| (public)/radicacion | Ventanilla única en línea. Tipo de trámite del catálogo que la entidad declara recibir (admite_ventanilla_publica), remitente que puede ser persona o entidad, datos de notificación y hasta 5 soportes (PDF/JPG/PNG/TIFF, comprobados por contenido). El acuse destaca el código de verificación y advierte que no puede volver a mostrarse. Avisa antes de enviar si alguien se identifica sin dejar medio de respuesta. Si la entidad no ha declarado tipos o no ha habilitado una dependencia de ventanilla (depe_rad_tp2), la pantalla lo dice en vez de dejar rellenar un formulario que acabaría en error. T13: lleva una casilla obligatoria de autorización de tratamiento de datos (Ley 1581; con versión y enlace a la política que la entidad publica —la sirve GET /public/{tenant}/ventanilla/politica, ya no hay variables POLITICA_TRATAMIENTO_*—; si no hay política publicada no deja radicar, si cambia mientras se llena (409 politica_desactualizada) la recarga y pide autorizar de nuevo; una nota fija recuerda que mientras corra el plazo legal de conservación no procede la supresión, con enlace a #supresion), un campo trampa fuera de pantalla (honeypot) y una Idempotency-Key por contenido, de modo que reintentar un envío devuelve el mismo número |
| (public)/consulta | Seguimiento por código. Devuelve número, tipo, estado, fecha y dependencia responsable — no el contenido. Mantiene neutro el «no encontrado»: el backend responde igual para un código inexistente y para uno de un radicado clasificado, y la pantalla no deshace esa indistinción |
| (public)/eliminacion · (public)/eliminacion/[id] | Publicidad previa de la eliminación | El inventario de lo que la entidad va a eliminar, visible sin sesión (RF-TRD-07 fase 2). Dice hasta cuándo se puede uno oponer y por dónde: la ventanilla única que ya existe, citando el código del lote — montar un formulario de oposición aparte sería una segunda bandeja que nadie mira. La denominación de un expediente clasificado no se publica y la pantalla lo DICE («denominación reservada por ley», con la norma) en vez de dejar un hueco mudo: quien lee tiene que poder distinguir «no tiene nombre» de «su nombre está reservado», porque de eso depende que sepa si tiene algo que objetar. La fila sí sale, con serie, fechas extremas y volumen, porque omitirla dejaría de cumplir la publicidad justo sobre lo que más importa objetar. Un lote ya ejecutado sigue consultable como constancia de qué se eliminó, y uno retirado también, con su motivo: la constancia de que algo NO se eliminó le importa a quien leyó que iba a eliminarse (2026-08-29) | Público, sin autenticar. El recorte de la denominación va en el SELECT del backend, no en la pantalla | GET /api/v1/public/archive/eliminacion/{tenant}/lotes[/{id}] |
La entidad de estas pantallas sale de configuración de despliegue (PORTAL_TENANT_SLUG), no
de la URL: a un ciudadano no se le explica qué es un inquilino ni se le deja cambiarlo. Sin esa
variable, ambas se declaran no habilitadas.
Autenticación¶
| Ruta | Propósito |
|---|---|
(public)/login |
Inicia el flujo OAuth2 Authorization Code + PKCE contra Keycloak. Composición split: en móvil solo la tarjeta de ingreso; desde 960px aparece el panel de marca. Si Keycloak devuelve ?error=, muestra un mensaje mapeado desde una tabla cerrada en español (nunca el string crudo del IdP) con enlace «Reintentar» — evita el bucle de redirección |
(public)/login/callback |
Recibe el código de autorización, lo intercambia por tokens (server-side) y establece la sesión |
(public)/logout |
Cierra sesión local y en Keycloak. Solo por POST: la ruta no exporta load. Antes el cierre ocurría en el load —es decir, en un GET— y el topbar lo enlazaba con un <a>; con data-sveltekit-preload-data="hover" activo, SvelteKit ejecuta el load al pasar el ratón por encima, así que rozar el icono cerraba la sesión. Un GET con efectos lo dispara además cualquier prefetch, escáner o <img src> remoto. La página que ve un GET es una confirmación con un <form method="POST"> que funciona sin JavaScript; el POST cruza la comprobación de origen de SvelteKit (403 sin Origin) |
/403 |
Página de acceso denegado cuando el usuario no tiene el permiso requerido |
+error.svelte (raíz) |
Página de error de toda la aplicación — no existía: un 404 o un fallo del SSR caían en la pantalla por defecto de SvelteKit, sin marca ni salida. Texto propio para 404, 403 y 503; para el resto usa el mensaje que fijó handleError, que ya no incluye el mensaje crudo del error (devolvía cosas como connect ECONNREFUSED 172.18.0.7:8080). Publica una referencia de correlación de 8 caracteres que también queda en el log del servidor: es el único dato técnico que se muestra, y no describe el fallo. Deliberadamente autónoma (no usa AppLayout): si el error es que el layout no cargó, un error que dependa del layout no se puede renderizar |
Pantallas operativas ((app)/)¶
| Ruta | Pantalla | Propósito | Rol típico | Endpoints principales (vía gateway) |
|---|---|---|---|---|
/dashboard |
Panel personal | Cuatro módulos personales visibles sin scroll: 01 Radicados pendientes (los pasos de la bandeja de entrada, GET /workflows/inbox, misma fuente que el tablero), 02 Actividad reciente, 03 Alertas (radicados abiertos —incluido registered— por due_date, RF-RAD-04), 04 Favoritos. Las métricas agregadas del tenant quedan en una sección secundaria colapsable; su tarjeta «Dónde está el trámite» lee by_ubicacion (ubicación actual de lo que está en trámite), no la dependencia de radicación, cerrada por defecto y gateada por SGD_PERM_ESTADISTICA |
Todos los usuarios autenticados (métricas: SGD_PERM_ESTADISTICA) |
GET /api/v1/workflows/inbox, GET /api/v1/documents, GET /api/v1/reports/radicados (solo la sección de métricas) |
/bandeja |
Bandeja de entrada/trámites | Ver, tramitar, devolver, anular o responder radicados asignados; la lista y el detalle dicen dónde está el radicado AHORA (ADR-029): columna «Ubicación actual» (la que deriva el flujo; sin ella, la de radicación; nunca se inventa una por otra; a ≤768 px, donde la columna se oculta, sube como línea «En X» bajo el asunto), y en el detalle «Ubicación actual» —la primera fila, destacada— con el responsable cuando el flujo lo tiene (se nombra por su usuario, «Tú» si es quien mira; nunca el identificador crudo; si el perfil no puede leer el catálogo de usuarios —PERM_RADI—, dice «A cargo de un responsable de la dependencia» sin avisar de error ni repetir la llamada), «Cerrado en X» / «Anulado en X» para los terminales —que congelan la ubicación— y, aparte, la «Dependencia de radicación» (a quién iba dirigido, que no cambia con el trámite). Un radicado que el flujo aún no movió lo dice («Sin repartir — en X»; una Salida/Interno sin destino, «en la dependencia productora: X»; un terminal, «Anulado/Cerrado sin pasar por el flujo — en X»); el estado del radicado no se edita a mano (ADR-028): el detalle ofrece «Marcar en trámite», «Finalizar trámite» (motivo de cierre obligatorio —Respondido, No requiere respuesta (NRR), Traslado por competencia u Otro—, comentario y confirmación: cierra el radicado y no se deshace; exige PERM_TRAMITAR con nivel ≥3 y poseer el paso activo, o haber poseído el último paso completado si no hay paso activo) «Devolver» solo a quien posee el paso activo (incluido el paso sin dueño de su dependencia) y «Asignar a flujo» únicamente sin paso activo y a quien poseía el último paso completado o, sin pasos, a quien radicó (registered_by; si es nulo —importado o ingesta— no se ofrece y el reparto es automático o por cascada; R16); en el Tablero, «En trámite», Trasladar, Trámite, Completar, Revertir (solo con el paso pending, R19) y el arrastre son solo del poseedor del paso (quien lo ve pero no lo posee no los tiene); solicitar/otorgar vistos buenos (individual o en cadena); ajustar nivel de seguridad; ver/fijar la disposición final (TRD) del radicado individual, con confirmación explícita para la acción de eliminación; el drawer de detalle incluye una sección "Respuestas" con las Salidas vinculadas y una sección «Acceso» —plegada y cargada bajo demanda— con quién ha consultado la ficha y quién ha descargado sus anexos, con actor, canal e instante: distingue las dos cosas con palabras («consultó la ficha» no es «descargó un anexo») y avisa si el listado está truncado. Existe desde 2026-09: la traza se escribía desde siempre y no había forma de pedirla salvo desde Administración → Auditoría. Sobre un radicado por encima del clearance responde 404, indistinguible de uno inexistente; ejecutar el acto de publicación de oficio (Ley 1712/2014 art. 12) desde el drawer "Publicación", que lee el estado vigente del servidor y ofrece siempre el acto contrario (publicar / retirar), con fundamento obligatorio en ambos sentidos y advertencia explícita de que publicar deja el radicado cosechable por OAI-PMH y CMIS. Sobre un radicado reservado o anulado no se ofrece formulario: se explica por qué. El botón exige PERM_RECLASIFICAR con nivel de escritura (crud≥3), el umbral real del backend |
Operador de correspondencia, funcionario de dependencia | GET/PATCH /api/v1/documents, /api/v1/workflows, /api/v1/documents/{id}/anulacion, /api/v1/documents/{id}/respuesta, GET /api/v1/documents/{id}/respuestas, GET/PATCH /api/v1/documents/{id}/disposition, POST /api/v1/documents/{id}/publicacion |
/radicar |
Radicación de documentos | Registrar un radicado de entrada, salida o interno con sus anexos; botón "Sugerir con antecedentes" (RAG con citas, F6) para el cuerpo de Salida/Interno y chips de tipo documental sugerido (nunca autoaplicados) junto al selector de clasificación | Ventanilla única, operador de correspondencia (PERM_RADI, PERM_RADI_SALIDA) Destinatarios (RF-RAD-05, 2026-08-30): una Salida admite uno o varios, elegidos del catálogo de terceros. El buscador va PRIMERO y el alta solo aparece si no hubo resultados — los duplicados no se crean por saltarse una restricción, se crean por no encontrar la ficha que ya existe. El contacto se puede corregir para ese envío y la pantalla DICE que eso no cambia el catálogo. El canal solo ofrece lo que el contacto permite (sin correo no hay envío electrónico, sin dirección no hay físico) y avisa nombrando el dato que falta si el canal quedó marcado y el dato se vació. El municipio se ELIGE, no se escribe (cascada DIVIPOLA, 2026-08-30): departamento primero, municipio después; cambiar el departamento borra el municipio anterior en vez de dejarlo colgando; y si el catálogo no responde se puede seguir sin municipio, porque lo que el envío físico exige es la dirección. |
POST /api/v1/documents, POST /api/v1/storage/upload, POST /api/v1/knowledge/rag, POST /api/v1/knowledge/antecedentes |
/borradores |
Borradores | Preparar un radicado antes de asignarle número; previsualizar el documento antes de gastar el número; abrir el circuito de visto bueno y firma (E23 — antes ninguna pantalla creaba cadenas de firma); aprobarlo o radicarlo definitivamente si el circuito no tiene turnos pendientes (E23, RF-PRO-09: con un circuito abierto no se ofrece ni «Aprobar» ni «Radicar», y se dice cuántos turnos faltan — el backend responde 409 circuito_pendiente); botón "Sugerir con antecedentes" (RAG con citas, F6) en el editor del cuerpo |
Funcionario redactor | GET/POST /api/v1/documents/borradores, acción "radicar", POST /api/v1/knowledge/rag |
/busqueda |
Búsqueda de documentos | Tres pestañas deep-linkeables (?tab=): Exacta (full-text FTS PostgreSQL con filtros) con columna (en móvil, línea «En X» bajo el asunto) y filtro por «Ubicación actual», un selector del catálogo de dependencias («nombre — código»; filtra por ubicacion_dept_code, así que un radicado de nombre ambiguo, sin código, no aparece; el «Código de dependencia de radicación» es el de siempre y son filtros distintos), Semántica (documentos parecidos por significado, similitud vectorial) y Antecedentes (trámites relacionados, por texto o por un radicado pivote verificado) — F6. Los resultados de Semántica/Antecedentes se presentan siempre como "parecidos", nunca como coincidencia exacta; el recorte por permisos de acceso es invisible por diseño |
Todos los usuarios con permiso de consulta | GET /api/v1/search, POST /api/v1/knowledge/search, POST /api/v1/knowledge/antecedentes, GET /api/v1/documents/by-tracking/{tracking} |
/expedientes |
Listado de expedientes | Ver expedientes abiertos/cerrados/transferidos, crear uno nuevo | Archivista, funcionario con USUA_PERM_EXPEDIENTE |
GET/POST /api/v1/expedientes |
/expedientes/[id] |
Detalle de expediente | Agregar/quitar radicados, ver anexos, cerrar el expediente, descargar el índice electrónico XML firmado, exportar ZIP, iniciar transferencia; ver (solo lectura) la disposición final materializada al cierre o, si sigue abierto, una vista previa de la regla de la serie TRD asignada; ejecutar el acto de publicación de oficio (Ley 1712/2014 art. 12) desde el drawer «Publicación», que lee el estado ya cargado en la ficha y ofrece siempre el acto contrario al vigente, con fundamento obligatorio en ambos sentidos. Sobre un expediente reservado no ofrece publicar —y explica por qué— pero sí ofrece retirar una publicación anterior: es la asimetría real del backend, y es donde más urge poder hacerlo. Un expediente cerrado o transferido sigue siendo publicable (no es una retirada de la circulación). El aviso nombra lo que se difunde —código, nombre, descripción, fechas extremas y el número de radicados— y que los números de radicado hijos no se enumeran nunca. Gate PERM_RECLASIFICAR con nivel de escritura (crud≥3), el umbral real del backend |
Archivista | GET/PATCH /api/v1/expedientes/{id}, GET /api/v1/expedientes/{id}/indice, GET /api/v1/expedientes/{id}/export, GET /api/v1/trd/{serie_id}, POST /api/v1/expedientes/{id}/publicacion |
/archivo-fisico |
Archivo físico | Gestionar ubicaciones (direcciones recursivas), unidades de conservación, préstamos y su FUID | Custodio de archivo central | GET/POST /api/v1/ubicaciones, /api/v1/unidades, /api/v1/prestamos |
/transferencias |
Transferencias documentales | Enviar/recibir/rechazar transferencias entre archivo de gestión y central, generar FUID | Archivista, jefe de dependencia | POST /api/v1/transferencias, acciones enviar/recibir/rechazar |
/transferencias/[id] |
Detalle de transferencia (acta-certificado) | Ver el acta de entrega en apariencia de certificado (qué/entre quién/cuándo), el sello institucional XAdES, y firmar personalmente como elaborador/remitente/receptor — el rol de cada persona se deriva de su identidad, nunca se elige; verificar sello y firmas on-demand Firma inobtenible (AREA-07-10, ADR-030): bajo el acta, la sección «Firmas inobtenibles» lista las constancias (vigentes y revocadas distinguidas, con el estado del sello, el compromiso con sal del fundamento, ordinal de la cadena, soporte y personas resueltos, estado WORM y verificación de enlaces; el texto del fundamento solo lo ve un administrador de nivel 3) y ofrece declarar (parte sin firma ni excepción vigente —tras revocar se puede declarar de nuevo, FIRMA-2—, motivo de catálogo, fundamento de 20 a 2000 caracteres, radicado y anexo de soporte elegidos (ambos obligatorios), código TOTP siempre disponible (el servidor lo exige a quien tiene certificado), botón «Verificar fundamento» para administración 3, y una confirmación explícita de que es irreversible salvo revocación) y revocar (causa de vocabulario cerrado, fundamento, TOTP). Administración nivel 3 ve además «Mostrar fundamento» (carga el texto bajo demanda; cada lectura queda asentada), «Verificar fundamento», «Descargar sobre» (el XML firmado se descarga, no se pinta) y «Reintentar protección» (WORM) cuando un eslabón no está protegido. La puerta es USUA_PERM_ADMIN ≥ 3 y PERM_FIRMA ≥ 2, y se oculta a quien creó, envió o recibió la transferencia. La completitud distingue firmada_con_excepcion de sellada_con_excepcion («ninguna parte firmó; solo el sello de la entidad»), cuenta las constancias sin firma (no válidas) y muestra advertencias[] y la política XAdES. Una parte con excepción vigente no ve «Firmar como …». |
Archivista, jefe de dependencia con USUA_PERM_EXPEDIENTE (firma personal: además PERM_FIRMA de facto, gate del backend) |
GET /api/v1/transferencias/{id}, .../acta, .../acta/verificacion, POST .../acta/firmar-personal, POST .../acta/firmar POST .../acta/firma-inobtenible, POST .../acta/firma-inobtenible/revocar, GET .../firma-inobtenible/{id}/fundamento/verificacion, GET .../firma-inobtenible/{id}/fundamento?acto=, GET .../firma-inobtenible/{id}/sobre, POST .../firma-inobtenible/{id}[/revocacion]/worm/reintentar |
/firmas |
Firmas electrónicas | Firmar (individual o en lote) radicados/expedientes pendientes, rechazar firma; dar visto bueno cuando el turno propio es de revisión (E23): la pantalla ofrece el acto que corresponde al rol del turno, y el rol se enseña en todos los turnos de la cadena —también en los ya resueltos— distinguido por forma y no solo por color. La firma en lote solo alcanza los turnos de firma: la casilla no se pinta sobre un visto bueno y la barra de «seleccionar todas» desaparece si no hay ninguno firmable (el backend responde 409 turno_es_de_visto_bueno). El detalle ofrece «Ver documento» para los borradores — antes se aprobaba a ciegas, y la aprobación se ancla al hash del texto. El asistente de firma tiene dos formas del segundo factor, y confundirlas dejaba un acto imposible: con 409 2fa_requerido pide el OTP que llega por correo; con 409 totp_requerido —el firmante tiene credencial personal ACTIVA y el acto se firma con su certificado (XAdES)— pide el código de su app autenticadora y no emite ningún reto. Mientras el backend devolvía el mismo código en ambos casos, quien tenía credencial quedaba en un bucle: la pantalla mandaba correo, el backend volvía a pedir totp_code, y tener credencial de firma quitaba la capacidad de firmar cadenas. Limitación declarada: quien tiene TOTP activo pero no credencial firma igualmente por correo — la rama TOTP del backend solo se entra si el cliente manda el código, y esta pantalla solo lo manda en el caso de la credencial |
Firmante autorizado (PERM_FIRMA) |
GET/POST /api/v1/signature, /api/v1/signature/cadena |
/envios |
Envíos/correspondencia postal | Gestionar remesas hacia el operador postal externo, su confirmación de entrega, y "Actualizar rastreo" (consulta PULL contra el operador, F5) que declara explícitamente cuando no hay integración en vivo (operador_conectado=false) en vez de aparentar una consulta real. T13: pasar a mano un envío a «Entregado» pide adjuntar el acuse de recibo (se sube al almacén y queda ligado al envío por su SHA-256) |
Operador de correspondencia | POST /api/v1/documents/{id}/envios, GET /api/v1/envios, GET /api/v1/envios/{id}/tracking, webhook entrante del operador |
/anulaciones |
Resolver anulaciones | Cola de trabajo del aprobador de anulaciones (rol distinto del solicitante, que pide desde /bandeja con PERM_SOL_ANULAR): buscar por número de radicado o asunto, ver el estado de la solicitud en el drawer (causal, motivo, fechas, observación), abrir el radicado sobre el que se decide —enlace al deep-link /bandeja?radicado={id}, cerrado 2026-09: el drawer enseñaba todo lo que dijo quien pidió la anulación y nada del documento, así que el acto irreversible se resolvía con la sola versión del solicitante— y aprobar o rechazar con motivo obligatorio. Aprobar es el acto destructivo e irreversible (el radicado pasa a anulado) y por eso va en variant="danger" con la advertencia "Esta acción no se puede deshacer", mientras que rechazar es conservador. El motivo es obligatorio aunque el esquema del backend lo tenga como opcional —es un acto con efectos jurídicos y traza de auditoría (Ley 594/2000 art. 28)— y el proxy rechaza con 422 en vez de truncar silenciosamente a 2000 caracteres. Cola de trabajo real (GET /api/v1/anulaciones, cerrado 2026-08): la pantalla abre con las solicitudes pendientes de decidir, la más antigua primero, y se refresca tras cada decisión. Hasta entonces el backend no exponía ninguna cola —solo el estado por radicado— y la pantalla lo declaraba en un banner permanente obligando a buscar radicado por radicado: el aprobador solo se enteraba de que tenía trabajo si alguien se lo decía por otro canal. El buscador se conserva como vía directa cuando se tiene el número en la mano; sigue sin acciones en lote. Honestidad de interfaz: "no se pudo consultar el servidor de búsqueda" es un estado distinto de "ningún radicado coincide", y el vacío legítimo advierte además que "esto no significa que no haya anulaciones pendientes"; cada fila resuelve su estado por separado, con chips propios para Sin solicitud / No se pudo consultar y botón de reintento por fila |
Aprobador de anulaciones (PERM_PANU_CODI, gate real del backend en POST /anulacion/{aprobar,rechazar}). Desajuste UI↔backend: GET /documents/{id}/anulacion y GET /search, que la pantalla también consume, no tienen require_permission en el backend hoy (solo aislamiento por tenant y no-read-up por clearance) — la restricción de esos dos es únicamente la del gate de la pantalla |
GET /api/v1/search, GET /api/v1/documents/{id}/anulacion, POST /api/v1/documents/{id}/anulacion/aprobar, POST /api/v1/documents/{id}/anulacion/rechazar |
/perfil |
Mi perfil | Cuatro secciones en orden deliberado: identidad (usuario, correo, tenant, roles y permisos efectivos, solo lectura, sin llamada extra); segundo factor TOTP (Stepper generar secreto → verificar código → activo, con reemplazo y revocación tras step-up con el código vigente); credencial de firma electrónica PKI (solicitar CSR, ver vigencia, reemplazar o revocar), y dependencia activa (conmutador de dependencia). El secreto TOTP y el CSR se muestran una sola vez y la pantalla lo dice: si se pierden, la única salida es generar otros. La sección de firma está bloqueada hasta tener TOTP activo, con enlace en página a la sección requerida, en vez de dejar que el usuario choque contra el error del backend. Reemplazar una credencial activa la revoca de inmediato y la deja inservible hasta una re-emisión fuera de línea, así que exige código TOTP y escribir una frase exacta de confirmación —una barrera deliberadamente más alta que la del reemplazo de TOTP. Limitaciones declaradas: no hay criptografía en el navegador (la clave privada se genera y custodia en auth-service), no hay QR para el TOTP, la emisión del certificado es fuera de banda, y no hay cambio de contraseña, edición de identidad ni gestión de sesiones. Nota legal explícita: custodia en servidor ⇒ no hay control exclusivo del firmante, equivalencia funcional de firma electrónica (Ley 527/1999 art. 7), no firma digital acreditada (art. 28). Las dos facetas remotas se cargan con Promise.allSettled y cada una lleva su propio aviso de error Incluye los códigos de recuperación del segundo factor (se muestran una sola vez al activarlo, con aviso al quedar ≤2 y opción de regenerar) y oculta las acciones de la credencial de firma a quien no tiene PERM_FIRMA. |
Cualquier usuario autenticado (identidad propia: los routers de /auth/me/totp, /auth/me/signing-key y /auth/context autorizan por identidad, sin permiso). Desajuste UI↔backend: POST /auth/me/signing-key/enroll sí exige PERM_FIRMA en el backend y la UI no lo refleja — un usuario con TOTP activo pero sin PERM_FIRMA ve el botón habilitado y recibe un 403 presentado como fallo genérico; el comentario de navItems.js que afirma que todos los routers consumidos "solo exigen get_caller" es falso para ese endpoint. Asimetría adicional: la auto-revocación (DELETE /auth/me/signing-key) no exige PERM_FIRMA, solo step-up TOTP |
GET /api/v1/auth/me/totp, POST /api/v1/auth/me/totp/{enroll,activate}, DELETE /api/v1/auth/me/totp, GET /api/v1/auth/me/signing-key, POST /api/v1/auth/me/signing-key/enroll, DELETE /api/v1/auth/me/signing-key, GET /api/v1/auth/context, POST /api/v1/auth/context/switch |
/reportes |
Reportes | Generar y exportar reportes (CSV) de gestión documental; el resumen separa «Por ubicación actual (en trámite)» (by_ubicacion: dónde está ahora lo que sigue en trámite, sin cerrados ni anulados) de «Por dependencia de radicación» (by_dependencia: a quién iba dirigido); el CSV del servidor renombra la sección dependencia a dependencia_radicacion y añade ubicacion_actual; pestaña "Índice de clasificados" (Ley 1712 art. 20) — registro content-free de radicados reservados/clasificados con su fundamento jurídico, sin exponer asunto/contenido |
Jefe de dependencia, administrador (índice de clasificados: PERM_RECLASIFICAR) |
GET /api/v1/reports/radicados, GET /api/v1/reports/indice-reservado[.csv] |
AssistantDock (no es una ruta) |
Asistente conversacional | Chat en lenguaje natural que consulta el sistema (RAG con citas) traduciendo la pregunta a llamadas MCP de solo lectura. Es un panel flotante montado en AppLayout.svelte, disponible desde cualquier pantalla: no existe una página /asistente, solo el proxy BFF (app)/asistente/api/message |
Cualquier usuario autenticado | POST /api/v1/assistant/message (vía mcp-server) |
/admin |
Índice de administración | Mapa de tarjetas del panel de administración (19 áreas hoy). Cada tarjeta declara el require_permission real del router que consume su subruta, no un USUA_PERM_ADMIN genérico — por eso TRD / CCD, TVD y Metadatos exigen USUA_PERM_TRD, Colas de errores exige PERM_DLQ_ADMIN, Preservación exige USUA_PERM_EXPEDIENTE e Interoperabilidad abre con USUA_PERM_EXPEDIENTE o PERM_RADI. Una tarjeta cuya área aún no tiene subruta se muestra deshabilitada con badge de texto «Próximamente» + candado (no solo por color) en vez de omitirse: el mapa de administración se comunica completo sin mentir sobre lo que funciona; hoy las 19 están implementadas. La pantalla no llama al gateway: ADMIN_AREAS es un catálogo estático de la UI filtrado con la misma filterVisibleNavItems que usan SidebarNav y el /dashboard. Sin ningún permiso de administración se muestra un EmptyState, no una página vacía. Corregido en el lote 1 de la revisión UX 2026-08-02, que cruzó las tarjetas contra el gate real (dos declaraban mal su permiso) y enlazó las dos pantallas que existían sin figurar en ningún sitio |
Administrador (USUA_PERM_ADMIN para el panel; cada tarjeta por su permiso propio) |
— |
/admin/preservacion |
Administración de preservación digital | Consultar y actualizar el plan de preservación (versionado), ver su historial; declarar los formatos en riesgo de obsolescencia (formatos_origen, RF-PRE-04) — la política que consume el barrido periódico de migración, que vive aquí y no en una variable de entorno del despliegue precisamente porque la declara la entidad, queda versionada y dice quién la puso; visibilidad operacional de índices electrónicos pendientes de sellado XAdES (con reintento manual) y de artefactos WORM (índice/acta/AIP) pendientes de renovación de retención de Conservación Total. No ofrece empaquetado manual de AIP ni protección WORM manual de un artefacto arbitrario: ambos requieren datos (documentos[]/file_id) que solo archive-service conoce en el contexto de un expediente, y la renovación de retención es, por diseño, disparo de sistema únicamente. No estaba enlazada —solo se alcanzaba escribiendo la URL— hasta el lote 1 de la revisión UX 2026-08-02, que la añadió al índice de tarjetas de /admin |
Archivista, administrador (USUA_PERM_EXPEDIENTE; reintento de sellado además PERM_FIRMA) |
GET/PUT /api/v1/preservacion/plan, GET /api/v1/preservacion/plan/versions, GET /api/v1/expedientes/indices/pendientes-firma, GET /api/v1/expedientes/indices/pendientes-renovacion, POST /api/v1/expedientes/{id}/indice/firmar |
/admin/plantillas |
Administración de plantillas | CRUD de plantillas de cuerpo de documentos de salida, con editor de texto enriquecido. No es un constructor de formularios: las plantillas de metadatos (JSON Schema por serie TRD/tipo documental) se administran en /admin/metadatos |
Administrador (USUA_PERM_ADMIN) |
GET/POST /api/v1/plantillas |
/admin/trd |
Administración de TRD / CCD | Dos pestañas: series TRD/CCD en árbol indentado (jerarquía real vía parent_id, nunca lista plana) con alta/edición (código inmutable, append-only fuera de borrador); tipos documentales (tercer nivel), con alta/edición/borrado y filtro por serie. Circuito de convalidación (aprobar → convalidar → registrar RUSD → derogar, más devolver) vía el componente compartido InstrumentLifecycleActions (ADR-026) — cada acto irreversible (convalidar, derogar) advierte dentro del cuadro de confirmación, antes de confirmar Cuarta pestaña «Disposición vencida» (RF-TRD-08, 2026-08-28): expedientes cuyo archivo central ya venció según el calendario congelado al cerrarlos, con la acción que corresponde por fila. La pantalla dice explícitamente que vencer no es eliminar —en Conservación Total significa transferir al histórico y conservar para siempre— y que el sistema no ejecuta ningún acto todavía: la eliminación efectiva exige aval del Comité, publicidad previa del inventario y acta firmada. Una fila con disposición de eliminación cuyo instrumento congelado no está convalidado lo advierte, en vez de dejar que el funcionario lo descubra chocando con el gate. Desde la misma pestaña se decide (RF-TRD-06): confirmar deja constancia de que procede la disposición de la TRD —con ejecutada: false y un aviso de que el sistema no ha eliminado nada— y aplazar exige motivo y fecha de regreso, tras la cual el vencimiento vuelve solo. Los botones son espejo del gate real (USUA_PERM_TRD con crud≥3) y fallan cerrados si la sesión no trae el nivel. |
Administrador de TRD (USUA_PERM_TRD, gate real del backend — más específico que USUA_PERM_ADMIN, que solo controla la visibilidad del panel /admin) |
GET/POST /api/v1/trd, PATCH /api/v1/trd/{id}, GET/POST /api/v1/trd/{code}/versiones, POST /api/v1/trd/{code}/versiones/{version}/{aprobar,devolver,convalidar,registrar-rusd,derogar}, GET/POST/PATCH/DELETE /api/v1/tipos-documentales · Revisión pendiente (3.ª pestaña, ADR-025 D4/D6): cola de expedientes ya cerrados que el sistema no pudo amarrar a un instrumento, con los tres motivos distinguidos por lo que los resuelve — uno se limpia al convalidar la serie, los otros dos no tienen vía automática y así se declara, sin ofrecer un botón inútil |
/admin/tvd |
Administración de TVD (fondo acumulado) | Espejo estructural de /admin/trd (ADR-026): árbol de agrupaciones de valoración con columnas propias -- fondo/productora, fechas extremas, estado del instrumento -- y sin columna de archivo de gestión (no existe para TVD: un fondo acumulado ya está en central/histórico). El formulario de creación exige fechas extremas y trata la justificación de la valoración como el campo principal, no secundario. Mismo circuito de convalidación que TRD, mismo componente compartido |
Administrador de TRD (USUA_PERM_TRD -- ADR-026 reutiliza a propósito el permiso de TRD: misma potestad archivística, mismo Comité) |
GET/POST /api/v1/tvd, PATCH /api/v1/tvd/{id}, GET/POST /api/v1/tvd/{code}/versiones, POST /api/v1/tvd/{code}/versiones/{version}/{aprobar,devolver,convalidar,registrar-rusd,derogar} |
/admin/eliminacion |
Eliminación documental (lotes) | La pantalla que faltaba (RF-TRD-07, fases 1-3, 2026-08-29): se podía proponer un lote desde la cola de vencimientos y no había dónde verlo después. Lista los lotes con su estado, abre el inventario congelado —recortado por el clearance de quien mira, y con la resta EXPLICADA en vez de muda— y ofrece los tres actos: publicar el inventario, registrar el aval del Comité (acta, fecha y acto administrativo) y retirar el lote con motivo. Las cinco fases están siempre a la vista y la quinta se declara no construida: quien publica un inventario tiene que saber que faltan tres pasos antes de que se destruya nada. El botón de avalar no se ofrece con el plazo de publicidad todavía abierto, y la pantalla dice hasta cuándo — no es un límite técnico, es que un tercero aún puede oponerse. Desde el aval, la sección de acta de eliminación (fase 4): generarla —una sola vez, declarando el método de borrado—, ver su SHA-256 y su estado de sellado, y reintentar la firma con el motivo del último fallo a la vista. El acta se muestra entera o no se muestra: si cubre expedientes por encima del nivel de acceso de quien mira, la pantalla lo DICE («no se puede mostrar recortada: rompería su firma») en vez de dejar un hueco. Y declara que la firma es del art. 7 de la Ley 527 y no una firma digital acreditada. Con el acta firmada aparece Ejecutar la eliminación (fase 5), que es el único acto del sistema que destruye documentos de archivo: el aviso dice qué queda —lápida, acta y auditoría— y qué no —contenido y denominación—, y el botón exige escribir el código del lote, porque un acto irreversible no debe estar a un clic. El resultado se queda en el modal y no en un toast: hay que poder leer qué se destruyó y, sobre todo, la lista de los que NO, que quedan intactos y sin lápida. Un lote ejecutado muestra las lápidas. Un 403 al cargar no se presenta como «no hay lotes» |
Administrador de TRD (USUA_PERM_TRD, crud≥3 para los tres actos — mismo permiso que TRD/TVD: eliminar no es una potestad aparte, es ejecutar el instrumento convalidado) |
GET /api/v1/eliminacion/lotes, GET /api/v1/eliminacion/lotes/{id}, POST /api/v1/eliminacion/lotes/{id}/{publicar,avalar,anular} |
/admin/metadatos |
Administración de metadatos | Tres pestañas: plantillas de metadatos de expediente (por serie TRD, archive-service) y de documento (por tipo documental, document-service) — ambas de solo alta (inmutables, JSON Schema editado como texto con validación de sintaxis antes de enviar; nueva versión = nuevo registro); elementos de metadato reutilizables (CRUD completo). Reparto con /admin/trd: TRD administra la clasificación, esta pantalla administra los campos de esos esquemas |
Administrador (USUA_PERM_ADMIN del lado del cliente; los routers de plantilla/elemento de document-service no gatean por permiso en el backend hoy — hallazgo de dominio documentado en el código, no corregido en el front) |
GET/POST /api/v1/expediente-metadata/templates, GET/POST /api/v1/metadata/templates, GET/POST/PATCH/DELETE /api/v1/metadata/elements |
/admin/terceros |
Directorio de terceros | Remitentes y destinatarios del catálogo de la entidad. Lista y busca sin tildes ni mayúsculas (pena encuentra Peña), muestra en qué radicados aparece cada ficha, y detecta posibles duplicados por dos ejes que no valen lo mismo: mismo documento es certeza —son la misma persona— y mismo nombre normalizado es sospecha, porque puede haber dos homónimos; por eso muestra y nunca fusiona sola. La fusión es irreversible y se confirma con una frase tecleada. Fusionar no borra ni reescribe: los radicados de la ficha absorbida siguen nombrándola —el día del envío existían dos fichas y se usó una, y un trigger de la base impide falsificarlo— y el superviviente hereda el historial al leerlo. La pantalla lo dice ANTES de confirmar. Existe desde 2026-09: hasta entonces un tercero solo podía nacer, como efecto colateral de elegir un destinatario al radicar, y no había dónde revisarlo ni consolidarlo |
Quien radica (PERM_RADI); la fusión exige además nivel CRUD 3 |
GET/POST /api/v1/terceros, GET /api/v1/terceros/duplicados, GET /api/v1/terceros/{id}/radicados, POST /api/v1/terceros/{id}/fusionar |
/admin/catalogos |
Administración de catálogos | Maestro-detalle: lista de catálogos de referencia del tenant a la izquierda, items del seleccionado a la derecha (CRUD completo); en móvil degrada a dos niveles navegables con botón «Volver», nunca a columnas apretadas | Administrador (USUA_PERM_ADMIN del lado del cliente; catalogos.py tampoco gatea por permiso en el backend hoy) |
GET /api/v1/catalogos, GET/POST /api/v1/catalogos/{catalogo}, PATCH/DELETE /api/v1/catalogos/{catalogo}/{id} |
/admin/parametros |
Administración de parámetros | Tres secciones: parámetros del tenant (clave→valor JSON libre), calendario de festivos (alta puntual + precarga Ley 51/1983 sin llamada externa), y calculadora de días hábiles que muestra el desglose de fines de semana/festivos descontados en el rango (el backend solo devuelve la fecha resultante; el desglose se reconstruye en el cliente a partir del mismo catálogo de festivos ya cargado) Parámetros: escribir exige USUA_PERM_ADMIN a nivel 3 (con un nivel menor la pantalla no ofrece editar ni crear, y un 403 se explica); firma_personal_require_xades se edita con un selector off/clasificados/todos (null se muestra «heredado del entorno») y, al relajar la política, pide un motivo de al menos 10 caracteres (PUT con {value, motivo}; 422 motivo_requerido). |
Administrador (USUA_PERM_ADMIN del lado del cliente; config.py documenta explícitamente que el gate de permiso está pendiente) |
GET/PUT /api/v1/config/params[/{key}], GET/POST/DELETE /api/v1/config/holidays, POST /api/v1/config/holidays/seed, GET /api/v1/business-days/calculate |
/admin/usuarios |
Administración de usuarios | Listado paginado de usuarios del tenant (usuario, correo, estado, chip ROOT) con alta y un drawer de detalle que muestra los permisos efectivos con su nivel CRUD y la URD del usuario (dependencia + rol/grupo + marca de principal), con alta y baja de asignaciones URD. No hay editar, desactivar ni borrar un usuario: rbac.py solo expone alta + lectura + gestión de URD, y la pantalla lo declara como ausencia del backend, no como omisión de la UI. La casilla «Otorgar ROOT» solo se muestra a quien ya es ROOT (ADR-024: el backend responde 403 en caso contrario) y falla cerrada si no se pudo determinar la condición ROOT del solicitante —un control que solo puede fallar no es una opción— y el valor se recalcula igualmente al enviar, porque ocultar la casilla es UI, no control. El borrado de una URD se confirma en línea dentro del drawer, nunca apilando un Modal sobre él. Honestidad de interfaz: si falla el catálogo de dependencias o de grupos, el listado de usuarios no se cae; solo los selectores del formulario URD quedan vacíos, con su propio aviso «No se pudo cargar el catálogo de dependencias» |
Administrador (USUA_PERM_ADMIN, gate a nivel de router en rbac.py y urd.py de auth-service). Desajuste UI↔backend: GET /api/v1/dependencias, que la pantalla consume como catálogo, no tiene gate de permiso en tenant-service (sus escrituras sí exigen USUA_PERM_ADMIN). Además es la única de las cuatro pantallas nuevas de /admin sin EmptyState de bloqueo propio: quien entre sin el permiso ve la cáscara de la página con un error de carga en vez de un mensaje de "sin permiso" |
GET/POST /api/v1/auth/users, GET /api/v1/auth/users/{id}/permissions, GET/POST /api/v1/auth/users/{id}/urd, DELETE /api/v1/auth/users/{id}/urd/{urd_id}, GET /api/v1/auth/groups, GET /api/v1/dependencias?activo=true |
/admin/grupos |
Administración de grupos | Listar/crear grupos; drawer de detalle con miembros ACTUALES, permisos concedidos con su nivel CRUD y clearance vigente (RF-SEG-08), leídos del servidor al abrirlo y recargados tras cada cambio. No hay edición ni borrado de grupo (el backend solo tiene alta+lectura). Hasta 2026-08-31 el drawer administraba a ciegas —mostraba una lista "de esta sesión" con lo que él mismo había hecho— por un comentario que afirmaba que el backend no exponía esas lecturas: sí las exponía | Administrador (USUA_PERM_ADMIN) |
GET/POST /api/v1/auth/groups, GET /api/v1/auth/permissions, GET /api/v1/auth/security-levels, POST/DELETE /api/v1/auth/groups/{id}/members/{user_id}, GET /api/v1/auth/groups/{id}/members, PUT/GET /api/v1/auth/groups/{id}/permissions/{permission_id}, PUT/GET /api/v1/auth/groups/{id}/clearance |
/admin/dependencias |
Administración de dependencias | Vista árbol del organigrama institucional (GET /dependencias/tree); crear, editar y activar/desactivar. Renombrar o desactivar dispara un modal de confirmación explícito (no un tooltip): las dependencias se correlacionan por nombre, no por id/código, en workflow_rules.assign_to_dept y en los flow_steps en vuelo — ambos pueden quedar huérfanos. No ofrece borrado duro (el backend lo tiene, bloqueado si hay hijos, pero el alcance de esta pantalla es crear/editar/desactivar) |
Administrador (USUA_PERM_ADMIN) |
GET /api/v1/dependencias, GET /api/v1/dependencias/tree, POST /api/v1/dependencias, PATCH /api/v1/dependencias/{id} |
/admin/flujos/reglas |
Reglas de enrutamiento | CRUD de las reglas que asignan automáticamente un radicado a una dependencia (E05): tabla con prioridad ordenable, estado, destino y número de condiciones, y un Modal de alta/edición con condiciones dinámicas (campo/operador/valor) y match_mode (todas Y / alguna O). Tres decisiones de honestidad: (1) una condición incompleta o fuera del catálogo bloquea el guardado y señala cuál es, en vez del comportamiento anterior que las descartaba en silencio —un administrador podía creer que guardó tres condiciones cuando se guardaron dos, o ninguna y por tanto una regla atrapa-todo—; (2) guardar una regla sin condiciones exige una confirmación aparte que advierte que se aplicará a cualquier radicado que entre; (3) la prioridad fuera de 1..999 devuelve 422 explícito en vez de recortarse en silencio a un valor distinto del que se escribió. PATCH es edición parcial real (solo se reenvían las claves presentes, porque en el backend "presente" significa "reemplazar"). Limitaciones declaradas: las cuatro columnas heredadas (doc_type, doc_class, dest_dept, subject_contains) no se exponen a propósito —conditions[] es estrictamente más expresivo y tiene prioridad en el motor—; y como el backend no devuelve total en el listado, la pantalla pide una página amplia y filtra en cliente en vez de fingir paginación de servidor |
Administrador (USUA_PERM_ADMIN, gate por endpoint en los siete de workflow-service/app/routers/rules.py). Sin desajustes |
GET/POST /api/v1/workflow/rules, GET/PATCH/DELETE /api/v1/workflow/rules/{id} |
/admin/flujos/reasignacion |
Reasignación en cascada | Traslado en bloque de todos los pasos de flujo pendientes de un usuario o de una dependencia hacia otro usuario, en cuatro secciones numeradas: origen (por usuario o por dependencia, con selector sobre el catálogo real —antes era texto libre sobre un nombre de columna del backend, donde un error de escritura reasignaba en silencio cero o los pasos equivocados—), destino, vista previa e confirmación escribiendo REASIGNAR, exigida de nuevo del lado del servidor en el proxy (422) porque el cliente es potencialmente hostil. Banner permanente de advertencia: es una acción masiva e irreversible, sin «deshacer» automático. La vista previa se declara como estimación y se explica por qué al usuario, no solo en el código: workflow-service no tiene endpoint de simulación, así que el conteo se toma de GET /workflows/pending —el mismo predicado que la reasignación, pero con el filtro de clearance del solicitante (RF-SEG-08), que la reasignación no aplica por ser un acto de custodia—, de modo que el número solo puede quedarse corto, nunca pasarse: los radicados clasificados por encima del nivel de acceso no se cuentan pero sí se reasignan. Cambiar el origen tras estimar invalida la vista previa; tras ejecutar se informa el número exacto de pasos y se limpia el formulario para impedir un doble envío |
Administrador (USUA_PERM_ADMIN, gate real de POST /workflows/reassign/cascade). Desajuste UI↔backend en las tres lecturas: GET /workflows/pending no tiene require_permission (solo aislamiento por tenant y filtro de clearance), GET /auth/users/pickable está gateado por PERM_RADI —no por USUA_PERM_ADMIN— y GET /api/v1/dependencias no tiene gate. Solo el acto de escritura está realmente reservado al administrador |
GET /api/v1/auth/users/pickable, GET /api/v1/dependencias?activo=true, GET /api/v1/workflows/pending, POST /api/v1/workflows/reassign/cascade |
/admin/auditoria |
Consulta del registro de auditoría | Lectura del audit_log inmutable con filtros de igualdad exacta (acción, tipo de objeto, referencia, actor) resueltos en el servidor, drawer con el asiento completo (incluidos hash y payload crudo) y panel de integridad que verifica la cadena de hashes contra PostgreSQL real, sin caché, tanto al cargar como con el botón «Reverificar cadena». Es la pantalla que define la honestidad de interfaz del inventario: el veredicto tiene cuatro estados, nunca dos —comprobando / íntegra / comprometida / no se pudo comprobar—; el cuarto dice literalmente que «esto NO significa que la cadena esté íntegra, significa que todavía no se sabe», y hay una guarda específica para que un 200 con cuerpo ilegible no se resuelva como «comprometida», porque afirmar una ruptura que nadie comprobó es tan falso como afirmar la integridad y además dispara una alarma de incidente sin incidente. Solo lectura, sin mutaciones. Las dos limitaciones que esta pantalla declaraba están cerradas, ambas en el servidor, que es donde la pantalla insistió en que debían cerrarse: (1) el listado aplica ahora no-read-up por clearance (AuditQueryRepository, RF-SEG-08) — las entradas sobre radicados o expedientes por encima del nivel del llamante no se listan ni se cuentan, con resolución en vivo y fail-closed sobre lo que no resuelve; la pantalla no añadió ningún filtro en cliente («sería una falsa sensación de seguridad: el dato ya viajó al navegador») y tenía razón: filtrado en cliente el dato habría seguido saliendo por la API. La verificación de la cadena no se recorta, porque solo es probatoria si se recalcula entera y su respuesta es content-free; (2) el api-gateway ya reenvía X-Total-Count (añadida a FORWARDED_RESPONSE_HEADERS), así que la paginación es alcanzable |
Administrador (USUA_PERM_ADMIN, gate a nivel de router en auth-service/app/routers/audit.py, cubre listado y verificación). Sin desajustes |
GET /api/v1/audit, GET /api/v1/audit/verify |
/admin/notificaciones |
Notificaciones y webhooks | Dos facetas independientes cargadas con Promise.allSettled —cada una con su propio estado de carga, error y vacío, para que un fallo nunca oculte lo que sí funcionó—: historial de envíos (fecha, destinatario, asunto, estado, evento, intentos, con botón de actualizar) y webhooks salientes (alta con URL, tipos de evento y secreto HMAC opcional; baja con confirmación). El historial no tiene paginación real y la pantalla lo dice: el backend solo acepta limit, sin total ni cursor, así que la nota explícita advierte que la lista «NO es todas las notificaciones, es una ventana reciente» (últimos 50). Encuadre proporcional al riesgo: los webhooks se presentan como un canal de salida de datos hacia un tercero, y la baja advierte la irreversibilidad nombrando la URL — el destino deja de recibir eventos y volver atrás exige crear una suscripción nueva con un secreto nuevo |
Administrador (USUA_PERM_ADMIN: GET /notifications/history gateado por endpoint y el router de webhooks gateado a nivel de router en notification-service). Sin desajustes ni endpoints sin gate |
GET /api/v1/notifications/history, GET/POST /api/v1/webhooks, DELETE /api/v1/webhooks/{id} |
/admin/colas |
Colas de errores (dead-letter) | Consola de los eventos en dead-letter de los tres servicios que la tienen (ADR-021), en tres pestañas: Flujos (workflow), Notificaciones y Firma. Filtro por estado y por origin_stream, tabla con chip de estado y drawer de detalle con el failure y el sobre del evento completo. Dos acciones, con confirmación proporcional al daño: «Reintentar» es una confirmación simple que advierte que la reejecución dispara el efecto real (puede incluir un envío de correo o una distribución de verdad) y que un segundo fallo lo deja igual; «Descartar» es danger, exige una justificación de 10 caracteres mínimo que queda en el audit_log inmutable, y deletrea exactamente lo que se pierde: el mensaje original nunca se reprocesará, ese efecto no ocurrirá jamás. El mínimo de 10 caracteres se aplica en el proxy a los tres servicios (dos de ellos solo exigen 1) para que el requisito no cambie en silencio según la pestaña. Honestidad de interfaz: el 404 del backend es ambiguo por diseño (no-read-up, RF-SEG-08) y el proxy se niega a desambiguarlo — la UI muestra la unión honesta, «no se encontró el evento, o tu clearance no permite verlo»; el error de listado tiene precedencia sobre el vacío legítimo. Limitación real: la pantalla muestra un banner de aviso cuando el backend trunca los candidatos (X-Truncated), pero el api-gateway no reenvía esa cabecera ni X-Total-Count, así que hoy ese banner es código muerto y el total colapsa al tamaño de la página |
PERM_DLQ_ADMIN (gate a nivel de router en los tres admin_deadletter.py de workflow, notification y signature), no USUA_PERM_ADMIN. Sin desajustes. No está en el menú lateral: solo se llega por la tarjeta de /admin |
GET /api/v1/{workflow,notification,signature}/admin/deadletter/, GET .../{event_id}, POST .../{event_id}/replay, POST .../{event_id}/discard |
/admin/pinar |
Listado de planes PINAR | MVP (Fase 7): listar planes con filtro por estado, crear una versión nueva (siempre en borrador; reformular = crear otra versión, el histórico se conserva). No hay edición ni borrado — el plan solo cambia a través de su ciclo de vida en el workspace. Sin el permiso, bloqueo con su nombre: la pantalla dice qué permiso falta en vez de dejar un error de carga, y hace caso al backend aunque la sesión diga otra cosa —el 403 viaja como sinPermiso, no mezclado con los fallos de red— |
Administrador (USUA_PERM_ADMIN), gate real de todos los endpoints del PINAR desde 2026-08-28, lecturas incluidas: antes las 11 GET quedaban solo autenticadas mientras la interfaz escondía la pantalla |
GET/POST /api/v1/pinar/planes |
/admin/pinar/[id] |
Workspace de un plan PINAR | Stepper informativo del ciclo de vida (borrador→aprobado→en_ejecucion→cerrado, no navegable por clic) + pestañas Resumen y Tablero (avance por objetivo/por eje). Acciones aprobar/pasar a ejecución/cerrar con Modal de confirmación que advierte la irreversibilidad ANTES de ejecutar — aprobar congela el contenido estructural del plan de forma permanente (asiento en audit_log inmutable); cerrar es terminal. Cerrado el 2026-08-29 el desfase que quedaba (la pantalla consumía 6 de los 31 endpoints y el tablero salía siempre vacío, declarándose «fase posterior»): tres pestañas más — Aspectos y priorización (la matriz aspecto×eje de 1 a 10; la prioridad es la Σ y la calcula el backend, la tabla se presenta ya ordenada por ella, y «sin priorizar» no se pinta como prioridad cero); Objetivos y proyectos con su seguimiento append-only (el avance de un proyecto sale de sus cortes, no se teclea; un proyecto sin objetivo se señala porque no entra en el tablero); e Instrumentos articulados por referencia. La pantalla espeja los dos ciclos de vida del servicio y no los solapa: la estructura solo se edita en borrador y el seguimiento solo con el plan aprobado o en_ejecucion — nunca las dos cosas a la vez—, y el 409 del plan congelado se muestra tal cual en vez de colapsarse a «no se pudo guardar» |
Administrador (USUA_PERM_ADMIN; el detalle/tablero es solo-lectura para cualquier autenticado) |
GET /api/v1/pinar/planes/{id}, POST .../aprobar, POST .../ejecutar, POST .../cerrar, GET .../tablero, GET .../ejes, GET/POST .../aspectos, PATCH/DELETE /aspectos/{id}, GET/PUT .../priorizacion, GET/POST .../objetivos, PATCH/DELETE /objetivos/{id}, GET/POST .../proyectos, PATCH/DELETE /proyectos/{id}, PUT /proyectos/{id}/aspectos, GET/POST /proyectos/{id}/seguimiento, GET/POST .../instrumentos, DELETE .../instrumentos/{id} |
/correo |
Buzones de correo | La puerta que faltaba. Lista corta de los buzones que la persona puede trabajar, cada uno enlazando a su bandeja. Existe porque la bandeja se gatea con PERM_RADI —quien trabaja el correo recibido es la unidad de correspondencia— pero la única entrada a /correo/{id} era un botón dentro de /admin/correo, que exige USUA_PERM_ADMIN: el funcionario al que la pantalla está dirigida no podía llegar a ella ni conocer el identificador de su buzón. Distingue los tres finales —no se pudo preguntar, no tiene buzones asignados, aquí están—, que sin ella se veían igual: como una URL que nadie sabía escribir |
Radicador (PERM_RADI). Qué buzones se ven lo recorta el WHERE del backend, no el permiso: personal para su dueño, institucional para los asignados |
GET /api/v1/email/cuentas |
/correo/[cuentaId] |
Bandeja de un buzón institucional | La bandeja compartida de la ventanilla (E19): los correos traídos del buzón, pendientes de radicar, en orden por fecha de recibo —no por la del sondeo que los trajo: una ventanilla atiende por orden de llegada—. Cada correo se abre, se completa lo que el correo no puede saber —dependencia competente y tipo documental, elegidos del catálogo, no escritos a mano (2026-08-31): la dependencia viaja como dest_dept_code, que es lo que vincula el radicado con el organigrama, y el tipo documental como su código, que es lo que lo hace casar con una serie de la TRD— y se radica con sus anexos y el mensaje original conservados; o se aparta con motivo si no debía radicarse —y esa decisión deja asiento en la auditoría inmutable, porque es la que hace desaparecer el documento del archivo—. Un correo que otro funcionario está trabajando aparece marcado como ocupado y no se puede abrir: el backend ya lo impide, pero sin mostrarlo el segundo lo descubriría pulsando. Un sondeo cada 12 s pide solo el resumen —pendientes, ocupados y la marca de la última novedad— y recarga la lista solo si cambió; si el operador tiene un correo abierto avisa en vez de recargar, para no borrarle lo escrito, y si el sondeo falla lo dice en vez de dejar una pantalla que parece viva. Cerrar el formulario suelta la reserva, y la fila propia ofrece «Soltarlo»: mirar un correo y decidir que no es para uno no puede bloquearlo quince minutos para toda la ventanilla. Abrir la pantalla no sincroniza: traer del buzón es un acto que el operador dispara |
Radicador (PERM_RADI, gate real de los siete endpoints de bandeja). Qué buzón alcanza cada quien lo recorta el backend en el WHERE —una cuenta personal es solo de su dueño, ni el administrador la ve—, y el 404 no distingue «no existe» de «no la alcanza» |
GET /api/v1/email/cuentas/{id}/bandeja[/estado], POST .../bandeja/sincronizar, POST .../bandeja/{mensaje}/{reservar,liberar,radicar,descartar} |
/admin/correo |
Ingesta de correo | Configuración de los buzones de la entidad (E19), capacidad que existía en el backend desde F2 sin ninguna pantalla. Tres cosas: los buzones, con alta de los tres tipos —institucional compartida, personal de un funcionario, y varias de recepción a la vez— y por cada uno su carpeta de filtrado (a la que alguien arrastra lo que sí se radica) y la de destino tras radicar; probar conexión, que devuelve las carpetas del buzón para elegirlas de una lista en vez de teclearlas —una errata se manifestaría como «la ingesta no trae nada», indistinguible de «no ha llegado correo»—; y la aplicación OAuth de la entidad para Google o Microsoft, con el flujo de autorización y su ruta de retorno. La pantalla explica por qué y no solo qué: dice que Google y Microsoft no admiten contraseña por IMAP y desde cuándo, y señala una cuenta OAuth2 aún sin autorizar —sin eso el síntoma sería «no llega nada», indistinguible de un buzón vacío—. Aquí no se radica: cada ficha enlaza a la bandeja de su buzón, que es donde se decide correo por correo | Administrador (USUA_PERM_ADMIN, gate real de la configuración: alta de cuentas, probar, OAuth y apps). El LISTADO de cuentas ya no: pasó a PERM_RADI (2026-08-31) para que el funcionario de ventanilla pueda encontrar su buzón. El recorte de QUÉ cuentas ve cada quien vive en el backend: una personal solo la ve su dueño —ni el administrador—, una institucional sus funcionarios asignados |
GET/POST /api/v1/email/cuentas, POST /api/v1/email/cuentas/{id}/{probar,oauth/iniciar,oauth/canjear}, GET/POST /api/v1/email/apps-oauth |
/admin/correo/oauth/callback |
Retorno de OAuth2 de correo | No es una pantalla de trabajo: es el punto de aterrizaje al que Google/Microsoft devuelven el code tras el consentimiento. Canjea, guarda las credenciales custodiadas y redirige a /admin/correo. Se documenta porque una ruta sin fila en este inventario parece un olvido, y esta existe a propósito y no se navega a mano |
Administrador de correo (USUA_PERM_ADMIN) |
GET /api/v1/email/oauth/callback |
/admin/seguridad/claves |
Revocación administrativa de credenciales de firma | Permite a un administrador revocar la credencial de firma personal PKI (E17/F4) de otro usuario, exigiendo escribir una frase de confirmación exacta. Es una revocación a ciegas y la pantalla lo dice: ningún endpoint lista el estado de firma por usuario (solo GET /auth/me/signing-key, el propio), así que los usuarios del tenant se cargan en una página amplia y se filtran en cliente, y el backend responde 404 no_signing_key cuando el destinatario no tiene ninguna. Faltaba en el índice de tarjetas de /admin —que se presenta como el mapa completo de administración— hasta el lote 1 de la revisión UX 2026-08-02 |
Administrador (USUA_PERM_ADMIN, gate a nivel de router en admin_router de auth-service/app/routers/signing_key.py) |
GET /api/v1/auth/users, POST /api/v1/auth/admin/users/{user_id}/signing-key/revoke |
/admin/interoperabilidad |
Interoperabilidad y carga masiva | Cuatro pestañas (Fase 8, última del cierre del desfase API↔UI): Exportar (paquete ZIP interoperable, descarga en cliente, con contadores de incluidos/excluidos por nivel de seguridad); Importar (Stepper subir→validación→confirmar→resultado; la validación de fixity/esquema es atómica en el backend, un 422 muestra el archivo/checksum EXACTO que falló, nunca un mensaje genérico); Carga masiva de documentos (hasta 1000) y Carga masiva de expedientes (hasta 100), ambas con modal de confirmación con el número exacto de ítems y sondeo de progreso cada 2s que distingue "no se pudo consultar" de "aún sin resultados". Las cuatro pestañas operan de punta a punta; la carga masiva no reportaba resultado (revisión UX 2026-08-02: el sondeo se había quedado con la forma de URL anterior al fix de ruteo del gateway y el panel de progreso no era reactivo), corregido en el lote 1 junto con un test de contrato que fija la forma de la URL Lote con errores (2026-10-02): si el lote termina en completado_con_errores o failed, la pestaña de documentos dice «El lote terminó con errores» con el conteo, lista los fallidos (asunto, motivo en español, texto técnico como detalle secundario, intentos) y, solo si el backend responde reprocesable y la persona tiene PERM_RADI, ofrece Procesar los faltantes con confirmación (cuántos se procesan; lo ya radicado conserva su número). Tras el 202 vuelve a sondear hasta que termine; los 404/409 salen en español. Proxy POST /admin/interoperabilidad/api/lotes/documentos/{jobId}/reprocesar → POST /api/v1/batch/documents/{job_id}/reprocesar. |
Administrador con USUA_PERM_EXPEDIENTE o radicador con PERM_RADI: el gate es heterogéneo por pestaña —la carga masiva de documentos exige PERM_RADI— así que la pantalla se abre con cualquiera de los dos y cada pestaña filtra con su permiso propio, en vez de ofrecer lo que negará u ocultarse a quien sí puede usarla |
POST /api/v1/export, POST /api/v1/import, POST/GET /api/v1/batch/documents[/{job_id}/status], POST/GET /api/v1/batch/expedientes[/{job_id}/status] |
/_kit |
Kit de componentes UI | Showcase interno de componentes del sistema de diseño Orpyca (solo entorno de desarrollo, no producción) | Desarrolladores | — |
Radicar un borrador (B6): el modal de confirmación captura los datos mínimos del radicado (Ac. AGN 060/2001 §5.2): tipo documental del catálogo (se precarga el de la plantilla solo si está en el catálogo), dependencia de origen de las dependencias activas del usuario en una Salida (sin ninguna, no se ofrece «Radicar» y se explica por qué), el destino del borrador en un Interno y los metadatos del tipo (
SchemaField). Un 422 (metadata_validation_failed,datos_minimos_incompletos,invalid_dependencia) deja el modal abierto con el error junto al campo. El proxy reenvía solotipo_documental,origin_dept_code,dest_dept_codeymetadata. Los metadatos guardados se leen en solo lectura (SchemaValue) en/bandeja(detalle) y/expedientes/[id];EditarMetadatosModalyTransaccionModalsiguen sin migrar aSchemaFieldporque el backend no admite editar esos campos (ExpedienteUpdateesextra="forbid";TransactionTypeResponseno traedetalles_schema). En/envios, el error del acuse de recibo se asocia al campo del archivo (aria-describedby).
Navegación global¶
Presentes en todas las pantallas (app)/, provistas por AppLayout y SidebarNav:
- Búsqueda global (
role="search"en la barra superior): navega a/busqueda?q=…, la misma vista de búsqueda full-text con sus filtros avanzados. Atajo de teclado /, inhibido cuando el foco ya está en un campo editable. En pantallas estrechas se despliega desde un icono. Como el término viaja en la URL, toda búsqueda es enlazable y compartible. - Favoritos: cada entrada del menú lateral tiene un control fijar/desfijar
(
aria-pressed, nunca señalizado solo por color). Los ítems fijados encabezan el menú y alimentan el módulo 04 del/dashboard. Se persisten enlocalStoragenamespaced por tenant + usuario (lib/stores/favorites.js, con guardia SSR). El store no filtra por sí mismo: recibe la lista ya filtrada por permisos desdelib/utils/navItems.js, de modo que un favorito cuyo permiso se retira desaparece solo. El catálogoNAV_ITEMSy su filtro RBAC son la fuente única compartida porSidebarNavy el/dashboard.
Notas de diseño¶
- Sistema de diseño Orpyca v1.0: todos los componentes reutilizables
(
DataTable,Drawer,Modal,StatusChip,FormField,Button,Loader,Stepper,Card,EmptyState,Toast,Stepper,UserPicker) viven enfrontend/src/lib/components/ui/y usan tokens de diseño--op-*(colores/espaciados/tipografía/radios/sombras/movimiento), nunca valores hex sueltos (ADR-020). La fuente única essrc/styles/_tokens.scss. - Contraste y uso del verde de marca: el primario v1.0 (
--op-primary,#2A8C3A) mide 4.28:1 sobre blanco, por debajo del 4.5:1 que WCAG AA exige a texto normal. Por eso el token está partido por rol:--op-primarysolo para elementos no textuales (bordes, iconos, anillos de foco, umbral ≥3:1) y--op-primary-dark(#1C6B33, 6.56:1) para todo texto —incluidos$primary/$linkde Bulma— o los pares--op-*-fgverificados. Los semánticos crudos (--op-warning,--op-error, …) son colores de borde/icono; su versión de texto es--op-*-fg.--op-accent(#8DBF3C, 2.18:1) nunca se usa como texto sobre fondo claro. - Tipografía: Space Grotesk (display, títulos, cifras) + Public Sans (UI y cuerpo),
self-hosted vía
@fontsource— sin CDN externo, por soberanía y por no filtrar la navegación de los usuarios a un tercero. - Movimiento:
_tokens.scssdeclara una regla globalprefers-reduced-motionque neutraliza toda transición y animación del sitio; ninguna pantalla necesita repetirla. DataTablees la tabla del sistema: expone ocho capacidades opt-in por prop (filtros por columna, ordenamiento conaria-sort, exportación CSV, columnas configurables, vistas guardadas, selección múltiple, virtualización a nivel de render y edición en celda) con el comportamiento simple como valor por defecto. La virtualización usacontent-visibilityen vez de retirar filas del DOM: es menos agresiva, pero no rompe la semántica nativa de<table>para lectores de pantalla. La usan/busqueda,/expedientes,/firmas,/enviosy los cuatro paneles de/reportes;/bandejay/archivo-fisicosiguen con tabla propia (ver Roadmap).- Edición de texto enriquecido (respuestas, observaciones): usa TipTap 3, con el HTML saneado server-side antes de guardar y antes de renderizar — nunca se interpola HTML crudo del usuario.
- Exportación CSV desde el navegador neutraliza inyección de fórmulas (antepone
'a celdas que empiezan con= + - @). - No existen capturas de pantalla en esta documentación — las tablas anteriores
describen la funcionalidad real implementada; para verla en ejecución, levantar la
plataforma (ver Primeros pasos) y navegar a
http://localhost:19300.
Ver también¶
- Arquitectura — cómo cada pantalla se conecta con los microservicios a través del gateway.
- Referencia de API — contrato completo de cada endpoint listado arriba.
Escritura administrativa a nivel 3. En
/admin/dependencias,/admin/catalogosy/admin/pinar(listado y workspace) los controles de escritura se ofrecen solo conUSUA_PERM_ADMINa nivel CRUD >= 3, igual que el backend; con nivel 1 o 2 la pantalla es de solo lectura y un403se explica como falta de nivel.