Upcoming
Coming soonCorrección— 2026
Seguridad— 2026
- Cómo se hizo (útil para futuros proyectos): 1. Cloudflare no expone API pública para crear R2 S3 tokens permanentes directamente (sólo `temp-access-credentials` con TTL ≤36h). Pero sí se pueden crear vía el endpoint general `POST /user/tokens` con permission groups R2 y resource scoped a bucket. 2. Crear ese "token-manager" requiere un meta-token con `User:API Tokens:Edit` + `Account:Workers R2 Storage:Edit` + `Account:Account Settings:Read`. Ninguno de los tokens previos (`api_token_claude`, `r2_management_token`) tenía `User API Tokens Edit`, así que todos devolvían `9109 Unauthorized`. Dashboard manual obligatorio para ese bootstrap — una sola vez. 3. El nuevo meta-token queda guardado en `services.cloudflare.credentials.api_token_manager` (config-expert, fuera del repo). A partir de ahora, crear tokens R2 scoped para cualquier bucket en cualquier proyecto es un `curl` de 10 líneas — sin pasar por dashboard. 4. El Access Key ID del token R2 scoped es el `id` del token; el Secret Access Key es literalmente el `value` (NO el SHA-256 como decía una lectura previa de la doc). Validado con `aws s3 cp/rm` sobre el bucket. Rotación aplicada:
- `UPDATE compose SET env=REPLACE(..., culturalh_key, new_scoped_key) WHERE composeId='eDmrRo24OOh2t2wtflaDs'` en dokploy-postgres.
- `compose.deploy` de mail-inbound vía tRPC API.
- Healthcheck `/health/ready` → 200 tras redeploy. DB OK, R2 OK (verificado listando `raw/` con las creds nuevas y accediendo al último EML).
- Scoping verificado: `aws s3 ls s3://lexiel/` devuelve `AccessDenied` con las nuevas keys; R/W completo sólo sobre `codelabs-mail-inbound`. Tech debt restante:
- `mi_projects.apiKey` sigue en plaintext (hashing pendiente de decisión — breaking vs migration con gracia).
Añadido— 2026
- Rollout aplicado:
- DB `mail_inbound` + user dedicado en `db.codelabs.studio` (postgres container compartido). Migraciones Drizzle ejecutadas (11 tablas).
- Bucket R2 `codelabs-mail-inbound` (credenciales compartidas `culturalh` por ahora, ver tech debt abajo).
- Dokploy compose `mail-inbound` (`composeId=Eu8nGQbpTbNFTiJiwpA8G`) con 15 env vars, Traefik labels, dual network (`dokploy-network` + `codelabs-net`). Dominio `inbound.codelabs.studio` con Let's Encrypt DNS-01.
- DNS `casos.lexiel.ai`: MX → `mail.codelabs.studio`, SPF/DKIM/DMARC por Cloudflare API. MX de nivel root sin tocar.
- Mailcow (`46.62.213.212`): dominio `casos.lexiel.ai` añadido con DKIM 2048, `custom_transport.pcre` regex → `mail-inbound-pipe`, `master.cf` entry, script `/opt/postfix/conf/mail-inbound-pipe.sh` + env file `mail-inbound.env` (mode 640, owner `root:nogroup` para evitar warning postfix "not owned by root").
- Tenant Lexiel onboarded en mail-inbound admin API. `webhookSecret` seteado como `MAIL_INBOUND_WEBHOOK_SECRET` en compose Lexiel vía sed + redeploy tRPC. Flag `EMAIL_INBOUND_PROVIDER=mail-inbound` aplicado.
- Blockers resueltos durante el rollout: 1. Mailcow firewall FORWARD DROP: el host Mailcow (separado del Xeon) tiene iptables FORWARD con policy DROP y solo permite outbound 443 para el container ACME (`172.22.1.13`). El container postfix (`172.22.1.253`) no podía alcanzar `inbound.codelabs.studio` (mensajes colgados con `curl: (28) Connection timed out` en la cola, msg `81C34A48B2`). Fix: `iptables -I FORWARD 22/23` permitiendo 443 bidireccional entre `172.22.1.253` ↔ `136.243.111.118`, persistido en `/etc/iptables/rules.v4`. Verificado desde dentro del container: `HTTP 200 in 0.174s`. 2. Lexiel `/v1/email-inbound` 401 "Not authenticated" (`packages/api/src/index.ts`): Primer commit `315b685` añadió `/v1/email-inbound` a `DEMO_SKIP_PATHS` para saltar `authWithApiKey` global. Debugging en caliente con `console.log` mostró `isPublic:true`, pero el 401 seguía (venía de otro middleware). Causa raíz: `clientPortalRouter` (export default `combined`) se monta en `/v1` y dentro tiene `createPortalRouter.use('*', auth)`, que ataca cualquier POST `/v1/*` en orden de registro antes de que Hono llegue al router específico `emailInboundRouter`. El fix del skip list era falso positivo (el skip sí funciona, pero hay un segundo middleware de auth a través del route-tree). Solución commit `64bee1b`: revertir el skip spurious y mover el mount a `/webhooks/email-inbound` (fuera del scope `/v1/*` conflictivo, siguiendo el patrón ya usado por `/webhooks/mail-tracker`). Actualizado `webhookUrl` del tenant mail-inbound al nuevo path via admin API. 3. Dokploy build cache stale: primer deploy post-push no reflejaba el fix (la imagen tenía timestamp posterior al commit pero el binario ejecutaba código viejo). `docker rmi -f lexiel-iiln7r-lexiel-api:latest` + `compose.deploy` regeneró layers. Siguiente iteración requirió además `docker compose up -d --force-recreate --no-deps lexiel-api` porque Dokploy reconstruyó la imagen pero no recreó el container. Posible mejora futura: patrón Dokploy force-recreate por defecto tras build.
- +3 more
Corrección— 2026
- README.md attachment example: faltaban `content_disposition` y `content_id` en el JSON de ejemplo. Los tenants que copian ese bloque como fixture de test verían tipos más estrechos que el payload real (loop #6 ya arregló el tipo en `client/node.ts`, pero el README quedó desincronizado).
- README.md test count: "23 unit tests" → "33" (purge-dedup y webhook-contract suites se añadieron en loops #4 y anteriores).
- docs/DEPLOY.md env table: `ADMIN_MASTER_KEY` estaba marcado `required=yes` pero el código lo trata como opcional (unset => `/v1/admin/*` devuelve 404). Un operador nuevo siguiendo los docs al pie de la letra habría gastado tiempo generando una clave innecesaria. `INGEST_SECRET` nota actualizada para reflejar el floor de entropía del loop #8 (≥32 chars en prod, con el comando `openssl rand -hex 32`). Commit mail-inbound `efe0d5b`.
Corrección— 2026
- drizzle-orm 0.44.6 → 0.45.2 (`mail-inbound/package.json`): parchea `GHSA-gpj5-g38j-94v9` (SQL injection via identificadores mal escapados). Nuestro código nunca interpola input en identificadores, así que la exposición efectiva era ≈ 0, pero mantener una versión flagged por GitHub advisories es mal default. Lexiel ya estaba en 0.45.2, versiones ahora alineadas. Tests 33/33 verdes tras el upgrade. Commit mail-inbound `9f31e74`.
- `.dockerignore` nuevo (`mail-inbound/.dockerignore`): el daemon recibía el repo completo (~180 MB incluyendo `node_modules` local + `.git` + `coverage`) antes de que las COPY explícitas del Dockerfile filtraran. Builds Dokploy más rápidos y runtime image sin `src/__tests__`. Commit `ba743fe`.
Corrección— 2026
- Entropy floor en producción (`mail-inbound/src/env.ts`): `INGEST_SECRET` y `ADMIN_MASTER_KEY` (si está set) ahora exigen ≥ 32 caracteres cuando `NODE_ENV=production`. `docs/DEPLOY.md` documenta 64 hex chars pero nada lo enforceaba; un `.env` con `INGEST_SECRET=dev` habría pasado el boot silenciosamente. El mensaje de error apunta al `openssl rand -hex 32` correcto. Dev/test conservan libertad de usar placeholders. Commit mail-inbound `1320e37`.
Corrección— 2026
- Sentry health noise (`mail-inbound/src/sentry.ts`): Traefik/Dokploy golpean `/health` y `/health/ready` cada 30s. Cada hit generaba una transaction que dominaba la trace quota en tenants low-volume y ocultaba señal real. `beforeSendTransaction` dropea ambas; errores siguen firing. Commit mail-inbound `739d36f`.
- HMAC secret leak en host (`mail-inbound/docs/mailcow-catchall.sh`): el script usaba `openssl dgst -sha256 -hmac "$INGEST_SECRET"`, que expone la clave en argv (visible en `ps aux` a cualquier usuario del host Mailcow). Cambiado a `python3` one-liner que lee `INGEST_SECRET` del entorno — mismo output, zero exposición. Python3 está preinstalado en Debian/Ubuntu (Mailcow target). Commit `141ab19`.
- Resource caps compose (`mail-inbound/docker-compose.yml`): sin `deploy.resources.limits` ni `mem_limit` un mensaje grande o fuga tumbaba otros servicios Dokploy del Xeon. Añadidos 1 CPU / 1 GiB (compatibilidad v2 + Swarm). Commit `141ab19`.
- DoS en ingest (`mail-inbound/src/routes/ingest.ts`): `c.req.arrayBuffer()` leía el cuerpo completo antes del chequeo de tamaño. Un peer malicioso mandando 2 GB forzaba allocación de 2 GB solo para devolver 413. Añadido pre-flight check sobre header `Content-Length`; post-read check se mantiene (belt-and-braces para chunked). Commit `1a6d89a`.
- DoS en webhook response (`mail-inbound/src/webhook.ts`): `res.text()` leía la respuesta del webhook tenant sin límite. Con timeout fetch de 15s y red rápida, un tenant hostil podía stream-ear ~100 MB antes de cortar. Nueva `readBoundedText()` con stream reader y cap de 1 KB (solo se usa para logs diagnósticos, 1 KB sobra). Commit `1a6d89a`.
Corrección— 2026
- Perf purge storage deletes: `mail-inbound/src/routes/maintenance.ts` hacía `deleteObject` secuencial. 500 blobs × ~200ms = ~100s por run, suficiente para solapar con el siguiente tick del cron en un tenant activo. Paralelizado con `Promise.allSettled`. Un 5xx aislado de R2 ya no aborta el batch; cada fallo sigue yendo a Sentry con hash/messageId. Commit mail-inbound `60518cd`.
- Rate-limit Retry-After header: `rate-limit.ts` devolvía el `retryAfterSeconds` en el JSON body pero no el header estándar RFC 7231 §7.1.3. Traefik, Cloudflare y los SDK de cliente esperan el header para hacer backoff automático. Añadido `c.header('Retry-After', ...)`. Commit `4004b42`.
- Client verifier type drift: `mail-inbound/client/node.ts` (template copy-paste para tenants nuevos) declaraba un tipo de adjunto más estrecho que el payload real — los consumers no verían `content_disposition` ni `content_id`, crítico para renderizar html con imágenes `cid:`. Tipos alineados con `src/webhook.ts`. Commit `b5f52f6`.
Corrección— 2026
- Regresión pinneada (`mail-inbound/src/__tests__/purge-dedup.test.ts`): nuevo test que pasa por `PgDialect.sqlToQuery()` para verificar la forma SQL generada por el `and(inArray(...), notInArray(...))` del purge. Además descubre y fija una invariante de seguridad: `notInArray(col, [])` colapsa a SQL `"true"` (fail-closed: marca todo como "aún referenciado" y el purge se salta los borrados), mientras que `inArray(col, [])` colapsa a `"false"`. Un upgrade de drizzle que cambie ese comportamiento hará ruido en CI. Commit mail-inbound `2d37ef1`.
- Docker hardening (`mail-inbound/Dockerfile`, `docker-compose.yml`, `package.json`): `tsx` movido a `dependencies` porque es el entrypoint de runtime. `npm ci --omit=dev --omit=optional` → imagen más pequeña y menos superficie de ataque. `USER node` → no ejecuta como root. `SENTRY_TRACES_SAMPLE_RATE` y `GIT_SHA` ahora se propagan al container. Commit `c0d6cff`.
- SSRF defense en admin (`mail-inbound/src/routes/admin.ts`): `validateWebhookUrl()` rechaza loopback, RFC 1918, link-local, CGNAT y la dirección de metadata de AWS/GCP/Azure; además fuerza https fuera de `development`. Aplicado al create y al patch de webhook. Comentado en el código que NO protege contra DNS rebinding (eso requeriría resolver DNS pre-flight). Commit `02c98e7`.
- MAX_ATTACHMENTS 100 (`mail-inbound/src/parse-mime.ts`): cap anti-abuse — un mensaje de 30 MB podía explotar en 300 PDFs pequeños y disparar 300 PUTs a R2 + 300 filas en `mi_attachments`. Commit `02c98e7`.
- Payload type honesty (`mail-inbound/src/webhook.ts`): quitado `cc?: string[]` del tipo de payload porque nunca se populaba (el schema hace merge de to+cc en un solo array). Consumer que confiaba en él estaba leyendo un campo fantasma.
- Lexiel adapter byte cap (`packages/api/src/routes/email-inbound.ts`): `fetch().arrayBuffer()` del adjunto se reemplaza por stream reader con hard cap de 35 MB. Antes solo había timeout 30s, que en una red rápida alcanza para descargar varios GB en memoria. Commit Lexiel `3a5967985`.
Corrección— 2026
- CRITICAL purge dedup bug (`mail-inbound/src/routes/maintenance.ts`): `sql\`messageId NOT IN ${ids}\`` interpolaba el array como un único parámetro, así la comprobación de "aún referenciado" devolvía vacío y la purga eliminaba blobs compartidos. Reemplazado con `notInArray()` de drizzle. Commit mail-inbound `1e7fae2`.
- CRITICAL retry cron sequential (`maintenance.ts`): 100 due × 15s timeout = 25m, más largo que el intervalo del cron (overlap). Worker pool bounded por `concurrency` query param (default 5, max 20).
- CRITICAL pending huérfano (`webhook.ts`): mensajes que crasheaban entre ingest ACK y la entrega fire-and-forget quedaban en `pending` para siempre. `fetchDueRetries` ahora recoge `pending > 60s` además de `retrying` vencidos.
- Perf ingest (`routes/ingest.ts`): uploads de adjuntos paralelizados con `Promise.allSettled` (~2s → ~200ms en 10-PDF emails, crítico para ACK window de Mailcow).
- Ops (`src/index.ts`): graceful SIGTERM/SIGINT (drain + `Sentry.close(2000)` + 15s hard deadline). Dokploy redeploys no tiran requests en vuelo. CORS scoped a `/v1/projects/*` + `/v1/admin/*`. DB health-check rota ahora dispara `Sentry.captureException`.
- Safety: purge de raw EML usa `rawStorageKey` stored en lugar de recomputar `rawKey(id)` (protege ante futuros cambios de esquema de claves).
- Response clarity: ingest 201 devuelve `domain` en lugar de `project_slug` mal etiquetado.
- Lexiel adapter (`packages/api/src/routes/email-inbound.ts`): body cap 33MB (`MAX_WEBHOOK_BODY_BYTES`) con pre-check Content-Length + post-read → 413 `PAYLOAD_TOO_LARGE`. Evita saturar memoria antes de verificar firma.
- +3 more
Añadido— 2026
- Nuevo servicio `@codelabs/mail-inbound` (repo separado `/Users/josediaz/Dev/code/mail-inbound`) para sustituir Resend Inbound con gateway multi-tenant propio (Mailcow → HTTP ingest → webhook HMAC). Motivación RGPD: los adjuntos son comunicaciones abogado-cliente, eliminar procesador US es ganancia legal directa.
- Schema multi-tenant: 6 tablas (`mi_projects`, `mi_domains`, `mi_messages`, `mi_attachments`, `mi_webhook_deliveries`, `mi_maintenance_runs`). Idempotency por `(project_id, external_message_id)`. Adjuntos content-addressed en R2 (`attachments/<sha256-prefix>/<hash>`) con dedup cross-message.
- Payload Resend-compatible (`type: 'email.received'`, `data.email_id`, `data.attachments[].download_url`, `data.authentication.{spf,dkim,dmarc}`), así los adaptadores existentes solo cambian la verificación de firma.
- Pipeline Lexiel (`packages/api/src/routes/email-inbound.ts`): soporta ambos proveedores vía `EMAIL_INBOUND_PROVIDER=resend|mail-inbound`. Con `mail-inbound` la firma es HMAC-SHA256 hex (`X-MailInbound-Signature`), el body/adjuntos viajan inline + presigned URL (sin segundo round-trip a Resend API). Preserva anonymizer + classifier intactos.
- Deploy target: Dokploy en Xeon (`inbound.codelabs.studio`). Docs completas: `docs/DNS.md`, `docs/DEPLOY.md`, `docs/mailcow-catchall.sh`, `client/node.ts` (verifier copy-paste para otros tenants).
- Tests: 23 unit tests (MIME parse incl. sanitización path-traversal, HMAC signature, rate-limit multi-IP).
Corrección— 2026
- email.ts: `require('../lib/crypto')` dinámico fallaba en bundle ESM (`ReferenceError`). Reemplazado por import estático — fix de trial-ending emails rotos en prod. Commit `e9874258`.
- cron observability: `trackCronRun` ahora dispara `Sentry.captureException` con tags `{ kind: 'cron', job }`. Antes los errores solo iban a logger. Commit `e9874258`.
- lex-voice webhook: Hume CLM payload migrado a Zod schema (`messages` 1..200). Commit `e9874258`.
- fire-and-forget backfills en `admin.ts` envueltos en try/catch con Sentry para evitar unhandledRejection silenciosos. Commit `3411c9c1`.
- admin feedback threads: reemplazado N+1 por `selectDistinctOn([threadId])` ordenado por `createdAt desc`. Commit `d2b7dd1f`.
- suggested-cache cron que fallaba cada 10 min: `sql\`${jsArray}::text[]\`` producía `($1,$2,...)::text[]` inválido. Fix con `sql.join` + `ARRAY[...]::text[]` literal. Commit `b44f24d1`.
- knowledge-agent `acquire.ts`: reject de content-types no text-like + strip de NUL/surrogates antes de insertar en Postgres (antes crasheaba con URLs binarias tipo JPEG). Commit `83832f52`.
- agenteuno-client: helper `snippet(body, 300)` para truncar HTML de errores Cloudflare 5xx en logs (antes se inundaba el log stream). Commit `f3360b1f`.
- +3 more
Añadido— 2026
- Partial HNSW indexes para PE/BR/PT (países <5% del corpus con p95 >800ms): nueva columna denormalizada `legal_source_embeddings.country` + trigger `lse_set_country_trg` (BEFORE INSERT) + trigger `ls_propagate_country_trg` (AFTER UPDATE OF country) para consistencia. 3 índices `idx_lse_hnsw_{pe,br,pt}` de 98-165 MB con predicado `WHERE country='<cc>'`.
- Denormalización del filtro country en la vector query (`packages/api/src/lib/rag.ts` + `packages/api/src/routes/jurisprudencia.ts`): WHERE por `lse.country` en vez de `ls.country` vía JOIN. Sin este predicado directo el planner no empareja con el WHERE de los índices parciales y cae al global.
- Backfill completo de 42,411 filas con `country=NULL` (rows insertadas antes del despliegue del trigger).
- pg_prewarm + pg_buffercache extensions instaladas. Script `packages/api/src/scripts/hnsw-prewarm.ts` para pre-cargar los 4 índices HNSW en shared_buffers tras restarts del Postgres.
- `work_mem` 32MB → 64MB via ALTER SYSTEM (no downtime). Impacto medido (samples=20):
- ES p95 224→57 ms (4×) · AR 290→71 ms (4×) · CO 583→55 ms (10×) · MX 199→67 ms (3×) · CL 178→64 ms (3×)
- PE p95 972→9 ms (108×) · BR 2301→6 ms (383×) · PT 913→7 ms (130×) Criterio para nuevo parcial: país con <5% del corpus **y** índice resultante <300 MB (cabe entero en shared_buffers). Por encima la mejora decae a 2-5×. ES no se parcializa: ~8 GB, no compensa. Commits: d83ea2dd · 340c8e72 · 147d7c2d · 9a790e90 · 919e56ca.
Corrección— 2026
- maintenance.ts: LIKE injection en `numeroProcedimiento` de LexNET (wildcards no escapados); N+1 org lookup en keycloak-retry → batch pre-fetch; `.limit(500)` en pioneer-engagement-nudge
- firm-sites.ts: `inviteToken` no se devuelve en respuesta HTTP (solo llega por email); `.limit(500)` en domain-map
- partner-crm.ts: validación del parámetro `slug` con regex en el GET endpoint (sin zValidator)
Corrección— 2026
- cases.ts `mutateCaseBrief`: UPDATE interno de la transacción solo filtraba por `caseId` sin `organizationId` (defensa en profundidad)
- marketplace.ts: escape de `q.specialty` con `replace(/[%_]/g, '\\$&')` faltaba; también se añadió cláusula `ESCAPE '\\'` al `ILIKE` de `q.q`
- portal.ts: validación token `max(512)` → `min(32)/max(128)` en los 5 endpoints; `orderBy(asc(users.id))` en lookup de admin para determinismo
- push.ts: cap de suscripciones por usuario subido de 20 → 50
- organizations.ts: `.limit(500)` añadido a `GET /insurance-policies` (query ilimitada)
- storage.ts: `assertSafeKey()` previene path traversal en modo local-dev (valida prefijo + ausencia de `..`)
- email.ts: 4 `logger.error` con `params.to` en claro reemplazados por `hashEmail(params.to)`
- meeting-notes.ts `processNoteBackground`: 5 `db.update(meetingNotes)` sin `organizationId` en WHERE → añadido en todos
- +4 more
Corrección— 2026
- citizen.ts: query historial LLM sin cap → OOM/token blowout en conversaciones largas. Fix: DESC LIMIT 40 + reverse (patrón de `conversations.ts`)
- citizen.ts: `GET /conversations/:id` devolvía todos los mensajes sin límite. Fix: LIMIT 200
- fiscal-view: botones de exportación visibles para `member`-role → 403 garantizado. Fix: `useAuth()` + `canExport = role in ['owner','admin']`, ocultar con `{canExport && ...}`
- email-shell.ts: `headerTitle` y `headerSubtitle` interpolados sin escapar en HTML. Fix: helper `esc()` local aplicado a ambos campos (previene inyección HTML con firm names que contengan `<>&"'`)
Corrección— 2026
- Bug fixes encontrados en scan:
- marketplace: `PATCH /my-profile` enviaba R2 key (`marketplace-photos/…`) en el campo `photoUrl`; Zod lo validaba como `.url()` y fallaba. Fix: solo incluir `photoUrl` en el PATCH si es una URL HTTP externa (el upload dedicado ya persistió la foto)
- start-client: `?plan=particular-plus` (guion) no coincidía con `VALID_PLANS` (underscore) → usuario llegaba al checkout de `starter`. Fix: normalizar el parámetro reemplazando guiones por underscores
- ciudadano page: links CTA usaban `plan=particular-plus` (guion). Normalizados a `plan=particular_plus` para consistencia
- appointments-client: toast calls con API de shadcn `{ title, variant }` en lugar de la API local `(string, type)`. Corregido 4 calls
- Cambios pendientes commiteados:
- academy: columna `leaderboard_pseudonym` (nombre público para leaderboard, sobrescribe guestName); fix GROUP BY usando `max()` para columnas no agrupadas; CHECK constraint `(user_id IS NOT NULL) <> (guest_email IS NOT NULL)` en `academy_user_progress`; migraciones 0090 y 0091
- firm-sites: agendador de consultas (appointments) — page + client con gestión de reglas de disponibilidad y estado de citas (página `/settings/firm-site/appointments`)
Añadido— 2026
- Marketplace - completado:
- Photo upload endpoint `POST /v1/marketplace/my-profile/photo`: multipart, magic bytes JPEG/PNG/WebP, max 5MB, R2 bajo `marketplace-photos/{userId}/{ts}.{ext}`, elimina foto antigua automáticamente
- Proxy público `GET /v1/marketplace/public/photo/:encodedKey`: sirve fotos del marketplace (base64url previene path traversal, 24h cache)
- my-profile-client: file picker con preview circular + Loader2 spinner (reemplazó input URL)
- resolvePhotoUrl() en API: todos los endpoints que devuelven `photoUrl` ahora resuelven R2 keys a URLs proxy (GET /search, /lawyers/:slug, /lawyers-by-id/:id, GET /intros); clientes no necesitan lógica de resolución
- Auth wildcard: `marketplaceRouter.use('/my-profile/*', auth)` — sub-paths no estaban protegidos
- Bug fixes críticos:
- Race condition en accept/reject: `UPDATE lawyerIntros WHERE status = ANY(['pending'])` idempotente — previene doble aceptación concurrente; retorna estado actual si ya transitado
- +7 more
Añadido— 2026
- Feature: Editor de perfil de directorio para abogados (`/my-profile`)
- Nueva ruta `apps/app/.../my-profile/` con page.tsx + client + error.tsx + loading.tsx
- `GET /v1/marketplace/my-profile` — devuelve el perfil reclamado del abogado autenticado
- `PATCH /v1/marketplace/my-profile` — actualiza firmName, bio, photo, contact, specialties, precio; Zod refine que valida priceMin <= priceMax
- Sidebar entry "Mi perfil en el directorio" en grupo Clientes (lawyerOnly, sectionEs: Directorio)
- i18n ES+EN en `packages/shared/src/i18n/dictionaries`
- Bug fixes (análisis integral)
- reveal idempotente: `WHERE AND status='accepted'` en UPDATE previene doble-reveal concurrente; retorna estado actual si ya revelado
- +7 more
Añadido— 2026
- Flujo marketplace ciudadano→abogado completamente funcional y desplegado
- Directorio público `/abogados-directorio`: filtros por especialidad (ILIKE partial match), provincia (30 provincias correctas), nombre/despacho; búsqueda SSR con revalidación 5 min; cards con badge de plan, precio formateado, rating
- Perfil público `/abogado/[slug]`: JSON-LD `LegalService` enriquecido (`knowsAbout`, `Review[]`, `telephone`, `sameAs`, `aggregateRating`); OG image desde foto del abogado o fallback; precio con caso especial "Sin honorarios si no hay resultado"; reseñas verificadas con fuente
- Nuevo-intro page (`/nuevo-intro?lawyer=ID&name=hint`): `PlanGate` que bloquea todos los planes excepto `particular_plus`; formulario con fetch de perfil del abogado vía nuevo endpoint `GET /v1/marketplace/lawyers-by-id/:id`; campos `aiSummary` (min 20), `initialMessage` (opcional), `anonymize` toggle; manejo de error `TOO_MANY_PENDING`; estado de éxito con CTA a citizen-inbox
- Review modal en citizen-inbox: lee `?review=ID` de URL, auto-selecciona y abre modal de reseña; star rating (1-5), título opcional, cuerpo reseña; `POST /v1/marketplace/reviews`; CTA "Dejar reseña" en panel de contactos revelados
- Notificaciones email+push completas: nuevo intro → email+push al abogado; aceptar/rechazar → email+push al ciudadano; reveal → email+push a ambas partes; cron 14 días post-reveal → email review request con deep-link `?review=ID`
- Cron `sync-lawyer-badges` (03:45 UTC diario): sincroniza `planBadge` en `lawyerProfiles` desde plan Stripe real del abogado via join `users→organizations`; bulk-update solo los perfiles con badge cambiado
- Cron `marketplace-review-requests` (09:00 UTC diario): detecta intros con `status=contacts_revealed` y `contactsRevealedAt` entre 14-15 días atrás sin reseña existente; envía email de solicitud de reseña con deep-link
- +10 more
Añadido— 2026
- 8 nuevas mejoras commiteadas + Sentry alerts vía API + actualización runbook
- Cron `/v1/cron/cleanup-anon-sessions`: SCAN+PTTL+DEL pattern `lexiel:freemium:anon:*` para reapar Redis keys expirados o cuyo TTL es <= 24h. Idempotente, max 500 iterations safety, target weekly Sun 03:00 UTC. Devuelve `{scanned, deleted, iterations}`.
- SEO FAQPage schema en 3 landings `/para/` (asesores-fiscales, administradores-fincas, graduados-sociales): 4 Q&A por landing × ES+EN, contenido específico por vertical (IRPF/IS/IVA + AEAT, LPH + comunidades, ET + TS Sala 4 + SS regimes). Mejora elegibilidad para Google rich results FAQ accordion.
- Onboarding celebration splash post-checkout: nuevo `SubscribedSplash` modal en `/citizen/chat`, detecta `?subscribed=1` from Stripe success_url, muestra 3 features unlocked (preguntas+documentos ilimitados+alertas BOE), CTA directo al wizard de monitorio. Auto-strip query param via `history.replaceState` para que reload no re-trigger. Bilingüe ES/EN.
- Email transaccional `sendCitizenProWelcomeEmail`: tras webhook Stripe `subscription.created/updated` con citizen+free→particular upgrade, REEMPLAZA el genérico `sendSubscriptionChangedEmail` con un HTML celebratorio. Incluye `emailSuccessBox` con 5 features, 3 quick links (chat/cuenta/documentos), CTA al wizard de monitorio. Cadence detectada vía `subscription.items[0].price.recurring.interval`.
- Email transaccional `sendCitizenProCancelEmail`: en `POST /v1/citizen/billing/cancel` tras `cancelSubscriptionAtPeriodEnd`, envía confirmación con `emailInfoBox` mostrando fecha exacta de fin (formateada `fmtDateWeekday`), CTA reactivar, soft churn-survey "responde a este correo". Non-blocking: el cancel succeeds aunque el email falle.
- A/B variant tracking server-side completo: - `POST /v1/public/freemium/ab-event` (rate-limited 30/min): incrementa Redis counters `lexiel:ab:paywall-inline:{event}:{variant}:{YYYY-MM-DD}` con TTL 90d - PaywallInline reporta impression once-per-mount (`useEffect+useRef` guard) + conversion al CTA click (con `keepalive: true`) - Counters server-side son inmunes a ad-blockers (Plausible misses ~30%) - `/v1/admin/citizen-funnel/metrics` extendido con `paywallAb {variant: {impressions, conversions, conversionRate}}` agregado a 7 días via `redis.mget` - `CitizenFunnelSection` nuevo card con 3 variants + barras + estrella ★ marker en winning variant
- Editor de documentos post-generación (el feature grande pendiente): - Nuevo `GET /v1/citizen/documents/:id` devuelve full doc + metadata estructurado - Nuevo `PATCH /v1/citizen/documents/:id` valida per-template (zod), regenera PDF via builders existentes, re-upload R2 (overwrites old key), persiste new metadata + contentMd, devuelve fresh presigned URL - Editing NO consume cuota mensual — citizen_free puede editar existing docs sin gastar 2/month - Sentry capture en regeneration failure con tag `surface=citizen-document-edit` - Nueva ruta `/citizen/documentos/[id]/editar` con `DocumentEditorClient` que pre-fills form via GET, save → PATCH → success toast → download refreshed - 3 specialized field components (MonitorioFields, ReclamacionFields, EscritoFields) con los mismos campos del wizard original - Bilingüe ES/EN - Documents list tiene nuevo botón "Editar" junto a "Descargar" - Killer feature que justifica citizen_pro: arregla typos, ajusta importes, cambia destinatarios sin re-correr el wizard Sentry alerts vía API directa (4 nuevas reglas creadas en `agenteuno/lexiel-api`):
- +8 more
Añadido— 2026
- Stripe, Cloudflare, Railway — accesos directos automatizados
- Stripe citizen_pro productos creados vía API (test + live mode) - Test: `prod_UJtulajr9RsLWG` + `price_1TLG6LLJL8tSbPQRQMIvQfhY` (99€/año) + `price_1TLG6NLJL8tSbPQR7HJeYEWQ` (12,90€/mes) - Live: `prod_UJtuCDAceoRFk9` + `price_1TLG72Lo0CCa9fhgEK9gIzat` (99€/año) + `price_1TLG72Lo0CCa9fhgIdv67dh1` (12,90€/mes) - Los 4 price IDs persistidos en `services.json` y añadidos a `.env` local - Pendiente en Railway: setear las mismas 4 env vars + `TURNSTILE_*` (ver runbook)
- Deploy runbook en `docs/runbooks/citizen-tier-deploy-runbook.md` con 20-step smoke test post-deploy, CLI Railway instructions, manual Cloudflare Turnstile steps, rollback plan
- Mejora continua — 5 commits adicionales
- Pagination en `/v1/citizen/conversations`: `?offset&limit` (max 100) + response con `{pagination: {offset, limit, total, hasMore}}`. Previene breakage cuando un usuario tiene >50 conversaciones.
- A/B test infrastructure del PaywallInline: 3 variantes de copy (`factual` | `emotional` | `scarcity`), asignación estable por navegador vía `localStorage.lexiel:ab:paywall-inline`, variant enviado al tracker en el `handleGoogleSignup`, variant persisted como `lexiel:ab:paywall-inline:converted` al clicar CTA para attribution post-signup, atributo `data-ab-variant` en el contenedor para debugging
- SEO rich snippets en `/precios/ciudadano`: SoftwareApplication JSON-LD enriquecido con `applicationSubCategory`, `featureList` (5 bullets), `provider` Organization con logo, 3 ofertas distintas (Free + Pro anual + Pro mensual) con `availability`, `category`, `priceSpecification` (P1Y / P1M). Mejora elegibilidad para Google rich results con carousel de Offer.
- Sentry en `/v1/auth/citizen-signup` error path: `captureException` con tags `{surface: 'citizen-signup', endpoint}` y extras `{locale, hasExternalToken}`. Cierra el gap crítico — antes solo `logger.error`, ahora alertable vía Sentry.
- +1 more
Añadido— 2026
- Tier intermedio ciudadano con conversión de funnel 3-tier
- Nuevo tier `citizen_free`: cuenta gratuita con Google OAuth (5 preguntas/mes, respuestas completas, historial persistente) — activado por la combinación `users.audienceProfile='citizen'` + `organizations.plan='free'`
- Vista previa anónima con corte limpio: `truncateForPreview()` hace buffer server-side del LLM response (~200-400 palabras, corta en párrafo/oración limpia) y emite preview word-by-word en SSE — el contenido oculto NUNCA llega al cliente. Aplicado tanto al live LLM como al canonical answer cache (fix de leak detectado durante implementación)
- Identidad anónima compuesta (signalA): `computeAnonSignal()` genera hash SHA256 de `IP + User-Agent + Accept-Language + canvas-fingerprint + fonts-fingerprint`, counter en Redis `lexiel:freemium:anon:{signal}` con TTL 180d. Cliente envía header `x-anon-signal`; legacy IP-counter (24h) preserved como fallback durante rollout
- Componentes web nuevos: `PaywallInline` (corte limpio + Google CTA, bilingüe ES/EN, sin blur — honest paywall) y `CitationPartial` (referencia visible, texto oculto con lock icon)
- Integración en chat anónimo: `/es/consulta-legal-gratis` computa canvas+fonts fingerprint, llama `/v1/freemium/identify` en mount, persiste mensajes en `sessionStorage` (restore en reload), muestra PaywallInline + CitationPartials bajo la última respuesta, guarda conversación en `localStorage.lexiel:pending_migrate` al clicar Google CTA para migrar tras signup
- Endpoints nuevos: - `POST /v1/freemium/identify` — registra device anónimo - `POST /v1/citizen/chat` — SSE chat citizen con quota mensual por tier - `GET /v1/citizen/conversations` / `/conversations/:id` / `DELETE` — listado y archive - `GET /v1/citizen/usage` — consumo mensual del tier - `POST /v1/citizen/import-anon` — migración de historial anónimo a cuenta registrada - `POST /v1/auth/citizen-signup` — verifica Keycloak JWT, crea org personal + user idempotentemente, setea cookie con claim `audienceProfile: 'citizen'`
- Layout `(citizen)` en apps/app: `apps/app/app/[locale]/(citizen)/` grupo con `requireCitizenAuth` helper, `CitizenSidebar` con usage indicator en tiempo real, chat page + client que consume SSE, onboarding `/onboarding/citizen` con migración automática via `/v1/citizen/import-anon`
- +7 more
Añadido— 2026
- ESLint `no-raw-pathname-compare` - `eslint.config.mjs`: custom `no-restricted-syntax` rules that ban `pathname.includes/startsWith/endsWith('/cases')`, `pathname === '/cases'` etc. across ~45 EN slugs, preventing reintroduction of the silent i18n matcher bug class
- `useDashboardNavigate` hook - `use-dashboard-navigate.ts`: `router.push`/`replace` wrapper that takes a canonical EN path and localizes through `dashboardPath` before navigating (eliminates 302 redirect hops for ES users)
- Particular plan full LAWYER_ONLY_SLUGS coverage - `particular-plan.spec.ts`: table-driven `test.describe` block iterates over the full `LAWYER_ONLY_SLUGS` set so any new slug added to the registry is automatically covered by redirect tests
- AgenteUno widget production guard - `lib/env.ts`: `isAgenteUnoConfigured()` helper replaces hardcoded placeholder prefix check; `layout.tsx` reports a Sentry warning in production when widget is skipped due to missing/placeholder `NEXT_PUBLIC_AGENTEUNO_AGENT_ID`
- `.env.local.example` documents AgenteUno vars - explains placeholder sentinel behavior
Cambio
- 10 files migrated to `useDashboardNavigate` - `plan-gate`, `case-chat-view`, `particular-dashboard`, `case-creation-wizard`, `conflict-checker-modal`, `case-summary-view`, `case-close-modal`, `meeting-detail-view`, `time-tracking-view`, `case-expenses-view`, `documents-view` (eliminates 15+ raw EN `router.push` call sites that caused 302 hops)
Audit
- Dead route audit - `vistas-judiciales`, `portal-messages`, `document-requests`, `declarations`, `writing`: all 5 are real implementations (300-1000 line view components), not stubs. No dead code to remove.
Añadido— 2026
- normalizePathname + useNormalizedPath hook - `use-normalized-path.ts`: single utility for stripping locale prefix and translating ES slugs to EN, replacing 4 duplicated patterns across layout/chatbot/command-palette/demo
- LAWYER_ONLY_SLUGS centralized registry - `route-access.ts`: single source of truth for citizen-blocked routes, replacing 5 independent hardcoded lists (layout guard, command-palette, global-search, chatbot-context, sidebar)
- Sentry silent-fail detectors - `layout.tsx`: widget script load failure, widget not initialized after 10s, zero suggestions on known route
- Partner validation script - `scripts/validate-partner-ready.ts`: 8-point checklist (user/org/partner/missions) with exit code, reusable for all pioneer partners
- Dual-locale E2E smoke test - `dual-locale-smoke.spec.ts`: 20 route loads (10 routes x ES+EN), command palette navigation, chatbot launcher check
Corrección
- Command palette 404 for ES users - `command-palette.tsx`: `router.push` now localizes paths via `dashboardPath` instead of pushing raw EN slugs
- Command palette recent pages never tracked - `command-palette.tsx`: `trackPage` normalized ES pathname to match EN ACTIONS entries
- Demo section tracking broken for ES - `demo-profile-switcher.tsx`: normalized localized pathname before matching DEMO_SECTIONS
- Widget injection with placeholder agent ID - `layout.tsx`: skip AgenteUno widget when AGENTEUNO_AGENT_ID is the dev placeholder UUID
Añadido— 2026
- AgenteUno idle detection - `analytics.ts`: 45s inactivity trigger shows proactive chatbot message (500ms throttle, pointer/click/scroll/keypress listeners)
- AgenteUno systemContext - `socket.ts`: new config field passes role/nav context to WS handshake, backend injects as system prompt (max 2000 chars)
- AgenteUno suggestion tracking - `state.ts`: `suggestion_shown`, `suggestion_clicked`, `suggestion_dismissed` events via existing `/widget/analytics` endpoint
- Role-aware chatbot context - `chatbot-context.ts`: `buildSystemContext()` generates dynamic system prompt with user role, plan, current page, navigation map, tone instructions (ES+EN). Rewritten with 40 translated routes (was 15 hardcoded EN slugs), imports `dashboardRoutes` from shared, citizen filter 10 routes (707 chars), lawyer 40 routes (1918 chars within 2000 limit)
- Role-aware suggestions - `layout.tsx`: `getSuggestionsByPage()` returns different suggestions for lawyers (10) vs citizens (5), with contextual deep links
- Dock mode + idle config - `layout.tsx`: `dockMode: false, dockSide: 'right', dockWidth: 400, idleTimeout: 45` in AgenteUno widget config
- Playwright E2E test - `partner-onboarding-e2e.spec.ts`: full partner QA flow (dashboard, sidebar nav, chatbot, missions, feedback, RRWeb, screenshots)
Corrección
- Feedback tab hidden for testers - `layout.tsx`: changed `hideTabButton` from always-true to conditional on `isBetaTester || isFeedbackTester`
- Subscription hidden for particular users - `app-sidebar.tsx`: removed `particularHidden: true` from subscription nav item (page fully handles particular plan)
- Javier Toro data gaps - DB: set `bar_association = 'ICAM (Malaga)'`, `pioneer_access_until = 2027-04-10`, `firm_wizard_completed_at = now()`
Corrección
- Atomic isFavorite toggle - `conversations.ts`: replaced read-then-flip with single `UPDATE SET NOT is_favorite RETURNING` (eliminates TOCTOU + saves 1 DB round-trip)
- Missing .catch() on fire-and-forget calls - `chat-client.tsx` (2x conversation auto-create), `testimony-prep-view.tsx` (guide refetch): unhandled rejections on network failure
Seguridad
- Gemini API key in URL - `health.ts`: moved from query string `?key=` to `x-goog-api-key` header (prevents key leak in access/CDN logs)
- Response body drain - 9 additional fetch error paths now drain body before throw/return: `gemini.ts`, `rag.ts`, `corpus-auto-updater.ts`, `cendoj.ts`, `admin-prospecting`, `admin-partner-testing`, `feedback-tester`, `partner-missions`, `freemium`, `pitch`
- Dead code removal - removed unused `HTTP_CODES` constant from `errors.ts`, dead `BOETextBlock` type from `boe.ts`
Performance
- Case watchdog batching - `automation.ts`: processes active cases in batches of 500 (was loading all + caseBrief JSONB unbounded)
- BOE verify cache cap - `boe.ts`: capped at 5,000 entries with FIFO eviction (was growing unbounded)
- WhatsApp rate-limit eviction - `demo.ts`: hourly cleanup of expired entries (was growing unbounded)
Corrección— 2026
- ILIKE wildcard injection (6 endpoints) - `invoices.ts` (3 search endpoints), `search.ts` (global search), `meetings.ts`, `quotes.ts`, `jurisprudencia.ts` (ponente filter), `conversations.ts`: unescaped `%` and `_` in ILIKE patterns allowed wildcard DoS forcing full table scans. All now use `escapeLike()`
- Facturae error information leak - `invoices.ts`: raw `err.message` from XML generation was returned to client, potentially exposing internal paths and library errors. Now returns generic message + server-side log
- Escritos lock/unlock TOCTOU race - `escritos.ts`: lock and unlock endpoints used check-then-act pattern (SELECT then UPDATE) vulnerable to concurrent requests. Replaced with atomic `UPDATE ... WHERE ... RETURNING` pattern
- Academy progress upsert race - `academy.ts`: concurrent progress submissions could trigger duplicate INSERT. Replaced with `INSERT ... ON CONFLICT DO UPDATE`
- Rate cards isDefault race - `rate-cards.ts`: two concurrent PATCH requests setting `isDefault: true` could leave two defaults. Wrapped in transaction
Performance
- Partial indexes on soft-delete tables - Migration 0063: `api_keys`, `contract_clauses`, `feedback_tester_entries`, `organizations` now have `WHERE deleted_at IS NULL` partial indexes; reconciles pending `case_activities.organization_id` + `case_timeline_events` partial indexes
- Response body drain (5 sites) - `signatures.ts` (2x S3 fetch), `automation.ts` (Google Calendar), `notifications.ts` (BOE RSS + digest): undrained response bodies on `!res.ok` caused TCP connection pool exhaustion
- Safety limits (4 queries) - `time-tracking.ts` (case lookup 5K), `escritos.ts` (placeholders 500x2), `analytics.ts` (risk dashboard 5K)
- Thundering-herd batching (3 endpoints) - `document-requests.ts` (bulk-remind capped 200 + batches of 10), `admin.ts` (presigned URLs batches of 20), `invoices.ts` (bulk-generate time-entry UPDATEs serialized inside transaction)
Corrección— 2026
- PII in keycloak logs - `keycloak.ts`: 6 logger.error calls leaked raw email; replaced with `hashEmail()` for GDPR compliance
- Gemini response body leak - `ai.ts`: streaming error path discarded response body, preventing HTTP connection reuse; now drains up to 200 chars for diagnostics
- Cross-org query gaps round 1 (7 queries) - `case-ai.ts` (deadlines, documents, caseExpenses), `cases.ts` (doc/conv/deadline counts), `invoices.ts` (invoice ownership check on /:id/cases)
- Cross-org query gaps round 2 (15 queries) - `cases.ts` (11: case-detail, report-gen, health-score, summary-gen, deadline-list, documentChunks), `case-pdf.ts` (3 deadline queries in PDF exports), `workflows.ts` (caseEscritos in workflow status)
- Cross-org query gaps round 3 (8 queries) - `conversations.ts` (4 message queries: delete, chat history, export, report), `escritos.ts` (documentChunks in Vista Contraria)
- Cross-org invoice bulk export - `invoices.ts`: billingProfiles and rectified invoices lookups in /export/pdf-zip lacked orgId filter
Corrección
- Timezone bug in deadline detection - `conversations.ts`: `detectPrescriptionDeadline` used `new Date()` (server UTC) causing off-by-one day for LATAM users. Now reads org timezone from DB (zero extra queries: piggybacks on existing org SELECT)
- Soft-delete leaks (8 queries) - `invoices.ts` (3 client lookups without `isNull(deletedAt)`), `case-timeline.ts` (caseEscritos), `export.ts` (caseEscritos), `profile.ts` (caseEscritos count), `cases.ts` (documents in clone), `criminal-records.ts` (clients batch + migrated from raw sql.join to inArray), `testimony-prep.ts` (cases)
Cambio
- Email constants centralized - `email.ts`: exported `EMAIL_FROM`, `EMAIL_REPLY_TO`, `EMAIL_SUPPORT` as single source of truth; replaced 10+ hardcoded sender addresses across `notifications.ts`, `billing.ts`, `partner-emails.ts`, `orchestrator.ts`
Performance
- Push subscription limits - `push.ts`: sendPushToUser capped at 20, sendPushToOrg at 500
- Conversation ownership .limit(1) - `conversations.ts`: chat message handler ownership check now includes .limit(1)
- Search parallelization - `conversations.ts`: title ILIKE + message FTS queries now run in Promise.all() (latency ~halved)
Corrección— 2026
- IDOR session-replay - `session-replay.ts`: batch UPDATE lacked `userId` filter; any authenticated user could inflate another user's session counters by sending events with a known sessionId
- Cross-org saved searches - `search.ts`: DELETE and PATCH `/saved/:id` lacked `organizationId` filter (cross-org delete/update possible if UUID guessed)
- Cross-org writing drafts - `writing-drafts.ts`: restore, delete, and 4 other queries lacked `organizationId` filter (defense-in-depth)
- OAuth fail-open - `calendar.ts`: Google/Outlook OAuth secrets defaulted to empty string; connect/callback endpoints now return 503 when secrets are not configured
- AI fetch timeouts - `ai.ts`: 3 fetch calls (geminiNonStreaming, Gemini fallback, OpenRouter) lacked AbortSignal.timeout; could block worker indefinitely on hung connections
- Stripe metering silent drop - `usage.ts`: missing API key silently returned without logging; now emits `logger.warn` for observability
Añadido
- Idempotency-Key on invoices/quotes - `invoices.ts`, `quotes.ts`: Redis-based idempotency header support (5min TTL) prevents duplicate invoice/quote creation from double-submit
- Bulk signature parallelization - `signatures.ts`: sequential HTTP calls replaced with `Promise.allSettled` batches of 5 (latency reduced ~5x for large recipient lists)
Corrección
- Timer cache invalidation - `time-tracking.ts`: timer start/stop were missing `invalidateOrgAnalytics()` (analytics stale after timer usage)
- Unbounded portal quotes - `portal.ts`: quotes query had no `.limit()` or `.orderBy()`; added limit(100) + orderBy
- Unbounded recurring invoices - `invoices.ts`: recurring invoices list had no limit; added limit(200)
- Query param validation - `analytics.ts` sort, `partners.ts` commission status, `workflows.ts` role: all validated against allowlists (were raw casts)
- Admin rate limits - 7 admin route files now have router-level rate limits (10-60/min depending on cost)
Performance
- Deadline reminders N+1 elimination - `deadline-reminders.ts`: batch user+preferences queries with `inArray` before loop (was 2N queries for N users), bulk INSERT notification logs (was N inserts per user)
- Narrow SELECT * on hot paths - `conversations.ts`: case lookup fetches 4 cols instead of all; `case-ai.ts`: billingProfiles 6 cols, rateCards 2 cols (avoids transferring large JSONB/TEXT blobs)
Añadido— 2026
- FOR UPDATE locks on 15 JSONB patterns - `cases.ts`, `case-pruebas.ts`: `mutateCaseBrief()` transactional helper prevents lost-update race conditions on `_quickNotes`, `_tasks`, `_settlement`, `_pruebas`, `_aiSummary`, `_outcome`, `_incidentDate`, `_solvencyCheck`
- inArray caps - `invoices.ts` (.slice 1000), `calendar.ts` (.slice 1000/500), `analytics.ts` (.limit 500), `escritos.ts` (eliminated round-trip with direct UPDATE)
- Email inbound hardening - 20 attachment cap, 50MB total limit, upload retry with backoff (500ms/2s/5s), 30s AI classification timeout, PII removed from logs
- Migration 0062 - Partial indexes on documents/cases WHERE deleted_at IS NULL + ANALYZE case_activities
- Export limit fix - Missing `.limit()` on users query in export.ts
- Timeline cache invalidation - 20+ invalidation calls across 9 files (case-finances, deadlines, meetings, escritos, signatures, email-inbound, case-emails)
- Clone rate limit - Dedicated `cases-clone` bucket (10/min) instead of shared `cases-write` (60/min)
- Idempotency on clone - Redis-based `Idempotency-Key` header support
Añadido— 2026
- API client retry/backoff - `apps/app/lib/api.ts`: nuevo `fetchWithRetry()` con backoff exponencial (300ms -> 900ms -> 2700ms, 3 intentos). Reintenta 500/502/503/504 y errores de red; respeta `Retry-After`; no reintenta 4xx/AbortError. Aplicado a get/post/patch/put/delete. postBlob/postFormData/postSSE sin cambios (cuerpos grandes y streams requieren otra semantica).
- Rate-limit Redis circuit breaker - `middleware/rate-limit.ts`: estado de breaker por proceso que cuenta fallbacks consecutivos. Tras 10 fallos emite `logger.error` con impacto ("rate limits per-instance, distributed abuse possible") y throttle de 60s. Emite `logger.info` cuando Redis se recupera. Antes: un `logger.warn` por request durante un outage (ruido sin alerta real).
Performance
- Parallel LLM extraction - `routes/organizations.ts:217`: bulk insurance extraction ahora usa `Promise.allSettled()` para paralelizar las N llamadas a `extractInsuranceFromDoc()` (antes eran secuenciales). Para MAX_EXTRACT_DOCS=20, drop de ~30s a ~3s.
- Redis MGET batch - `cron/quality.ts:230`: dashboard de calidad ahora hace un unico `MGET` para los ~22 paises en vez de N `GET` secuenciales (~100ms -> ~5ms).
Corrección
- SSE debug silent catches - `routes/conversations.ts`: 6 `.catch(() => {})` en `sse.write()` de eventos debug reemplazados por `logger.debug(...)` con contexto de fase (rag/llm/citations/total). Los bloques estan dentro de `IS_DEV` asi que sin ruido en prod, pero ahora hay audit trail si el debug stream falla en dev.
Añadido
- GIN trigram index on caseBrief procedimiento - migration `0057_case_brief_procedure_trgm`: `CREATE INDEX idx_cases_brief_procedure_trgm ON cases USING gin ((case_brief->>'numeroProcedimiento') gin_trgm_ops)`. Elimina el seq scan en el cron LexNET (`cron/maintenance.ts:200`) que ejecuta ILIKE %...% por notificacion. pg_trgm ya estaba habilitado.
- Shared time constants - `lib/time.ts`: nuevo modulo con `SECOND_MS/MINUTE_MS/HOUR_MS/DAY_MS/WEEK_MS` + versiones `_S` + presets comunes (`TTL_24H_MS`, `TTL_7D_MS`, `TTL_30D_MS`). Aplicado en case-ai.ts (trial prep cache, 28 dias), demo.ts (query dedup, rate limits), client-portal.ts (token default 30d). El resto puede migrar gradualmente.
- Bulk payment links con semaphore - `routes/invoices.ts:3013`: el for..await secuencial reemplazado por chunks de 5 en paralelo via `Promise.allSettled`. Para batch de 50 facturas el tiempo baja de ~75s a ~15s respetando el Stripe rate limit (100 req/s, 95 de headroom). Fix drive-by: 2 emdashes en descripciones Stripe reemplazadas por guiones.
Añadido— 2026
- Generic multi-channel notification sender - `notification-sender.ts`: refactored from deadline-only to 6 notification types (deadline, pioneer_engagement, trial_nurture, invoice_overdue, case_update, case_followup). Each type has WhatsApp, SMS, push, and email message builders. Cascade respects user channelPriority and quiet hours.
- Pioneer engagement now multi-channel - `cron/maintenance.ts`: engagement nudge uses sendNotification() with full WhatsApp/SMS/push/email cascade (was email-only). Fetches user notification preferences and phone.
- useT() i18n hook - `apps/app/lib/i18n.ts`: infrastructure for migrating 2,600+ inline ternaries to dictionary lookups. ~50 initial keys (common, trialBanner, referralCard, demoBanner, planGate, push, kyc). New components should use useT() going forward.
- Error boundaries 100% coverage - Added error.tsx to 2 remaining admin pages (ab-llm, suggested-cache). All dashboard routes now have Sentry-reporting error boundaries.
- 15+ env vars documented in `.env.example`: Gemini key rotation, RAG tuning, A/B LLM, Alma sync, Hub API, HMAC secrets
Corrección— 2026
- KYC case-sensitivity bypass - `kyc.ts:83-89`: anti-abuse check ahora usa `lower()` para comparar barAssociation y licenseNumber, previniendo bypass via variantes de casing (ej. "CGAE" vs "cgae")
- Task-escalation cron OOM risk - `cron/automation.ts:409-456`: query cargaba TODOS los casos activos en memoria. Ahora procesa en batches de 500 con LIMIT/OFFSET
- Broken demo-data imports - 4 archivos en `scripts/demo-data/` importaban `./basico` (con tilde) pero el archivo se llama `basico.ts` (sin tilde). Funcionaba en macOS (case-insensitive) pero fallaba en Linux/Docker/Railway
- A11y: aria-labels - 5 botones/inputs en web marketing sin atributos de accesibilidad (close buttons, email input, URL input, textarea anonimizacion)
Añadido— 2026
- `privateSwr()` cache helper - `lib/cache.ts`: private stale-while-revalidate para endpoints autenticados con datos que cambian poco (analytics)
- Grafana alert definitions - `docs/grafana/onboarding-funnel-alerts.json`: 4 alertas (no events, conversion <30%, invariant, TTFV >5min) + 4 paneles dashboard
Corrección
- requireAdmin middleware - `strategy.ts`: 15 checks inline `isAdmin(email)` consolidados en 1 `use('*')` middleware (−27 LOC)
- Transaction wrapping - `portal.ts` (item upload + status check), `admin-prospecting.ts` (prospect update + interaction), `cron/maintenance.ts` (VeriFactu invoice + failure resolution)
- Calendar upsert race condition - `calendar.ts`: SELECT+INSERT/UPDATE reemplazado por `onConflictDoUpdate()` atomico via unique index `(userId, provider)`
- Silent `.catch(() => {})` audit - 6 catches silenciosos reemplazados por `logger.warn/error` en: admin-pioneers (Keycloak orphan), automation (rule log), invoices (Redis lock), academy (webhook), cron/maintenance (notification log)
Añadido
- E2E onboarding funnel test - `apps/app/tests/onboarding-funnel.spec.ts`: 5 tests (started auto-emit, dedup, elapsedMs, sessionStorage state, welcome_viewed ordering)
- `publicCache()` sMaxAge parameter - `lib/cache.ts`: optional 3rd param for asymmetric browser/CDN TTLs. Applied in suggestions.ts (60s browser, 300s CDN)
- React key fix - `clients-view.tsx`: BORME results key={i} reemplazado por key={r.nif} (estable)
Performance
- Safety `.limit()` en queries - academy.ts (8 queries), auth.ts (4 lookups), document-requests.ts (batch items)
- Error normalization - newsletter.ts: `{ ok: false, error }` migrado a `apiError()` format
- `privateSwr` aplicado - analytics.ts: inline Cache-Control reemplazado por helper
Añadido— 2026
- ReferralCard on Pioneer dashboard - Pioneers ven su codigo de referido en el dashboard principal
- E2E Playwright tests - Pioneer registration flow (6 tests: loading, missing token, invalid token, EN locale, noindex, title)
- AEPD/ENS/ISO certification roadmap - `docs/strategy/aepd-ens-roadmap.md` con analisis de 8 certificaciones, prioridad por segmento, estimaciones de coste
Corrección
- Emdash sweep - 190 instancias de `'-'` (emdash) reemplazadas por `'-'` (hyphen) en 74 archivos (app + web + API + format utils). Cumplimiento de convencion del proyecto.
- Pioneer redirectUrl mismatch - API retornaba `/dashboard?welcome=pionero`, cliente redirigía a `/partner-welcome`. Alineados.
- Hardcoded domain en T&C - Link de terminos usaba `https://lexiel.ai` en vez de `WEB_URL` env var, rompiendo staging/dev.
- Unsafe type cast en admin pioneers - Query param `profile` era `z.string()` con cast inseguro. Ahora validado contra enum `pioneer_profile`.
- Strategy admin middleware - Consolidado guard admin de N rutas individuales a 1 middleware global en `strategy.ts`.
Añadido— 04
- POST /v1/onboarding/event — nuevo endpoint autenticado con zod schema de 11 pasos (`started`, `welcome_viewed`, `plan_selected`, `firm_wizard_opened`, `firm_wizard_completed`, `first_chat_opened`, `first_case_created`, `first_invoice_created`, `checklist_dismissed`, `completed`, `abandoned`). Rate-limit 60/min por usuario. Emite `onboarding_funnel` structured log para que Loki/Grafana pinte drop-off por paso sin tabla dedicada.
- `apps/app/lib/onboarding-funnel.ts` — helper `trackOnboardingStep()` con dedup por sesión (sessionStorage), emisión automática de `started` en la primera llamada (denominador garantizado), `elapsedMs` calculado desde el inicio de sesión, fire-and-forget (nunca rompe la UX).
- Call sites wired: `welcome_viewed` + `firm_wizard_opened` (OnboardingOverlay mount), `firm_wizard_completed` + `completed` (layout onboarding callback), `first_chat_opened` (ChatClient mount), `first_case_created` (case-creation-wizard post-create), `first_invoice_created` (invoices-view post-create).
Performance
- GET /v1/onboarding/status — rate-limit + browser cache: añadido `rateLimit('onboarding-status', 60/min, keyBy: user)` y `Cache-Control: private, max-age=300`. El checklist polleaba en cada mount del dashboard; con esto se elimina el round-trip a DB durante 5 min por navegación.
Refactored— 04
- HTTP Cache-Control helpers — `privateCache()`, `publicCache()`, `swrCache()`, `noCache()` en `lib/cache.ts`. Structural typing via `HonoCtxLike` para desacoplar de Hono. Migrados 12 endpoints de strings hardcodeados a helpers (index, admin, docs, demo-rapido, kyc, suggestions, public-benchmark, onboarding).
- Error response standardization — 25+ endpoints migrados de `{ error: "string" }` a `apiError(code, message)` en settings-email-templates, time-tracking, newsletter, boe, kyc, team, auth, borme, analytics, email-templates. Todos los endpoints user-facing retornan ahora `{ error: { code, message } }` consistente.
- Onboarding funnel runbook — `docs/ONBOARDING_FUNNEL_RUNBOOK.md` con LogQL queries, alerts, troubleshooting, architecture diagram.
Corrección— 04
- Error response standardization ronda final — 46 endpoints internos (strategy, webhooks, demo, email-inbound, public-storage, admin-suggestions, document-requests, cron/notifications) migrados a `apiError()`. 0 error responses inconsistentes en toda la API.
- plan_selected funnel step — wired en `start-client.tsx` handleSubmit con metadata de interval y corpus.
- Grafana alert definitions — `docs/grafana/onboarding-funnel-alerts.json` con 4 alertas (no events, conversion drop, invariant violation, TTFV regression) y 4 panel definitions.
- Transaction wrapping en cases.ts — PATCH status (UPDATE cases + INSERT caseActivities) y case conversation creation (INSERT conversations + INSERT caseActivities) ahora atómicos via `db.transaction()`.
Corrección— 04
- SSE writer: abort LLM on client disconnect — `createSseWriter` tragaba todos los errores de escritura para mantener la cadena interna, lo que impedía a los callers detectar que el stream estaba muerto. Si un usuario cerraba el navegador a mitad de respuesta, el servidor seguía pagando tokens LLM que nadie recibía. Fix: flag `errored` en SseWriter + `result.abort?.()` en 17 rutas SSE.
- A/B LLM testing system — Gemini 3.1 Flash Lite vs Claude Sonnet 4.6. Asignación estable por SHA-256, circuit breaker (3 fallos → 60s cooldown), cron digest diario, panel admin con weighted averages y alertas.
- SSE writer migration — 21 rutas migradas de `startHeartbeat()` a `createSseWriter()` con serialización de writes via Promise chain.
- Embedding cache observability — contadores L1/L2/inflight/miss + `getEmbeddingCacheStats()` en /health/deep.
- Dynamic ef_search — RAG ajusta `hnsw.ef_search` por longitud de query (40/100/150).
Corrección— 04
- Theme: 211 archivos con tokens shadcn/ui fantasma — Tailwind v4 ignora silenciosamente tokens sin CSS variable definida. Reemplazados: `text-foreground`→`text-dark`, `text-destructive`→`text-red-600 dark:text-red-400`, `bg-primary text-primary-foreground`→`bg-brand text-white`, `text-primary`→`text-dark`, `text-secondary`→`text-muted`, `bg-surface-elevated`→`bg-elevated`. 202 error boundaries + 9 componentes + 2 páginas (error.tsx, not-found.tsx).
- Route guards: particular plan desincronizado — 5 rutas lawyer-only (`devils-advocate`, `team`, `calendar`, `reports`, `portal-messages`) presentes en API pero faltaban en frontend layout redirect y command palette. Un particular navegando directamente veía una página rota en vez de un redirect limpio.
- HNSW migration refs — `indexer.ts` y `rag.ts` actualizados de IVFFlat a HNSW (`ef_search=100`, `iterative_scan=relaxed_order`). Redis keys namespaced a `lexiel:` prefix.
- Lint: 0 warnings, 0 errors — Removed unused eslint-disable, moved constants to module scope in contract-analysis.
- A11y: `type="button"` en 22 archivos con forms — ~180 botones dentro de archivos que contienen `<form>` sin `type=` explícito. Sin ello, el default es `type="submit"`, causando submits accidentales al hacer click en botones de filtro, modales, etc.
Corrección— 04
- `public-sign.ts` firma atómica: update `documents.signedAt` + update `documentSigningRequests.signedAt` ahora en `db.transaction()`. Sin transacción, un fallo parcial dejaba el documento con firma pero sin registro en la solicitud.
- `quotes.ts` tenant isolation: re-fetch de quote+factura en el path de concurrencia ("already claimed") estaba sin scope de `organizationId`. Un user de org-A podía leer datos de factura de org-B usando un UUID ajeno.
- `quotes.ts` client fetch: fetch del email del cliente para la notificación sin scope de `organizationId` ni `deletedAt`. Añadido ambos filtros.
- `cron/maintenance.ts` N+1: loop de re-verificación KYC ejecutaba un UPDATE individual por usuario. Reemplazado por `UPDATE WHERE id IN (...)` — hasta 49 round-trips menos por cron run.
Corrección— 04
- `escritos.ts` 6 operaciones: create (escrito + version + log), patch (version + escrito + log condicional), rollback (version + escrito + log), ai-workflow (escrito + version + log), placeholder-fill (version + escrito), ai-refine (version + escrito) — todas envueltas en `db.transaction()`
- `strategy.ts`: insert `initiativeUpdates` + bump `updatedAt` en `strategicInitiatives` — ahora atómico
- `admin.ts` seed-course: insert curso + N módulos + M preguntas (bucle anidado) — ahora atómico. Sin transacción, un fallo de módulo/pregunta dejaba el curso en estado parcial irrecuperable
- `documents.ts` upload-new-version: insert `documentVersions` + update `documents.storageKey` — ahora atómico (upload S3 fuera de la transacción, operaciones DB atómicas)
- `compliance.ts` custom-control: insert `complianceRequirements` + insert `complianceChecks` — ahora atómico
- `quotes.ts` convert-to-invoice (2 paths): incremento de `billingProfiles.nextInvoiceNumber` + insert `invoices` + update `quotes.convertedToInvoiceId` — ahora atómico. Critical: sin transacción, un fallo entre el incremento del contador y la creación de la factura dejaba gaps permanentes en la numeración (problema fiscal AEAT)
Corrección— 04
- Data integrity: transacciones DB en 7 operaciones multi-paso — sin transacción, un fallo parcial dejaba filas huérfanas: - `invoices.ts`: convert-proforma (factura + items), bulk-generate (facturas + items + time entries), duplicate (factura + items) - `cases.ts`: demo-seed (caso + actividades + plazos), case creation (caso + activity log), reassignment (assignedTo + log), archival (status + log), deadline extraction (plazos + logs)
- Cron: deduplicación push socios inactivos (`partner-health.ts`) — cron semanal enviaba push repetidas sin chequear `notificationLogs`. Añadida deduplicación por semana ISO con keys `partner-inactive-{30d|60d|90d}-{userId}-{weekLabel}`.
- Cron: X-Cron-Secret header (`_shared.ts`) — Railway envía el secret vía `X-Cron-Secret` pero `checkSecret` solo leía `Authorization`. Todos los crons de Railway fallaban silenciosamente con 401. Fix: `X-Cron-Secret` primero, fallback a `Authorization: Bearer`.
- Command palette: acciones lawyer-only visibles a particular — fallback sin query usaba `ACTIONS` sin filtrar. Fix: `visibleActions` en el fallback.
- Feat: HNSW migration scripts + academia CTA particular — 4 SQL scripts para migrar a HNSW (m=16, ef_c=128). CTA Academia en onboarding particular. `useMemo` en command-palette para prevenir recálculos.
Corrección— 04
- Redis TTL en freemium refund paths: ambos paths de "reembolso" de crédito (respuesta vacía y error de streaming) usaban `redis.set(key, 0)` sin TTL. Si la clave no existía, se creaba permanente y el usuario quedaba potencialmente bloqueado para siempre. Fix: `redis.set(key, '0', 'EX', WINDOW_SECS)`.
- Rate limit en `demo.ts` `/query`: endpoint público que invoca RAG+LLM sin protección → 20 req/min por IP.
- Rate limits en `signatures.ts`: `/:id/remind` (5/min, llama a Signaturit API) y `/:id/cancel` (10/min) sin protección.
- Rate limit en `onboarding.ts` `POST /complete`: endpoint autenticado sin rate limit → 10/min por usuario.
- Rate limit global en `strategy.ts`: router admin con 12 endpoints de mutación sin límite → 60/min global.
- `a11y`: type="button" en 50+ botones interactivos: usando perl mass-replace se añadió `type="button"` a todos los `<button onClick=` y `<button key=` sin tipo explícito en `apps/app` y `apps/web`. Previene submit accidental de formularios cuando el botón está dentro de un `<form>`. 0 errores TS tras el fix.
- `a11y`: type="button" en 6 botones adicionales: `kyc-required-modal.tsx` (X de cierre), `page-intro.tsx` (X de cierre), `benchmark-launcher.tsx` (Cancelar), `prescripcion-calculator.tsx` (categorías), `procedimientos-calculator.tsx` (3 back-nav), `admin/heroes/page.tsx` (X de crop).
- Consistency: `cases.ts` raw `c.req.param('id')` en impact-alerts dismiss reemplazado por `c.req.valid('param').id` (ya validado por zValidator).
Corrección— 04
- Safety limits en case-timeline `fetchTimelineItems`: 9 sub-queries paralelas (documents, meetings, deadlines, escritos, signatures, expenses, invoices, emails, timeline events) capadas con límites apropiados (100-500). Previene que expedientes muy activos devuelvan miles de items al timeline.
- Rate limits en `demo-login`: 30 req/min por IP — endpoint público sin protección podía crear miles de sesiones demo.
- Rate limits en gestión de API keys: create/patch/rotate/delete → 10-20 req/min por usuario. Sin rate limit un atacante podía rotar claves rápidamente para denegar servicio a integraciones.
- Rate limits en portal de mensajes: inbox reply POST → 30/min por usuario; mark-read PATCH → 60/min.
- Rate limits en academia autenticada: progress POST → 60/min, courses GET → 60/min, certificates GET → 30/min.
- UX: ancho completo en derivaciones: eliminado `max-w-3xl`, header consistente con patrón del dashboard.
- UX: header añadido en vista de documentos: h1 + subtítulo antes de las tabs, consistente con otras vistas.
Añadido— 04
- `suggested_question_cache` table (migration 0052): nueva tabla con invalidación inteligente por ley. Campos: `questionHash`, `category`, `audience`, `countryCode`, `locale`, `answerMarkdown`, `sources` (JSONB), `modelUsed`, `relatedLaws` (text[], GIN index), `relatedSourceIds`, lifecycle timestamps. Unique index en (hash, locale, country). Estrategia: cron 6h detecta leyes actualizadas y marca entradas como `invalidatedAt`, cron 12h regenera.
- Sidebar: flag `isShortcut` en items de Tools: todos los items de la sección Herramientas tienen ahora `isShortcut: true`. La guarda `isActive` suprime el highlight tanto para items con `.query` como para cualquier shortcut, haciendo el comportamiento explícito en vez de implícito.
Corrección— 04
- Safety limits ronda 2 — 15+ endpoints adicionales capados: - `GET /v1/analytics/activity-feed`: 4 queries "últimos 30min" (conv/docs/entries/cases) → limit(100); streak days → limit(61) - `GET /v1/calendar/week`: deadlines y meetings de la semana → limit(500); lista de miembros → limit(200) - `GET /v1/cases/saved-searches`: limit(100) - `GET /v1/cases/tags`: limit(500) - `GET /v1/cases/custom-fields/defs`: limit(200) en 2 endpoints - `GET /v1/document-requests/:id/messages`: portal messages → limit(500) - `GET /v1/deadlines/templates`: templates → limit(200), items → limit(2000); apply → limit(500) - `GET /v1/compliance/:id/requirements`: limit(500) en 5 endpoints de compliance/audit
- Validación arrays de entrada: `feedback.ts` ragContext `chunkIds`/`sourceIds` → `.max(50)`; `product-feedback.ts` navigationLog → `.max(50)`.
- Glosario y Whats-New: ancho completo: eliminado `max-w-3xl` en ambas vistas para aprovechar el espacio en pantallas anchas (consistente con herramientas).
Añadido— 04
- Sidebar: shortcuts de categorías en herramientas: la barra lateral ahora incluye accesos directos a las 7 categorías de herramientas (Laboral, Fiscal, Procesal, SS, Civil/Familia, Mercantil, Privacidad) + Glosario. Cada shortcut abre `/tools?cat=<categoría>` sincroniando el filtro de la página automáticamente via URL param.
- Sincronización URL↔filtro en `/tools`: la página de herramientas lee el parámetro `?cat=` de la URL y sincroniza el estado `selectedCat` mediante ajuste en render-time (sin `useEffect`), evitando flash de contenido incorrecto al navegar directamente por URL.
Corrección— 04
- Auditoría sistemática de `orderBy` sin `limit` en todos los route handlers de la API. Se añadieron safety caps en ~20 endpoints que podían crecer sin límite: - `GET /v1/cases/:id/deadlines`: limit(500) - `GET /v1/cases/task-checklist-templates`: limit(200) - `GET /v1/cases/*/template-items`: limit(2000) bulk, limit(500) apply - `GET /v1/invoices/verifactu-status`: limit(500) - `GET /v1/documents/:id/versions`: limit(100) - `GET /v1/conversations/:id/report`: limit(200) messages - `GET /v1/admin/feedback/:threadId/messages`: limit(500) - `GET /v1/clients/:id/invoices`: limit(500) - `GET /v1/testimony-prep?caseId=*`: limit(50) - `GET /v1/testimony-prep/:id/simulations`: limit(100) - `GET /v1/testimony-prep/:id/stats`: limit(200) - `GET /v1/academy/my-certificates`: limit(100) - `GET /v1/templates`: limit(500), case templates limit(200) - `GET /v1/escritos/:id/versions`: limit(200)
Corrección— 04
- Analytics `/cases`: agregados scoped a los 50 casos visibles: `hoursPerCase` e `invoicesPerCase` escaneaban toda la historia de la organización para construir Maps y luego solo usaban 50 entradas. Corregido: se extraen los IDs de los 50 casos primero y se usa `inArray()` para limitar ambos agregados.
- Analytics `/risk-dashboard`: activación de índice parcial: la query de `timeEntries` no incluía `isNull(deletedAt)`, impidiendo que el índice parcial `time_entries_org_active_idx` (que tiene `WHERE deleted_at IS NULL`) se activara. Añadida la condición.
- Analytics `/invoice-aging` y `/outcomes`: safety caps (`.limit(500)` y `.limit(2000)`) en queries históricas sin límite.
- Dashboard `/watchdog-alerts`: cap a 300 casos activos — query sin límite podía escanear toda la org.
- Dashboard `/follow-ups`: cap a 200 en sub-queries de plazos y solicitudes de documentos pendientes.
- Dashboard `/follow-ups`: `eq()` en lugar de `sql\`col = 'pending'\`` para plazos y doc-requests — más type-safe y permite al planificador usar el índice correcto.
- Escritos: historial de versiones capado a 200 — `GET /v1/escritos/:id/versions` sin límite.
- Sidebar: shortcuts de categoría nunca se marcan como "active" — el estado activo vive solo en el ítem padre; los shortcuts individuales mostraban todos el indicador activo al estar en `/tools`.
- +3 more
Corrección— 04
- academy: rate limit en quiz autenticado: endpoint `POST /v1/academy/quiz` sin rate limit permitía fuerza bruta de respuestas. Añadido 10 req/min por usuario.
- academy: knowledge-export protegido por ALMA_SYNC_TOKEN: endpoint público que exponía todo el contenido educativo (cursos, módulos, texto completo) sin autenticación. Ahora requiere `Authorization: Bearer <ALMA_SYNC_TOKEN>` cuando la variable de entorno está configurada.
- academy: race condition en certificados: dos tabs/requests simultáneos podían emitir dos certificados. Añadido unique index `(courseId, userId)` en DB — el segundo INSERT lanza error de constraint capturado gracefully.
- academy: falsy guards en progreso de módulos: `timeSpentSeconds=0` y `videoWatchedPercent=0` no se guardaban porque la guarda `if (data.value)` era falsy para `0`. Corregido a `!== undefined`.
- academy: `passingScore=0` emitía certificados sin restricción: cualquier score >= 0 aprobaba. Guarda cambiada a `!course.passingScore` para rechazar también `0`.
- rate limits: public suggestions y billing-profiles: endpoint público `/v1/public/suggestions/cached` sin rate limit (30/min por IP). Ruta `billing-profiles` sin protección de tasa (60/min por usuario).
Corrección— 04
- Abort de stream al cambiar de conversación: `chat-client.tsx` ahora llama a `handleAbort()` al seleccionar otra conversación en el sidebar, evitando que el spinner de streaming aparezca en una conversación no relacionada.
- chat-sidebar: double-fire en Enter+blur (tags y título): el patrón `onKeyDown(Enter) + onBlur` disparaba `saveTagsForConv` y `onRename` dos veces. Corregido: Enter dispara `blur()` directamente (una sola fuente de verdad). Escape usa `cancelRef` para no guardar al cerrar.
- chat-sidebar: rollback de tags a array vacío: cuando fallaba el PATCH de tags, el optimistic update hacía rollback a `[]` en lugar de restaurar los tags previos. Corregido: `saveTagsForConv` recibe y usa los tags anteriores en el catch.
- chat-sidebar: rename en blur sin cambios: el editor inline de título llamaba `onRename` en cada blur aunque el título no hubiera cambiado — llamada API innecesaria eliminada con guard `trimmed !== conv.title`.
- chat-client: `handleRestore` sin error handling: un fallo de API en restaurar conversación eliminaba el item de la vista sin mostrar error al usuario. Añadido try/catch con toast de error.
- auth/me: promoción de token demo a cookie httpOnly: el endpoint `GET /me` promovía cualquier Bearer token válido a cookie persistente, incluidos tokens demo o de impersonación. Añadida guarda `!isDemo && !impersonatedBy`.
- conversations: `ragCountry` en debug SSE hardcodeado a 'es': el evento SSE de debug del panel de desarrollo siempre reportaba `country: 'es'` independientemente del país real de la org. Promovida la variable al scope exterior.
- auth: email sin trim en `check-email`: emails con espacios al inicio/final podían eludir la detección de duplicados. Añadido `.trim()`.
- +4 more
Añadido— 04
- Toast con acciones (Deshacer): el sistema de toasts ahora soporta un botón de acción inline opcional via tercer argumento `options?: { action?: { label, onClick } }`. Los toasts con acción se auto-descartan a los 6s (vs 4s sin acción). Retrocompatible — ninguna llamada existente cambia.
- Undo al mover conversación a la papelera: el toast "Conversación movida a la papelera" incluye botón "Deshacer" que llama inmediatamente a `PATCH status=active` y restaura la conversación en la lista visible, sin necesidad de navegar a la papelera.
Corrección— 04
- Link cross-dominio en `upgrade-modal`: el CTA "Plan Particular" en el modal de upgrade usaba `href="/{locale}/para/particulares"` (ruta interna) en lugar de `${WEB_URL}/{locale}/para/particulares` — en producción apuntaba a `app.lexiel.ai` en vez de `lexiel.ai`.
- Label 'Corpus + LSIE' en quick-links admin: el enlace a `/admin/knowledge` ahora muestra "Corpus + LSIE" en lugar de solo "LSIE" para mayor claridad.
- Eliminación de `chat-view.tsx` (890 líneas de código muerto): componente legacy nunca importado, sustituido hace meses por `chat-client.tsx`. Se eliminó para evitar confusión en mantenimiento.
- `academy fix` (session 117 cont.): `VideoPlayer` recibe `moduleId` como prop en lugar de traversal DOM roto (`reportToApi` nunca se ejecutaba); `passingScore===0` falsy guard corregido; insert de certificado envuelto en try/catch para race conditions; 107+ patrones de acento en DB academia.
Añadido— 04
Añadido— 04
- Academia quiz review deep-link: el email de resultado del quiz incluye un enlace `?review=1` que al abrirse restaura automáticamente el modo de repaso con las preguntas falladas persistidas en localStorage. El quiz guarda los IDs fallados en `lexiel-academy-failed-{moduleSlug}` tras cada envío (también en el catch de fallback offline).
- Email de resultado de quiz rediseñado: layouts separados para aprobado (verde) y suspendido (ámbar), sección de fallos más clara, botón de repaso con deep-link, eliminado layout genérico único.
Corrección— 04
- courseSlug dinámico en emails de academia: `sendAcademyWelcomeEmail` y `sendAcademyQuizResultEmail` ahora reciben el slug del curso real (obtenido por JOIN con `academyCourses`) en lugar del hardcoded `'certificacion-ia-legal'`. El enlace del email lleva al curso correcto.
- Test `auth.spec.ts` con texto inexistente: el test comprobaba `'Acceso rápido para demo'` (texto que no existe en ningún componente) y `'Abogado'` (tampoco existe). Corregido para usar el texto real del encabezado del panel dev (`'Cuentas demo · Acceso inmediato'`) y selectores `data-testid="dev-login-admin"` / `data-testid="dev-login-particular"`.
- `auth.setup.ts` migrado a `data-testid`: el setup de Playwright usaba `getByText('Admin (José)')` — frágil a cambios de copy. Migrado a `[data-testid="dev-login-admin"]`.
- `data-testid="dev-login-admin"` añadido a `login-form.tsx`: el botón de admin dev no tenía `data-testid`, imposibilitando los selectores estables en tests.
- `<Suspense>` wrapper en `ModuleLearning`: `module-learning.tsx` usa `useSearchParams()` (para leer `?review=1`) — Next.js 15 requiere envolver en `<Suspense>` o el build prerenderizado falla.
- Duplicado `/latam/cuba` en `sitemap.ts`: la entrada aparecía dos veces (línea 265 y 296). Eliminada la primera.
- Commits: `2b6e3e64`, `194bfd54`, `69591969`
Corrección— 04
- Rate limit en GET /v1/partners/ref/:code: endpoint público de tracking de clicks sin protección — añadido rateLimit('partner-ref-click', max=30/min by IP) para evitar inflación artificial de estadísticas de partners.
- Tildes faltantes en `comparador-fiscal-calculator.tsx`: 7 ocurrencias de `bonificacion`→`bonificación` y `Deduccion`→`Deducción` en datos territoriales de Ceuta y Melilla.
- Comentario muerto eliminado de `notifications.ts`: bloque de 5 líneas de comentario `// POST /v1/cron/keycloak-retry` sin implementación, dejado cuando el handler fue movido a `maintenance.ts`.
Añadido— 04
- Benchmark Nicaragua (Gemini 3.1 Pro + RAG): 86.7% (130/150), parser reparado. Áreas más débiles: Tributario 76.9%, Procesal Civil 80%, Familia 83.3%. Fallos analizados — requiere más chunks de Código Tributario (Ley 562, actualmente 243 chunks) y mejor cobertura del Código del Trabajo en artículos de vacaciones (Art. 76) e indemnización (Art. 45).
- Sitemap expandido: añadidas páginas faltantes `/partners/derecho-virtual`, `/partners/andres-millan`, `/enterprise`, `/latam/cuba`. `STATIC_PAGE_DATE` actualizado a 2026-04-07.
Corrección— 04
- Parser benchmark case-insensitive: Gemini 2.5 Pro (usado como fallback cuando 3.1 Pro rate-limita) respondía con letras en minúscula ("la respuesta es c"). El regex `\b([ABCD])\b` solo matcheaba mayúsculas — 130/131 fallos devolvían `?` (peor que aleatorio). Fix: normalizar texto a mayúsculas antes de todos los regex.
- `inputMode="numeric"` en campo offsetDays de case-templates: UX móvil — teclado numérico en campo de días de plazo.
- Gitignore: `docs/test-*.pdf` excluidos: los archivos PDF de respuestas de exámenes de prueba (benchmark answer keys locales) ya no rastrean en git.
- Modelo de producción en marketing pages: 8 páginas actualizadas para reflejar Claude Opus 4.6 como modelo principal (en lugar de Gemini 3.1 Pro).
Añadido— 04
- Corpus Portugal expandido: 4 nuevas fuentes ingresadas (+1,302 chunks, 20→24 fuentes, 12,618→13,920 chunks). Lei 145/2015 (Estatuto da Ordem dos Advogados, 325 chunks), Lei 62/2013 (LOSJ, 258 chunks), Lei 29/2013 (Lei da Mediação, 65 chunks), Lei 23/2007 (Lei de Imigração e Asilo, 654 chunks). NIDs verificados en PGDL Lisboa.
- Script `ingest-portugal-laws-2025.ts`: nuevo script para ingest de leyes PT faltantes desde PGDL Lisboa print endpoint.
Corrección— 04
- `inputMode="decimal"` en 28 calculadoras del dashboard: fix masivo de `inputMode="numeric"` → `"decimal"` en todos los inputs de herramientas (subsidio-desempleo, nomina, despido-colectivo, seg-social, mora-ss, clausulas-suelo, cese-actividad, test-insolvencia, pension-viudedad, excedencia, incapacidad-permanente, erte, reduccion-jornada, permiso-parental, comparador-fiscal, aranceles-notariales, fogasa, modelo-130, periodo-prueba, recargo-prestaciones, irpf-alquiler, tasa-judicial, itp, modelo-303, jornada-375, horas-extraordinarias, herencia, ganancia-patrimonial). En iOS, "decimal" muestra la coma decimal en el teclado numérico, necesaria para importes en español.
- Cron `cleanup-orphan-embeddings` wired al scheduler semanal: el endpoint creado en session 87n estaba sin registrar en `knowledge-scheduler.ts`. Ahora se ejecuta cada 7 días para limpiar embeddings huérfanos acumulados históricamente.
Corrección— 04
- Mensajes de error de límite de plan visibles en UI: cuando la API devuelve 429 (`PLAN_LIMIT_REACHED`, `DEMO_LIMIT`) los clientes SSE ahora muestran el mensaje del servidor (`error.message`) en lugar de un toast genérico. Clientes actualizados: `chat-view` (×2), `case-chat-view`, `case-chat-panel`, `burofax-client`, `legal-opinion-modal`, `legal-composer` (compose + refine). Añadida key `planLimitError` como fallback en i18n ES+EN.
- Eventos `error` mid-stream SSE procesados: la API emite `event: error` dentro del stream SSE cuando el LLM falla en mitad de la respuesta. Los clientes `chat-view` (×2), `case-chat-view` y `case-chat-panel` ahora lanzan el error al loop `catch` en lugar de ignorarlo silenciosamente, terminando el spinner y mostrando el mensaje al usuario.
- `case-chat-panel` maneja `type: 'error'` en stream JSON: el endpoint `/v1/cases/:id/chat` usa formato JSON en lugar de SSE estándar (`data: { type: 'error', message }`) — ahora el panel lo detecta y propaga correctamente.
Corrección— 04
- Timeout de 90s en 14 clientes SSE: todos los clientes de fetch directo ahora abortan automáticamente a los 90s si Railway corta la conexión sin enviar evento `done`. El spinner ya no queda girando indefinidamente. Clientes cubiertos: `informe-client`, `encargo-client`, `predictive-client`, `strategy-client`, `contract-client`, `burofax-client`, `chat-view` (×2), `legal-opinion-modal`, `lexnet-prep-modal`, `compare-client`, `case-chat-view`, `case-chat-panel`, `chat-client`, `consulta-legal-gratis-client`.
- Botón Stop en lexnet-prep-modal y legal-opinion-modal: añadidos botones para detener generaciones en curso (AbortController + unmount cleanup).
- `geo-pricing.tsx` añadido al repositorio: el archivo estaba importado en `/precios/particular/page.tsx` pero sin commitear — el deploy habría fallado.
- hreflang en `demo/anonimizacion`: añadido `alternates: webAlternates(...)` para SEO bilingüe.
- `/intake/` bloqueado en robots.txt: rutas tokenizadas de intake ya no son rastreables.
- AbortController en 6 clientes SSE adicionales: `onboarding-chat-step`, `onboarding-particular-step`, `actas-client`, `case-summary-view` (`handleDraftCommunication`), `legal-composer` (generate + refine) — no tenían ningún abort path en absoluto.
- Cobertura SSE completa: 0 componentes con streaming SSE sin AbortController + timeout en toda la app.
Corrección— 04
- Suspense para useSearchParams: `tools/escritos/page.tsx` y `derivaciones/page.tsx` envuelven sus clientes en `<Suspense>` — prevenía errores de prerenderizado estático en Next.js 15.
- UUID validation en PATCH /v1/boe/alerts/:id/read: `zValidator('param', ...)` previene errores de Postgres por IDs malformados.
- 62 tildes corregidas en `benchmark-data.ts`: `¿Qué es`, `¿Cuál es`, `¿En qué`, `Administración`, `artículo`, `Código`, `español`, `pena máxima`, `régimen`, etc. en questions y expectedAnswers del benchmark de evaluación.
- Tildes en `email.ts`: `módulos`, `Duración`, `CERTIFICACIÓN` en email de bienvenida academia.
- Tildes en `generate-onboarding-infographics.ts`: `Qué es`, `jurídico`, `estás`, `participación`, `pestaña`, `grabación`, `límites`, `Posición`.
Corrección— 04
- Dead sessionStorage writes eliminados: `lexiel_trial_days` y `lexiel_signup_source` en `signup-client.tsx` — ambas keys nunca tenían `getItem` correspondiente en ningún archivo del proyecto. Código muerto removido.
- 150+ tildes en 10 archivos de academy seeds: `digital-track`, `pi-track`, `rgpd-m4-fix`, `fiscalidad-track`, `fiscalidad-m4-fix`, `adr-track`, `content-update`, `content-en-expand`, `seed`, `scripts-m2-m5`. Corregidas tildes ausentes Y tildes en posición incorrecta (`interésados→interesados`, `retenciónes→retenciones`, `casílla→casilla`, `prácticada→practicada`, `mediaños→medianos`).
- 20 países LATAM en corpus daemon: todos los países registrados en `ingest-router` y daemon de corpus.
Añadido— 04
- Ecuador Asamblea Nacional bulk crawler: `ingest-ecuador-asamblea-bulk.ts` — 311 leyes de la Asamblea EC, 21 páginas paginadas, PDFs via pdftotext. EC corpus: 26 → 337 fuentes.
- Gemini key rotation fix: 403 CONSUMER_SUSPENDED ahora rota inmediatamente a siguiente key (igual que 429 rate limit). Key suspendida bloqueada 24h en rotación. Corrige INDEX FAILED masivos.
- `GOOGLE_GEMINI_API_KEY` actualizada: key suspendida reemplazada en .env local y Railway por key activa.
- Peru ingest resilience: pre-carga de URLs PE en Set (elimina 14,300 queries per-entry) + DB reconnect en Neon ETIMEDOUT.
- `exec-file.ts`: wrapper seguro de execFile (sin shell injection) para scripts de ingestión.
- Certificado CTA fix: `/register` → `/start` (consistente con todos los otros CTAs de la app).
- Conv2 EAA 2025 audit: verificados los 7 errores restantes — todos son errores genuinos del modelo, no del answer key. Score final 95.9% (163/170) confirmado definitivo.
- FEATURE_TRACKING actualizado: estado corpus por país al 2026-04-05.
Corrección
- Gemini embeddings: 403 CONSUMER_SUSPENDED causaba INDEX FAILED en todos los ingest nuevos. Fix: rotar key + bloquear 24h.
- Peru Phase 2: dedup per-entry causaba ETIMEDOUT inmediato al conectar a Neon tras idle largo de Phase 1.
- Ecuador ingest: ETIMEDOUT después de downloads largos de PDF — retry loop con reconexión DB.
Añadido— 04
- CRITICAL: AcademyTeaser en landing principal: nueva seccion con 9 cursos, 4 highlights, dual CTA. La landing tenia 0 menciones de academia pese a tener 9 cursos listos.
- Security: Stripe health check: eliminada request autenticada a Stripe API. Solo verifica existencia de la key en env.
- Funnel audit: 0 registros, 0 quiz, 0 certificados en produccion. Prioridad cambia de "mas features" a "distribucion".
- 0 errores TypeScript en los 3 paquetes (api, app, web).
Añadido— 04
- Academy newsletter cron: email mensual (1er lunes) con stats KPIs, top 5 leaderboard, CTA. Unsubscribe headers.
- Partner webhooks: POST a URL configurada del colegio cuando un colegiado obtiene certificado. Fire-and-forget.
- Certificados filtrables por colegio: dropdown en certificates tab del admin.
- A/B banner: 3 variantes de copy rotadas (feature, breadth, emotional). Sin infra externa.
- Mobile quiz: `touch-manipulation` + tap targets mas grandes en movil.
- API integration tests: 11 tests contra endpoints reales (stats, courses, modules, quiz, leaderboard, register, cert verify, knowledge-export, widget-event).
- OG images: leaderboard + API docs.
- 3 nuevos tracks en progreso: Propiedad Intelectual, Derecho Digital, Mediacion/ADR.
Añadido— 04
- Leaderboard publica (`/academia/leaderboard`): ranking anonimo con medallas (top 3), modulos completados, score medio, puntos. Nombres anonimizados "Jose D." (RGPD).
- API docs publica (`/academia/api`): documentacion interactiva de todos los endpoints publicos y autenticados. Rate limits, parametros, metodo/path.
- Streak calculation real: el quiz handler ahora calcula streaks consecutivos (yesterday check). Los puntos incluyen bonus por score alto.
- Alma KB sync (`GET /v1/public/academy/knowledge-export`): exporta contenido educativo como chunks RAG-optimizados con metadata para la knowledge base de Alma.
- Widget events table: tabla `widget_events` para tracking de B2B widget analytics (impressions, questions, leads).
- Track Fiscalidad (en progreso): IVA/IRPF, IS, VeriFactu, procedimientos tributarios.
- Certificate logging: issuance logged con structured info para monitoring.
Añadido— 04
- Audit log: tabla `academy_audit_log` (who/what/when) + `GET /v1/admin/academy/audit-log`
- Import JSON: `POST /v1/admin/academy/import` restaura curso completo desde export JSON
- Gamificacion: puntos (100 por aprobado + bonus score, 20 por intento), leaderboard anonimo `GET /v1/public/academy/leaderboard` (RGPD: "Jose D." format)
- Track Compliance Empresarial: 4 modulos (blanqueo PBC, compliance penal, MiFID II, ESG/CSRD), 20 preguntas quiz
- Webinar fecha fija: 24 de abril 2026, countdown timer client component
- OG images: /academia/stats (📊), /academia/webinar (🎥 con fecha), /academia/embed (🏛️)
- PDF certificado: ya genera para todos los tracks (era generico, verificado)
Añadido— 04
- Module reorder: botones up/down en courses-tab para reordenar modulos (swaps sortOrder via PATCHes paralelos).
- Bulk publish/unpublish: publicar/despublicar todos los modulos de un curso con un clic.
- Course export JSON (`GET /v1/admin/academy/export/:courseId`): exporta curso completo con modulos y preguntas como JSON portable (sin IDs de BD).
- Recent registrations feed (`GET /v1/admin/academy/recent-registrations`): ultimos 20 registros de guests.
- Marketing funnel (`GET /v1/admin/academy/funnel`): pipeline registros → quiz attempts → passes → certificados.
- Track Procesal (en progreso): 4 modulos (plazos, recursos, vista oral, ejecucion).
- Playwright E2E (en progreso): test de flujo completo registro → quiz → certificado.
- Rodrigo bugs: verificado que todos los issues del primer cliente estan resueltos (particular role Phase 1+2 complete).
Añadido— 04
- Facturas rectificativas R1/R5: flujo legalmente correcto (RD 1619/2012, VeriFactu RD 1007/2023). Wizard con selector tipo (sustitucion/diferencias) y motivo. Status 'rectified' en enum + banners de navegacion bidireccional "Rectifica a" / "Rectificada por".
- PDF premium para abogados: EB Garamond serif + Inter sans-serif, paleta navy/gold, barras de acento, tabla con filas alternas, seccion suplidos, IBAN con banco, footer VeriFactu QR, header FACTURA RECTIFICATIVA.
- legalDisclaimer en billing profile: campo para "Inscrita en el Registro Mercantil de..." que aparece al pie del PDF.
Corrección
- VeriFactu compliance: DELETE endpoint y bulk cancel ya no permiten cancelar facturas emitidas (solo borradores). Las emitidas requieren rectificativa.
- Sidebar light mode: borde visible entre sidebar y contenido.
- FacturaE tooltip: extension corregida de .xsig a .facturae.xml (no es firma digital).
- Cases layout: fix inline concatenacion setShowExportMenu, refactor botones en dropdowns.
- SSE heartbeat: type Promise-compatible, intervalo 10s, keepalive data.
- Chat gratis: logo LX completo en avatar bubble, brand color en user messages, dark mode gradient.
Eliminado— 04
- Embeddable B2B widget — Deleted `apps/web/public/embed/widget.js` and `packages/api/src/routes/widget.ts`. Orphaned feature with no active integrations. AgenteUno is the exclusive chatbot provider for the Lexiel ecosystem. Schema field `api_keys.keyType` preserved for historical data. Commit: 43a9e732.
Añadido— 04
- Compliance controls CRUD UI: "Nuevo control" button → form (title, category, regulation). List with status icons. Category badges. Last verified date.
- Testing/catas UI: "Registrar test" per control → result selector (Superado/No superado/Parcial) + notes. Saves to API, updates status instantly.
- Compliance 3-layer API (session 88i): Controls CRUD, testing records, audit view endpoints. Schema: complianceStandards + complianceRequirements + complianceChecks.
- ALL 3 layers from Raquel Arga feedback now complete: Normativa → Controles → Testing → Vista auditoría.
Añadido— 04
- Impacto alto:
- ElevenLabs v3 scripts: 5 scripts convertidos a formato audio tags (m1.txt-m5.txt en docs/academy-scripts/elevenlabs/). Tags: [calm], [serious], [emphasized], [warm], [excited], [whisper], [pause]. M5 en formato multi-speaker ADRIAN:/SOFIA:.
- Webinar landing (`/academia/webinar`): 30 min agenda, diferenciadores vs webinars de teoria, CTA registro. Fecha TBC.
- Track RGPD completado: 4 modulos (responsable tratamiento, EIPD, brechas seguridad, transferencias + DPO), 20 preguntas quiz, contenido ES+EN.
- Impacto medio:
- Magic link email: `sendAcademyWelcomeEmail()` con progressToken como URL clicable. Enviado automaticamente al registrarse.
- Railway crons configurados: academy-completion-nudge (lunes 08:00), case-followup-reminder (diario 08:15), case-weekly-summary (viernes 08:30), insurance-expiry-alerts (diario 09:00).
- Embed para colegios (`/academia/embed`): codigo de embed copy-paste + version co-branding con partner name + logo. Documentacion completa.
- +4 more
Añadido— 04
- Compliance controls CRUD: `POST/GET /v1/compliance/controls` — org-specific controls linked to regulation (title, category, frequency, responsible). Stored as custom complianceRequirements.
- Compliance testing: `POST /v1/compliance/tests` — record test results (pass/fail/partial) with tester, date, notes. Maps to compliance check status automatically.
- Audit view: `GET /v1/compliance/audit` — unified report showing normativa + custom controls + test results, grouped by standard, with per-group and overall scores. Ready for auditor review.
- This completes the 3 layers Raquel Arga described: normativa → controles → testing/catas → vista de auditoría unificada.
Añadido— 04
- Command palette + global search: "Actas societarias" and "Generador de contratos" added with keywords.
- 3 more contract presets (6 total): Suministro con SLA, Consultoría estratégica, NDA pre-inversión.
Añadido— 04
- Guest progress token: `progressToken` en `academy_user_progress`. Generado al registrarse, propagado a todos los quiz del mismo email. `GET /v1/public/academy/progress/:token` para recuperar progreso cross-device. Frontend guarda token en localStorage.
- Banner competitivo en landing academia con CTA "9 modulos, certificado verificable en LinkedIn, sin horario fijo".
- Outreach mejorado: emails a colegios con "a diferencia de webinars puntuales" + opcion 4 (formacion personalizada).
- 184 emdashes eliminados: 9 modulos BD + 4 seed files. Reemplazo context-aware.
- CTA opaco: boton hero bg-white/5 → bg-slate-800 (grid pattern fix).
Añadido— 04
- Draft history component: Reusable collapsible list of previously generated documents (actas, contratos). Shows in both /actas and /contratos-ia. Expandable preview, copy, delete.
- Compliance landing tools section: 4 cards on /para/compliance-officers showcasing GDPR anonymization, minutes generator, AI contracts, internal regulations. Links to demo + registration.
- Chunk fixer executed: 15 oversized chunks processed, 30 sub-chunks created. Core law chunks (LEC Art. 24: 42K) identified for future manual sub-chunking.
Añadido— 04
- Oversized chunk fixer script (`fix-oversized-chunks.ts`): Finds and splits oversized embeddings in core laws. Found 15 chunks > 4K chars in LEC (42K!), CC (27K), CP (10K). Splits by article boundaries, then paragraphs. Supports --dry-run.
- E2E smoke tests: actas-contratos.spec.ts verifying /actas and /contratos-ia routes exist.
- AEPD DPD exam analysis: 150 questions parsed and categorized by domain. No solucionario available — retrieval benchmark only.
- Art. 31 bis sub-chunking confirmed: 5 sub-chunks working. Q12 benchmark improvement expected.
Añadido— 04
- Compliance push notifications: When new BdE/CNMV circulars detected in BOE, push to all compliance/in-house org owners automatically. Non-blocking, 100 owner limit.
- Compliance dashboard quick actions: 4 cards at top of /compliance — generate contract, generate minutes, internal regulations, regulatory alerts. Fast navigation for compliance users.
- Compliance exam bank: AEPD DPD (150Q), BdE Inspector 2024 (46Q). 196 real exam questions from official institutions. Documented in docs/exams/compliance/.
- Linter auto-fixes: 271 files cleaned up (chat-gratis → consulta-legal-gratis rename, minor formatting).
Añadido— 04
- AI Contract Generator (`/contratos-ia`): 4 contract types (servicios, suministro, NDA, encargado tratamiento RGPD) with structured forms, 3 presets, SSE streaming, PDF export. Templates include 15+ clauses, RGPD art. 28 compliance, SLA/KPIs.
- Art. 31 bis CP sub-chunked: Split from 1 chunk (4316 chars) into 5 focused sub-chunks (31bis.1 through 31bis.5). Should improve Q12 benchmark score from 0.728 to ~0.85+.
- 4 acta presets: Junta ordinaria anual, junta extraordinaria (ampliación capital), consejo (presupuesto), consejo (compliance y riesgos).
- Compliance exams research: Found ~400 free questions from AEPD DPD (150Q), KPMG (60Q), BdE inspector (90Q), CESCOM sample. PDFs being downloaded.
- Compliance benchmark: 15/15 (100%) — all questions pass retrieval quality check.
Añadido— 04
- Actas PDF export: `POST /v1/legal/acta-pdf` generates professional A4 PDFs with PDFKit (navy headers, justified text, page breaks). UI button in generator toolbar.
- 3 more regulator circulars: CBdE 1/2013 (CIR), CBdE 5/2012 (transparencia bancaria), CCNMV 2/2020 (publicidad inversiones). Total: 5 circulars indexed.
- Compliance benchmark COMPLETE: 15/15 (100%) pass rate, avg score 0.784, range 0.728-0.840. RAG quality rated GOOD. All compliance laws correctly retrieved.
Añadido— 04
- Actas: "Enviar a firma" button: Connects to Signaturit (fully integrated). Pre-fills president + secretary as signers. Opens signature flow.
- Actas: custom template editor: Collapsible textarea for org-specific formatting instructions (letterhead, clauses, company format). Passed as additionalContext to AI.
- Actas: sidebar pathPrefix fix: `/actas` now correctly selects AI Assistant group.
- Compliance RAG benchmark script: 15 questions testing retrieval quality for CP 31bis, Ley 10/2010, Ley 2/2023, Ley 19/2013. Reports pass/fail with scores.
- BdE circulars added to corpus: Circular 4/2017 (normas información financiera), Circular 2/2016 (supervisión y solvencia).
Añadido— 03
- Save draft button in actas generator: "Guardar borrador" saves generated acta as writing draft via `/v1/writing-drafts`. Title auto-generated from doc type + company + date.
- Sidebar pathPrefix fix: `/actas` added so navigation selects correct group.
- Compliance corpus verified: 8 laws with 4,008 total chunks indexed and embedded. CP (591), Ley 10/2010 blanqueo (475), Ley 2/2023 denunciantes (173), Ley 19/2013 transparencia (124), Ley 11/2018 ESG (273), LOSSEC (831), TRLMV (1,418), LCCC (123).
Añadido— 03
- Interactive corporate minutes generator (`/actas`): Form-based UI for generating actas de juntas, consejos de administración, convocatorias y certificados de acuerdos. Structured form (company, attendees, agenda items, quorum) + AI generation with SSE streaming. Copy to clipboard.
- Sidebar entry: "Actas societarias" / "Corporate Minutes" in AI Assistant group with tooltip. i18n labels in ES + EN.
Añadido— 03
- AI-powered corporate document generator: 4 new document types in `/v1/legal/generate` — acta_junta (Junta General minutes, LSC arts. 191-199), acta_consejo (Board minutes, LSC arts. 245-251), convocatoria_junta (meeting notice with deadlines), certificado_acuerdos (corporate resolution certificate). Full LSC compliance, formal mercantile language, streaming SSE.
- Legal-writing endpoint anonymized: `containsPersonalData()` + `prepareForLLM()` added to `/v1/legal/generate` — was a missing RGPD gap.
Añadido— 03
- 4 banking/financial laws indexed: LOSSEC (supervisión entidades crédito, 776K), TRLMV (mercado valores, 1.3M), LCCC (crédito consumo), PSD2-ES (servicios de pago).
- Regulator crawler script (`seed-regulators.ts`): BdE dept 1020, CNMV dept 1040 via BOE API. Prepared for day-by-day circular iteration.
- Compliance corpus now covers: CP 31bis + Ley 10/2010 (blanqueo) + Ley 2/2023 (denunciantes) + Ley 19/2013 (transparencia) + Ley 11/2018 (ESG) + LOSSEC + TRLMV + LCCC.
Añadido— 03
- 3 more corporate clauses (47 total): Contrato marco de prestación de servicios, Transferencia internacional de datos RGPD Cap. V, Cesión de posición contractual.
- Compliance benchmark (15 questions): CP 31 bis, Ley 10/2010, Ley 2/2023, Ley 19/2013, practical cross-cutting cases. Ready to run against RAG.
- BdE/CNMV research: Both publish via BOE API (dept 1020/1040), ~300 circulars total, RSS available. Can reuse existing seed-from-boe-api.ts pattern.
Añadido— 03
- 3 compliance laws indexed from BOE API: Ley 2/2023 (protección de denunciantes/whistleblower), Ley 19/2013 (transparencia y buen gobierno), Ley 11/2018 (información no financiera/ESG). CP arts. 31bis and Ley 10/2010 blanqueo already in corpus.
- Raquel Instagram message finalized: Copy-paste ready with 3 URLs (demo anonimización, enterprise, docu.expert) + videocall offer. Internal notes included.
- Docu.expert assessed: Landing page already communicates "100% local, tus datos nunca salen". Presentable for Raquel. Gaps noted for future enterprise IT docs.
- Pricing recommendation documented: Same pricing structure for all org types, orgType determines UX not price. Enterprise custom for BBVA-type deals.
Añadido— 03
- Sidebar conditional by orgType: "Procedimientos" and "Escritos" hidden for in-house/compliance. Group labels: "Asuntos" (not "Expedientes"), "Normativa y recursos" (not "Biblioteca"). New `inHouseHidden` flag on NavItem + NavGroup types.
- Enterprise landing page (`/enterprise`): Security compliance page for corporate sales — anonymization visual flow, 6 security features with RGPD article refs, on-premise CTA (Docu.expert), compliance checklist (10 items). Dark theme, bilingual.
- Raquel message updated: Added enterprise URL.
Añadido— 03
- Onboarding FirstCaseStep adapted for in-house: "Primer asunto" (not "Primer expediente"), "Contraparte / proveedor" (not "Nombre del cliente"), placeholder "Contrato servicios TI — Proveedor X", button "Crear asunto". orgType flows from step 1 to step 3.
- 3 new in-house clauses (44 total): NDA reforzado con proveedores, Contrato de encargado del tratamiento RGPD completo (art. 28), Cláusula de compliance y prevención penal (art. 31 bis CP).
- Playwright E2E tests for `/demo/anonimizacion`: 5 tests covering page load, anonymization interaction, custom text detection, how-it-works section, enterprise CTA.
Añadido— 03
- 8 new corporate contract clauses: ANS/SLA (availability, response times, KPIs), SLA penalties (tiered by compliance %), service evolution, contract object. Total: 40 static clauses.
- 4 societario templates: Acta de Junta General, Acta de Consejo de Administración, Convocatoria de Junta, Certificado de Acuerdos Sociales.
- Anonymization demo page (`/demo/anonimizacion`): Interactive demo showing before/after of PII anonymization. Entity detection table, 3-step explanation, enterprise CTA with Docu.expert link.
- Raquel message updated: Added demo URL and docu.expert link.
Añadido— 03
- COMPLETE RGPD AUDIT: 41 of ~50 LLM endpoints now anonymize personal data. 25 additional endpoints fixed across 10 files (case-ai 6, documents 7, legal-composer 3, plugin 3, declarations 2, testimony-prep 3, devils-advocate 1, classify-document 1, dashboard 1, style-preferences 1).
- Dashboard differentiation for in-house: PageIntro, quick actions, and practice health strip adapt based on orgType. "Nuevo asunto" instead of "Nuevo caso", "Normativa interna" instead of "Procedimiento guiado". Billing KPIs hidden for in-house. All changes are ADDITIVE — despacho experience unchanged.
Corrección— 03
- SECURITY AUDIT: Found 50+ LLM call sites, only 2 were anonymizing. Fixed 12 critical/high-risk endpoints across 6 files: criminal-records, escritos (3 endpoints), contracts (3 endpoints), meeting-analyzer, intake, email-inbound.
- System prompt privacy rules: Claude/Gemini now instructed to work with placeholders, minimize PII usage, treat sensitive data confidentially (RGPD Art. 5.1.c).
- Registration consent: Checkbox now explicitly mentions AI processing with prior anonymization (RGPD Art. 6(1)(a)).
- Docu.expert reviewed: Technically mature (9/10), live at docu.expert, Ollama support for 100% local processing. Perfect cross-sell for enterprise clients (BBVA) requiring on-premise.
Corrección— 03
- SECURITY FIX: `document-analyzer.ts` now anonymizes personal data before sending to Claude. Previously sent full document text with PII to external API — RGPD Art. 32 compliance gap closed.
- Third Raquel Arga feedback: BBVA prohibits AI with personal data. Documented enterprise privacy concerns, validated Lexiel's placeholder approach, identified Docu.expert cross-sell opportunity.
- Message draft for Raquel: Technical-legal response covering anonymization, encryption, zero-retention, RGPD Art. 32 compliance, Docu.expert on-premise alternative.
Añadido— 03
- Second Raquel Arga feedback: detailed daily in-house work (contracts, SLA clauses, corporate secretary), internal corporate regulations upload, sectoral regulators.
- Onboarding org type selection: FirmInfoStep now asks organization type (despacho/in_house/compliance/mixto) with descriptions. Dynamic labels based on selection. Province dropdown (50 provinces) for RAG jurisprudence proximity boost.
- KB repositioned as "Normativa Interna": For in-house/compliance orgs, Knowledge Base shows as "Normativa Interna" with compliance-focused description. Zero new infrastructure — reuses existing org KB + RAG indexing.
- orgType in auth: `/v1/auth/me` now returns `orgType` field. Frontend User type updated. Sidebar conditionally shows "Normativa Interna" vs "Biblioteca".
- updateOrganizationSchema: Accepts `organizationType` and `defaultProvince` fields.
Añadido— 03
- Partner feedback knowledge base (`docs/partner-feedback/`): estructura para almacenar, clasificar y consultar feedback de partners. Perfiles de partners (mutables), sesiones de feedback (inmutables), y síntesis de gaps. Diseñado para RAG.
- Raquel Arga feedback documentado: 9 insights extraídos del feedback de la abogada in-house del departamento legal de BBVA. Insights clave: Lexiel no cubre in-house/compliance, necesita 3 verticales (despachos/in-house/compliance), jurisprudencia debe geolocalizarse.
- Province enum (50 provincias + 2 ciudades autónomas): para geolocalización judicial de sentencias.
- Organization type enum (`despacho` | `in_house` | `compliance` | `mixto`): diferencia el tipo de organización para experiencia personalizada.
- Province en legal_sources: campo para geolocalización de sentencias. Backfill script (242 TSJ Cat actualizadas vía ECLI).
- Default province en organizations: permite ponderar jurisprudencia cercana en RAG.
- RAG province boost: 30% de incremento en score para sentencias de la misma provincia que la organización. Se acumula con el foral boost.
- Pipeline automático de feedback (`POST /v1/cron/analyze-feedback`): cron que procesa `product_feedback` sin analizar. Claude extrae insights clasificados (gap, feature_request, market_insight, ux_issue, validation, competitive_intel), genera embeddings, y almacena todo sin supervisión manual.
- +1 more
Añadido— 03
- Social proof stats: live strip en `/academia` landing con registrations, certificates, courses, modules. Fetch SSR desde `/v1/public/academy/stats` (revalidate 1h).
- Public stats endpoint (`GET /v1/public/academy/stats`): registrations, certificates, courses, modules counts.
- M1 video script: guion completo para Adrián (~7 min, 4.622 chars ES + 2.584 EN). 5 bloques: LLMs, alucinaciones, caso Mata v. Avianca, protocolo verificación, flujo de trabajo. Guardado en `script_es`/`script_en`.
- Avatar+TTS research: análisis de HeyGen (avatar) + ElevenLabs v3 (TTS con audio tags) como pipeline recomendado. Documentado en memory.
Añadido— 03
- Track Ciudadano en landing: nueva sección en `/academia` con las 4 áreas de derechos (laboral, inquilino, consumidor, familia), tema emerald, CTA al curso ciudadano.
- Schema.org Course: JSON-LD del Track Ciudadano añadido a la landing de academia (4 CourseInstance con duración).
- PDF test: verificado que `generateCertificatePdf()` genera correctamente (2.893 bytes).
Añadido— 03
- Track Ciudadano — nuevo curso "Tus derechos básicos" (audience: citizen, certificación, 80% para aprobar). 4 módulos con contenido completo ES+EN y 20 preguntas de quiz: - M1: Derechos laborales básicos (despido, indemnización, ERTE, plazos para reclamar) - M2: Derechos como inquilino (LAU, fianza, desahucio, reparaciones, preaviso) - M3: Derechos del consumidor (garantías 2 años, desistimiento 14 días, OMIC, cláusulas abusivas) - M4: Familia y herencias (divorcio, custodia, pensión alimenticia, legítimas, testamento)
- PDF de certificado: `certificate-pdf.ts` genera PDF A4 landscape con PDFKit (tema indigo oscuro, acentos dorados, nombre, score, URL de verificación, branding Lexiel). Auto-generado al emitir certificado, subido a R2, pdfUrl guardado en BD. Botón "Descargar PDF" en página de verificación.
- Outreach a colegios (`GET /v1/admin/academy/outreach`): 8 colegios target (Valencia, Málaga, Sevilla, Bilbao, Zaragoza, Alicante, Granada, Valladolid) con email personalizado incluyendo stats de la academia. `OutreachSection` en admin con vista expandible, copy-to-clipboard, botón mailto.
- Sitemap: 5 nuevas URLs del Track Ciudadano × 2 locales.
Añadido— 03
- Academy completion nudge cron (`POST /v1/cron/academy-completion-nudge`): email semanal a guests que empezaron la certificación pero no la completaron (ventana 3-10 días). Email con checklist de progreso (✓/○ por módulo) + CTA "Continuar la certificación". Respeta suppression y unsubscribe headers.
- X/Twitter share: botón "Compartir en X" en la página de verificación de certificado.
Añadido— 03
- CourseProgress: barra de progreso visual en la página del curso — lee scores de localStorage, muestra checkmarks por módulo + badge de certificado.
- Per-module score tracking: el quiz guarda el score por `moduleSlug` en `lexiel-academy-progress` (localStorage) después de cada submit — permite tracking cross-page sin auth.
- Academy analytics API (`GET /v1/admin/academy/analytics`): registros, certificados, puntuación media, tasa de completación por módulo.
- Analytics strip en admin: 3 KPIs + barras de progreso por módulo con porcentaje de completación.
Corrección— 03
- Email persistence across modules: guest email+name now stored in localStorage after registration gate — survives module navigation and page refreshes. Certificate number also cached.
- OG image for certificates: dynamic `opengraph-image.tsx` fetches cert data at edge and renders recipient name, score, cert number with gold Lexiel branding. LinkedIn shares now show rich preview.
- Dynamic breadcrumb: module page breadcrumb uses API course title instead of hardcoded "Certificación IA Legal". Future-proofs for additional courses.
Añadido— 03
- Email de certificado: `sendCertificateEmail()` — badge dorado, número de cert, enlace de verificación. Enviado automáticamente desde `tryIssueCertificate()` (fire-and-forget).
- LinkedIn share: botón "Añadir a LinkedIn" en página de verificación de certificado usando URL oficial de LinkedIn profile/add certification.
- Registration gate: el quiz pide nombre+email antes del primer submit (necesario para progreso + certificado). Opción "Sin registro" disponible.
- Banner de certificado: cuando la auto-certificación emite un cert, el quiz muestra banner dorado con número y enlace.
- Contenido EN expandido: módulos 2-4 ampliados a paridad con ES (~6K chars cada uno vs ~3K previos).
- Proxy /api/academy/register: ruta API en apps/web para registro desde marketing site.
Añadido— 03
- Admin CRUD academia: 14 endpoints en `/v1/admin/academy/` (courses CRUD, modules CRUD, quiz questions CRUD, certificates list). Panel visual `AcademySection` en admin dashboard con árbol colapsable curso → módulo → preguntas, toggle publish, vista de certificados emitidos.
- Certificación automática: `tryIssueCertificate()` — cuando un usuario completa todos los módulos de un curso de certificación con ≥ passing score, se emite un certificado automáticamente (número LEX-CERT-YYYY-XXXXX). Funciona tanto para usuarios autenticados como guests con email.
- `generateStaticParams`: pre-renderizado de cursos y módulos en build time (ambas locales).
- Contenido educativo completo: ~33K caracteres ES + ~19K EN para los 5 módulos de Certificación IA Legal: - M1: LLMs, alucinaciones, verificación de fuentes, caso Mata v. Avianca - M2: RGPD, técnicas de anonimización, modelos de infraestructura, caso práctico filtración - M3: 5 recomendaciones CGAE con ejemplos prácticos y cláusula modelo para hoja de encargo - M4: 6 casos de uso reales (investigación, redacción, contratos, plazos, atención al cliente, vista contraria) - M5: Clasificación AI Act, sesgo algorítmico, futuro de la profesión, código ético de 10 principios
Añadido— 03
- Academia pública — curso reader (`/academia/cursos/[slug]/[moduleSlug]`): módulo SSR con vídeo player (fallback "próximamente"), contenido Markdown con `react-markdown` + `remark-gfm`, quiz interactivo (`ModuleQuiz` client) con multi-choice/multi-select/true-false + score local fallback, navegación prev/next módulo.
- Academia pública — overview del curso (`/academia/cursos/[slug]`): hero con stats (módulos, minutos, passing score), lista de módulos con links, sección de beneficios, CTA "Empezar ahora" al primer módulo.
- Academia pública — verificación de certificado (`/academia/certificado/[number]`): página pública `cache: no-store` que verifica autenticidad de certificado — muestra recipient, score, fecha, número de cert.
- API proxy quiz (`/api/academy/quiz`): Next.js API route en `apps/web` que proxea POST de quiz al backend API (evita CORS desde SSR).
- CTA certificación en `/academia` landing: botón "Empezar la certificación" dentro de la sección de beneficios + hero CTA corregido a URL real del curso (en lugar de anchor `#certificacion`). Schema.org url actualizada.
- Corpus health panel (admin): detección de fuentes obsoletas, health score por país, botón de refresh por fuente. `CorpusHealth` component + `/v1/admin/corpus/health`.
- Firm profitability analytics (`/analytics/firm`): monthly revenue, utilization rate, by-area y by-lawyer breakdown. `FirmAnalytics` page + `/v1/analytics/firm/profitability`.
- Email templates settings: personalización de plantillas por organización (bienvenida, cierre expediente, facturación). `EmailTemplatesSettings` component + `/v1/settings/email-templates`.
Añadido— 03
- Playwright `particular` project: `particular.setup.ts` + `particular-plan.spec.ts` — 9 tests (dashboard landing, redirect de rutas lawyer-only, sidebar, chat, analisis-contratos, boe-alerts, settings BOE prefs, deadline reminders NOT visible).
- `dev-login` particular: `[email protected]` → plan `particular`; botón verde en `login-form.tsx` con `data-testid="dev-login-particular"`.
- `playwright.config.ts`: proyecto `particular` separado con `storageState: tests/.auth/particular.json`; `testIgnore` en `chromium` para evitar doble ejecución.
- `RecurrentesView`: nuevo tab "Recurrentes" en facturas — CRUD completo de retainers (crear/editar/pausar/eliminar) con cliente, importe, descripción y día de facturación 1-28.
- Comparador de contratos (`/tools/comparar-contratos`): nueva página side-by-side de dos versiones de contrato con análisis estructurado de diferencias, riesgos y recomendaciones.
Corrección— 03
- `case-share-public.ts` — 2 push sin guard: view notification + comment notification ahora respetan `caseActivityPush` (async pref-check fire-and-forget pattern).
- `testimony-reminders` cron: push a creador de prep sin pref check. Pre-fetch de prefs para todos los `createdBy` + guard `caseActivityPush !== false`.
- `jurisprudence-alerts` cron: push a usuario de saved search sin pref check. Añadido `leftJoin(notificationPreferences)` en query inicial + guard `caseActivityPush !== false`.
- `deadline-reminders.ts` (lib): bonus push en canal email sin pref check. `sendViaChannel` recibe `deadlineRemindersPush`; guard `!== false` protege el fire-and-forget.
- Auditoría completa: 21 llamadas a `sendPushToUser` auditadas — todas respetan el pref apropiado (`caseActivityPush`, `boeAlertsPush`, `deadlineRemindersPush`, `documentExpiryPush`, `caseFollowupPush`, `weeklyDigestEmail`).
Corrección— 03
- `caseActivityPush` en rutas transaccionales: `quotes.ts` (presupuesto aceptado/rechazado), `cases.ts` (handoff a nuevo asignado), `automation.ts` (`send_notification` a miembros) — todos sin pref guard. Añadido `leftJoin(notificationPreferences)` en cada query de usuario + guard `caseActivityPush !== false`.
- `deadlines.ts` bloqueo global incorrecto: `lawyerOnly` en `use('*')` impedía que usuarios `particular` vieran sus plazos de prescripción auto-detectados. Migrado a per-endpoint: GET accesible para todos; POST/PATCH/DELETE/templates/bulk siguen con `lawyerOnly`.
- `deadlines-client.tsx` redirect erróneo: redirigía a `/chat` en vez de mostrar plazos. Ahora muestra `DeadlinesView` (botones de creación ocultos para `particular` via `isParticular` en `deadlines-view.tsx`).
- Dashboard 403 en ciudadanos: `Promise.all` inicial incluía `/v1/cases` + `/v1/analytics/overview` (bloqueados para `particular`). Añadido early return para `isParticular` que solo fetchea `/v1/deadlines/upcoming`. Elimina el error de carga en el dashboard ciudadano.
Corrección— 03
- `documentExpiryPush` canal faltante: cron `document-expiry-alerts` solo enviaba email; ahora envía push si `documentExpiryPush !== false`.
- `weekly-digest` cron inconsistente: no chequeaba `weeklyDigestEmail` del admin ni enviaba push. Añadido `leftJoin(notificationPreferences)` + early return + push send.
- TS2367 en `notifications.ts:985`: redundant `!== false` tras early return TypeScript-narrowing. Eliminado el guard duplicado.
Corrección— 03
- `boeAlertsPush` en `boe-alerts` (por expediente): push se enviaba sin comprobar el toggle. Añadido `boeAlertsPush` a `ownerPrefs` select + guard.
- `boeAlertsPush` en `boe-digest` (por área): mismo patrón. Añadido a `orgRows` + type `OrgOwner` + guard.
- `caseActivityPush` en `portal-message-alerts`: push a abogado sin consultar pref. Añadido leftJoin en query de `caseRow` + guard.
- `caseActivityPush` en `budget-alert` cron: pre-fetch de prefs para todos los usuarios involucrados + guard.
- `caseActivityPush` en `stale-case-alerts`: pre-fetch de prefs por `assignedTo` + guard.
- `caseActivityPush` en `profitability-alerts`: pre-fetch de prefs por `assignedTo` + guard.
- `deadlineRemindersPush` en `criminal-record-alerts`: ambas fases (cancelación + 30d previos) ahora añaden leftJoin a `notificationPreferences` + guard.
- Settings UX — `particular` users: `boeAlertsEmail`/`Push` por canal extraídos del bloque `!isParticular` → ahora visibles y configurables por ciudadanos.
- +1 more
Corrección— 03
- Crons `case-followup-reminder` + `case-weekly-summary`: no filtraban organizaciones soft-deleted. Añadido `innerJoin(organizations, isNull(deletedAt))`.
- Crons `task-priority-escalation` + `case-watchdog` en `automation.ts`: misma corrección de org soft-delete.
- `notification-preferences` PATCH + dismiss: patrón check-then-insert con race condition. Convertido a `onConflictDoUpdate` atómico.
- BOE alerts (abogados + ciudadanos): no respetaban `boeAlertsEmail` per-channel toggle. Añadida verificación `boeAlertsEmail !== false` en los 3 crons BOE.
- Deadline reminders cron: no pasaba `deadlineRemindersEmail`/`Push` al `sendDeadlineReminder`. Ahora filtra `channelPriority` según toggles.
- Case followup reminder: nunca consultaba preferencias. Añadida query de prefs + check `caseFollowupEmail`/`Push`.
- Weekly digest cron: solo checkeaba `weeklyDigestEnabled` global, no `weeklyDigestEmail`. Añadido check.
- `dashboard.ts` TS error: `users.firstName`/`lastName` no existen en schema (solo `name`). Corregido a `users.name` + derivación de iniciales.
Añadido— 03
- Notification preferences granulares (`migration 0021`): 10 columnas nuevas — `deadlineRemindersEmail/Push`, `documentExpiryEmail/Push`, `caseFollowupEmail/Push`, `caseActivityPush`, `weeklyDigestEmail`, `boeAlertsEmail/Push`. UI en settings.
- Benchmark regression spec (`apps/web/tests/benchmark-regression.spec.ts`): test automatizado de benchmark.
Corrección— 03
- `pricing.tsx`: `equipo` plan CTA generaba `?plan=professional` — `planToApiName['equipo']` mapeaba a `'professional'` en vez de `'team'`. Todos los clicks en "Plan Equipo" creaban suscripciones Professional (precio incorrecto, seats incorrectas). Corregido a `'team'`.
- `trackPlanSelect` type no incluía `particular` — analytics drops silenciosos para el plan particular. Corregido en `packages/shared/src/analytics.ts`.
- Plan particular con precios EUR hardcodeados para LATAM — el pricing component mostraba €14.90/€9.90 incluso con divisa MX/CO seleccionada. Añadidos `particularMonthly`/`particularAnnual` a `CURRENCY_BY_LOCALE` sincronizados con `start-client.tsx`.
- Email broken links `settings/billing` → `settings/subscription` — dos CTAs en emails (trial ending + subscription changed) enlazaban a `/settings/billing` que no existe. Ruta correcta es `/settings/subscription`.
- Sidebar `/guias` visible para plan particular — la página `/guias` llama a `/v1/compliance/overview` (contenido para despachos). Añadido `particularHidden: true`.
- Sidebar `/tendencias` visible para plan particular — contenido de AI regulation/legaltech, irrelevante para ciudadanos. Añadido `particularHidden: true`.
- Hero precios page excluía ciudadanos — "IA legal al alcance de todos los abogados" corregido a "IA legal al alcance de todos".
Corrección— 03
- Firm setup wizard: logo upload endpoint no existía — `/v1/profile/organization/logo` nunca fue creado; la subida fallaba silenciosamente para todos los usuarios. Eliminado el campo de logo del wizard.
- Firm setup wizard: billing profile con datos hardcoded — `addressCity`, `addressPostalCode`, `addressProvince`, `addressCcaa` hardcodeados como "Madrid" para todos los abogados independientemente de su ubicación. Paso reemplazado por vista informacional con CTA a Settings.
- `usage-counter.tsx`: "Ampliar plan" incorrecto para `particular` — cuando el usuario particular alcanzaba el límite mensual, el comptador mostraba "Ampliar plan" (acción imposible). Ahora muestra "Reinicia en Xd" (día de reset del período).
- `isLawyer` en `app-sidebar.tsx`: ignora downgrade a `particular` — `isLawyer = !!user?.barAssociation` mostraba items `lawyerOnly` a usuarios que tenían `barAssociation` pero plan `particular`. Corregido a `!isParticular && !!user?.barAssociation`.
- Link settings/subscription incorrecto en `referrals-client.tsx` — usaba `settings?tab=subscription` (query param inválido). Corregido a `settings/subscription`.
- Rules of Hooks en `my-day-client.tsx` — `return null` condicional antes de dos `useEffect`, violando React Rules of Hooks. Guard movido dentro del effect, `return null` movido tras todos los hooks.
- UUID validation en `referrals.ts` — IDs no-UUID llegaban a Postgres causando "invalid input syntax for type uuid". Añadido `parseUuid()` en `GET /:id` y `POST /:id/rate`.
- Banner "Plan Particular" en `upgrade-modal.tsx` para todos — banner se mostraba a abogados con planes de pago. Restringido a `user?.orgPlan === 'free'`.
Añadido— 03
- Firm setup wizard: wizard de 4 pasos para nuevas cuentas de abogado (despacho, facturación, primer expediente, equipo). Redirect automático desde dashboard layout para `owner` sin wizard completado. Ruta `/[locale]/onboarding`.
- `PATCH /v1/cases/bulk`: operaciones bulk estructuradas en expedientes (max 50 IDs) — cambio de estado, añadir etiqueta, archivar. Rate limit 10/min por org.
- `/v1/organizations` route: endpoint de estado del wizard (`GET /onboarding-status`, `PUT /onboarding-status`). Protegido con `lawyerOnly`.
- Schema: `onboardingData` + `firmWizardCompletedAt` en tabla `organizations` (migración `0018_sour_the_stranger.sql`).
Corrección— 03
- Evento huérfano en PlanGate: `plan-gate.tsx` disparaba `lexiel:upgrade` que nadie escuchaba. `UpgradeModal` solo escucha `lexiel:plan-limit`. El botón "Ver planes" no abría el modal de upgrade.
- Upsell CTA en PlanGate para particular: añadido "¿Eres abogado? Ver planes profesionales →" como segunda opción en el bloqueo ciudadano; enlaza a `/settings/subscription`.
- Tildes en blog posts: `part-2/5/10/11.ts` — correcciones de días, expedición, inscripción, hábiles.
Corrección— 03
- docClassFilter incluye `doc.docType`: el filtro de clasificación AI en DocumentsView solo comparaba `doc.type` (campo manual), ignorando `doc.docType` (Claude Haiku). Los tipos AI nunca filtraban resultados.
- Badge `docType` IA clickeable: el badge de clasificación AI era `<span>` inerte; ahora es `<button>` que aplica el filtro al hacer clic (consistente con el badge `doc.type`).
- PlanGate en 8 páginas del grupo Expedientes + partner-missions: `procedures`, `workflows`, `deadlines`, `calendar`, `vistas-judiciales`, `signatures`, `declarations`, `partner-missions` — todo el grupo 2 del sidebar es `particularHidden: true` pero ninguno tenía `PlanGate` en `page.tsx`.
- PlanGate en `writing/page.tsx`: `/writing` marcado `lawyerOnly: true` en tools-client, sin PlanGate en page. Ciudadanos podían acceder por URL.
- Botón "Redactar escrito" oculto para `particular`: en DocumentsView el botón llevaba a `/writing` (API bloqueada) pero era visible en la UI.
- `user` desestructurado en DocumentsView: `useAuth()` retornaba solo `logout`; ahora desestructura también `user` para acceso a `orgPlan`.
Corrección— 03
- IDOR en case tags PATCH/DELETE: sin `organizationId` en el WHERE, cualquier user autenticado podía modificar/eliminar tags de otra org por UUID. Añadido `and(eq(caseTags.id, tagId), eq(caseTags.organizationId, authData.org))`.
- Build 6/6 limpio: tras todos los cambios de sesión.
Añadido— 03
- PlanGate en 7 páginas sin protección URL: `my-day`, `kb`, `agenda`, `client-comms`, `portal-messages`, `intake`, `document-requests` — todas `particularHidden` en sidebar pero sin `PlanGate` en `page.tsx`. Ciudadanos podían acceder por URL directa.
- `partner-missions` ahora `particularHidden`: página de misiones del programa de partners era visible en sidebar para ciudadanos.
- Plan type casts — 3 rutas con 'particular' excluido: `cron/billing.ts` (paid-first provisioning), `auth.ts` (resend activation), `cron/maintenance.ts` (Keycloak retry). Un ciudadano que hacía checkout paid-first recibía el welcome email sin el plan correcto.
- `doc_type` en documentos: campo `varchar(50)` + Claude Haiku classifier fire-and-forget tras upload/OCR. 14 categorías legales. Migración `0015_quiet_raza.sql` aplicada.
- Case Tags (subagent): `case_tags` + `case_tag_assignments` tablas (migración `0014`), API CRUD completa, UI en CasesListView con chips de filtro + modal de gestión, URL sync del filtro.
- Subscription renewal reminder: cron `POST /v1/cron/subscription-renewal-reminder` (09:00 UTC), ventana 7-8 días, dedup, registrado en `railway.toml`.
- React 19 ESLint false positives: `react-hooks/set-state-in-effect` en `animated-input-border.tsx` y `chat-gratis-client.tsx`; `useRef(Date.now())` impuro → `useRef(0)`. Build 6/6 limpio.
Corrección— 03
- Chat PageIntro contextual: `chat-client.tsx:612` muestra "Tu copiloto jurídico... redacta documentos" a todos — ahora condicional: ciudadanos ven "Tu asistente legal personal... Respuestas verificadas en el BOE."
- PlanGate en meetings, jurisprudencia, presupuestos: sidebar ocultaba estas rutas para `particular` pero URL directa mostraba UI rota. Añadido `PlanGate blocked={['particular']}` en los 3 `page.tsx`.
- Email copy neutro en transaccionales: `sendPaymentFailedEmail` y `sendSubscriptionChangedEmail` usaban `'abogado/a'` como fallback de nombre → `'usuario/a'`. `sendUsageThresholdEmail` ya discrimina: "Tu despacho" vs "Tu cuenta" según `plan === 'particular'`.
- Weekly digest excluye plan particular: query en `notifications.ts` filtrada por `ne(organizations.plan, 'particular')` — ciudadanos con rol `owner` no reciben el digest de abogados.
- Loading skeletons para 3 rutas sin skeleton: `invoices/[id]/`, `invoices/[id]/pdf/`, `derivaciones/[id]/` ahora tienen `loading.tsx` con skeleton de 3-4 secciones.
Añadido— 03
- SEO `dateModified` en blog: Article JSON-LD usaba `post.date` para `dateModified` siempre → ahora `post.updatedAt ?? post.date`. Añadido `updatedAt?: string` a tipo `BlogPost`. Imagen del Article ahora usa URL absoluta con `heroFileName` como fallback.
- `Service` JSON-LD en `/para/*`: nuevo `layout.tsx` en `/para/` inyecta schema `Service` genérico (Lexiel como LegalInformationService con `offers` 9,90 €/mes) en las 24+ páginas de segmento — un solo punto de mantenimiento.
- `WebApplication` JSON-LD en 7 herramientas: `pension-viudedad`, `incapacidad-permanente`, `erte`, `modelo-303`, `recargo-prestaciones`, `tasa-judicial`, `test-insolvencia` — eran las únicas calculadoras sin schema `WebApplication/SoftwareApplication`.
- `getBulkMonthlyUsage` (elimina N+1): nueva función en `usage.ts` que pre-agrega usage de N orgs en 1 sola query `GROUP BY organizationId, type`. `usage-threshold-alerts` cron pasa de N queries a 1 — también filtra `unlimited` orgs antes del `processInBatches` y sube el batch size de 10 a 20.
- Rate-limit `GET /v1/public/freemium/status`: 60 req/min por IP — antes sin límite, podía usarse para sondear Redis.
- `case_share_tokens`: migración `0010` + `case-share-public.ts` endpoint `/v1/public/share/:token` + `POST /v1/cases/:id/share` + `DELETE /v1/cases/:id/share`. Comparte expediente con cliente vía URL con token 32-byte + TTL 30 días.
- `cleanup-expired-share-tokens` cron: borra tokens expirados (diario 03:30 UTC) — registrado en `railway.toml`.
- `lawyer-weekly-digest` cron: digest de productividad individual por abogado (lunes 09:00 UTC) — registrado en `railway.toml`. Contenido: casos activos, horas semana, alertas BOE últimos 7d, legal tip rotativo (8 consejos). Opt-out via `notificationPreferences.weeklyDigestEnabled`.
- +4 more
Corrección— 03
- Freemium `/status` endpoint: `GET /v1/public/freemium/status` expone `{ max, questionsLeft }` leyendo `MAX_FREE_QUESTIONS` desde env var. El frontend ya no tiene la constante `MAX_QUESTIONS=5` hardcodeada — se fetcha en mount y el contador/paleta de límite refleja el valor real del servidor sin redeploy.
- Race condition quota freemium: patrón read-check-incr reemplazado por incr-check-decrement (atómico). Dos requests concurrentes ya no pueden superar el límite.
- Mensaje de límite dinámico: "5 preguntas gratuitas" hardcodeado → `${MAX_FREE_QUESTIONS}` interpolado. Si se cambia la env var a 3 ó 10, el mensaje es coherente.
- `usage.ts` fallback a `free`: dos lugares en `checkAndRecordUsage` y `checkDocumentUploadLimit` que hacían fallback a `PLAN_LIMITS.professional` para plan desconocido → ahora `PLAN_LIMITS.free` (safe default).
- Org name para particular: webhook Stripe ya no crea `"Despacho de X"` para ciudadanos — usa solo el primer nombre.
- Onboarding "Completar más tarde": el botón de skip ahora redirige a `/chat` para particular en vez de dejar al usuario en `my-day` (flash con contenido de abogado).
- Cron retry nurture emails: `maintenance.ts` ahora hace LEFT JOIN con organizations para obtener el plan y despacha `sendCitizenTrialNurtureEmail` vs `sendTrialNurtureEmail` según corresponda.
- `welcomeHtml` plan-aware: ciudadanos ya no reciben bienvenida con "abogado/a" ni lista de features de Compositor Legal — mail adaptado con features reales del plan Particular.
- +5 more
Añadido— 03
- OG images en 7 páginas ciudadanas: divorciados, consumidores, herederos, trabajadores, inquilinos, propietarios-alquiler, autónomos — todas sin opengraph-image.tsx (preview genérica en redes sociales). Ahora generan preview contextual via `generateSpecialtyOG`.
- OG image en /para hub: el hub de segmentos (`/es/para/`) tenía openGraph sin `images:`. Añadida imagen dinámica via `/api/og/para`.
- FAQPage schema en /para/particulares: 6 Q&A (ES+EN) para Google rich snippets.
- Citizen crosslinks en 7 specialty pages: penal, fiscal, mercantil, extranjería, bancario, sanitario, tecnológico.
- Email inmediato de plazo prescriptivo: cuando IA detecta plazo `urgent`/`upcoming` en conversación particular, se envía email vía Resend al instante.
- `trackFreemiumCtaClick` en muro de pago: estaba importada pero nunca llamada — ahora ambos botones (anual/mensual) disparan el evento específico del funnel freemium.
- plan-gate redirect particular: redirigía a `/[locale]` genérico → ahora redirige a `/[locale]/chat` (su workspace real).
- Índice compuesto `ai_ratings_target_user_idx`: `(target_type, target_id, user_id)` — eliminado full table scan en el JOIN messages↔ai_ratings en cada load del chat. Migración `0009_real_khan.sql` aplicada.
- +1 more
Añadido— 03
- FAQPage schema en `/para/particulares`: 6 preguntas Q&A (ES+EN) añadidas al JSON-LD para rich snippets en Google. Cubre: "¿es Lexiel un abogado?", precio, cancelación, tipos de dudas, verificación legal.
- Citizen crosslinks en 7 páginas de especialidad: banners emerald en derecho-penal (→/particulares), derecho-fiscal (→/autonomos-y-freelancers), derecho-mercantil (→/autonomos-y-freelancers), derecho-extranjeria (→/particulares), derecho-bancario (→/propietarios-alquiler), derecho-sanitario (→/particulares), derecho-tecnologico (→/particulares).
- Email inmediato de alerta de plazo: cuando el AI detecta un plazo legal `urgent` (≤3d) o `upcoming` (≤7d) en conversación de plan particular, se envía email transaccional vía Resend al instante (fire-and-forget, no bloquea el chat stream).
Añadido— 03
- `/para` hub page: nueva página índice `/es/para/` (antes 404) con 8 segmentos ciudadanos + 10 segmentos profesionales. Añadida al sitemap (priority 0.85).
- Chat-gratis CTA en 6 páginas ciudadanas: CTAs secundarios "Prueba gratis sin registro →" en divorciados, consumidores, herederos, trabajadores, propietarios-alquiler, autónomos-y-freelancers.
- Citizen crosslink banners en 4 páginas de especialidad: banners emerald en derecho-laboral, derecho-familia, derecho-civil e derecho-inmobiliario redirigen ciudadanos SEO a sus páginas de segmento específicas.
- CitizenBoeAlertForm en chat-gratis: captura de email como alternativa de menor fricción cuando el usuario llega al límite de 3 preguntas gratuitas.
- Plan Particular en upgrade modal: banner emerald "¿Ciudadano sin despacho?" antes del grid de planes de abogado — evita que ciudadanos caigan en planes de 69€+.
Corrección
- `team-view.tsx` guard: usuarios del plan particular redirigidos a `/` al intentar acceder a `/team` directamente (URL directa, no sidebar).
- `comparativa/page.tsx`: precio inicial de Lexiel corregido de "69€/mes" a "desde 9,90€/mes".
- `onboarding-overlay.tsx` i18n: plan 'particular' mostraba 'Personal' en inglés (inconsistente). Extraído `PLAN_LABELS_EN` — ahora muestra 'Citizen'.
- Pricing JSON-LD: Plan Particular (9,90€) no aparecía en el schema.org SoftwareApplication offers de `/precios`.
Añadido— 03
- Normalización 14 días de prueba: ~110 strings en 65 archivos (LATAM, derecho-*, para/*, FAQs, componentes, OG images, API) mostraban "7 días" mientras el trust bar de /precios ya decía "14 días". Limpieza completa de la inconsistencia.
- Panel de Feature Flags (`/admin/feature-flags`): tabla de solo lectura que muestra todos los flags con estado, planes, roles y grupo sidebar. Útil para auditoría.
- llms.txt actualizado: añadidos segmentos consumidores, autónomos, jurídico-empresa, notarios, mediadores + nueva sección de integraciones B2B widget.
- `navbar-timer` idle state: green dot + `00:00` siempre visible cuando no hay cronómetro activo (Clio-inspired affordance, mejor discoverability).
- `SearchTrigger` acepta `className` override: permite que el sidebar indigo use colores propios sin duplicar el componente.
- Citizen profile en seed-demo-data: org citizen + 1 user demo + 1 case monitorio vecinos (c12) + 6 facturas adicionales para demo solo.
Corrección
- Colombia score benchmark: tabla de países mostraba 90% pero score RAG-hybrid confirmado es 96% (consistente con twitter meta y llms.txt). Actualizado `benchmark-page.tsx`.
- Aria-labels en botones icono: `clauses-library-view`, `meetings-view`, `client-comms-view`, `calendar-view` tenían botones X/eliminar sin aria-label.
- OG metadata en /contacto y /changelog: ambas páginas carecían de `openGraph` en `generateMetadata`.
- Sitemap: añadidas 5 páginas ausentes (`/academia`, `/latam/brasil`, `/latam/cuba`, `/latam/portugal`, `/latam/republica-dominicana`).
- `start-client.tsx` y `onboarding-overlay`: mostraban "7 días" en UI después de la normalización a 14 días.
- API `demo.ts` y `notifications.ts`: mostraban "7 días" en contexto LLM y emails de la secuencia de prueba.
Añadido— 03
- App sidebar indigo gradient: `from-[#312e81] to-[#1e1b4b]` (Harvey/Linear-inspired dark navigation). Logo → `logo-white.svg`. All text/icon colors updated: active = `bg-white/15 text-white`, hover = `hover:bg-white/[0.08] hover:text-white`. Dead CSS variables removed from `globals.css`.
- Widget `allowedOrigins` CORS enforcement: `isOriginAllowed()` helper validates `Origin` header against DB field. Returns 403 for unauthorized origins, reflects exact origin in `Access-Control-Allow-Origin` with `Vary: Origin`. Origin validation placed after auth, before quota.
- Widget IP rate limiting: `rateLimit('widget-ip', { max: 30, windowMs: 60_000 })` on `POST /v1/public/widget/chat`. Prevents abuse from single IPs before auth.
- Widget analytics UI: `GET /v1/api-keys/:id/widget-usage` endpoint. API keys page fetches daily usage in parallel (`Promise.allSettled`). Progress bar with color thresholds (teal ≤75%, amber ≤90%, red >90%).
- Widget key type UI: form pre-selects type from `?type=widget` URL param. Teal "widget" badge on key cards. One-time embed snippet display when new widget key is created.
- `PageIntro` onboarding hints: dismissable amber card (localStorage) added to API Keys, Webhooks, and VeriFactu pages — explains feature purpose on first visit.
- Suspense boundary on `/settings/api-keys/page.tsx` for `useSearchParams` compatibility with Next.js app router.
- Dual CTA on all 6 VS competitor pages: smartlaw, legalitas, tirant, aranzadi, harvey, lefebvre. Primary: "Empezar — 7 días gratis", secondary ghost: "3 preguntas gratis sin registro" → `/chat-gratis`. Removes friction for undecided visitors.
- +2 more
Corrección
- `logo-sidebar.svg` missing: Non-existent file reference in sidebar redesign. Fixed → `logo-white.svg`.
- Dead CSS vars in `globals.css`: 7 unused `--sidebar-bg-*` variables removed (10 lines dead code).
- TypeScript incremental cache false positives: `PageIntro` import errors on different files each run. Root cause: stale `.tsbuildinfo`. Fixed with clean `rm -f tsconfig.tsbuildinfo`.
Añadido— 03
- Embeddable B2B widget (`apps/web/public/embed/widget.js`): 249-line vanilla JS widget, Shadow DOM aislado, zero innerHTML con datos de usuario (XSS-safe). SSE streaming idéntico a `/chat-gratis`. Features: chat bubble, panel expandible, historial de mensajes, citation tags, fade-in al cargar.
- Widget API key type: columnas `keyType` y `allowedOrigins` añadidas a `api_keys` schema. Migración 0007 aplicada. Prefijo `lx_wgt_xxx` para validación rápida en `validateWidgetKey()`.
- `POST /v1/public/widget/chat`: auth por `X-Widget-Key` o `?key=`, cuota 200 preguntas/org/24h en Redis (`widget:org:{orgId}:{date}`), CORS abierto (`Access-Control-Allow-Origin: *`), RAG + citation verification.
- `/integraciones`: nueva tarjeta de widget (paleta teal, MessageSquare icon) + sección de código embed syntax-highlighted. CTA → `/settings/api-keys?type=widget`.
- Sitemap: `/chat-gratis` añadido con priority 0.95 / changeFrequency weekly.
- cláusulas-suelo: CTA mejorado — dual botón (consultar + guía propietarios).
Añadido— 03
- Freemium chat `/chat-gratis`: 3 preguntas legales sin registro — IP-gated via Redis (24h window), SSE streaming, verified citations, limit-reached CTA → subscription. Entry point for top-of-funnel citizen visitors.
- BOE citizen alerts: `boeTopics` jsonb column in `newsletter_subscribers` (migration 0006). `POST /v1/newsletter/boe-subscribe` with 6 topics (trabajo/alquiler/herencias/divorcio/autonomos/consumidor) + double opt-in email. `POST /v1/cron/citizen-boe-digest` weekly cron (Mondays 08:15 UTC) with personalised BOE RSS filtering per topic.
- `CitizenBoeAlertForm` component — topic chip selector (emerald active state) + email input. Added to 6 /para/ pages: particulares, trabajadores, inquilinos, herederos, divorciados, autonomos-y-freelancers.
- Navbar citizen section: "Soluciones" dropdown split into "Para profesionales" + "Para ciudadanos" (6 citizen links). Right-side navbar CTA: "3 preguntas gratis" → `/chat-gratis`.
- `/para/autonomos-y-freelancers`: New citizen landing page (~40K/mo searches). 6 problem cards, 4 FAQs, 5 tool links, OG images, BOE alerts section.
- OG images for /para/ pages: `apps/web/app/api/og/para/route.tsx` edge runtime endpoint. All 7 citizen pages now have social OG images (1200×630, emerald theme).
- BreadcrumbList JSON-LD: All 38 herramienta calculator pages now have BreadcrumbList schema (Google rich results eligible).
- Freemium-first CTAs: `particulares-section.tsx` primary CTA → `/chat-gratis` (secondary → subscription). `herramientas/page.tsx` bottom CTA → dual: chat-gratis + subscription.
Añadido— 03
- 41 herramientas adicionales con CTA: 26 citizen (laborales+fiscales+civiles), 7 split (profesionales mixtos), 8 lawyer-only. Total: 65/65 herramientas tienen CTA. Cero páginas sin conversión.
- Citizen: despido-colectivo, erte, reducciones-jornada, IMV, baja médica/maternidad/paternidad, fogasa, irpf-anual, ganancia-patrimonial, ITP, IBI, plazos, prescripción, intereses, indemnización, RGPD
- Split: honorarios, honorarios-abogado, costas, costes-proceso, tasa-judicial, arancel-procurador, aranceles-notariales
- Lawyer: ai-act, calendario-fiscal, comparador-fiscal, modelos 130/303, roi, seg-social-empresa, test-insolvencia
Añadido— 03
- CU 90.0% (135/150): Cuba cruzó el umbral ≥90% con Claude Sonnet RAG. Was 89.3% with Gemini 2.5-flash. 15 failures residuales (Laboral 6, Penal/Proc.Penal 3, Mercantil 2, Admin/Proc.Civil/Internacional/Laboral). CR+CU now both ≥90%.
- NI Q396 answer key fix: CT Art. 257 establece prescripción laboral en 1 año (opción A), no 3 años. Corrected from C→A. Third NI correction after Q400 (nocturna) and Q448 (PPL). Adjusted NI score to 84.7% (127/150 corrected).
- NI answer key audit (CT Art. 45/51/52/257): Verified Q364 (7h nocturna ✓), Q393 (1mes/año B ✓ best approx of tiered structure), Q395 (inamovilidad C=1año — CF Art.21 supports 18yo capacity but CT inamovilidad unclear), Q411 (CF Art.21 establishes 18yo capacity, C defensible).
Corrección
- NI Q396 wrong answer key: CT Ley 185 Art. 257 = "prescribirán en un año" — not 3 years. Art. 258 extends to 2 years only for occupational accidents (not general labor actions).
- BR benchmark duplicate IDs: `benchmark-brazil-oab-xl.ts` had IDs 1-80 colliding with `benchmark-brazil-oab.ts`. Renumbered XL to 81-160. Score unchanged (88.1%), failure report now clean with unique IDs.
Añadido— 03
- CR 90.0% (135/150): Costa Rica cruzó el umbral ≥90% con Claude Sonnet RAG. Was 88.7% with Gemini RAG-hybrid. Laboral 83.3% (was 66.7%), Civil 100%, Penal 95.8%. Laboral gap was model knowledge, not corpus.
- NI RAG 82.7% (124/150): Claude Sonnet RAG +1.4pp vs no-RAG 81.3%. Laboral 72.7% (+4.5pp), Constitucional 95%, Comercial 94.4%. Still needs Gemini 3.1-pro clean run (86.7% at 75Q).
- BR post-súmulas 87.5% (140/160): +3.7pp improvement from 83.8% pre-súmulas. 68 súmulas STJ/STF working. Admin/Proc.Penal/Tributário/Ambiental/Internacional/ECA/Filosofía 100%. Weak: Penal 70.6%, OAB Ética 81.2%, Trabalho 81.0%. Run with gemini-2.5-flash (3.1-pro rate-limited).
Añadido— 03
- NI 150Q benchmark: 81.3% (122/150) with Claude Sonnet no-RAG. First clean 150Q run — 0 `got=?`. Weak areas: Laboral 68.2%, Civil 77.3%, Procesal Penal 75%. Note: Gemini 3.1-pro rate-limited durante session (75Q baseline was 86.7%).
- Parser bug fix (`benchmark-latam-all.ts`): Added 3-level answer extraction — (1) word-boundary letter `\b([ABCD])\b`, (2) option text fuzzy match via `text.includes()`, (3) last resort `[ABCD]` anywhere. Fixes `got=?` for short options like percentages, durations, years.
Corrección
- Concurrent benchmark rate limiting: Identified root cause of `got=?` cascade — multiple concurrent Gemini API calls exhaust all 3 fallback models → silent `'?'` return. Fix: run benchmarks sequentially or use Claude for non-concurrent scenarios.
Añadido— 03
- HN no-RAG optimization: `FORCE_NO_RAG_COUNTRIES` set in `benchmark-latam-all.ts` for GT/HN/SV/EC/BO/VE. HN: 86.7% → 92.7% (+6pp, 139/150). Verified: small-corpus countries hurt by RAG noise.
- SV no-RAG optimization: 89.3% → 94.0% (+4.7pp, 141/150 no-RAG). Both HN and SV now ≥ 90% threshold.
- CU + NI extended to 150Q: `benchmark-cuba-150q.ts` (Q451–Q525, 75Q: Mercantil/Penal/Laboral/Familia/Tributario/Procesal Penal) and `benchmark-nicaragua-150q.ts` (Q376–Q450, 75Q: Proc.Civil Ley 902/Laboral/Mercantil/Civil/Tributario). Both verified 0 duplicate IDs.
- BR Súmulas STJ+STF: `ingest-brazil-sumulas.ts` ingests 38 STJ + 30 STF critical súmulas (68 total), targeting OAB exam failures in Empresarial (Súmula 233/258), Civil, Penal, Tributario.
Corrección
- benchmark-latam-all.ts mode field: Changed `'rag'` → `'rag-hybrid'` for default runs to accurately reflect the hybrid retrieval strategy.
Añadido— 03
- E2E GitHub Actions workflow (`.github/workflows/e2e-app.yml`): PostgreSQL service container, dev-login auth bypass (`NODE_ENV=test`, no Keycloak), local filesystem R2 fallback, `wait-on` readiness polling, 7 test files, Playwright artifact upload on failure.
- HN Código de Familia: Ingested from OAS PDF (893KB → 125,966 chars, 371 chunks). Fixes Q152 (edad matrimonio 18 años), Q164 (unión de hecho 2 años), Q166 (liquidación bienes).
Corrección
- HN duplicate Código del Trabajo: Deleted non-consolidated 478K entry (`e9cc8b00`) — kept consolidado 726K. Eliminates RAG confusion from duplicate sources.
- HN benchmark: 86.7% → 90.6% (+3.9pp, 135/149) after Familia corpus + 4 answer key corrections.
- SV benchmark: 89.3% → 89.3% (Q296 now passes; LLM variance holds overall score).
Añadido— 03
- 15 herramientas ciudadanas con CTA: finiquito, desempleo, jubilacion, nomina, incapacidad-permanente, incapacidad-temporal, baremo-accidente, herencia, hipoteca, horas-extraordinarias, clausulas-suelo, actualizacion-renta, fogasa, pension-viudedad, excedencia — todos con sección CTA `?plan=particular&interval=year` emerald.
- Split CTAs en cuota-autonomo y procedimientos — 2 cards (ciudadano 9,90€ / abogado 69€).
- isd-sucesiones: primera sección CTA añadida (calculadora impuesto sucesiones, muy citizen-oriented).
- `/para/propietarios-alquiler`: nueva landing page desahucio/LAU (~30K/mes "desahucio España") con 6 tarjetas, 4 FAQs JSON-LD.
Añadido— 03
- `/para/herederos`: Landing page herencia/sucesiones (40K+ búsquedas/mes) — 6 tarjetas de problema, 4 FAQs con JSON-LD FAQPage, CTA emerald `?plan=particular&interval=year`.
- `/para/divorciados`: Landing page divorcio/familia (60K+ búsquedas/mes) — 6 tarjetas (pensión compensatoria, custodia, vivienda, convenio regulador, régimen económico, modificación medidas), 4 FAQs con JSON-LD FAQPage.
- Sitemap: 2 nuevas páginas ciudadanas (priority 0.9, changeFrequency monthly).
- CTAs herramientas → particular: pension-alimenticia, convenio-regulador, sanciones-aeat, burofax actualizados a `?plan=particular&interval=year` con color emerald.
Corrección
- vs/legalitas particular CTA: Faltaba `&interval=year` en el CTA del plan Particular.
Añadido— 03
- 3 citizen SEO landing pages (B001 captación): `/para/inquilinos` (6 problemas fianza/alquiler/vicios/cláusulas), `/para/trabajadores` (despido/horas/mobbing/finiquito), `/para/consumidores` (garantía/cláusula abusiva/estafa/desistimiento). Todos con FAQ JSON-LD Schema.org, CTA `?plan=particular&interval=year`, cross-links desde `/para/particulares`.
- Stripe webhook `checkout.session.expired`: Añadido a live y test webhooks via REST API — ambos con 10 eventos. Recuperación de carrito abandonado activa en producción.
- Sitemap: 3 nuevas páginas ciudadanas (priority 0.88, changeFrequency monthly).
Corrección
- Sidebar label 'Trabajo legal' → 'Alertas BOE' para ciudadanos: Añadido `particularLabelEs`/`particularLabelEn` al type `NavGroup` — grupo `legal` mostraba terminología de abogados a ciudadanos.
- trial-warning cron excluía plan 'particular': Ciudadanos con 7-day trial nunca recibían email pre-cobro. Añadida ventana d5-d6 para `particular` junto a d12-d13 para planes abogado.
- Email `trialEndingHtml`: `planLabel` 'Básico'→'Particular', `displayName` 'abogado/a'→'usuario/a', copy de continuidad correcto para ciudadanos.
- CTAs ciudadanas en herramientas: justicia-gratuita, embargo-salario, plusvalia → `?plan=particular&interval=year` con copy "Consulta tu caso por 9,90€/mes".
Corrección— 03
- IDOR removed from Academy public router: `GET /public/academy/progress/:email` allowed anyone to enumerate any user's quiz progress by email. Endpoint removed; re-add properly when guest token system exists.
- Academy register no-op fixed: `POST /public/academy/register` was returning success without saving to DB. Now upserts a lead record in `academyUserProgress` with name + bar association.
- LATAM-aware abandoned checkout recovery email: Recovery email no longer shows hardcoded EUR price (e.g., "9,90€/mes") to LATAM users. Spain sees price; LATAM users see just the plan name — correct local price shown on start page after clicking CTA.
- Academy rate limiting: Added missing rate limits to public quiz (10/min), register (5/min), and certificate verify (20/min) endpoints.
- Academia page NewsletterSection prop: Fixed prop mismatch — component takes `locale` not `dict`.
- env.example LATAM particular prices: All 20 LATAM particular plan price IDs (MX/CO/CL/AR/PE, test + live) added to `.env.example`; were missing despite being in production.
Añadido
- Academy seed data: `packages/db/src/seed/academy-seed.ts` — full "Certificación IA Legal" course with 5 modules, quiz questions, and presenter scripts (M1–M5).
- B010 E2E tests marked complete: All 4 critical test files were already committed; status updated in FEATURE_TRACKING.
Añadido— 03
- Abandoned checkout recovery (B006): Handle `checkout.session.expired` Stripe webhook — sends recovery email with direct link back to `/start?plan=X` if user entered email but didn't pay. Typically recovers 8-15% of abandoned checkouts.
- Marketplace deep-link fix: `/derivaciones/:id` page now redirects to `/derivaciones?ref=id` so email notification links work correctly (were 404). Card auto-expands + scrolls into view + shows brand ring border.
- Upgrade modal citizen UX: `UpgradeModal` detects `particular` plan users and shows limit-reset date + referral CTA instead of irrelevant lawyer plan options (starter/professional/team).
- vs/legalitas SEO citizen segment: Updated title/description/keywords to target both lawyers AND citizens. Added citizen FAQ entry, green CTA for Plan Particular (9,90€/mes), updated hero copy.
- LATAM corpus preselection (all 20 pages): All LATAM country landing pages now pass `?corpus=XX` to `/start` links (was: all pointing to bare `/start` defaulting to Spain).
- locale in Stripe session metadata: Added `locale` field to checkout session metadata for correct language selection in recovery/notification emails.
Añadido— 03
- DO 92% / GT 91.3% / HN 86.7% / SV 89.3% (150Q): All 4 countries extended from 75Q to 150Q. New questions cover Procesal, Familia, Tributario, Mercantil. Results saved to benchmark_runs.
- UY CPP (Ley 19.293) — 420 chunks: Ingested from Parlamento.gub.uy (free alternative to IMPO subscription). UY corpus now covers Derecho Procesal Penal.
- benchmark-latam-do-gt-hn-sv-150q.ts: 300 new legal questions (75 × DO/GT/HN/SV)
Corrección
- Usage limit message for particular: Changed "Amplía tu plan para seguir consultando" to "Se renueva el 1 del próximo mes" — `particular` users can't upgrade to lawyer plans.
Añadido— 03
- Academia landing page: Full `/academia` page (ES+EN), course catalog, certification tracking, navbar "Recursos" dropdown entry. Routes registered (`academia`/`academy`). DB schema: `academy_courses`, `academy_certificates`, `academy_modules`, `academy_lessons`, `academy_quiz_questions`, `academy_user_progress` — migration applied ✅
- UY 92.7% (150Q RAG): Uruguay 139/150. First 150Q run for UY. CPP source gap noted (IMPO subscription required)
- LATAM CTA corpus preselection: All 20 LATAM country pages pass `?corpus=XX` to `/start` CTA links for better onboarding (preselects correct legal corpus)
Añadido— 03
- MX 96% (150Q RAG-hybrid): México 144/150, 140K chunks. Q57 fixed (SCJN 11→9 reforma judicial 2024)
- CO 96.7% (150Q RAG-hybrid): Colombia 145/150, 740K chunks — best LATAM individual score this session
- CR 88.7% (150Q RAG-hybrid): Costa Rica improved from 86.7%/75Q. 16 source titles fixed (SCIJ→proper names), reindex 7,110 chunks, NA-option bug fixed (3-option questions showed '(opción no disponible)' as D), Q73/Q74 benchmark corrected
- NI 86.7% (75Q RAG): Nicaragua post-fix from 80%. 5 benchmark errors corrected (Q315/318/329/330/333 — confirmed against CP/CPP corpus)
- PE 98.7% + CU 93.3%: Confirmed post-fix (Q47 PE + Q395 CU corrected)
- DO 96% no-RAG saved: baseline saved to benchmark_runs; no-RAG > rag-hybrid (-1.3pp — model knows RD law baseline)
- BR source title fixes (total 109): 24 raw filenames + 15 "L-number" titles corrected to proper law names for RAG quality
Corrección
- benchmark-latam-all NA filter: 3-option questions (Costa Rica Colegio Abogados exam format) were showing '(opción no disponible)' as option D to the model. Fixed by filtering before passing to Gemini and dynamically adjusting valid letters in prompt. CR Deontología: 75%→87.5%, Familia: 77.8%→94.4%
- Benchmark MX Q57: SCJN ministers 11→9 after 2024 judicial reform (art. 94 CPEUM amended)
- Benchmark CR Q73: Answer B→D — CT art. 507 defines 'conflicto jurídico' as interpretación/aplicación de norma (D), not example (B)
- Benchmark CR Q74 (official): Answer B→C — funcionarios sin derecho a huelga → arbitraje obligatorio (CT art. 397)
- Duplicate NI benchmark entry: Removed duplicate benchmark_runs entry from concurrent session saves
- UY CPP bad source: Deleted source with "Acceso no válido" content (IMPO subscription required), cleaned 1 orphan embedding
Corrección
- Auth token null-safety (Critical — Rodrigo bug): Added `api.getAuthHeaders()` method to ApiClient; replaced 20+ raw `Authorization: Bearer ${api.getToken()}` patterns across all fetch calls (documents, chat, deadlines, writing, burofax, etc.) — was sending `Bearer null` on first render before auth initialized
- listCasesQuerySchema missing practiceArea values: `fiscal`, `propiedad_intelectual`, `internacional` now valid as filter params (were returning 400)
- recordUsage non-blocking: Billing counter failure no longer returns 500 and orphans already-uploaded documents
- legal-opinion-modal: Token fallback to empty string removed — uses getAuthHeaders() consistently
Añadido
- B001 Plan Particular — Phase 1 MVP (session 23): Full citizen (non-lawyer) plan implementation: - DB: `planEnum` + `'particular'` value, Drizzle migration applied, `PLAN_LIMITS.particular` (30 queries/mo, 5 docs) - AI: `PARTICULAR_MODE_SUFFIX_ES/EN` (plain language + mandatory disclaimer), `particularMode` flag propagated through `streamChat()`, `streamMultiModelChat()`, Gemini path, and conversations route - Sidebar: `particularHidden` prop hides 20+ lawyer-only items (cases, clients, billing, calendar, team, etc.) for plan particular - Onboarding: 2-step citizen flow — situation selector (laboral/vivienda/familia/consumo/herencia/otro) → chat; skips lawyer profile form - Marketing: `/para/particulares` landing page with JSON-LD Product schema, pricing comparison (7€ vs 69€), 6 use cases, 6 benefits, social proof - Billing: `PARTICULAR_MONTHLY/YEARLY_PRICE_ID` env vars in stripe.ts; checkout route already accepted `particular`
- Benchmarks DO+PA: República Dominicana and Panamá (150 questions), benchmark-latam-all updated
- SEO/LLM discoverability: JSON-LD Organization+WebSite schema, llms.txt link rel, LLM crawlers opened
Añadido
- 13 LATAM Countries Live: GT, CL, PE, EC, PY, VE, BO, SV, HN, UY activated (from 3 to 13)
- E2E Playwright Tests: Chat, invoicing, tools, landing (35 tests, 460 lines)
- Web Vitals → PostHog: LCP, CLS, INP, TTFB performance monitoring
- Sentry Client Configs: Complete error tracking for both frontends with session replay
- Expanded Corpus: UY +10 labor laws, NI +3 sources, CR +22 sources from SCIJ
Corrección
- RAG Country-Scoped Search (Critical): Fixed 0-context bug for non-ES/MX/CO countries
- RAG Corpus Gate: 50K → 2K threshold — 9 countries regained citation context
- Email Templates: All 11 remaining → emailShell (Noir Refinado)
Cambio
- Migration Squash: 97 → 1 baseline (-1.2M lines)
- Marketing Data Sync: 20 countries, 130K sources, 2.59M chunks (zero stale claims)
- 11 LATAM Landing Pages: Real corpus data from DB
- Missions System: Unified onboarding + engagement checklist inspired by Stripe Setup Guide - 27 missions across 2 tracks: Lawyer (15, 4 secciones) + Partner (12, 3 secciones) - 9 badges with points gamification (225 pts per track) - Auto-detection engine with 5 JSONB condition types, hooked into 13 API handlers - Floating Stripe-style widget (expanded/collapsed/closed), stacked below Lex Assistant - Admin panel for mission management and user progress tracking - Badge unlock toast notifications with slide-up animation - i18n support (ES + EN) - Replaces previous setup-checklist.tsx component