doctorlinux commited on
Commit
ec543fe
·
verified ·
1 Parent(s): ce8cc64

Upload 5 files

Browse files
Files changed (3) hide show
  1. README.md +17 -1
  2. app.py +33 -18
  3. index.html +202 -92
README.md CHANGED
@@ -8,4 +8,20 @@ app_file: app.py
8
  pinned: false
9
  ---
10
 
11
- # Autoevaluación de Ciberseguridad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  pinned: false
9
  ---
10
 
11
+ # Autoevaluación de Ciberseguridad Operativa
12
+
13
+ Herramienta interactiva para evaluar tu postura de ciberseguridad con diagnóstico automatizado.
14
+
15
+ ## 📋 Dominios Evaluados
16
+
17
+ - 🔥 Perímetro y Firewall
18
+ - 🖥️ Servidores y Hardening
19
+ - 💾 Backups y Recuperación
20
+ - 📊 Monitoreo y Alertas
21
+ - 🌐 Red y Conectividad
22
+ - 💻 Usuarios y Estaciones
23
+ - 🌐 Web y Correo
24
+
25
+ ---
26
+
27
+ *Desarrollado por Doctor Linux - Expertos en Ciberseguridad Operativa*
app.py CHANGED
@@ -1,10 +1,14 @@
1
  from fastapi import FastAPI
2
  from fastapi.responses import HTMLResponse
 
3
  from pydantic import BaseModel
4
  from typing import List
5
 
6
  app = FastAPI()
7
 
 
 
 
8
  class DomainScore(BaseModel):
9
  name: str
10
  score: int
@@ -15,37 +19,48 @@ class AnalysisRequest(BaseModel):
15
 
16
  @app.post("/analyze")
17
  async def analyze(data: AnalysisRequest):
 
18
  analysis = f"""
19
- 🔒 **ANÁLISIS DE CIBERSEGURIDAD - DOCTOR LINUX**
20
 
21
- **PUNTAJE GENERAL:** {data.overall}%
22
 
23
  **RESUMEN:**
24
- {"✅ Excelente postura de seguridad" if data.overall >= 80 else
25
- "⚠️ Postura media que requiere mejoras" if data.overall >= 60 else
26
- "🚨 Postura crítica que necesita atención inmediata"}
27
 
28
- **ÁREAS EVALUADAS:**
29
  {chr(10).join([f"• {domain.name}: {domain.score}%" for domain in data.domains])}
30
 
31
  **RECOMENDACIONES PRIORITARIAS:**
32
- 1. Revisar configuración de firewall y reglas
33
- 2. Implementar estrategia de backups 3-2-1
34
- 3. Establecer sistema de monitoreo 24/7
35
- 4. Implementar autenticación multifactor
36
- 5. Realizar pruebas de recuperación mensuales
37
-
38
- **PRÓXIMOS PASOS:**
39
- Agenda diagnóstico técnico detallado
40
- Desarrolla plan de remediación prioritario
41
- Establece métricas de seguimiento continuo
 
 
 
42
  """
43
  return {"analysis": analysis}
44
 
45
  @app.get("/")
46
  async def read_root():
47
- with open("index.html", "r", encoding="utf-8") as f:
48
- return HTMLResponse(content=f.read())
 
 
 
 
 
 
 
49
 
50
  if __name__ == "__main__":
51
  import uvicorn
 
1
  from fastapi import FastAPI
2
  from fastapi.responses import HTMLResponse
3
+ from fastapi.staticfiles import StaticFiles
4
  from pydantic import BaseModel
5
  from typing import List
6
 
7
  app = FastAPI()
8
 
9
+ # Servir archivos estáticos
10
+ app.mount("/static", StaticFiles(directory="."), name="static")
11
+
12
  class DomainScore(BaseModel):
13
  name: str
14
  score: int
 
19
 
20
  @app.post("/analyze")
21
  async def analyze(data: AnalysisRequest):
22
+ # Análisis básico sin IA para evitar problemas
23
  analysis = f"""
24
+ 🔒 **DIAGNÓSTICO DE CIBERSEGURIDAD - DOCTOR LINUX**
25
 
26
+ **PUNTAJE GLOBAL:** {data.overall}%
27
 
28
  **RESUMEN:**
29
+ {"✅ POSTURA SÓLIDA - Buen trabajo en controles de seguridad" if data.overall >= 80 else
30
+ "⚠️ POSTURA MEDIA - Se requieren mejoras en algunos controles" if data.overall >= 60 else
31
+ "🚨 RIESGO ELEVADO - Atención inmediata requerida"}
32
 
33
+ **DETALLE POR DOMINIOS:**
34
  {chr(10).join([f"• {domain.name}: {domain.score}%" for domain in data.domains])}
35
 
36
  **RECOMENDACIONES PRIORITARIAS:**
37
+ 1. Endurecer perímetro con reglas mínimas necesarias
38
+ 2. Implementar estrategia de backups 3-2-1 con pruebas
39
+ 3. Establecer monitoreo centralizado 24/7
40
+ 4. Aplicar hardening en servidores y estaciones
41
+ 5. Implementar autenticación multifactor (MFA)
42
+
43
+ **PRÓXIMOS PASOS SUGERIDOS:**
44
+ Solicitar diagnóstico técnico gratuito
45
+ Desarrollar plan de remediación priorizado
46
+ Establecer métricas de seguimiento continuo
47
+
48
+ ---
49
+ *Diagnóstico generado por Doctor Linux - Expertos en Ciberseguridad Operativa*
50
  """
51
  return {"analysis": analysis}
52
 
53
  @app.get("/")
54
  async def read_root():
55
+ try:
56
+ with open("index.html", "r", encoding="utf-8") as f:
57
+ return HTMLResponse(content=f.read())
58
+ except Exception as e:
59
+ return HTMLResponse(content=f"<h1>Error cargando la aplicación</h1><p>{e}</p>")
60
+
61
+ @app.get("/health")
62
+ async def health_check():
63
+ return {"status": "healthy", "service": "Cibertest Doctor Linux"}
64
 
65
  if __name__ == "__main__":
66
  import uvicorn
index.html CHANGED
@@ -12,16 +12,20 @@
12
  .dlx-sub{color:#5c6b5f;margin-bottom:16px}
13
  .dlx-progress{height:10px;background:#eff4f0;border-radius:999px;overflow:hidden;margin:10px 0 18px}
14
  .dlx-bar{height:100%;width:0;background:#7fbf7f;transition:width .3s}
15
- .dlx-btn{background:#214424;color:#fff;border:none;border-radius:10px;padding:12px 16px;cursor:pointer;transition:background .2s}
16
- .dlx-btn:hover{background:#1a361c}
17
- .dlx-btn:disabled{background:#ccc;cursor:not-allowed}
18
  .dlx-options{display:flex;gap:10px;flex-wrap:wrap;margin:6px 0 12px}
19
- .dlx-pill{border:1px solid #d7e1d9;padding:8px 12px;border-radius:999px;cursor:pointer;transition:all .2s}
20
- .dlx-pill:hover{background:#f0f7f0}
21
  .dlx-pill.active{background:#214424;color:#fff;border-color:#214424}
 
 
 
 
 
 
22
  .dlx-note{color:#6a786d}
23
  .dlx-score{font-size:40px;font-weight:800;color:#214424}
24
- .loading{opacity:0.7;pointer-events:none}
25
  </style>
26
  </head>
27
 
@@ -29,94 +33,178 @@
29
  <div class="dlx-wrap" id="dlxApp">
30
  <div class="dlx-card">
31
  <h2 class="dlx-title">Autoevaluación de Ciberseguridad Operativa</h2>
32
- <div class="dlx-sub">Responde con honestidad y obtendrás un diagnóstico inmediato.</div>
33
  <div class="dlx-progress"><div class="dlx-bar" id="dlxBar"></div></div>
34
- <div id="dlxStepNote" class="dlx-note"></div>
35
  </div>
36
 
37
  <div id="dlxSteps"></div>
38
 
39
  <div class="dlx-card" id="dlxNavBox">
40
- <div style="display:flex;justify-content:space-between;">
41
  <button class="dlx-btn" id="prevBtn">← Anterior</button>
42
  <button class="dlx-btn" id="nextBtn">Siguiente →</button>
43
  </div>
44
  </div>
45
 
46
  <div class="dlx-card" id="dlxResult" style="display:none">
47
- <h3 class="dlx-title">Resultado</h3>
48
- <div class="dlx-score" id="dlxScore">—%</div>
49
- <div class="dlx-note" id="dlxLabel"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- <button id="analyzeBtn" class="dlx-btn" style="margin-top:20px;">🔍 Analizar con IA</button>
52
  <div id="iaBox" class="dlx-card" style="display:none;margin-top:16px;">
53
- <h4>Análisis generado por IA</h4>
54
  <pre id="iaOutput" style="white-space:pre-wrap;font-family:inherit;"></pre>
55
  </div>
56
  </div>
57
  </div>
58
 
59
  <script>
60
- const DOMAINS=[
61
- {id:'perimetro',name:'Perímetro / Firewall',qs:[
62
- '¿Usas un firewall dedicado (Mikrotik/OPNsense/etc.)?',
63
- '¿Tienes listas de acceso/IPS/DoS activas?',
64
- '¿VPN con 2FA implementada?']},
65
- {id:'backups',name:'Backups',qs:[
66
- '¿Aplicas la regla 3-2-1?',
67
- '¿Pruebas restauración recientes?',
68
- '¿Copias protegidas contra ransomware?']},
69
- {id:'monitor',name:'Monitoreo',qs:[
70
- '¿Monitoreo 24/7 (Zabbix/PRTG)?',
71
- '¿Alertas integradas con soporte?',
72
- '¿Métricas revisadas mensualmente?']}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  ];
 
 
74
  const SCALE=[
75
  {label:'No',score:0},
76
  {label:'Parcial',score:50},
77
  {label:'Sí',score:100},
78
  {label:'No sé',score:null}
79
  ];
 
 
 
 
 
 
 
 
 
 
 
80
  let current=0,answers={};
81
- const steps=document.getElementById('dlxSteps'),bar=document.getElementById('dlxBar'),
82
- prev=document.getElementById('prevBtn'),next=document.getElementById('nextBtn');
 
 
 
 
 
83
 
84
  function render(){
85
- steps.innerHTML='';
86
- const d=DOMAINS[current];
87
- const c=document.createElement('div');
88
- c.className='dlx-card';
89
- c.innerHTML=`<h3>${d.name}</h3>`;
90
- d.qs.forEach((q,i)=>{
91
- const row=document.createElement('div');
92
- row.className='dlx-options';
93
- row.innerHTML=`<p style="margin:0 0 8px 0;font-weight:500;">${q}</p>`;
94
- SCALE.forEach(opt=>{
95
- const lbl=document.createElement('label');
96
- lbl.className='dlx-pill';
97
- lbl.textContent=opt.label;
98
- lbl.onclick=()=>{
99
- row.querySelectorAll('.dlx-pill').forEach(x=>x.classList.remove('active'));
100
- lbl.classList.add('active');
101
- if(!answers[d.id])answers[d.id]=[];
102
- answers[d.id][i]=opt.score;
103
- updateButtons();
104
- };
105
- if(answers[d.id]&&answers[d.id][i]===opt.score)lbl.classList.add('active');
106
- row.appendChild(lbl);
107
- });
108
- c.appendChild(row);
 
109
  });
110
- steps.appendChild(c);
111
- bar.style.width=Math.round(current/DOMAINS.length*100)+'%';
112
- updateButtons();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
 
115
- function allAnswered(i){const d=DOMAINS[i];return answers[d.id]&&answers[d.id].length===d.qs.length&&answers[d.id].every(v=>v!==undefined);}
116
- function avg(a){const n=a.filter(v=>typeof v==='number');return n.length?n.reduce((x,y)=>x+y,0)/n.length:0;}
117
  function updateButtons(){
118
  prev.disabled=current===0;
119
- next.textContent=current===DOMAINS.length-1?'Ver resultado →':'Siguiente →';
 
120
  next.disabled=!allAnswered(current);
121
  }
122
 
@@ -126,52 +214,74 @@ next.onclick=()=>{
126
  if(!allAnswered(current))return;
127
  current++;render();
128
  }else{
 
129
  showResult();
130
  }
131
  };
132
 
133
  function showResult(){
134
- document.getElementById('dlxNavBox').style.display='none';
 
135
  steps.style.display='none';
136
- document.getElementById('dlxResult').style.display='block';
137
- let total=0;
138
- DOMAINS.forEach(d=>{total+=avg(answers[d.id]);});
139
- const global=Math.round(total/DOMAINS.length);
 
 
 
 
 
 
 
 
 
 
 
140
  document.getElementById('dlxScore').textContent=global+'%';
141
- const lbl=document.getElementById('dlxLabel');
142
- lbl.textContent=global>=80?'Nivel Sólido':global>=60?'Nivel Medio (revisar)':'Riesgo Alto';
143
- lbl.style.color=global>=80?'green':global>=60?'#c6a500':'red';
144
- window.finalData={overall:global,domains:DOMAINS.map(d=>({name:d.name,score:Math.round(avg(answers[d.id]))}))};
 
 
 
 
 
 
 
 
 
 
145
  }
146
 
147
- // 🔹 Mejorado: Manejo robusto de errores
148
- document.getElementById('analyzeBtn').onclick=async()=>{
149
- const box=document.getElementById('iaBox'),out=document.getElementById('iaOutput');
150
- const btn=document.getElementById('analyzeBtn');
 
151
 
152
- box.style.display='block';
153
- out.textContent='Analizando con IA...';
154
- btn.disabled=true;
155
- btn.classList.add('loading');
156
 
157
- try{
158
- const res=await fetch('/analyze',{
159
- method:'POST',
160
- headers:{'Content-Type':'application/json'},
161
- body:JSON.stringify(window.finalData)
162
  });
163
 
164
- if(!res.ok) throw new Error('Error del servidor');
165
-
166
- const data=await res.json();
167
- out.textContent=data.analysis||'Análisis completado.';
168
-
169
- }catch(error){
170
- console.error('Error:',error);
171
- out.textContent=' Evaluación guardada. Puntaje: ' + window.finalData.overall + '%\n\nPara análisis detallado, contacta a Doctor Linux.';
172
- }finally{
173
- btn.disabled=false;
174
- btn.classList.remove('loading');
175
  }
176
  };
177
 
 
12
  .dlx-sub{color:#5c6b5f;margin-bottom:16px}
13
  .dlx-progress{height:10px;background:#eff4f0;border-radius:999px;overflow:hidden;margin:10px 0 18px}
14
  .dlx-bar{height:100%;width:0;background:#7fbf7f;transition:width .3s}
15
+ .dlx-q h4{margin:0 0 6px;font-size:18px}
 
 
16
  .dlx-options{display:flex;gap:10px;flex-wrap:wrap;margin:6px 0 12px}
17
+ .dlx-pill{border:1px solid #d7e1d9;padding:8px 12px;border-radius:999px;cursor:pointer}
18
+ .dlx-pill input{display:none}
19
  .dlx-pill.active{background:#214424;color:#fff;border-color:#214424}
20
+ .dlx-nav{display:flex;gap:10px;justify-content:space-between;margin-top:8px}
21
+ .dlx-btn{background:#214424;color:#fff;border:none;border-radius:10px;padding:12px 16px;cursor:pointer}
22
+ .dlx-btn[disabled]{opacity:.5;cursor:not-allowed}
23
+ .dlx-meter{display:grid;grid-template-columns:1fr auto;gap:6px 10px;margin:8px 0}
24
+ .dlx-badge{font-weight:600}
25
+ .dlx-tag{display:inline-block;background:#eff4f0;border:1px solid #dfe9e2;border-radius:8px;padding:6px 10px;margin:6px 6px 0 0}
26
  .dlx-note{color:#6a786d}
27
  .dlx-score{font-size:40px;font-weight:800;color:#214424}
28
+ .dlx-muted{color:#7b8b7f}
29
  </style>
30
  </head>
31
 
 
33
  <div class="dlx-wrap" id="dlxApp">
34
  <div class="dlx-card">
35
  <h2 class="dlx-title">Autoevaluación de Ciberseguridad Operativa</h2>
36
+ <div class="dlx-sub">Responde con honestidad. Obtendrás tu <b>puntaje</b>, los <b>riesgos clave</b> y un plan sugerido de mejora.</div>
37
  <div class="dlx-progress"><div class="dlx-bar" id="dlxBar"></div></div>
38
+ <div class="dlx-note dlx-muted" id="dlxStepNote"></div>
39
  </div>
40
 
41
  <div id="dlxSteps"></div>
42
 
43
  <div class="dlx-card" id="dlxNavBox">
44
+ <div class="dlx-nav">
45
  <button class="dlx-btn" id="prevBtn">← Anterior</button>
46
  <button class="dlx-btn" id="nextBtn">Siguiente →</button>
47
  </div>
48
  </div>
49
 
50
  <div class="dlx-card" id="dlxResult" style="display:none">
51
+ <h3 class="dlx-title">Tu diagnóstico</h3>
52
+ <div class="dlx-sub">Este resultado es orientativo. Si quieres un informe firmado por ingeniería, solicita el <b>diagnóstico gratuito</b>.</div>
53
+ <div style="display:flex;gap:24px;align-items:center;flex-wrap:wrap">
54
+ <div>
55
+ <div class="dlx-muted">Puntaje global</div>
56
+ <div class="dlx-score" id="dlxScore">—%</div>
57
+ <div class="dlx-tag" id="dlxLabel">—</div>
58
+ </div>
59
+ <div style="flex:1;min-width:260px">
60
+ <div class="dlx-progress"><div class="dlx-bar" id="dlxBarFinal" style="background:#3aa655"></div></div>
61
+ <div class="dlx-note">≈ Mayor a 80%: postura sólida. 60–79%: revisar. Menor a 60%: riesgo elevado.</div>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="dlx-card" style="margin-top:16px">
66
+ <h4 style="margin:0 0 8px">Detalle por dominio</h4>
67
+ <div id="dlxBreakdown" class="dlx-meter"></div>
68
+ </div>
69
+
70
+ <div class="dlx-card">
71
+ <h4 style="margin:0 0 8px">Recomendaciones inmediatas</h4>
72
+ <div id="dlxRecs"></div>
73
+ <div class="dlx-note" style="margin-top:8px">Tip: prioriza los dominios con menor puntaje. Podemos ayudarte a cerrar brechas con <b>hardening, firewalls, backups 3-2-1 y monitoreo 24/7</b>.</div>
74
+ </div>
75
+
76
+ <div class="dlx-nav">
77
+ <button class="dlx-btn" onclick="window.location.reload()">Refrescar test</button>
78
+ <button class="dlx-btn" id="analyzeBtn">🔍 Análisis Detallado</button>
79
+ </div>
80
 
 
81
  <div id="iaBox" class="dlx-card" style="display:none;margin-top:16px;">
82
+ <h4>Análisis Generado por IA</h4>
83
  <pre id="iaOutput" style="white-space:pre-wrap;font-family:inherit;"></pre>
84
  </div>
85
  </div>
86
  </div>
87
 
88
  <script>
89
+ /* === lógica actualizada === */
90
+ const DOMAINS = [
91
+ { id:'perimetro', name:'Perímetro / Firewall', qs:[
92
+ '¿Usas un firewall dedicado (Mikrotik/OPNsense/etc.) con reglas mínimas por servicio?',
93
+ '¿Tienes listas de acceso/geo-bloqueo/IPS/DoS activas?',
94
+ '¿Los servicios expuestos están detrás de NAT o Port-Forward controlado?',
95
+ '¿Hay políticas VPN de acceso remoto con 2FA?'
96
+ ]},
97
+ { id:'servers', name:'Servidores / Hardening', qs:[
98
+ '¿Cuentas con hardening básico (SSH seguro, usuarios/roles, auditoría)?',
99
+ '¿Actualizaciones y parches aplicados regularmente?',
100
+ '¿Servicios innecesarios deshabilitados/controlados?'
101
+ ]},
102
+ { id:'backups', name:'Backups y Recuperación', qs:[
103
+ '¿Aplicas la regla 3-2-1 (3 copias, 2 medios, 1 offsite)?',
104
+ '¿Pruebas de restauración realizadas en los últimos 6 meses?',
105
+ '¿Backups protegidos contra ransomware (inmutables/air-gap)?'
106
+ ]},
107
+ { id:'monitor', name:'Monitoreo y Alertas', qs:[
108
+ '¿Monitoreo centralizado (Zabbix/PRTG/Grafana) con alertas 24/7?',
109
+ '¿Métricas revisadas mensualmente?',
110
+ '¿Alertas integradas con soporte?'
111
+ ]},
112
+ { id:'network', name:'Red y Conectividad', qs:[
113
+ '¿Segmentación VLANs para aislar servicios críticos?',
114
+ '¿Control de ancho de banda/Proxy/filtrado de contenidos?',
115
+ '¿Wi-Fi empresarial con WPA2-Enterprise?'
116
+ ]},
117
+ { id:'endpoints', name:'Usuarios / Estaciones', qs:[
118
+ '¿Antimalware/EDR y políticas de actualización?',
119
+ '¿Gestión de contraseñas y MFA?',
120
+ '¿Políticas de mínimo privilegio + bloqueo de USB?'
121
+ ]},
122
+ { id:'webmail', name:'Web y Correo', qs:[
123
+ '¿Sitios con HTTPS válido y cabeceras seguras?',
124
+ '¿Correo con SPF, DKIM y DMARC publicados?',
125
+ '¿WAF/antispam/antiphishing activos?'
126
+ ]}
127
  ];
128
+
129
+ // 🟢 Ahora "No sé" no suma ni resta
130
  const SCALE=[
131
  {label:'No',score:0},
132
  {label:'Parcial',score:50},
133
  {label:'Sí',score:100},
134
  {label:'No sé',score:null}
135
  ];
136
+
137
+ const RECS={
138
+ perimetro:'Endurecer perímetro: reglas mínimo necesario, VPN 2FA, IDS/IPS y protección DoS.',
139
+ servers:'Aplicar hardening, cuentas/roles y mantener parches al día.',
140
+ backups:'Implementar 3-2-1 con pruebas de restauración y backups inmutables.',
141
+ monitor:'Centralizar monitoreo (Zabbix/PRTG) y definir umbrales.',
142
+ network:'Segregar VLANs, reforzar proxy y aislar Wi-Fi invitados.',
143
+ endpoints:'Mínimo privilegio, EDR/antimalware y MFA.',
144
+ webmail:'Forzar HTTPS, cabeceras OWASP, SPF+DKIM+DMARC y WAF.'
145
+ };
146
+
147
  let current=0,answers={};
148
+ const bar=document.getElementById('dlxBar'),
149
+ steps=document.getElementById('dlxSteps'),
150
+ note=document.getElementById('dlxStepNote'),
151
+ prev=document.getElementById('prevBtn'),
152
+ next=document.getElementById('nextBtn'),
153
+ result=document.getElementById('dlxResult'),
154
+ nav=document.getElementById('dlxNavBox');
155
 
156
  function render(){
157
+ steps.innerHTML='';
158
+ const d=DOMAINS[current];
159
+ note.innerHTML=`<b>${current+1}/${DOMAINS.length}</b> · ${d.name}`;
160
+ const card=document.createElement('div');
161
+ card.className='dlx-card';
162
+ card.innerHTML=`<h3 class="dlx-title">${d.name}</h3>`;
163
+ d.qs.forEach((q,i)=>{
164
+ const b=document.createElement('div');
165
+ b.className='dlx-q';
166
+ b.innerHTML=`<h4>${q}</h4>`;
167
+ const row=document.createElement('div');
168
+ row.className='dlx-options';
169
+ SCALE.forEach(opt=>{
170
+ const lbl=document.createElement('label');
171
+ lbl.className='dlx-pill';
172
+ lbl.textContent=opt.label;
173
+ lbl.onclick=()=>{
174
+ row.querySelectorAll('.dlx-pill').forEach(p=>p.classList.remove('active'));
175
+ lbl.classList.add('active');
176
+ if(!answers[d.id])answers[d.id]=[];
177
+ answers[d.id][i]=opt.score;
178
+ updateButtons();
179
+ };
180
+ if(answers[d.id]&&answers[d.id][i]===opt.score)lbl.classList.add('active');
181
+ row.appendChild(lbl);
182
  });
183
+ b.appendChild(row);
184
+ card.appendChild(b);
185
+ });
186
+ steps.appendChild(card);
187
+ bar.style.width=Math.round(current/DOMAINS.length*100)+'%';
188
+ updateButtons();
189
+ }
190
+
191
+ // ✅ Acepta "No sé" como respuesta
192
+ function allAnswered(i){
193
+ const d=DOMAINS[i];
194
+ return answers[d.id] && answers[d.id].length===d.qs.length && answers[d.id].every(v=>v!==undefined);
195
+ }
196
+
197
+ // ✅ Ignora "No sé" al promediar
198
+ function avg(a){
199
+ const nums=a.filter(v=>typeof v==='number' && isFinite(v));
200
+ if(nums.length===0) return 0;
201
+ return nums.reduce((x,y)=>x+y,0)/nums.length;
202
  }
203
 
 
 
204
  function updateButtons(){
205
  prev.disabled=current===0;
206
+ const last=current===DOMAINS.length-1;
207
+ next.textContent=last?'Ver resultado →':'Siguiente →';
208
  next.disabled=!allAnswered(current);
209
  }
210
 
 
214
  if(!allAnswered(current))return;
215
  current++;render();
216
  }else{
217
+ if(!allAnswered(current))return;
218
  showResult();
219
  }
220
  };
221
 
222
  function showResult(){
223
+ bar.style.width='100%';
224
+ nav.style.display='none';
225
  steps.style.display='none';
226
+ note.style.display='none';
227
+ result.style.display='block';
228
+ let weighted=0,total=0;
229
+ const breakdown=document.getElementById('dlxBreakdown'),
230
+ recs=document.getElementById('dlxRecs');
231
+ breakdown.innerHTML='';
232
+ recs.innerHTML='';
233
+ DOMAINS.forEach(d=>{
234
+ const a=answers[d.id]?avg(answers[d.id]):0;
235
+ weighted+=a;total+=100;
236
+ const pct=Math.round(a);
237
+ breakdown.innerHTML+=`<div>${d.name}</div><div><b>${pct}%</b></div>`;
238
+ if(pct<75){recs.innerHTML+=`<div class="dlx-tag">• ${RECS[d.id]}</div>`;}
239
+ });
240
+ const global=Math.round(weighted/DOMAINS.length);
241
  document.getElementById('dlxScore').textContent=global+'%';
242
+ document.getElementById('dlxBarFinal').style.width=global+'%';
243
+ const label=document.getElementById('dlxLabel');
244
+ if(global>=80){label.textContent='Nivel Sólido';label.style.background='#e7f7e7';}
245
+ else if(global>=60){label.textContent='Nivel Medio (revisar)';label.style.background='#fff6e1';}
246
+ else{label.textContent='Riesgo Alto';label.style.background='#ffe9e9';}
247
+
248
+ // Guardar datos para el análisis IA
249
+ window.finalData = {
250
+ overall: global,
251
+ domains: DOMAINS.map(d => ({
252
+ name: d.name,
253
+ score: Math.round(avg(answers[d.id]))
254
+ }))
255
+ };
256
  }
257
 
258
+ // 🔍 Análisis con IA
259
+ document.getElementById('analyzeBtn').onclick = async function() {
260
+ const box = document.getElementById('iaBox');
261
+ const out = document.getElementById('iaOutput');
262
+ const btn = this;
263
 
264
+ box.style.display = 'block';
265
+ out.textContent = 'Generando análisis detallado...';
266
+ btn.disabled = true;
 
267
 
268
+ try {
269
+ const res = await fetch('/analyze', {
270
+ method: 'POST',
271
+ headers: {'Content-Type': 'application/json'},
272
+ body: JSON.stringify(window.finalData)
273
  });
274
 
275
+ if (res.ok) {
276
+ const data = await res.json();
277
+ out.textContent = data.analysis || 'Análisis completado.';
278
+ } else {
279
+ throw new Error('Error del servidor');
280
+ }
281
+ } catch (error) {
282
+ out.textContent = 'El análisis detallado no está disponible en este momento.\n\nPuntaje: ' + window.finalData.overall + '%\n\nPara un diagnóstico completo, contacta a Doctor Linux.';
283
+ } finally {
284
+ btn.disabled = false;
 
285
  }
286
  };
287