T-HUB/docs/T-HUB-VALIDACION.txt

205 lines
9.2 KiB
Text

T-HUB - Guía de validación de cambios
=====================================
Para: quien revise y valide los cambios de T-HUB antes de llevarlos a
upstream (gitlab.moxexley.com/mediatools/tacticalmediahub) o a producción.
Base: upstream main a6843e9.
Commits a revisar (en orden):
feat: wire the Instagram connector into connect and publishing
feat: outreach contacts - track hand-written DMs per collective
chore(db): force row-level security on media_assets
style / docs: estilo de la web, logo y documentación
Cada commit se puede revisar y aplicar por separado.
0. PREPARACIÓN
--------------
corepack enable
pnpm install --frozen-lockfile
pnpm exec turbo run build typecheck --filter=@tactical-media-broadcast-hub/api...
Para los tests que usan Postgres hacen falta las variables *_IT_DATABASE_URL
de cada suite; sin ellas se saltan (comportamiento de upstream).
Lint: apps/api ya tenía 54 errores antes de estos cambios y sigue teniendo
los mismos 54. Los archivos tocados pasan eslint sin errores.
1. INSTAGRAM
----------------------
Qué cambia
- createInstagramConnector registrado en
apps/api/src/connections/connector-registry.ts y en connectorFor de
apps/api/src/publishing/publishing.service.ts.
- Política de publicación instagram: { idempotency: "none" } en
packages/connector-runtime/src/policy.ts (Graph no tiene clave de
idempotencia; un envío dudoso no se repite).
- Las credenciales de la app de Meta se leen SOLO de
INSTAGRAM_CLIENT_ID / INSTAGRAM_CLIENT_SECRET. Se quitan los campos
clientId/clientSecret del formulario y ya no se copian al vault.
- El proveedor sale desactivado (connectEnabled=false) sin esas dos
variables, con una nota que lo explica.
- Si la persona autoriza varias cuentas de Instagram en el diálogo de
Meta, la conexión falla en vez de elegir la primera.
Tests automáticos
pnpm --filter @tactical-media-broadcast-hub/connector-instagram test
esperado: 37 pass
cd apps/api && npx vitest run src/connections
esperado: 18 pass (incluye "offers Instagram only with the Hub's
Facebook app configured, and keeps the app secret out of the browser")
Prueba manual (entorno de pruebas, app de Meta en modo desarrollo)
1. Sin variables: GET /connections/providers -> instagram enabled=false.
2. Con INSTAGRAM_CLIENT_ID/SECRET: enabled=true.
3. En la app de Meta, Valid OAuth Redirect URI =
${API_URL}/connections/callback (exacto).
4. Canales -> Conectar Instagram con una cuenta Tester Business/Creator
vinculada a una página -> vuelve con connect=success y la conexión
sale healthy con @usuario.
5. Autorizar dos cuentas a la vez -> connect=failure.
Límites conocidos (no resueltos en este commit)
- Publicar imágenes NO funciona todavía: Meta descarga la imagen de una
URL pública y los medios están cifrados en el almacén. Hace falta un
endpoint temporal que sirva el JPEG descifrado (TTL corto, solo
borradores aprobados). Además la regla del proyecto es que los bytes
no pasen por la API. Decisión de diseño pendiente.
- refreshCredentials usa grant_type=ig_refresh_token contra
graph.facebook.com, que es el método de "Instagram Login", no de
Facebook Login. Comprobar con un token real; posible caducidad a 60
días.
2. OUTREACH
---------------------
Qué cambia
- Migración 041_outreach.sql: tabla outreach_contacts con RLS
ENABLE+FORCE y WITH CHECK, índice único (workspace, platform,
lower(handle)), trigger de workspace inmutable, permisos
outreach.view (Owner, Admin, Editor, Moderator) y outreach.manage
(Owner, Admin, Editor).
- API: GET/POST/PATCH/DELETE /outreach/contacts (apps/api/src/outreach).
Importación de hasta 500 por petición; las cuentas ya existentes se
saltan. La auditoría guarda altas, bajas y cambios de estado, nunca el
texto del mensaje.
- Web: /outreach (barra lateral y "More"). Importar CSV, plantilla con
{name} y {handle}, filtros, "Copy & open profile", contador diario
con aviso a partir de 20.
- El Hub no envía mensajes. No hay llamadas a APIs de redes en este
módulo. Ver README, sección "Panel Outreach", para el motivo.
Tests automáticos
cd apps/api && npx vitest run src/outreach esperado: 8 pass
pnpm --filter @tactical-media-broadcast-hub/web test:outreach
esperado: 4 pass
Verificado además (fuera del repo): la migración 041 y todas las
consultas del servicio se ejecutaron sobre PGlite (Postgres compilado a WASM)
como rol sin privilegios con app.workspace_id fijado: aislamiento entre
colectivos, salto de duplicados en mayúsculas/minúsculas, sellos de
sent_at/replied_at, vuelta a "todo" limpia los sellos.
Prueba manual
1. Aplicar migraciones (src/db/migrate.cli.ts).
2. Como Editor: importar
name,platform,handle,profileUrl,category,language
Prueba,bluesky,prueba.bsky.social,https://bsky.app/profile/prueba.bsky.social,test,es
-> "1 contacts added". Reimportar -> "0 added, 1 already on the list".
3. Plantilla "Hola {name}" -> el contacto pendiente recibe "Hola Prueba".
4. Copy & open profile -> abre el perfil en otra pestaña con el texto
en el portapapeles.
5. Mark sent -> estado sent y sello de fecha; aparece outreach.sent en
/audit sin el texto del mensaje.
6. Como Analyst (sin outreach.view) -> GET /outreach/contacts da 403.
7. Desde otro colectivo -> la lista no muestra los contactos del primero
y PATCH sobre su id da 404.
8. profileUrl con http:// o javascript: -> 400.
3. RLS EN media_assets
----------------------
Qué cambia
- 042_media_assets_rls.sql: ENABLE + FORCE RLS y política con
WITH CHECK.
- Las consultas a media_assets (media.service.ts, publishing.service.ts)
filtran también por app.workspace_id.
Verificación
- cd apps/api && npx vitest run src/media src/publishing
esperado: 114 pass, 49 skipped (necesitan Postgres).
- En la base de datos, después de migrar:
SELECT relrowsecurity, relforcerowsecurity
FROM pg_class WHERE relname = 'media_assets'; -> t, t
- Prueba manual: subir un medio en el colectivo A y pedir
GET /media/<id> con sesión del colectivo B -> 404.
3b. PUBLICAR CON IMÁGENES (scheduler)
-------------------------------------
Problema
Desde 71db0a0/a6843e9 un borrador aprobado puede llevar mediaAssetIds y
altTexts, pero el registro del scheduler
(packages/connector-runtime/src/registry.ts) creaba los conectores sin
loadAsset/openAsset: Mastodon y Telegram devolvían rejected_permanent
INVALID_POST y el post no salía. Los altTexts no llegaban al conector.
Qué cambia
- packages/connector-runtime/src/media.ts: loadAsset lee media_assets
dentro del colectivo de la ejecución (app.workspace_id, RLS forzado);
openAsset descarga el objeto, abre el marco SPENC1 con S3_KEK y
comprueba el hash del contenido. Solo lectura. Los bytes van
worker -> red; nunca pasan por la API.
- preparePostForDispatch convierte altTexts[i] en
providerMetadata.altText[mediaAssetIds[i]] (lo que lee Mastodon).
- Instagram registrado también en el scheduler: rechaza con error
explícito en vez de "proveedor desconocido". Sigue sin publicar fotos
(necesita URL pública; decisión pendiente).
Tests
cd packages/connector-runtime && npx vitest run esperado: 33 pass
cd apps/scheduler && npx vitest run esperado: 96 pass
Prueba manual (entorno de pruebas)
1. Compose: post con 2 imágenes y texto alternativo a Mastodon.
2. Aprobar y programar a 1 minuto.
3. El post sale con las 2 imágenes y su descripción en la instancia.
4. Con una imagen de otro colectivo forzada en mediaAssetIds -> falla
(MEDIA_ASSET_NOT_FOUND), no se publica nada.
3c. PRODUCTOR DE VÍDEO
----------------------
- Quitado MEDIA_DEBUG_STDERR=1 del compose de producción (escribía la
salida de ffmpeg sin censurar en /tmp).
- El logo superpuesto solo acepta imágenes del propio colectivo
(adjuntos, recursos de grupo o la marca). Antes bastaba una clave con
formato válido.
cd apps/api && npx vitest run src/video esperado: 38 pass
4. ESTILO Y LOGO
----------------
- apps/web/src/app/globals.css: paleta de acampada26s.net. Fondo
blanco, texto y bordes negros, botones negros con sombra neón
desplazada, foco y elemento activo en neón #00e5ff. Títulos h1/h2 en
Anton en mayúsculas con la sombra neón escalonada de la web.
- Iconos de navegación en negro; iconos de redes con trazo neón.
- Logo: tres tiendas iglú cableadas a un switch con antena, en el
estilo a tinta del logo de la acampada (docs/brand/t-hub.svg;
t-hub-lockup.svg/png con el nombre en Anton convertido a trazos).
Componente LogoMark sustituido; nombre visible "T-HUB".
- Fuente Anton en apps/web/public/fonts (licencia SIL OFL incluida).
- Página pública de buzón (/submit/[token]) en claro.
Revisado con capturas (Chromium sin interfaz, modo offline con datos de
ejemplo): Streams, Compose, Outreach, Channels, Approvals, Onboarding,
/submit en escritorio y móvil. Falta revisar Control Room y Calendar con
datos reales.
5. SEGURIDAD
------------
La revisión de seguridad se entrega en privado a quien mantiene el
despliegue. Los fallos se comunican por canal privado, no en issues.