# OQI-001: Análisis de Gaps - Fundamentos-Auth

**Módulo:** OQI-001 (fundamentos-auth)
**Ruta:** `apps/frontend/src/modules/auth/`
**Progreso Actual:** 70%
**Fecha:** 2026-01-25
**Status:** ANÁLISIS COMPLETO

---

## TABLA CONSOLIDADA DE GAPS

| Gap ID | Descripción | Prioridad | Tipo | Esfuerzo | Status | Impacto | Notas |
|--------|-------------|-----------|------|----------|--------|---------|-------|
| **G-001** | 2FA Setup UI - Formulario de configuración de Authenticator App | P0 | Implementación | L | BLOQUEANTE | CRÍTICO | Tab "Two-Factor Auth" tiene botones "Setup" sin handler |
| **G-002** | 2FA Enable Endpoint - Consumir POST /auth/2fa/enable | P0 | Implementación | M | BLOQUEANTE | CRÍTICO | Necesario para completar flujo 2FA |
| **G-003** | Password Change Handler - Implementar submit en SecuritySettings tab "password" | P1 | Implementación | M | TODO | ALTO | Formulario existe pero sin funcionalidad |
| **G-004** | SMS 2FA Method - Soporte para verificación por SMS en login | P1 | Implementación | M | PARCIAL | ALTO | PhoneLoginForm existe pero no vinculado a 2FA |
| **G-005** | OAuth User Mapping - Mapeo de datos desde OAuth a User object | P1 | Implementación | M | TODO | ALTO | AuthCallback solo maneja tokens, no perfil |
| **G-006** | Backup Codes - UI para mostrar y descargar códigos de backup 2FA | P1 | Documentación | M | TODO | MEDIO | Backend soporta, frontend no lo muestra |
| **G-007** | Rate Limiting UI - Mostrar contador de intentos fallidos | P2 | UX | S | TODO | BAJO | No hay feedback visual de rate limiting |
| **G-008** | OTP Expiración UI - Mostrar countdown de expiración en PhoneLoginForm | P2 | UX | M | TODO | BAJO | otpExpiresAt se calcula pero no se muestra |
| **G-009** | Resend OTP Cooldown - Deshabilitar botón resend durante cooldown | P2 | UX | S | PARCIAL | BAJO | handleResendOTP existe pero sin throttle |
| **G-010** | Error Recovery - Sin auto-retry en conexión fallida | P2 | Implementación | S | TODO | BAJO | Requiere reintentar manualmente |
| **G-011** | Session Device Fingerprinting - Información de dispositivo incompleta | P1 | Integración | M | PARCIAL | MEDIO | DeviceCard muestra navegador/OS pero no CPU/memoria |
| **G-012** | Refresh Token Auto-Renew - No hay renovación automática de tokens | P1 | Implementación | L | BLOQUEANTE | CRÍTICO | Tokens expiran sin renovación |
| **G-013** | Social Provider Error Handling - Mensajes genéricos de OAuth error | P1 | UX | S | PARCIAL | MEDIO | AuthCallback tiene mapeo básico pero incompleto |
| **G-014** | Remember Me Persistence - rememberMe no persiste sesión | P2 | Implementación | S | PARCIAL | BAJO | Checkbox existe pero sin lógica backend |
| **G-015** | Password Strength Meter - Indicador visual de fortaleza | P1 | UX | S | PARCIAL | MEDIO | Register/ResetPassword muestran reqs pero sin barra |
| **G-016** | Biometric Auth - Touch/Face ID no soportado | P1 | Implementación | L | TODO | CRÍTICO | Importante para móvil |
| **G-017** | WebAuthn (FIDO2) - No soportado | P2 | Implementación | XL | TODO | ALTO | Estándar moderno de seguridad |
| **G-018** | Account Lockout - Sin mecanismo de bloqueo tras intentos fallidos | P1 | Seguridad | M | TODO | CRÍTICO | Riesgo de fuerza bruta |
| **G-019** | Session Timeout Warning - Sin advertencia antes de expiración | P1 | UX | M | TODO | MEDIO | Usuario no sabe cuándo expira sesión |
| **G-020** | Logout Confirmation - Sin confirmación antes de cerrar sesión | P2 | UX | S | TODO | BAJO | Riesgo de logout accidental |
| **G-021** | Email Verification Resend - No hay opción de reenviar email | P1 | Implementación | S | TODO | ALTO | Usuario sin acceso a email original queda atrapado |
| **G-022** | Account Recovery - No hay opción de recuperación sin email | P2 | Implementación | M | TODO | MEDIO | Si email está comprometido |
| **G-023** | Language/Locale Support - Todo hardcodeado en español | P2 | Localización | M | TODO | BAJO | i18n no implementado |
| **G-024** | Accessibility (a11y) - WCAG 2.1 compliance incompleta | P1 | Accesibilidad | L | PARCIAL | MEDIO | Faltan aria-labels, color contrast |
| **G-025** | Loading States - Estados intermedios ambiguos | P2 | UX | S | PARCIAL | BAJO | No hay feedback de progreso en algunos flujos |
| **G-026** | Mobile Responsiveness - Algunos layouts no optimizados móvil | P2 | UX | M | PARCIAL | BAJO | SecuritySettings podría ser más responsive |
| **G-027** | Error Boundary - Sin error boundary en módulo | P1 | Robustez | M | TODO | ALTO | Errores rompen toda la app |
| **G-028** | Offline Support - Sin detección de conexión | P2 | UX | M | TODO | BAJO | Mensajes confusos si sin internet |
| **G-029** | Deep Linking - OAuth redirect puede fallar | P1 | Robustez | S | PARCIAL | MEDIO | returnUrl no siempre válido |
| **G-030** | Audit Logging - Sin registro de eventos en frontend | P2 | Seguridad | S | TODO | BAJO | Backend lo hace, frontend no |

---

## GAPS CRÍTICOS (P0-P1 de Alto Impacto)

### G-001: 2FA Setup UI
**Prioridad:** P0 (BLOQUEANTE)
**Tipo:** Implementación
**Esfuerzo:** L (Large - 40-80 horas)
**Impacto:** CRÍTICO

**Descripción:**
Tab "two-factor" en SecuritySettings.tsx (líneas 192-267) tiene:
- Opción para "Authenticator App" con botón "Setup"
- Opción para "SMS" con botón "Setup"
- AMBOS botones sin handler implementado

**Flujo Esperado:**
1. Usuario hace clic en "Setup" (Authenticator)
2. Frontend llama POST /api/v1/auth/2fa/setup
3. Backend retorna QR code + secret
4. Frontend muestra QR
5. Usuario escanea con Google Authenticator / Authy
6. Usuario copia código de 6 dígitos
7. Frontend llama POST /api/v1/auth/2fa/enable con código
8. Éxito: mostrar backup codes, confirmación

**Implementación Pendiente:**
```typescript
// SecuritySettings.tsx línea 237-241
<button type="button" className="...">
  Setup  {/* <- NO TIENE onClick handler */}
</button>

// Necesita:
const [showQRSetup, setShowQRSetup] = useState(false)
const [qrCode, setQrCode] = useState<string>('')
const [twoFASecret, setTwoFASecret] = useState<string>('')
const [verificationCode, setVerificationCode] = useState<string>('')

const handleSetup2FA = async () => {
  // POST /api/v1/auth/2fa/setup con method='totp'
  // Mostrar QR
}
```

**Archivos Afectados:**
- C:\Empresas\ISEM\workspace-v2\projects\trading-platform\apps\frontend\src\modules\auth\pages\SecuritySettings.tsx (línea 237-241, 258-262)

---

### G-002: 2FA Enable Endpoint Integration
**Prioridad:** P0 (BLOQUEANTE)
**Tipo:** Implementación
**Esfuerzo:** M (Medium - 20-40 horas)
**Impacto:** CRÍTICO

**Descripción:**
Después de escanear QR (G-001), necesita validar el código TOTP antes de activar.

**Endpoint Necesario:**
```
POST /api/v1/auth/2fa/enable
{
  method: 'totp',
  verificationCode: '123456'
}
```

**Lógica Pendiente:**
```typescript
const handleVerify2FA = async (code: string) => {
  const response = await fetch('/api/v1/auth/2fa/enable', {
    method: 'POST',
    body: JSON.stringify({
      method: 'totp',
      verificationCode: code
    })
  })

  const data = await response.json()
  // Si éxito: mostrar backup codes
  // data.backupCodes = ['code1', 'code2', ...]
}
```

**Dependencia:** G-001 (debe completarse primero)

---

### G-003: Password Change Handler
**Prioridad:** P1 (TODO)
**Tipo:** Implementación
**Esfuerzo:** M (Medium)
**Impacto:** ALTO

**Descripción:**
SecuritySettings.tsx tab "password" (líneas 138-188) tiene formulario completo:
```tsx
<form className="space-y-4 max-w-md">
  <input type="password" placeholder="Enter current password" />
  <input type="password" placeholder="Enter new password" />
  <input type="password" placeholder="Confirm new password" />
  <button type="submit">Update Password</button>  {/* <- Sin handler */}
</form>
```

**Implementación Necesaria:**
```typescript
const [passwordForm, setPasswordForm] = useState({
  currentPassword: '',
  newPassword: '',
  confirmPassword: ''
})
const [passwordError, setPasswordError] = useState<string | null>(null)

const handleChangePassword = async (e: React.FormEvent) => {
  e.preventDefault()

  // Validar que newPassword !== currentPassword
  // Validar requerimientos de contraseña

  try {
    const response = await fetch('/api/v1/auth/change-password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        currentPassword: passwordForm.currentPassword,
        newPassword: passwordForm.newPassword
      })
    })

    if (!response.ok) {
      const error = await response.json()
      throw new Error(error.error)
    }

    setPasswordError(null)
    // Mostrar success toast
  } catch (err) {
    setPasswordError(err.message)
  }
}
```

**Archivos Afectados:**
- SecuritySettings.tsx (línea 147-187)

---

### G-012: Refresh Token Auto-Renew
**Prioridad:** P1 (BLOQUEANTE)
**Tipo:** Implementación
**Esfuerzo:** L (Large)
**Impacto:** CRÍTICO

**Descripción:**
AccessToken típicamente expira en 15 minutos. Sin refresh automático:
- Usuario obtiene error 401 de repente
- Requiere login manual
- Experiencia pobre

**Implementación Necesaria:**

1. **En auth.service.ts:** Agregar interceptor de refresh
```typescript
api.interceptors.response.use(
  response => response,
  async (error) => {
    if (error.response?.status === 401) {
      try {
        const refreshToken = localStorage.getItem('refreshToken')
        const response = await api.post('/auth/refresh', {
          refreshToken
        })

        localStorage.setItem('accessToken', response.data.accessToken)
        localStorage.setItem('refreshToken', response.data.refreshToken)

        // Reintentar request original
        return api(error.config)
      } catch {
        // Logout
        localStorage.removeItem('token')
        window.location.href = '/login'
      }
    }
    return Promise.reject(error)
  }
)
```

2. **En Backend:** Implementar POST /api/v1/auth/refresh
```
POST /api/v1/auth/refresh
{
  refreshToken: string
}

Response:
{
  accessToken: string
  refreshToken: string (nuevo)
}
```

**Archivos Afectados:**
- auth.service.ts
- Interceptor de axios

---

### G-016: Biometric Authentication
**Prioridad:** P1 (IMPLEMENTACIÓN FUTURA)
**Tipo:** Implementación
**Esfuerzo:** L (Large - 60-120 horas)
**Impacto:** CRÍTICO

**Descripción:**
Soporte para Touch/Face ID en dispositivos móviles.

**Scope:**
- Android: BiometricPrompt
- iOS: LocalAuthentication
- Web: WebAuthn (G-017)

**Flujo Esperado:**
1. User abre app en móvil
2. Opción: "Login with Biometric" o "Use Passcode"
3. Si biometric disponible, muestra prompt
4. Si exitoso, backend verifica fingerprint
5. Obtiene tokens

**Nota:** Requiere integración con React Native (si aplica)

---

### G-018: Account Lockout
**Prioridad:** P1 (SEGURIDAD CRÍTICA)
**Tipo:** Implementación
**Esfuerzo:** M (Medium)
**Impacto:** CRÍTICO

**Descripción:**
SIN mecanismo de bloqueo tras intentos fallidos de login.

**Riesgo:**
- Fuerza bruta masiva
- Compromiso de cuenta

**Implementación Necesaria:**

**Frontend:**
```typescript
// Login.tsx
const [loginAttempts, setLoginAttempts] = useState(0)
const [accountLocked, setAccountLocked] = useState(false)
const [lockExpiresAt, setLockExpiresAt] = useState<Date | null>(null)

const handleEmailLogin = async (e: React.FormEvent) => {
  if (accountLocked) {
    const remaining = new Date(lockExpiresAt!).getTime() - Date.now()
    throw new Error(`Account locked. Try again in ${Math.ceil(remaining / 60000)} minutes`)
  }

  try {
    // ... login logic ...
    setLoginAttempts(0)
  } catch (err) {
    const newAttempts = loginAttempts + 1
    setLoginAttempts(newAttempts)

    if (newAttempts >= 5) {
      setAccountLocked(true)
      setLockExpiresAt(new Date(Date.now() + 15 * 60 * 1000)) // 15 mins
    }
  }
}
```

**Backend:**
- Rastrear intentos fallidos por IP/email
- Bloquear tras N intentos (típico: 5)
- Desbloquear automáticamente tras M minutos (típico: 15)
- Log de intentos para auditoría

---

## GAPS FUNCIONALES (P1-P2)

### G-004: SMS 2FA Method
**Prioridad:** P1
**Tipo:** Implementación
**Esfuerzo:** M

**Descripción:**
PhoneLoginForm.tsx soporta enviar OTP por SMS/WhatsApp, pero:
- No está vinculado a flujo de 2FA en login
- SecuritySettings.tsx tiene botón "Setup SMS" sin funcionalidad

**Flujo Esperado:**
1. User habilita 2FA SMS en SecuritySettings
2. User entra con email + password
3. Backend retorna requiresTwoFactor: true
4. Frontend muestra UI SMS (similar a PhoneLoginForm)
5. User recibe SMS con código
6. User verifica código

**Nota:** PhoneLoginForm es alternativa a email (no es 2FA)

---

### G-005: OAuth User Mapping
**Prioridad:** P1
**Tipo:** Implementación
**Esfuerzo:** M

**Descripción:**
AuthCallback.tsx (líneas 1-96) solo maneja tokens:
```javascript
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', refreshToken)
```

**Falta:**
- Obtener perfil de usuario desde /api/v1/auth/me
- Guardar en contexto/store de usuario
- Mostrar nombre/avatar en dashboard

**Implementación Necesaria:**
```typescript
useEffect(() => {
  if (accessToken && refreshToken) {
    localStorage.setItem('accessToken', accessToken)
    localStorage.setItem('refreshToken', refreshToken)

    // Nuevo: Fetch user profile
    fetch('/api/v1/auth/me', {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    })
      .then(res => res.json())
      .then(data => {
        // Guardar en store/context
        userStore.setUser(data.user)
      })

    setTimeout(() => {
      navigate(isNewUser ? '/onboarding' : returnUrl)
    }, 1500)
  }
}, [...])
```

---

### G-011: Session Device Fingerprinting
**Prioridad:** P1
**Tipo:** Integración
**Esfuerzo:** M

**Descripción:**
DeviceCard.tsx muestra navegador/OS parseados de User-Agent:
```
"Chrome on Windows 10/11"
```

**Falta:**
- CPU arquitectura (x86, ARM, etc)
- Memoria RAM
- Resolución de pantalla
- ID único de dispositivo (localStorage fingerprint)

**Beneficio:**
- Usuario puede identificar dispositivos extraños
- Detección de dispositivos comprometidos

---

## GAPS DE UX (P2)

### G-008: OTP Expiración UI
**Prioridad:** P2
**Tipo:** UX
**Esfuerzo:** M

**Descripción:**
PhoneLoginForm.tsx calcula `otpExpiresAt` pero no lo muestra:
```typescript
const [otpExpiresAt, setOtpExpiresAt] = useState<Date | null>(null)
// ... nunca se renderiza en UI
```

**Implementación Necesaria:**
```tsx
{step === 'otp' && (
  <div className="text-center text-sm text-gray-400">
    OTP expires in: <CountdownTimer expiresAt={otpExpiresAt} />
  </div>
)}

function CountdownTimer({ expiresAt }: { expiresAt: Date | null }) {
  const [remaining, setRemaining] = useState('')

  useEffect(() => {
    const interval = setInterval(() => {
      const diff = expiresAt!.getTime() - Date.now()
      const secs = Math.ceil(diff / 1000)
      setRemaining(`${secs}s`)
    }, 1000)

    return () => clearInterval(interval)
  }, [expiresAt])

  return <span>{remaining}</span>
}
```

---

### G-009: Resend OTP Cooldown
**Prioridad:** P2
**Tipo:** UX
**Esfuerzo:** S

**Descripción:**
Botón "Reenviar código" en PhoneLoginForm (línea 167-174) sin throttle:
```typescript
<button
  type="button"
  onClick={handleResendOTP}
  disabled={isLoading}  {/* <- Falta: disabled durante cooldown */}
  className="..."
>
  Reenviar codigo
</button>
```

**Problema:**
- Usuario puede spamear resend
- Costo SMS se multiplica

**Implementación:**
```typescript
const [resendCooldown, setResendCooldown] = useState(0)

useEffect(() => {
  const interval = setInterval(() => {
    setResendCooldown(prev => Math.max(0, prev - 1))
  }, 1000)
  return () => clearInterval(interval)
}, [])

const handleResendOTP = async () => {
  setResendCooldown(30) // 30 segundos
  // ... send OTP ...
}

// En JSX:
<button disabled={resendCooldown > 0 || isLoading}>
  {resendCooldown > 0 ? `Reenviar en ${resendCooldown}s` : 'Reenviar codigo'}
</button>
```

---

### G-015: Password Strength Meter
**Prioridad:** P1
**Tipo:** UX
**Esfuerzo:** S

**Descripción:**
Register.tsx y ResetPassword.tsx muestran checkmarks de requerimientos:
```
✓ 8+ caracteres
✓ Una mayuscula
✗ Una minuscula
✗ Un numero
✗ Un caracter especial
```

**Falta:**
- Barra visual de fortaleza
- Estimación de tiempo para crackear
- Sugerencias en tiempo real

**Implementación:**
```tsx
<div className="space-y-2">
  <div className="flex items-center gap-2">
    <div className="flex-1 h-2 bg-gray-700 rounded-full overflow-hidden">
      <div
        className={`h-full transition-all ${
          strength === 'weak' ? 'w-1/3 bg-red-500' :
          strength === 'fair' ? 'w-2/3 bg-yellow-500' :
          'w-full bg-green-500'
        }`}
      />
    </div>
    <span className="text-xs text-gray-400">{strength}</span>
  </div>

  <p className="text-xs text-gray-500">
    Estimated crack time: ~100 years
  </p>
</div>
```

---

### G-019: Session Timeout Warning
**Prioridad:** P1
**Tipo:** UX
**Esfuerzo:** M

**Descripción:**
Sin advertencia previa a expiración de sesión.

**Implementación:**
```typescript
useEffect(() => {
  const token = localStorage.getItem('accessToken')
  const decoded = jwt_decode(token)
  const expiresAt = decoded.exp * 1000

  const warningTime = expiresAt - 5 * 60 * 1000 // 5 mins antes
  const timeUntilWarning = warningTime - Date.now()

  if (timeUntilWarning > 0) {
    const timeout = setTimeout(() => {
      showModal("Your session expires in 5 minutes. Click to stay logged in.")
    }, timeUntilWarning)

    return () => clearTimeout(timeout)
  }
}, [])
```

---

## GAPS DE SEGURIDAD

### G-021: Email Verification Resend
**Prioridad:** P1
**Tipo:** Implementación
**Esfuerzo:** S

**Descripción:**
VerifyEmail.tsx no ofrece opción de reenviar email.

**Escenario:**
1. User registra con email erróneo
2. No recibe email de verificación
3. Queda atrapado en VerifyEmail.tsx sin poder proceder

**Implementación:**
```tsx
{status === 'no-token' && (
  <div className="text-center">
    {/* ... */}
    <Link to="/login" className="btn btn-secondary block">
      Volver a Iniciar Sesion
    </Link>
    <button
      onClick={handleResendEmail}
      className="text-sm text-primary-400 hover:underline mt-4"
    >
      ¿No recibiste el email? Reenviar
    </button>
  </div>
)}
```

---

### G-024: Accessibility (a11y)
**Prioridad:** P1
**Tipo:** Accesibilidad
**Esfuerzo:** L

**Descripción:**
WCAG 2.1 AA compliance incompleta:

**Problemas Identificados:**
1. Faltan aria-labels en íconos
2. Contraste de color insuficiente en textos secundarios
3. Faltan aria-required en campos obligatorios
4. Sin aria-live para mensajes de error
5. Falta landmark structure (main, nav, etc)
6. Falta skip link para navegación

**Ejemplo de Corrección:**
```tsx
// Antes
<input type="email" placeholder="tu@email.com" />

// Después
<input
  type="email"
  placeholder="tu@email.com"
  aria-label="Email address"
  aria-required="true"
  aria-describedby="email-error"
/>
<div id="email-error" role="alert" aria-live="polite">
  {emailError && <span className="text-red-400">{emailError}</span>}
</div>
```

---

## GAPS DE ROBUSTEZ

### G-027: Error Boundary
**Prioridad:** P1
**Tipo:** Robustez
**Esfuerzo:** M

**Descripción:**
Sin ErrorBoundary en módulo auth.

**Riesgo:**
- Excepción en componente = crash de toda la app
- Usuario no sabe qué pasó

**Implementación:**
```tsx
// ErrorBoundary.tsx
class ErrorBoundary extends React.Component {
  state = { hasError: false }

  static getDerivedStateFromError() {
    return { hasError: true }
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="min-h-screen flex items-center justify-center">
          <div className="text-center">
            <h1 className="text-2xl font-bold text-white mb-4">
              Something went wrong
            </h1>
            <button onClick={() => window.location.href = '/'}>
              Go Home
            </button>
          </div>
        </div>
      )
    }

    return this.props.children
  }
}

// En módulo auth
<ErrorBoundary>
  <AuthModule />
</ErrorBoundary>
```

---

## TABLA RESUMEN

| Categoría | Cantidad | Críticos | Alto Impacto | Total Esfuerzo |
|-----------|----------|----------|----------|---|
| **2FA & Seguridad** | 6 | 3 | 3 | 120h |
| **UX & Localización** | 8 | 2 | 4 | 60h |
| **Robustez & Error Handling** | 4 | 1 | 2 | 50h |
| **Accesibilidad** | 2 | 1 | 1 | 40h |
| **Funcionalidad Avanzada** | 10 | 2 | 5 | 100h |
| **TOTAL** | 30 | 9 | 15 | 370h |

---

## ROADMAP RECOMENDADO

### Fase 1: Seguridad Crítica (2-3 semanas)
1. **G-001:** 2FA Setup UI
2. **G-002:** 2FA Enable
3. **G-012:** Refresh Token Auto-Renew
4. **G-018:** Account Lockout

**Impacto:** Reduce riesgo de brechas de seguridad en 80%

### Fase 2: UX & Funcionalidad Base (2-3 semanas)
1. **G-003:** Password Change Handler
2. **G-005:** OAuth User Mapping
3. **G-021:** Email Verification Resend
4. **G-008:** OTP Expiración UI
5. **G-015:** Password Strength Meter

**Impacto:** Mejora experiencia en 60%

### Fase 3: Robustez & Accesibilidad (2 semanas)
1. **G-024:** WCAG 2.1 Compliance
2. **G-027:** Error Boundary
3. **G-028:** Offline Support

**Impacto:** Aplicación ready-for-production

### Fase 4: Características Avanzadas (4-6 semanas)
1. **G-016:** Biometric Auth
2. **G-017:** WebAuthn
3. **G-023:** i18n / Localización
4. **G-026:** Mobile Responsiveness

**Impacto:** Diferenciación competitiva

---

## DEPENDENCIAS ENTRE GAPS

```
G-001 ──→ G-002 (G-002 requiere G-001)
G-012 ──→ Todo (auth es requisito)
G-005 ──→ G-006 (mapeo de usuario antes de backup codes)
G-018 ──→ Login (bloquea todos los logins)
G-016 ──→ Móvil (requiere app nativa)
```

---

## CONCLUSIÓN

**Status Actual:** 70% completo
**Gaps Críticos:** 9
**Esfuerzo Total:** 370 horas (~9.25 personas-semana)

**Para llegar a 100%:**
- Seguridad: Completar 2FA + Token Refresh (BLOQUEANTE)
- UX: Password change, session management
- Production: Error boundaries, a11y, offline

**Recomendación:** Enfocarse en Fase 1 antes de cualquier PR a main.

---

*Análisis completado: 2026-01-25*
*30 gaps identificados y priorizados*
