01 · Resumen ejecutivo

Vista individual ejecutiva: qué resolver, qué vence hoy, qué evidencia falta, qué impacto Score tiene

FARO-UI-002 es la pantalla donde FARO deja de ser "inteligencia ejecutiva" y se convierte en "disciplina operativa". El responsable entra, ve sus acciones del día y trabaja. No mira gráficos: ejecuta.

La Bandeja de Tensiones (FARO-UI-001) responde la pregunta de la empresa: ¿qué tensiones tenemos abiertas y cómo están priorizadas? El Dashboard Responsable responde la pregunta del individuo: ¿qué tengo que resolver hoy, qué se me vence, qué evidencia me falta, qué impacto Score tiene mi acción?

Sin esta vista, FARO puede detectar mucho y ejecutar poco. El sistema se llenaría de tensiones brillantes sin nadie persiguiendo el cierre. Perseguir manualmente tareas críticas es un deporte caro: por eso esta UI existe como tope obligatorio del pipeline canónico.

Cada responsable de Empresa Demo Cuyo S.A. entra cada mañana y ve, en este orden:

  1. Lo vencido critical: rojo arriba de todo, con badge Vencida y motivo de demora si lo hay.
  2. Lo vencido: resto de acciones fuera de SLA, ordenadas por fecha de vencimiento.
  3. Lo que vence hoy: ámbar, urgencia inmediata.
  4. Lo crítico no vencido: prioridad critical, aunque tenga 5 días para resolver.
  5. Lo de alta prioridad: high dentro de la semana.
  6. El resto: medium y low del período.

Esta priorización no es decorativa: es la cláusula ORDER BY de la query principal de faro.v_responsible_action_dashboard (ver sección 8). Si la UI muestra otro orden, está rota y se rechaza.

El dashboard cumple tres reglas duras del MVP:

  • No cerrar sin evidencia: el botón Cerrar está deshabilitado mientras evidence_missing_count > 0 en acciones con evidence_required = true. Sin respaldo no hay cierre.
  • No mostrar acciones ajenas: el endpoint /api/v1/me/actions usa session.user_id; no acepta responsible_user_id libre por query param. Gerentes y directores usan endpoints distintos con permisos superiores.
  • Status enum único: los 10 estados (new, in_progress, blocked, waiting_evidence, in_review, approved, closed, expired, cancelled, rejected) están coordinados con WF-001 · Workflow de Acción. El evento escalated es un evento del timeline (D4), no un estado terminal.

El UI vive en app/faro/my-actions/page.tsx, consume faro.v_responsible_action_dashboard y se alimenta con datos de Empresa Demo Cuyo S.A. en el seed canónico (FARO-SQL-003.1). Sin este módulo el sistema detecta problemas pero depende de que alguien los persiga manualmente, y eso ya sabemos cómo termina.

02 · UI-001 Bandeja vs UI-002 Dashboard

Empresa vs individual: dos pantallas que no compiten

UI-001 ordena el mapa. UI-002 mueve la ejecución. Mezclar ambas en una sola pantalla termina en un dashboard genérico que sirve para nada porque pretende servir para todo.

FARO-UI-001 · BANDEJA

Bandeja de Tensiones

Vista de la empresa. Lectura ejecutiva para dirección, gerencia general y validadores.

Pregunta
¿Qué tensiones tiene la empresa?
Usuario
Dirección, gerencia general, validadores
Foco
Mapa colectivo de tensiones abiertas
Eje
Tensión → severidad → área
Acción
Asignar responsable, priorizar, escalar
Endpoint
GET /api/v1/tensions
Vista SQL
v_tension_board
FARO-UI-002 · DASHBOARD

Dashboard Responsable

Vista individual ejecutiva. Cada responsable ve solo lo suyo, priorizado por urgencia y criticidad.

Pregunta
¿Qué tengo que hacer yo hoy?
Usuario
Responsable operativo, CFO, COO, áreas
Foco
Lista individual de acciones asignadas
Eje
Acción → vencimiento → evidencia
Acción
Iniciar, cargar evidencia, enviar a revisión, escalar bloqueo
Endpoint
GET /api/v1/me/actions
Vista SQL
v_responsible_action_dashboard

Regla canónica. La Bandeja muestra tensiones; el Dashboard muestra acciones. Una tensión puede tener N acciones, cada una con su responsable. La Bandeja se filtra por empresa, área y severidad. El Dashboard se filtra implícitamente por session.user_id y explícitamente por estado, prioridad, vencimiento y búsqueda.

03 · 7 tipos de usuario

Responsabilidades reales por rol

El Dashboard sirve a 7 perfiles operativos en Empresa Demo Cuyo S.A. Cada uno ve un subset distinto del catálogo de acciones canónicas (FARO-SQL-005) según el rol asignado en action_definitions.default_owner_role y el responsable concreto en actions.responsible_user_id.

Rol 01 · CFO ~12 acciones/sem

CFO · Finanzas

Cobranza, mora, caja proyectada, riesgo crediticio, dependencia de pocos clientes en cartera. Filtra area_code = finance.

  • Acciones ACT-FIN-001 a ACT-FIN-008
  • Tensiones TNS-004, TNS-005, TNS-017, TNS-019
  • Evidencia: cobros aplicados, gestión documentada
Rol 02 · COO ~8 acciones/sem

COO · Operaciones

Visión transversal: acciones cruzadas entre áreas, decisiones operativas pendientes, escalamientos recibidos de gerencias funcionales.

  • Acciones ACT-OPS-001, ACT-OPS-002, ACT-DIR-001
  • Tensiones TNS-009, TNS-010
  • Evidencia: minutas, decisiones registradas, plan correctivo
Rol 03 · Comercial ~15 acciones/sem

Gerencia Comercial

Descuentos, vendedores erosionando margen, política comercial, sucursales fuera de plan, concentración de clientes. Filtra area_code = commercial.

  • Acciones ACT-COM-001 a ACT-COM-006
  • Tensiones TNS-001, TNS-002, TNS-003, TNS-011, TNS-013
  • Evidencia: política firmada, comunicación interna, ajuste pricing
Rol 04 · RRHH ~5 acciones/sem

RRHH · Capital humano

Acciones de capacitación derivadas de tensiones operativas, asignación de responsables faltantes, soporte a gerencias en cambios estructurales.

  • Acciones derivadas (no canónicas en MVP)
  • Tensiones TNS-029 (responsable no asignado)
  • Evidencia: plan capacitación, asignación formal
Rol 05 · Stock ~10 acciones/sem

Gerencia Stock / Logística

Quiebres con venta perdida, reposición, inmovilizado, alta rotación crítica. Filtra area_code = stock.

  • Acciones ACT-STK-001 a ACT-STK-005
  • Tensiones TNS-006, TNS-007, TNS-008, TNS-021
  • Evidencia: orden de compra, reposición ejecutada, ajuste mínimo
Rol 06 · IT / Data Owner ~6 acciones/sem

Data Owner · IT

Fuentes atrasadas, KPIs de baja confianza, integraciones rotas, calidad de datos. Las únicas tensiones que un Data Owner debe ver y resolver.

  • Acciones de integración (derivadas)
  • Tensiones TNS-026, TNS-027, TNS-028
  • Evidencia: pipeline ejecutado, sync confirmado, QA pasado
Rol 07 · Calidad ~4 acciones/sem

Calidad · Aprobador

Revisión y aprobación de evidencia subida por otros responsables. No carga evidencia propia; valida y aprueba o rechaza con motivo.

  • Cola: acciones en estado in_review
  • Cualquier TNS-* con acción aprobada por su rol
  • Evidencia: revisada y firmada digitalmente

Cada rol mapea a faro.user_roles.role_code. El Dashboard no presenta acciones de otros roles aunque el usuario tenga curiosidad: la query del endpoint filtra por responsible_user_id = $session.userId. Para mirar acciones de otro responsable se requiere rol superior y endpoint distinto (GET /api/v1/responsibles/:user_id/actions).

04 · 13 casos de uso

Lo que el responsable hace en la pantalla

Los 13 casos de uso principales del Dashboard. Cubren lectura (ver), escritura (modificar) y escalamiento (cuando no puede resolver). Si alguno no está soportado por la UI, hay un bug funcional.

Caso Acción Descripción Tipo
UC-01Ver mis accionesLista completa de acciones asignadas al usuario en el período activo.Lectura
UC-02Ver vencidasFiltrar por due_bucket = overdue. Cards marcadas con badge Vencida y borde coral.Lectura
UC-03Ver vencen hoyFiltrar por due_bucket = today. Cards en ámbar fuerte. Urgencia inmediata.Lectura
UC-04Ver por prioridadToggle multi-select sobre critical, high, medium, low.Lectura
UC-05Ver por tensiónBuscador acepta tension_code (ej. TNS-001) y filtra acciones vinculadas.Lectura
UC-06Ver evidencia requeridaPanel derecho lista las EVD-* que faltan para cerrar la acción seleccionada.Lectura
UC-07Cargar evidenciaAbre modal de carga (FARO-UI-004). Adjunta archivo, comentario o validación según trust level.Escritura
UC-08Cambiar estadonewin_progresswaiting_evidencein_review. Transiciones controladas por WF-001.Escritura
UC-09Pedir validaciónEnvía acción a estado in_review. Notifica al aprobador asignado por email + push.Escritura
UC-10Escalar bloqueoGenera evento action_escalated en timeline (D4). Levanta acción a jefe directo + alerta dirección.Escalamiento
UC-11Justificar demoraSolicita extensión de fecha. Requiere motivo y nueva fecha propuesta. Aprueba gerencia.Escalamiento
UC-12Ver impacto ScoreBloque ScoreRecoveryBlock muestra +min a +max puntos potenciales si se cierra la acción.Lectura
UC-13Ver historialTimeline de eventos de la acción y de la tensión asociada. Auditoría completa.Lectura

UC-01 a UC-06 y UC-12 a UC-13 son obligatorios para MVP 1. UC-07 a UC-11 caen en MVP 2 (workflow completo). UC-13 con eventos enriquecidos de timeline depende de la tabla faro.action_events (ver sección 7).

05 · Layout + 15 componentes

Estructura visual y árbol de componentes React

El layout es desktop-first con grilla 430px + 1fr a partir de lg:. En mobile la lista pasa a fila única y el detalle se abre como pantalla completa o bottom sheet. Las tablas en celular son el Excel vengándose del usuario.

Esquema de layout desktop

▸ Layout desktop · lg breakpoint
┌────────────────────────────────────────────────────────────────┐
│ Header: Mi ejecución FARO                                      │
│ Responsable · Período · Score asociado · Acciones críticas      │
├────────────────────────────────────────────────────────────────┤
│ Resumen ejecutivo (5 métricas)                                 │
│ Total · Vencidas · Hoy · Críticas · Recuperación Score          │
├────────────────────────────────────────────────────────────────┤
│ Filtros: prioridad · vencimiento · estado · búsqueda            │
├────────────────────────────┬───────────────────────────────────┤
│ Columna izquierda (430px) │ Panel derecho (1fr)                │
│ ──────────────────────────│ ──────────────────────────────────│
│ Mis acciones (lista)      │ Detalle acción seleccionada        │
│                           │                                    │
│ ┌─────────────────────┐   │ ┌────────────────────────────────┐ │
│ │ ACT-COM-001         │   │ │ Encabezado + Score recovery    │ │
│ │ Crítica · Vencida   │   │ ├────────────────────────────────┤ │
│ │ Revisar política…   │   │ │ Propósito ejecutivo            │ │
│ │ TNS-001 · …         │   │ ├────────────────────────────────┤ │
│ └─────────────────────┘   │ │ Criterio de cierre             │ │
│ ┌─────────────────────┐   │ ├────────────────────────────────┤ │
│ │ ACT-FIN-001         │   │ │ Tensión asociada (link a UI-001)│ │
│ │ Alta · Vence hoy    │   │ ├────────────────────────────────┤ │
│ │ Gestionar mora…     │   │ │ Evidencia requerida (lista)    │ │
│ └─────────────────────┘   │ ├────────────────────────────────┤ │
│ ┌─────────────────────┐   │ │ Aprobador + Vencimiento        │ │
│ │ ACT-STK-001         │   │ ├────────────────────────────────┤ │
│ │ Alta · Esta semana  │   │ │ Botones: Iniciar · Cargar EVD  │ │
│ │ Reponer stock…      │   │ │ Enviar revisión · Escalar      │ │
│ └─────────────────────┘   │ └────────────────────────────────┘ │
└───────────────────────────┴────────────────────────────────────┘

Layout mobile (< 900px)

▸ Layout mobile · prioridad: entender → vencimiento → cargar evidencia
┌─────────────────────────┐
│ Header compacto         │
│ Métricas en 2 columnas  │
├─────────────────────────┤
│ Filtros tipo pills      │
│ (scroll horizontal)     │
├─────────────────────────┤
│ Lista de acciones       │
│ ┌─────────────────────┐ │
│ │ ACT-COM-001         │ │
│ │ Crítica · Vencida   │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ ACT-FIN-001         │ │
│ │ Alta · Hoy          │ │
│ └─────────────────────┘ │
│                         │
│ Tap acción              │
│ → detalle pantalla      │
│   completa o bottom     │
│   sheet                 │
│ → botón "Cargar         │
│   evidencia" fijo abajo │
└─────────────────────────┘

15 componentes React canónicos

Cada componente tiene una responsabilidad acotada. Si un componente hace dos cosas, se divide. La carpeta components/responsible/ contiene todo lo específico de UI-002; helpers compartidos (priority, action-status, formatters) viven en lib/faro/.

ResponsibleDashboardPage
Página

Componente raíz. Gestiona estado de filtros, carga inicial, selección de acción y orquesta los hijos. Single source of truth de la pantalla.

ResponsibleSummaryHeader
Header

Eyebrow + título + sub + 5 métricas (Total, Vencidas, Hoy, Críticas, Recuperación). Lectura ejecutiva de 3 segundos.

ResponsibleActionFilters
Controles

Búsqueda + toggles multi-select de prioridad, vencimiento, estado. Estado controlado por el padre; emite onChange.

ResponsibleActionList
Lista

Aside scrolleable. Renderiza N ResponsibleActionCard y propaga selección. Max-height calculada para no romper viewport.

ResponsibleActionCard
Card

Card compacta por acción. Badges de prioridad y vencida, título, código TNS asociado y 3 chips: Estado, Evidencia faltante, Score recovery.

ResponsibleActionDetail
Panel

Panel derecho. Encabezado + propósito + criterio de cierre + tensión + evidencia + aprobador + vencimiento + quick actions.

LinkedTensionBlock
Bloque

Muestra la tensión asociada: código TNS, título, pregunta de negocio, diagnóstico ejecutivo y priority_score numérico.

RequiredEvidenceBlock
Bloque

Lista de evidencias requeridas con su estado (missing, submitted, approved, rejected, needs_more_info). Color-coded por trust_level.

EvidenceUploadBox
Modal

Modal de carga de evidencia. Acepta archivo, comentario, captura o validación de aprobador. Bloquea cierre si trust_level no se respeta.

ActionTimeline
Bloque

Timeline vertical de eventos (action_created, action_started, evidence_uploaded, sent_to_review, action_escalated, etc.).

ActionQuickActions
Botones

CTA principal: Iniciar, Cargar evidencia, Enviar a revisión, Escalar bloqueo. Visibilidad condicionada por estado actual de la acción.

BlockedReasonBox
Bloque

Si la acción está blocked, muestra motivo, fecha de bloqueo y responsable de desbloqueo. Sin estos campos la UI rechaza el estado.

ScoreRecoveryBlock
Bloque

Bloque destacado en el encabezado del detalle. Muestra +min a +max puntos. Si la acción está vencida, indica que la recuperación está bloqueada.

EmptyResponsibleState
Empty

Estado vacío. Mensaje sobrio: "No tenés acciones activas asignadas en este período". Nunca dice "todo perfecto".

OverdueWarning
Alerta

Banner superior si summary.overdue > 0. Mensaje: "Tenés N acciones vencidas. Sugerencia: priorizar arriba o escalar".

06 · Data contract

Endpoint GET /api/v1/me/actions

Endpoint principal del Dashboard. Devuelve el contrato JSON completo: usuario, período, summary, score y items (lista de acciones). Todo lo que la pantalla necesita en una sola request.

Query params soportados

ParamTipoDescripciónEjemplo
statuscsv enumFiltro por estado de acción. Lista de valores separados por coma.new,in_progress,waiting_evidence
prioritycsv enumFiltro por prioridad. Valores: low, medium, high, critical.critical,high
duecsv enumFiltro por bucket de vencimiento. Valores: overdue, today, this_week, later.overdue,today
tension_codetextFiltro exacto por código TNS canónico.TNS-001
area_codetextFiltro exacto por área funcional.commercial
qtextBúsqueda libre. Match parcial sobre title, action_code y tension_code.descuento
pageintPaginación. Default: 1.1
page_sizeintTamaño de página. Default: 50, máximo: 200.50

Request de ejemplo

▸ HTTP
GET /api/v1/me/actions?status=new,in_progress,waiting_evidence&priority=critical,high&due=overdue,today HTTP/1.1
Host: app.farodireccion.com
Accept: application/json
Cookie: faro_session=<jwt>

Response esperado (JSON canónico)

▸ JSON · 200 OK
{
  "user": {
    "user_id": "12000000-0000-0000-0000-000000000002",
    "full_name": "María Fernández",
    "role": "commercial_manager"
  },
  "period": {
    "period_start": "2026-05-01",
    "period_end": "2026-05-31",
    "label": "Mayo 2026"
  },
  "summary": {
    "total": 8,
    "overdue": 2,
    "due_today": 1,
    "critical": 3,
    "high": 4,
    "waiting_evidence": 3,
    "in_review": 1,
    "blocked": 1,
    "closed_this_week": 2
  },
  "score": {
    "potential_recovery": 16,
    "blocked_recovery": 5,
    "recovered_this_week": 4
  },
  "items": [
    {
      "action_id": "23000000-0000-0000-0000-000000000001",
      "action_code": "ACT-COM-001",
      "title": "Revisar política de descuentos",
      "description": "Analizar descuentos aplicados por vendedor, producto, cliente y sucursal.",
      "status": "new",
      "priority": "critical",
      "due_date": "2026-06-06",
      "is_overdue": false,
      "due_bucket": "this_week",
      "action_type": "corrective",
      "closure_criteria": "Nueva política de descuentos aprobada por dirección y comunicada al equipo comercial.",
      "expected_impact": "Mejora de margen, disciplina comercial y rentabilidad.",
      "expected_score_recovery_min": 3,
      "expected_score_recovery_max": 8,
      "tension": {
        "tension_id": "22000000-0000-0000-0000-000000000001",
        "tension_code": "TNS-001",
        "title": "Crecimiento no rentable",
        "severity": "critical",
        "priority_score": 92,
        "score_impact": -8.5
      },
      "evidence": {
        "required": true,
        "required_codes": ["EVD-007", "EVD-012"],
        "missing": 2,
        "submitted": 0,
        "approved": 0,
        "requirements": [
          {
            "evidence_code": "EVD-007",
            "name": "Cambio de política",
            "trust_level": "critical",
            "status": "missing"
          },
          {
            "evidence_code": "EVD-012",
            "name": "Validación de dirección",
            "trust_level": "critical",
            "status": "missing"
          }
        ]
      },
      "approver": {
        "user_id": "12000000-0000-0000-0000-000000000001",
        "full_name": "Tomás Pombo"
      }
    }
  ]
}

Endpoint detalle individual

Cuando el usuario selecciona una acción y se requiere información extendida (timeline completo, descripción de evidencia con definición, business question de la tensión), se llama a:

▸ HTTP
GET /api/v1/actions/:action_id HTTP/1.1
Host: app.farodireccion.com
Accept: application/json

El response amplía el item base con tension.business_question, tension.executive_diagnosis, evidence_requirements[].description y timeline[] de eventos auditados (vienen de faro.action_events).

07 · Vista SQL canónica

Vista faro.v_responsible_action_dashboard

Vista materializada lógica (no MATERIALIZED VIEW) que enriquece faro.actions con definición de catálogo (FARO-SQL-005), tensión asociada (FARO-SQL-004 + datos), usuarios responsables y aprobadores, y conteo agregado de evidencia desde v_action_evidence_status.

Por qué vista, no tabla. Las acciones cambian de estado seguido; un campo derivado como is_overdue o due_bucket calculado en una columna persistida quedaría desincronizado. La vista garantiza que cada lectura refleja CURRENT_DATE al momento del query.

▸ SQL · V034__create_v_responsible_action_dashboard.sql
CREATE OR REPLACE VIEW faro.v_responsible_action_dashboard AS
SELECT
  a.company_id,
  a.action_id,
  a.action_code,
  ad.name AS catalog_name,
  a.title,
  a.description,
  a.action_type,
  a.status,
  a.priority,
  a.due_date,
  CASE
    WHEN a.status NOT IN ('closed', 'cancelled', 'rejected')
     AND a.due_date < CURRENT_DATE THEN true
    ELSE false
  END AS is_overdue,
  CASE
    WHEN a.due_date < CURRENT_DATE THEN 'overdue'
    WHEN a.due_date = CURRENT_DATE THEN 'today'
    WHEN a.due_date <= CURRENT_DATE + INTERVAL '7 days' THEN 'this_week'
    ELSE 'later'
  END AS due_bucket,

  a.responsible_user_id,
  ru.full_name AS responsible_name,
  ru.email AS responsible_email,

  a.approver_user_id,
  au.full_name AS approver_name,
  au.email AS approver_email,

  a.evidence_required,
  a.closure_criteria,
  a.expected_impact,
  a.expected_impact_amount,

  ad.executive_purpose,
  ad.success_metric,
  ad.expected_business_impact,
  ad.expected_score_recovery_min,
  ad.expected_score_recovery_max,
  ad.score_dimension AS action_score_dimension,

  t.tension_id,
  t.tension_code,
  t.title AS tension_title,
  t.severity AS tension_severity,
  t.priority_score AS tension_priority_score,
  t.score_impact AS tension_score_impact,
  td.business_question,
  td.executive_diagnosis,
  td.area_code,
  td.module_code,

  COALESCE(ev_summary.required_count, 0) AS evidence_required_count,
  COALESCE(ev_summary.submitted_count, 0) AS evidence_submitted_count,
  COALESCE(ev_summary.approved_count, 0) AS evidence_approved_count,
  GREATEST(
    COALESCE(ev_summary.required_count, 0) - COALESCE(ev_summary.approved_count, 0),
    0
  ) AS evidence_missing_count,

  a.payload

FROM faro.actions a

LEFT JOIN faro.action_definitions ad
  ON ad.action_code = a.action_code
 AND ad.status = 'active'

LEFT JOIN faro.tensions t
  ON t.tension_id = a.tension_id
 AND t.company_id = a.company_id

LEFT JOIN faro.tension_definitions td
  ON td.tension_code = t.tension_code
 AND td.status = 'active'

LEFT JOIN faro.users ru
  ON ru.user_id = a.responsible_user_id

LEFT JOIN faro.users au
  ON au.user_id = a.approver_user_id

LEFT JOIN (
  SELECT
    aes.company_id,
    aes.action_id,
    COUNT(*) AS required_count,
    COUNT(*) FILTER (WHERE aes.evidence_status IN ('submitted', 'approved')) AS submitted_count,
    COUNT(*) FILTER (WHERE aes.evidence_status = 'approved') AS approved_count
  FROM faro.v_action_evidence_status aes
  GROUP BY aes.company_id, aes.action_id
) ev_summary
  ON ev_summary.company_id = a.company_id
 AND ev_summary.action_id = a.action_id;

Tabla complementaria faro.action_events

Tabla auxiliar para timeline y auditoría. Se crea en migración paralela V035. Recibe cada evento del workflow: action_created, action_started, evidence_uploaded, sent_to_review, evidence_approved, evidence_rejected, action_blocked, action_escalated, extension_requested, action_closed. El evento escalated es evento de timeline (D4), no estado terminal.

▸ SQL · V035__create_action_events.sql
CREATE TABLE IF NOT EXISTS faro.action_events (
  action_event_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  company_id uuid NOT NULL,
  action_id uuid NOT NULL,
  event_type text NOT NULL,
  actor_user_id uuid NULL,
  from_status text NULL,
  to_status text NULL,
  title text NOT NULL,
  description text NULL,
  payload jsonb NOT NULL DEFAULT '{}'::jsonb,
  created_at timestamptz NOT NULL DEFAULT now(),
  CONSTRAINT action_events_event_type_check CHECK (
    event_type IN (
      'action_created', 'action_started', 'evidence_uploaded',
      'sent_to_review', 'evidence_approved', 'evidence_rejected',
      'action_blocked', 'action_escalated', 'extension_requested',
      'action_closed'
    )
  )
);

CREATE INDEX idx_action_events_action ON faro.action_events (company_id, action_id, created_at DESC);
CREATE INDEX idx_action_events_actor ON faro.action_events (company_id, actor_user_id, created_at DESC);
08 · Query principal con priorización

Orden canónico: overdue critical → overdue → today → critical → high → resto

El ORDER BY de esta query es ley del MVP. Define qué ve el responsable cuando entra al Dashboard. Si la UI ordena distinto, está rota.

Lógica de prioridad visual

Bucket Criterio Tratamiento visual Desempate
1Overdue + criticalBorde coral oscuro + badge "Vencida" + fondo rojizo tenuedue_date ASC
2Overdue (cualquier prioridad)Borde coral + badge "Vencida"due_date ASC
3Due bucket = todayBorde ámbar + chip "Hoy"priority DESC, due_date ASC
4Critical (no vencida)Badge critical (rojo sólido)due_date ASC
5HighBadge high (coral suave)due_date ASC
6Resto (medium, low)Badge neutraltension_priority_score DESC, due_date ASC

Query SQL parametrizada

▸ SQL · query del endpoint /api/v1/me/actions
SELECT
  action_id,
  action_code,
  COALESCE(title, catalog_name) AS title,
  description,
  action_type,
  status,
  priority,
  due_date,
  is_overdue,
  due_bucket,
  closure_criteria,
  expected_impact,
  expected_score_recovery_min,
  expected_score_recovery_max,

  tension_id,
  tension_code,
  tension_title,
  tension_severity,
  tension_priority_score,
  tension_score_impact,
  business_question,
  executive_diagnosis,

  evidence_required,
  evidence_required_count,
  evidence_submitted_count,
  evidence_approved_count,
  evidence_missing_count,

  approver_user_id,
  approver_name,
  approver_email,

  payload

FROM faro.v_responsible_action_dashboard
WHERE company_id = $1
  AND responsible_user_id = $2
  AND ($3::text[] IS NULL OR status = ANY($3))
  AND ($4::text[] IS NULL OR priority = ANY($4))
  AND ($5::text[] IS NULL OR due_bucket = ANY($5))
  AND ($6::text IS NULL OR area_code = $6)
  AND (
    $7::text IS NULL
    OR lower(title) LIKE '%' || lower($7) || '%'
    OR lower(action_code) LIKE '%' || lower($7) || '%'
    OR lower(tension_code) LIKE '%' || lower($7) || '%'
  )
ORDER BY
  CASE
    WHEN is_overdue = true AND priority = 'critical' THEN 1
    WHEN is_overdue = true THEN 2
    WHEN due_bucket = 'today' THEN 3
    WHEN priority = 'critical' THEN 4
    WHEN priority = 'high' THEN 5
    ELSE 6
  END,
  due_date ASC,
  tension_priority_score DESC
LIMIT $8 OFFSET $9;

Query auxiliar de summary

El bloque summary del response se computa en una sola query agregada para evitar contar en el frontend (lo que sería frágil con paginación):

▸ SQL · summary agregado
SELECT
  COUNT(*)::int AS total,
  COUNT(*) FILTER (WHERE is_overdue = true)::int AS overdue,
  COUNT(*) FILTER (WHERE due_bucket = 'today')::int AS due_today,
  COUNT(*) FILTER (WHERE priority = 'critical')::int AS critical,
  COUNT(*) FILTER (WHERE priority = 'high')::int AS high,
  COUNT(*) FILTER (WHERE evidence_missing_count > 0)::int AS waiting_evidence,
  COUNT(*) FILTER (WHERE status = 'in_review')::int AS in_review,
  COUNT(*) FILTER (WHERE status = 'blocked')::int AS blocked,
  COUNT(*) FILTER (
    WHERE status = 'closed'
      AND due_date >= CURRENT_DATE - INTERVAL '7 days'
  )::int AS closed_this_week,
  COALESCE(SUM(expected_score_recovery_max), 0)::numeric AS potential_recovery,
  COALESCE(SUM(expected_score_recovery_max) FILTER (WHERE status = 'blocked'), 0)::numeric AS blocked_recovery
FROM faro.v_responsible_action_dashboard
WHERE company_id = $1
  AND responsible_user_id = $2
  AND status NOT IN ('cancelled', 'rejected');
09 · Tipos TypeScript + helpers

Contrato fuerte entre API, lib y componentes

TypeScript estricto es la red de seguridad del MVP. Sin tipos, cualquier cambio en el contrato API rompe la UI silenciosamente y se descubre en producción. Estos tres archivos son obligatorios.

Tipos del Dashboard

▸ TS · lib/faro/responsible.types.ts
export type ActionPriority = "low" | "medium" | "high" | "critical";

// Status enum único — coordinado con WF-001 · Workflow de Acción.
// El evento `escalated` es un evento de timeline (D4), NO un estado terminal.
export type ResponsibleActionStatus =
  | "new"
  | "in_progress"
  | "blocked"
  | "waiting_evidence"
  | "in_review"
  | "approved"
  | "closed"
  | "expired"
  | "cancelled"
  | "rejected";

export type DueBucket = "overdue" | "today" | "this_week" | "later";

export type EvidenceRequirementStatus = {
  evidence_code: string;
  name: string;
  trust_level: "low" | "medium" | "high" | "critical";
  status: "missing" | "submitted" | "approved" | "rejected" | "needs_more_info";
};

export type ResponsibleActionItem = {
  action_id: string;
  action_code: string;
  title: string;
  description: string | null;
  status: ResponsibleActionStatus;
  priority: ActionPriority;
  due_date: string | null;
  is_overdue: boolean;
  due_bucket: DueBucket;
  action_type: string;
  closure_criteria: string | null;
  expected_impact: string | null;
  expected_score_recovery_min: number | null;
  expected_score_recovery_max: number | null;
  tension: {
    tension_id: string | null;
    tension_code: string | null;
    title: string | null;
    severity: string | null;
    priority_score: number | null;
    score_impact: number | null;
    business_question?: string | null;
    executive_diagnosis?: string | null;
  };
  evidence: {
    required: boolean;
    required_codes: string[];
    missing: number;
    submitted: number;
    approved: number;
    requirements: EvidenceRequirementStatus[];
  };
  approver: {
    user_id: string | null;
    full_name: string | null;
    email?: string | null;
  };
};

export type ResponsibleDashboardResponse = {
  user: {
    user_id: string;
    full_name: string;
    role: string;
  };
  period: {
    period_start: string;
    period_end: string;
    label: string;
  };
  summary: {
    total: number;
    overdue: number;
    due_today: number;
    critical: number;
    high: number;
    waiting_evidence: number;
    in_review: number;
    blocked: number;
    closed_this_week: number;
  };
  score: {
    potential_recovery: number;
    blocked_recovery: number;
    recovered_this_week: number;
  };
  items: ResponsibleActionItem[];
};

Helpers de prioridad

▸ TS · lib/faro/priority.ts
import type { ActionPriority } from "./responsible.types";

export function priorityLabel(priority: ActionPriority): string {
  const map: Record<ActionPriority, string> = {
    critical: "Crítica",
    high: "Alta",
    medium: "Media",
    low: "Baja"
  };

  return map[priority] ?? priority;
}

export function priorityTone(priority: ActionPriority) {
  const map = {
    critical: {
      badge: "bg-red-100 text-red-800 border border-red-200",
      border: "border-red-400",
      tag: "critical"
    },
    high: {
      badge: "bg-amber-100 text-amber-800 border border-amber-200",
      border: "border-amber-300",
      tag: "high"
    },
    medium: {
      badge: "bg-yellow-100 text-yellow-800 border border-yellow-200",
      border: "border-yellow-300",
      tag: "medium"
    },
    low: {
      badge: "bg-slate-100 text-slate-700 border border-slate-200",
      border: "border-slate-300",
      tag: "low"
    }
  };

  return map[priority] ?? map.low;
}

// Orden canónico de prioridad para tie-breakers en frontend.
export const PRIORITY_RANK: Record<ActionPriority, number> = {
  critical: 4,
  high: 3,
  medium: 2,
  low: 1
};

Helpers de estado

▸ TS · lib/faro/action-status.ts
import type { ResponsibleActionStatus } from "./responsible.types";

export function actionStatusLabel(status: ResponsibleActionStatus): string {
  const map: Record<ResponsibleActionStatus, string> = {
    new: "Nueva",
    in_progress: "En ejecución",
    blocked: "Bloqueada",
    waiting_evidence: "Sin evidencia",
    in_review: "En revisión",
    approved: "Aprobada",
    closed: "Cerrada",
    expired: "Vencida",
    cancelled: "Cancelada",
    rejected: "Rechazada"
  };

  return map[status] ?? status;
}

// Estados en los que el responsable puede cargar evidencia.
export const EVIDENCE_UPLOADABLE_STATUSES: ResponsibleActionStatus[] = [
  "new",
  "in_progress",
  "waiting_evidence",
  "expired"
];

// Estados que NO se pueden cerrar sin evidencia aprobada.
export function canCloseAction(
  status: ResponsibleActionStatus,
  evidenceMissing: number,
  evidenceRequired: boolean
): boolean {
  if (status === "closed" || status === "cancelled") return false;
  if (evidenceRequired && evidenceMissing > 0) return false;
  return status === "approved" || status === "in_review";
}

API client

▸ TS · lib/faro/responsible.api.ts
import type { ResponsibleDashboardResponse } from "./responsible.types";

export type GetResponsibleActionsParams = {
  status?: string[];
  priority?: string[];
  due?: string[];
  area_code?: string;
  q?: string;
  page?: number;
  page_size?: number;
};

export async function getResponsibleActions(
  params: GetResponsibleActionsParams = {}
): Promise<ResponsibleDashboardResponse> {
  const search = new URLSearchParams();

  if (params.status?.length) search.set("status", params.status.join(","));
  if (params.priority?.length) search.set("priority", params.priority.join(","));
  if (params.due?.length) search.set("due", params.due.join(","));
  if (params.area_code) search.set("area_code", params.area_code);
  if (params.q) search.set("q", params.q);
  if (params.page) search.set("page", String(params.page));
  if (params.page_size) search.set("page_size", String(params.page_size));

  const response = await fetch(`/api/v1/me/actions?${search.toString()}`, {
    method: "GET",
    headers: { "Content-Type": "application/json" },
    cache: "no-store"
  });

  if (!response.ok) {
    throw new Error("No se pudo cargar tu tablero de acciones");
  }

  return response.json();
}

export async function updateActionStatus(actionId: string, status: string) {
  const response = await fetch(`/api/v1/actions/${actionId}/status`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ status })
  });

  if (!response.ok) {
    throw new Error("No se pudo actualizar la acción");
  }

  return response.json();
}
10 · Route handler Next.js

Implementación canónica de app/api/v1/me/actions/route.ts

Route handler completo con sesión, RLS por set_config, query a vista canónica, summary agregado y mapeo a contrato JSON. Es el patrón de referencia para todos los endpoints "me" del MVP.

Regla de seguridad. Este endpoint usa session.user_id directamente del JWT verificado. No acepta responsible_user_id como query param. Cualquier intento de cambiar la identidad efectiva se rechaza con 401. Para gerentes y directores existen endpoints distintos con permisos superiores y auditoría diferenciada (ver sección 12).

▸ TS · app/api/v1/me/actions/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/server/db";
import { getSessionContext } from "@/lib/server/session";

export async function GET(request: NextRequest) {
  const session = await getSessionContext();

  if (!session?.companyId || !session?.userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const search = request.nextUrl.searchParams;

  const status = search.get("status")?.split(",") ?? null;
  const priority = search.get("priority")?.split(",") ?? null;
  const due = search.get("due")?.split(",") ?? null;
  const areaCode = search.get("area_code");
  const q = search.get("q");
  const page = Number(search.get("page") ?? 1);
  const pageSize = Math.min(Number(search.get("page_size") ?? 50), 200);
  const offset = (page - 1) * pageSize;

  const client = await db.connect();

  try {
    await client.query("BEGIN");

    // RLS: bind session into Postgres GUC variables.
    // Row Level Security policies de faro.actions leen estos valores.
    await client.query(`SELECT set_config('app.company_id', $1, true)`, [session.companyId]);
    await client.query(`SELECT set_config('app.user_id', $1, true)`, [session.userId]);
    await client.query(`SELECT set_config('app.role_codes', $1, true)`, [session.roleCodes.join(",")]);

    const actions = await client.query(
      `
      SELECT *
      FROM faro.v_responsible_action_dashboard
      WHERE company_id = $1
        AND responsible_user_id = $2
        AND ($3::text[] IS NULL OR status = ANY($3))
        AND ($4::text[] IS NULL OR priority = ANY($4))
        AND ($5::text[] IS NULL OR due_bucket = ANY($5))
        AND ($6::text IS NULL OR area_code = $6)
        AND (
          $7::text IS NULL
          OR lower(title) LIKE '%' || lower($7) || '%'
          OR lower(action_code) LIKE '%' || lower($7) || '%'
          OR lower(tension_code) LIKE '%' || lower($7) || '%'
        )
      ORDER BY
        CASE
          WHEN is_overdue = true AND priority = 'critical' THEN 1
          WHEN is_overdue = true THEN 2
          WHEN due_bucket = 'today' THEN 3
          WHEN priority = 'critical' THEN 4
          WHEN priority = 'high' THEN 5
          ELSE 6
        END,
        due_date ASC,
        tension_priority_score DESC
      LIMIT $8 OFFSET $9
      `,
      [session.companyId, session.userId, status, priority, due, areaCode, q, pageSize, offset]
    );

    const summary = await client.query(
      `
      SELECT
        COUNT(*)::int AS total,
        COUNT(*) FILTER (WHERE is_overdue = true)::int AS overdue,
        COUNT(*) FILTER (WHERE due_bucket = 'today')::int AS due_today,
        COUNT(*) FILTER (WHERE priority = 'critical')::int AS critical,
        COUNT(*) FILTER (WHERE priority = 'high')::int AS high,
        COUNT(*) FILTER (WHERE evidence_missing_count > 0)::int AS waiting_evidence,
        COUNT(*) FILTER (WHERE status = 'in_review')::int AS in_review,
        COUNT(*) FILTER (WHERE status = 'blocked')::int AS blocked,
        COUNT(*) FILTER (
          WHERE status = 'closed'
            AND due_date >= CURRENT_DATE - INTERVAL '7 days'
        )::int AS closed_this_week,
        COALESCE(SUM(expected_score_recovery_max), 0)::numeric AS potential_recovery,
        COALESCE(SUM(expected_score_recovery_max) FILTER (WHERE status = 'blocked'), 0)::numeric AS blocked_recovery
      FROM faro.v_responsible_action_dashboard
      WHERE company_id = $1
        AND responsible_user_id = $2
        AND status NOT IN ('cancelled', 'rejected')
      `,
      [session.companyId, session.userId]
    );

    await client.query("COMMIT");

    return NextResponse.json({
      user: {
        user_id: session.userId,
        full_name: session.fullName,
        role: session.primaryRole
      },
      period: {
        period_start: "2026-05-01",
        period_end: "2026-05-31",
        label: "Mayo 2026"
      },
      summary: {
        total: summary.rows[0].total,
        overdue: summary.rows[0].overdue,
        due_today: summary.rows[0].due_today,
        critical: summary.rows[0].critical,
        high: summary.rows[0].high,
        waiting_evidence: summary.rows[0].waiting_evidence,
        in_review: summary.rows[0].in_review,
        blocked: summary.rows[0].blocked,
        closed_this_week: summary.rows[0].closed_this_week
      },
      score: {
        potential_recovery: Number(summary.rows[0].potential_recovery ?? 0),
        blocked_recovery: Number(summary.rows[0].blocked_recovery ?? 0),
        recovered_this_week: 0
      },
      items: actions.rows.map(mapResponsibleActionRow)
    });
  } catch (error) {
    await client.query("ROLLBACK");
    return NextResponse.json(
      { error: "Could not load responsible actions" },
      { status: 500 }
    );
  } finally {
    client.release();
  }
}

function mapResponsibleActionRow(row: any) {
  const payload = row.payload ?? {};
  const requirements = payload.evidence_requirements ?? [];

  return {
    action_id: row.action_id,
    action_code: row.action_code,
    title: row.title ?? row.catalog_name,
    description: row.description,
    status: row.status,
    priority: row.priority,
    due_date: row.due_date,
    is_overdue: row.is_overdue,
    due_bucket: row.due_bucket,
    action_type: row.action_type,
    closure_criteria: row.closure_criteria,
    expected_impact: row.expected_impact ?? row.expected_business_impact,
    expected_score_recovery_min: row.expected_score_recovery_min
      ? Number(row.expected_score_recovery_min)
      : null,
    expected_score_recovery_max: row.expected_score_recovery_max
      ? Number(row.expected_score_recovery_max)
      : null,
    tension: {
      tension_id: row.tension_id,
      tension_code: row.tension_code,
      title: row.tension_title,
      severity: row.tension_severity,
      priority_score: row.tension_priority_score ? Number(row.tension_priority_score) : null,
      score_impact: row.tension_score_impact ? Number(row.tension_score_impact) : null,
      business_question: row.business_question,
      executive_diagnosis: row.executive_diagnosis
    },
    evidence: {
      required: row.evidence_required,
      required_codes: payload.evidence_required_codes ?? [],
      missing: Number(row.evidence_missing_count ?? 0),
      submitted: Number(row.evidence_submitted_count ?? 0),
      approved: Number(row.evidence_approved_count ?? 0),
      requirements: requirements.map((item: any) => ({
        evidence_code: item.evidence_code,
        name: item.name,
        trust_level: item.trust_level,
        status: "missing"
      }))
    },
    approver: {
      user_id: row.approver_user_id,
      full_name: row.approver_name,
      email: row.approver_email
    }
  };
}

Componente raíz de la página

Para cerrar el circuito, así se monta el handler en la app router de Next.js. La página vive en app/faro/my-actions/page.tsx y monta el componente cliente:

▸ TSX · app/faro/my-actions/page.tsx
import { ResponsibleDashboardPage } from "@/components/responsible/ResponsibleDashboardPage";

export const metadata = {
  title: "Mis acciones · FARO Connect",
  description: "Vista diaria de acciones asignadas, vencimientos y recuperación de Score."
};

export default function Page() {
  return <ResponsibleDashboardPage />;
}
11 · Mockup visual estático

Dashboard de María Fernández · Gerencia Comercial · Empresa Demo Cuyo S.A.

Mockup estático del Dashboard tal como lo vería la responsable comercial de Empresa Demo Cuyo S.A. al entrar el martes 2 de junio de 2026. 7 acciones asignadas, 2 vencidas, 1 vence hoy, 3 críticas. Recuperación potencial: +16 puntos de FARO Score.

FARO Connect · Mi ejecución

Acciones asignadas

María Fernández · Gerencia Comercial · Empresa Demo Cuyo S.A. · Mayo 2026. Vista de acciones, vencimientos, evidencia pendiente y recuperación potencial de Score.

Total
7
asignadas
Vencidas
2
fuera de SLA
Hoy
1
vence hoy
Críticas
3
máxima prioridad
Score
+16
pts potenciales
Críticas Altas Medias Vencidas Hoy Semana Nuevas En ejecución Sin evidencia
ACT-COM-003 Crítica Vencida
29 May
vence

Activar política de descuento sobre venta repetida

TNS-002 · Descuento fuera de política

Estado
En ejecución
Evidencia
2 faltan
Score
+7
ACT-COM-001 Crítica Vencida
30 May
vence

Revisar política de descuentos

TNS-001 · Crecimiento no rentable

Estado
Nueva
Evidencia
2 faltan
Score
+8
ACT-COM-002 Alta
Hoy
vence

Reunión con vendedores top-3 por erosión de margen

TNS-003 · Vendedor erosiona margen

Estado
En ejecución
Evidencia
1 falta
Score
+4
ACT-COM-005 Crítica
04 Jun
vence

Plan de recuperación sucursal Mendoza Centro

TNS-013 · Caída de ventas en sucursal

Estado
Nueva
Evidencia
3 faltan
Score
+6
ACT-COM-004 Alta
05 Jun
vence

Plan de descongelado de cartera concentrada

TNS-011 · Ventas concentradas en pocos clientes

Estado
En ejecución
Evidencia
1 OK
Score
+5
ACT-COM-006 Alta
07 Jun
vence

Ajuste de pricing por familia con margen bajo

TNS-014 · Margen bajo por familia

Estado
Nueva
Evidencia
2 faltan
Score
+3
ACT-COM-007 Media
12 Jun
vence

Revisar caída de ticket promedio retail

TNS-012 · Ticket promedio cae

Estado
Nueva
Evidencia
2 faltan
Score
+2

Mockup estático con datos demostrativos de Empresa Demo Cuyo S.A. No representa empresa real. Estados, vencimientos y Score recovery son ilustrativos del contrato visual; los códigos ACT-*, TNS-* y EVD-* sí son canónicos y resuelven contra los catálogos MVP.

12 · Permisos por rol + API endpoints

Qué puede hacer cada rol y dónde se ejecuta

El Dashboard Responsable no es una pantalla "para todos". Tiene matriz de permisos clara y se apoya en 9 endpoints REST que cubren consulta y todo el workflow operativo de una acción.

Matriz de permisos

Regla canónica de visibilidad. Un responsable ve lo suyo. Un gerente ve lo de su área. Un director ve todo. El endpoint /api/v1/me/actions siempre devuelve solo lo del usuario en sesión. Para que un gerente vea acciones ajenas, debe usar /api/v1/responsibles/:user_id/actions con su rol elevado.

Acción Responsable Gerente Director Aprobador
Ver mis acciones
Ver acciones de otrosNoSí (su área)Solo en revisión
Iniciar acciónNo
Cargar evidenciaNo
Bloquear acciónNo
Cambiar responsableNoSí (su área)No
Aprobar evidenciaSolo si tiene rol
Cerrar acciónParcialSi fue aprobador
Escalar acciónNo
Rechazar evidenciaNo
Solicitar extensión fechaSolicitaApruebaApruebaNo

9 endpoints API canónicos

Los 9 endpoints que la UI consume directa o indirectamente. GET para lectura, POST para acciones idempotentes, PATCH para mutaciones parciales. Todos requieren sesión válida con company_id y user_id.

GET
/api/v1/me/actions
Lista de acciones del usuario en sesión con summary y score. Endpoint principal del Dashboard.
GET
/api/v1/me/summary
Resumen agregado del responsable. Útil para widgets de header en otras pantallas.
GET
/api/v1/actions/:action_id
Detalle individual con timeline completo, business question, executive diagnosis y descripción de evidencia.
PATCH
/api/v1/actions/:action_id/status
Cambia el estado de la acción. Transiciones controladas por WF-001. Bloquea si la transición no es válida.
POST
/api/v1/actions/:action_id/evidence
Carga una evidencia. Acepta archivo, comentario o validación. Valida trust_level vs rol del cargador.
POST
/api/v1/actions/:action_id/block
Marca la acción como bloqueada. Requiere motivo, fecha y responsable de desbloqueo en payload.
POST
/api/v1/actions/:action_id/escalate
Crea evento action_escalated (D4) y dispara notificación a jefe directo y Dirección.
POST
/api/v1/actions/:action_id/submit-review
Envía acción a in_review. Notifica al aprobador. Requiere al menos una evidencia submitted.
POST
/api/v1/actions/:action_id/request-extension
Solicita extensión de vencimiento. Requiere motivo y nueva fecha propuesta. Aprueba gerencia.

Auditoría obligatoria

Cada uno de los 8 endpoints de escritura inserta un row en faro.action_events (ver sección 7). Eventos típicos: action_started, evidence_uploaded, sent_to_review, action_blocked, action_escalated, extension_requested, action_closed. Esta tabla es la fuente única del timeline visible en el detalle y de los reportes de SLA por área (MVP 4).

13 · Cross-references

Dónde se cruza este Dashboard con el resto del pack

UI-002 no vive sola. Consume catálogos canónicos, alimenta workflows y se complementa con otras pantallas del MVP. Estos son los puntos donde se conecta el Dashboard Responsable con el resto del pipeline FARO Connect.

PARALELO FARO-UI-001
ui-bandeja-tensiones.html

Bandeja de Tensiones MVP. Vista colectiva de la empresa. UI-002 es la cara individual; UI-001 es la cara organizacional. Comparten v_tension_board y conviven sin solapar audiencia.

CONTINÚA FARO-UI-003
ui-workflow-accion.html

Detalle de Acción + Workflow completo. Pantalla a la que se navega desde el Dashboard para ejecutar el ciclo completo: cambiar estado, aprobar evidencia, cerrar.

CONTINÚA FARO-UI-004
ui-carga-evidencia.html

Modal o pantalla de carga de evidencia. Se invoca desde el botón "Cargar evidencia" del Dashboard. Valida metadata, archivo y trust_level antes de persistir.

USA FARO-WF-001
workflow-escalamiento-mvp.html

Workflow canónico de escalamiento. Define qué pasa cuando una acción se escala (D4): notificación a jefe directo, alerta dirección, marcado en timeline.

USA FARO-SCORE
motor-score-mvp.html

Modelo FARO Score. Define cómo se calcula expected_score_recovery_min/max que el Dashboard muestra en cada acción y en el header global.

CONSUME
catalogo-acciones-mvp.html

Catálogo Canónico de Acciones MVP. Cada acción mostrada en el Dashboard resuelve contra una ACT-* de este catálogo (propósito, criterio cierre, evidencia requerida).

CONSUME
catalogo-tensiones-mvp.html

30 tensiones canónicas TNS-001..TNS-030. Cada acción del Dashboard apunta a una tensión asociada con su severidad, priority_score y diagnosis.

CONSUME
catalogo-evidencias-mvp.html

Catálogo de evidencias EVD-001..EVD-012. La lista "Evidencia requerida" del detalle de acción resuelve sus códigos contra este catálogo y muestra trust_level.

IMPLEMENTA
modelo-sql.html

DDL completo del sistema. Incluye faro.actions, faro.tensions, faro.evidence, faro.action_events y la vista v_responsible_action_dashboard de sección 7.