IntraVox¶
Propósito y diseño¶
IntraVox es la intranet de la suite: el fork de APS Conecta de IntraVox, la aplicación de VoxCloud (nextcloud/IntraVox), que la suite instala como aplicación propia. Los equipos de comunicaciones publican y el personal lee. El personal la encuentra como «Inicio», la portada del establecimiento (Inicio (IntraVox)).
Decisiones de diseño:
Una página es una carpeta. La página
<pageId>/contiene<pageId>.json(widgets y diseño) y_media/, dentro de una carpeta de grupo. La página hereda de la carpeta todo lo que APS Conecta Gestión da a una carpeta —compartir, ACL, versiones, papelera— e IntraVox no reimplementa nada de eso.La identidad es el
uniqueId, nunca la carpeta. El nombre de la carpeta se deriva del título y solo es único entre hermanas (una colisión recibe el sufijo-2,-3); renombrar o sufijar una carpeta no rompe enlaces, compartidos ni grupos de traducción.Doce widgets. Texto, título, imagen, enlaces, separador, vídeo, noticias, personas, calendario, fuente, historia fotográfica e historia de archivos (
ALLOWED_WIDGET_TYPESenlib/Service/Sanitize/PageShapeSanitizer.php); el saneador descarta cualquier otro tipo y admite como máximo cinco columnas.Comunicación de la organización, no productividad personal. IntraVox publica para un público amplio; el Dashboard es la vista personal de cada cuenta. En la suite, la capa personal se queda en páginas de equipo con ACL, porque el personal usa computadores compartidos en sesiones cortas (ADR-0012).
El repositorio se mantiene como fork con funciones locales (ADR-0001): el upstream es el motor y se incorpora; las diferencias del fork son pocas y quedan registradas. Una incorporación desde el upstream que toca una de ellas vuelve a leer ese ADR antes de fusionarse.
Diferencia del fork |
Qué hace |
Registro |
|---|---|---|
Idiomas |
El valor por defecto de idiomas habilitados es |
|
Raíz de almacenamiento |
El nombre de la carpeta de grupo es un valor de la app, |
|
Siembra gestionada |
|
|
Muros |
Una página con |
|
Datos de demostración |
Sin descarga remota: la única fuente es |
|
Plegado compartido |
|
|
Documentación |
Los enlaces |
ADR-0001 |
La suite instala el fork así:
El manifiesto de la suite declara el fork (
source: fork): en el momento de instalar, la resolución descarga su publicación firmada más reciente compatible con la versión de APS Conecta Gestión, la verifica contra la clave de aplicaciones y registra la versión en el candado.La fase 15 fija
defaultappenintravox: un inicio de sesión aterriza en la intranet (ADR-0010).La política de apps fija
intravox:telemetry_enabled:false: la telemetría del motor viene activada por defecto y la suite la apaga con el literal'false'(provisioning/app-policy.sh).La fase 41 (
provisioning/phases/41-intravox.sh) escribegroupfolder_nameantes deocc intravox:setup --skip-demo, asigna los grupos del registro a los del motor, renderiza el árbol que el establecimiento declara y lo importa por sección, y escribe las ACL de las páginas de equipo. La guía de la portada —qué se siembra, el flujo editorial, el presupuesto de páginas y la recuperación— vive en esta página y en Inicio (IntraVox).
Contratos de componentes y API¶
La lista de rutas es la referencia generada, Referencia de IntraVox, y en el fork docs/route-table.md, que npm run route-table genera y CI rechaza cuando queda desactualizada. Su resumen por exigencia de quien llama:
Exigencia |
Rutas |
Ejemplos |
|---|---|---|
Cualquier cuenta con sesión |
120 |
Páginas, medios, navegación, comentarios |
Administración |
12 |
Datos de demostración, idiomas, datos huérfanos |
Administración, comprobada en el cuerpo |
22 |
Ajustes, licencia, operaciones masivas, liberar un bloqueo ajeno |
Administración de la carpeta de equipo, comprobada en el cuerpo |
6 |
Exportar e importar ZIP y Confluence |
Anónima |
19 |
|
La superficie OCS externa (/api/v1/pages y sus medios) admite autenticación básica. openapi.json describe la API (OpenAPI 3.1.0, versión 3.1.7). La protección de fuerza bruta de la plataforma puede responder 429 en cualquier ruta autenticada tras credenciales erróneas repetidas, y sigue respondiendo 429 aunque las credenciales ya sean correctas, hasta que vence la ventana: un cliente trata el 401 como terminal y no reintenta.
Contrato de la página:
Campo |
Quién lo escribe |
Contrato |
|---|---|---|
|
El cliente al crear ( |
Identidad estable; el servidor lo conserva en cada guardado. |
|
El editor |
Al crear, el nombre de la carpeta se deriva de él. |
|
El editor |
|
|
Solo el renderizador de gestion y |
Un guardado conserva el valor almacenado, nunca el del cliente. |
|
El editor |
Filas, columnas y widgets del conjunto permitido; el saneador vuelve a aplicarse en cada lectura. |
|
El cliente, solo en el transporte |
El mtime del archivo que el cliente cargó; nunca se persiste. |
|
El servidor, en la respuesta |
El enlace a la carpeta de la página en Archivos; lo usan «Abrir en Archivos» y el enlace de ubicación de la barra lateral. |
El importador gestionado tiene su propio contrato de árbol: home.json, navigation.json, footer.json, images/ y una carpeta <page>/<page>.json por página, en forma recursiva. Una carpeta que no contiene su propio <folder>/<folder>.json no se importa ni se recorre, y los medios de una página van en <page>/images/. La importación de los editores (ZIP o Confluence, ImportService) usa la convención _media/: dos públicos, dos convenciones, sin fusión por diseño. Con --skip-existing, un nodo existente se informa como Skipped (exists) y el recorrido sigue bajando, de modo que una nueva ejecución agrega lo que falta sin aplanar lo editado.
Transiciones de estado¶
Publicación. PublicationStateService::effectivePublishState() combina la marca manual con los campos de fecha de MetaVox y devuelve uno de cuatro estados. Se evalúa al leer, sin cron.
Estado |
Condición |
Lo ven |
|---|---|---|
|
Marca manual en borrador y sin fecha de publicación |
Quienes pueden escribir en la página |
|
Marca manual publicada, o fecha de publicación pasada, sin vencimiento cumplido |
Todo lector con acceso |
|
Fecha de publicación futura |
Quienes pueden escribir en la página |
|
Fecha de vencimiento pasada |
Quienes pueden escribir en la página |
Sin MetaVox, o sin campos de fecha configurados, solo existen draft y published. gestion no instala MetaVox (ADR-0013): en la suite, un aviso vence cuando un editor lo devuelve a borrador.
Bloqueo de edición. PageLockService es un bloqueo pesimista por página en la tabla intravox_page_locks:
Desde |
Evento |
Hacia |
|---|---|---|
Libre |
|
Tomado por la cuenta |
Tomado |
|
Tomado, con el plazo renovado |
Tomado |
|
Sin cambio; la respuesta devuelve quién lo tiene |
Tomado |
|
Libre |
Tomado |
|
Libre |
Tomado |
15 minutos sin latido |
Vencido; se borra en la siguiente consulta del bloqueo o el siguiente intento de tomarlo |
Muro. Una página pasa de normal a muro solo por el renderizador de gestion, al declarar una sección wall, o por el comando intravox:protect de la administración; retirarlo es el paso deliberado antes de eliminar o mover un muro. El cambio es idempotente: una página que ya está en el estado pedido no se reescribe; si cambia, primero se guarda una versión, de modo que el historial de la página lo revierte.
Muro¶
Un muro es estructura fija, no contenido congelado. Al declarar una sección wall, la siembra marca con "protected": true su estructura: el centro de la sección, cada página sembrada que tiene subpáginas —los «Avisos» de noticias, que la lista de avisos de la portada lee— y, en equipos, el centro más cada página de equipo. El motor rechaza eliminar o mover una página así: el menú de la página y el árbol no la ofrecen, y una petición directa responde PAGE_PROTECTED. Un muro sigue siendo editable, título incluido; lo que no se puede es sacarlo del sitio por accidente. La carpeta de un muro tampoco se puede renombrar —el enlace del sitio y los avisos de la portada encuentran una sección por su carpeta— y restaurar una versión anterior conserva el muro. Las publicaciones de ejemplo sembradas («Aviso de ejemplo», «Bienvenida», «Cómo publicar») no son muros: como toda publicación del personal, son páginas comunes que un editor puede borrar. La marca es por página, nunca una regla de carpeta: una regla de la carpeta de grupo heredaría sobre todas las publicaciones de noticias, y bajo las ACL sembradas solo la administración puede eliminar de todos modos. Retirar un muro son dos pasos deliberados de la administración: occ intravox:protect <uniqueId> --off --user admin (se guarda una versión primero) y después eliminar o mover la página; --on lo restablece y --status muestra el estado. Una instancia sembrada antes de que existieran los muros no lleva la marca en sus páginas —la siembra nunca reescribe una página existente— y se alza con occ intravox:protect <uniqueId> --on --user admin. La marca vive en el JSON de la página, así que las herramientas de administración que trabajan por debajo de la API de páginas la ignoran: la app Archivos (un administrador de la carpeta de grupo puede eliminar la carpeta —el procedimiento de recuperación se apoya justo en eso—), una importación ZIP con sobrescritura y el reinicio limpio.
Siembra por sección. En cada siembra, la fase 41 compara la declaración SITE_WELCOME con la carpeta de grupo. El marcador de una sección es su página principal, es/<sección>/<sección>.json; el de una página de equipo, es/equipos/<gid>/<gid>.json; el de los archivos base, es/navigation.json.
Situación |
Resultado |
|---|---|
Declarada y ausente |
Se renderiza, se importa y se registra |
Declarada y presente |
No se toca y se registra |
Presente y no declarada |
|
Archivos base ( |
Se renderizan solo en la primera siembra; después son datos del personal |
Declarar el árbol¶
sites/<slug>/site.sh lleva una fila por sección, en el orden en que aparecen en el menú:
SITE_WELCOME=(
'noticias|wall' # sección|bandera — wall = estructura fija, vacío = editable
'vida-cesfam|'
'documentos|wall'
)
scripts/deis.py escribe este bloque por omisión para un sitio nuevo y nunca reescribe un archivo de sitio existente: un archivo anterior al bloque detiene la siembra con FATAL: sites/<slug>/site.sh must set SITE_WELCOME y el bloque se agrega a mano. Las secciones son carpetas de la biblioteca bajo provisioning/intravox/es/sections/; equipos es la única que se genera en vez de copiarse: declarada como 'equipos|wall', la siembra renderiza la página «Guía de equipos» más una página por fila de SITE_TEAMS, cada una enlazando la carpeta propia del equipo y legible por ese equipo y por las jefaturas. Una página restringida a un cargo (OIRS, Estadística REM) es una fila de equipo que se agrega a mano, por ejemplo 'role-oirs|OIRS': su página enlaza la única carpeta declarada que lleva su nombre (Unidades/OIRS), y un equipo cuya carpeta el sitio no declara detiene la siembra. La página de Documentos enlaza exactamente SITE_SUBFOLDERS; el menú y el pie listan las secciones declaradas y nada más, y la fila de accesos rápidos de la portada son tres fichas fijas —Recepción y admisión → la carpeta Transversal, Gestión y turnos, Teléfonos y anexos— seguidas de una ficha por sección declarada. Una fila que la biblioteca no conoce, una bandera desconocida o una página que enlazaría una carpeta que el sitio no tiene detienen la siembra con una línea FATAL antes de importar nada.
Agregar una sección después: agregar la fila y sembrar. La siembra registra welcome: section <nombre> created — add its menu entry and home tile by hand: el menú y la portada ya son datos del personal para entonces, así que la siembra no los toca; agregar la entrada en el editor de menús de la administración. Quitar una fila no cambia nada en la instancia: make divergence lista la sección como viva pero no declarada, y una persona elimina la carpeta si eso es lo previsto.
Taxonomía de errores¶
Código o excepción |
HTTP |
Significado |
Reintento |
Remedio del cliente |
|---|---|---|---|---|
|
400 |
Eliminar o mover un muro |
No |
Retirar el muro primero —un paso deliberado de la administración, descrito en «Muro» de esta página— y repetir |
|
400 |
Eliminar o mover la página de inicio configurada |
No |
Asignar otra página de inicio primero |
|
403 |
Escritura sobre una página o carpeta en la que la cuenta solo lee |
No |
Pedir permiso de escritura en la carpeta de grupo |
|
404 |
Ningún idioma contiene ese |
No |
Corregir el identificador |
|
409 |
Guardado obsoleto: el |
Sí, después de recargar |
Recargar la página y rehacer el cambio |
|
— |
Mover una página a la carpeta de otro idioma |
No |
Ninguno: el rechazo es deliberado; en una operación masiva se informa por página y el resto continúa |
|
— |
El ZIP de un editor no valida: |
No |
Corregir el archivo; el código es estable y el mensaje en inglés es solo el respaldo |
|
— |
|
No |
Corregir el valor; la fase 41 rechaza |
Un PageNotFoundException o un ForbiddenException nunca se convierte en 400: ambas excepciones extienden \RuntimeException para que los bloques que traducen \InvalidArgumentException a 400 no las absorban.
Concurrencia e invariantes¶
Guardado entero, dos guardas. El JSON de una página se escribe completo, así que un guardado obsoleto no se mezcla: borra todo lo escrito después. El bloqueo pesimista cubre el caso común; el
baseVersioncubre la pestaña que vuelve tras un bloqueo vencido. El token es el mtime del archivo, que el sistema de archivos fija en cada escritura, y no el campomodifieddel JSON, que es lo que el cliente envió. Un cliente que no envíabaseVersionno se bloquea.La carrera por el bloqueo la resuelve la base de datos. Un índice UNIQUE sobre
page_unique_idimpide dos bloqueos; quien pierde la inserción recibe el bloqueo existente.Los muros viven en el servidor. Ningún cliente puede activar la marca y ninguna refactorización retira la guarda del servidor.
Una sola raíz configurada. Ningún código nombra la carpeta de almacenamiento con un literal; todo pasa por
MountName.La siembra nunca sobrescribe.
--skip-existinges obligatorio en la fase 41, y una sección existente no se vuelve a importar.La asignación de grupos solo agrega.
intravox_group_map(gestion,provisioning/lib.sh) sumaall-staffaIntraVox Usersycat-jefaturasyrole-oirsaIntraVox Editors; nunca quita a nadie.
Dominios de fallo¶
La carpeta de grupo. Todo el contenido vive en una carpeta de grupo, y la app exige Team folders. Si
groupfolder_nameno coincide con la carpeta montada, cada página responde IntraVox folder not found hasta completar el cambio de nombre. La fase 41 se niega a sembrar mientras exista una carpeta con el nombre anterior:occ intravox:setupcrearía una segunda carpeta, vacía, junto a la real.La app desactivada.
defaultappcae al valor fijodashboard,filesy el inicio de sesión aterriza en el Dashboard. No se pierde contenido: las páginas son archivos.Herramientas bajo la API de páginas. La app Archivos (un administrador de la carpeta de grupo puede eliminar la carpeta), una importación ZIP con sobrescritura y el reinicio limpio de la administración ignoran la marca de muro.
MetaVox ausente. Las funciones que lo usan, como la importación y el filtrado de historias fotográficas, se degradan sin fallar.
La siembra interrumpida. El renderizador falla cerrado antes de importar nada. Una siembra que muere entre la importación y las reglas deja una página de equipo sin sus ACL, y la siguiente siembra no las reescribe: se reponen a mano con
occ groupfolders:permissions, o se elimina la carpeta de esa página y se vuelve a sembrar.Contenido de demostración. Si la marca
demo_data_importedexiste, la fase 41 se detiene: el contenido de demostración desplazaría a la portada sembrada.
Renombrar la carpeta de almacenamiento de una instalación existente¶
Una instalación nueva recibe el nombre de la carpeta de IV_MOUNT (scripts/env.sh, por omisión «Intranet»): la fase 41 lo escribe en el motor antes de intravox:setup, cada «Abrir en Archivos» y el enlace de ubicación de la barra lateral abren la carpeta con ese nombre, y las páginas sembradas que dicen dónde vive el contenido lo nombran. Una instalación sembrada antes de que esto existiera lleva el valor por omisión del motor, IntraVox, y la siembra se niega a correr sobre ella (FATAL), porque crearía una segunda carpeta vacía; la misma negativa aplica cuando IV_MOUNT cambia y la carpeta ya lleva el nombre anterior, o ante un valor que el motor rechazaría (vacío o con /). Renombrar es manual, una sola vez y en este orden, sin make seed en medio; los comandos corren en el contenedor de APS Conecta Gestión como www-data:
Antes de todo:
occ intravox:reindex --user admin --dry-runy anotar la N deWould index N of M page file(s)….occ config:app:set intravox groupfolder_name --value Intranet. Desde este momento el motor busca «Intranet», así que las páginas pueden responder IntraVox folder not found hasta el paso 2: hacerlo de inmediato.occ groupfolders:listpara anotar el id de la carpeta montada comoIntraVox; luegoocc groupfolders:rename <id> Intranet(silencioso al triunfar). Por unos segundos el servidor web puede responder todavía con el nombre anterior —recargar; no hace falta reiniciar—.occ intravox:reindex --user admin. Debe indexar la misma N del paso 0: las filas del índice escritas bajo el nombre anterior se retiran. Corrido justo tras el paso 2 puede fallar una vez con IntraVox folder not found (el montaje renombrado aún no llega a la caché): esperar unos segundos y repetir; la corrida fallada no cambia nada.make seed: esperawelcome: section … existspor cada fila, sin crear nada.
La reversión es los mismos tres comandos con IntraVox, más IV_MOUNT=IntraVox en .env para que la siembra coincida. El personal ve el nombre nuevo en Archivos de inmediato; los enlaces de las páginas siguen el nombre configurado sin vaciar cachés. (Ensayado en el laboratorio Compose el 2026-09-29, IntraVox 3.1.7: 30 páginas, el árbol y los 97 archivos idénticos antes y después.)
Deuda técnica y límites¶
Presupuesto de páginas. gestion presupuesta 50 páginas como máximo; los borradores cuentan y la papelera no. El motor declara un límite gratuito de 50 páginas por idioma (
LicenseService.php), que en el código actual solo se informa en las Configuraciones de administración:checkPageLimit()tiene un único llamador,getStats().Vencimiento editorial. Sin MetaVox, ningún aviso se despublica solo. El disparador de adopción es que el piloto informe despublicaciones olvidadas o que el volumen de avisos supere lo que se cambia a mano.
Idioma de fuentes y pie. Las fuentes y el pie de página consultan el idioma propio del lector; las cuentas de la planilla sin
core/langrecibenenen esas dos superficies hasta que el motor consolide su fuente de idioma.Evolución de plantillas. Una sección existente nunca se vuelve a importar: un cambio en la biblioteca de gestion llega solo a las instalaciones que aún no tienen esa sección.
Equipos. El informe de divergencia de gestion (
scripts/divergence.sh) cubre secciones, no páginas de equipo; un equipo retirado deSITE_TEAMSconserva su página hasta que una persona la elimina. Quien deja un grupo conserva la lectura, porque la asignación solo agrega.Tamaño. El tarball pesa 46 MB y trae 1546 archivos: la forma de envío del upstream, con traducciones automáticas, datos de demostración y vitrinas.
Traducción y nombre. Las pestañas del panel «Estructura de páginas» (
Pages,On this page) no tienen traducción enl10n/es.jsony se muestran en inglés. La entrada de la barra de aplicaciones dice «IntraVox», mientras que el instalador y la portada la llaman «Inicio».Registro de cambios. Todo lo posterior a 3.1.0 está bajo
[Unreleased]enCHANGELOG.md, aunqueappinfo/info.xmldeclara 3.1.7 y existen las etiquetasv3.1.1-aps1av3.1.7-aps1.Análisis estático. La línea base de PHPStan (
phpstan-baseline.neon) tiene 158 entradas; la regla del fork es que solo se reduce.Espejo. El repositorio de GitHub es un espejo que recibe
mainy las etiquetas; las ramas de trabajo se desarrollan y se validan antes del espejo.