205 lines
9.2 KiB
Text
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.
|