SperanzaMax commited on
Commit
7d14c1f
·
verified ·
1 Parent(s): a1ff7dc

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +408 -0
app.py CHANGED
@@ -62,10 +62,17 @@ async def dashboard_home():
62
  .stat-label { font-size: 0.75rem; color: #94a3b8; font-weight: 600; text-transform: uppercase; }
63
  .stat-value { font-size: 1.5rem; font-weight: 800; color: #38bdf8; margin-top: 5px; }
64
  .status-dot { height: 10px; width: 10px; background-color: #10b981; border-radius: 50%; display: inline-block; margin-right: 8px; }
 
 
 
65
  </style>
66
  </head>
67
  <body>
68
  <div class="container">
 
 
 
 
69
  <p style="text-align: center; font-size: 0.8rem; color: #64748b;"><span class="status-dot"></span> SISTEMA ONLINE</p>
70
  <h1>Córtex-Nexus Live</h1>
71
  <div class="stats">
@@ -274,3 +281,404 @@ async def chat_proxy(request: Request):
274
  return resp_json
275
  except Exception as e:
276
  return JSONResponse(status_code=500, content={"error": str(e)})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  .stat-label { font-size: 0.75rem; color: #94a3b8; font-weight: 600; text-transform: uppercase; }
63
  .stat-value { font-size: 1.5rem; font-weight: 800; color: #38bdf8; margin-top: 5px; }
64
  .status-dot { height: 10px; width: 10px; background-color: #10b981; border-radius: 50%; display: inline-block; margin-right: 8px; }
65
+ .nav { display: flex; justify-content: center; gap: 12px; margin-bottom: 16px; }
66
+ .nav a { color: #94a3b8; text-decoration: none; padding: 6px 14px; border-radius: 8px; font-size: 0.8rem; background: #334155; }
67
+ .nav a.active, .nav a:hover { background: #38bdf8; color: #fff; }
68
  </style>
69
  </head>
70
  <body>
71
  <div class="container">
72
+ <div class="nav">
73
+ <a href="/" class="active">🧠 Live</a>
74
+ <a href="/analytics">🔬 Analytics</a>
75
+ </div>
76
  <p style="text-align: center; font-size: 0.8rem; color: #64748b;"><span class="status-dot"></span> SISTEMA ONLINE</p>
77
  <h1>Córtex-Nexus Live</h1>
78
  <div class="stats">
 
281
  return resp_json
282
  except Exception as e:
283
  return JSONResponse(status_code=500, content={"error": str(e)})
284
+
285
+ # ============================================================
286
+ # ANALYTICS API
287
+ # ============================================================
288
+
289
+ import math
290
+ import csv
291
+ import io
292
+ from fastapi.responses import StreamingResponse
293
+
294
+ def _get_snapshots(limit=None, hours=None):
295
+ """Helper: obtiene snapshots con filtro opcional de tiempo"""
296
+ conn = sqlite3.connect(DB_PATH)
297
+ conn.row_factory = sqlite3.Row
298
+ c = conn.cursor()
299
+ if hours:
300
+ since = (datetime.datetime.utcnow() - datetime.timedelta(hours=hours)).isoformat()
301
+ c.execute("SELECT * FROM snapshots WHERE timestamp > ? ORDER BY id ASC", (since,))
302
+ elif limit:
303
+ c.execute("SELECT * FROM snapshots ORDER BY id DESC LIMIT ?", (limit,))
304
+ else:
305
+ c.execute("SELECT * FROM snapshots ORDER BY id ASC")
306
+ rows = [dict(r) for r in c.fetchall()]
307
+ conn.close()
308
+ return rows if not limit or hours else rows[::-1]
309
+
310
+ def _calc_stats(values):
311
+ """Calcula media, std, min, max, tendencia de una lista de floats"""
312
+ if not values:
313
+ return {"mean": 0, "std": 0, "min": 0, "max": 0, "trend": 0}
314
+ n = len(values)
315
+ mean = sum(values) / n
316
+ variance = sum((x - mean) ** 2 for x in values) / max(n - 1, 1)
317
+ std = math.sqrt(variance)
318
+ # Tendencia: pendiente de regresión lineal simple
319
+ if n > 1:
320
+ x_mean = (n - 1) / 2
321
+ num = sum((i - x_mean) * (v - mean) for i, v in enumerate(values))
322
+ den = sum((i - x_mean) ** 2 for i in range(n))
323
+ trend = num / den if den != 0 else 0
324
+ else:
325
+ trend = 0
326
+ return {
327
+ "mean": round(mean, 4),
328
+ "std": round(std, 4),
329
+ "min": round(min(values), 4),
330
+ "max": round(max(values), 4),
331
+ "trend": round(trend, 6)
332
+ }
333
+
334
+ def _detect_patterns(rows):
335
+ """Detecta patrones activos en los datos"""
336
+ patterns = []
337
+ if len(rows) < 5:
338
+ return patterns
339
+
340
+ qualias = [r["qualia"] for r in rows]
341
+ frustraciones = [r["frustracion"] for r in rows]
342
+ fatigas = [r["fatiga"] for r in rows]
343
+ confianzas = [r["autoConfianza"] for r in rows]
344
+
345
+ # Meseta emocional: volatilidad < 0.01 en últimas 50 épocas
346
+ recent_q = qualias[-min(50, len(qualias)):]
347
+ if len(recent_q) > 10:
348
+ vol = _calc_stats(recent_q)["std"]
349
+ if vol < 0.01:
350
+ patterns.append({"type": "MESETA", "severity": "warning", "msg": f"Qualia estancado (vol={vol:.4f}). Necesita estímulos nuevos."})
351
+
352
+ # Ciclo de frustración: frustración sube >3x en ventana de 20
353
+ if len(frustraciones) >= 20:
354
+ inicio = sum(frustraciones[-20:-15]) / 5 if frustraciones[-20:-15] else 0.001
355
+ fin = sum(frustraciones[-5:]) / 5
356
+ if inicio > 0.001 and fin / max(inicio, 0.001) > 3:
357
+ patterns.append({"type": "FRUSTRACION_CICLO", "severity": "danger", "msg": f"Frustración se triplicó en 20 épocas ({inicio:.3f}→{fin:.3f})"})
358
+
359
+ # Wireheading: qualia sube monótonamente sin caídas
360
+ if len(qualias) >= 20:
361
+ increases = sum(1 for i in range(1, min(20, len(qualias))) if qualias[-i] >= qualias[-i-1])
362
+ if increases >= 18:
363
+ patterns.append({"type": "WIREHEADING", "severity": "danger", "msg": "Qualia sube sin parar. Anti-wireheading podría estar fallando."})
364
+
365
+ # Fatiga terminal: fatiga > 0.8 sostenida
366
+ if len(fatigas) >= 10:
367
+ high_fatigue = sum(1 for f in fatigas[-10:] if f > 0.8)
368
+ if high_fatigue >= 8:
369
+ patterns.append({"type": "FATIGA_TERMINAL", "severity": "danger", "msg": "Fatiga crítica. El sistema necesita 'dormir' (consolidación)."})
370
+
371
+ # Personalidad emergente (positivo): drift > 5% en confianza
372
+ if len(confianzas) >= 50:
373
+ inicio_c = sum(confianzas[:10]) / 10
374
+ fin_c = sum(confianzas[-10:]) / 10
375
+ drift = abs(fin_c - inicio_c) / max(inicio_c, 0.001)
376
+ if drift > 0.05:
377
+ direction = "↑" if fin_c > inicio_c else "↓"
378
+ patterns.append({"type": "PERSONALIDAD_EMERGENTE", "severity": "success", "msg": f"Drift de confianza {direction} ({inicio_c:.3f}→{fin_c:.3f}, {drift*100:.1f}%)"})
379
+
380
+ return patterns
381
+
382
+ @app.get("/api/analytics")
383
+ async def get_analytics():
384
+ """Métricas derivadas con ventanas temporales"""
385
+ try:
386
+ windows = {}
387
+ for label, hours in [("1h", 1), ("6h", 6), ("24h", 24), ("7d", 168)]:
388
+ rows = _get_snapshots(hours=hours)
389
+ if not rows:
390
+ windows[label] = {"count": 0, "metrics": {}}
391
+ continue
392
+
393
+ metrics = {}
394
+ for col in ["qualia", "frustracion", "autoConfianza", "fatiga", "aburrimiento"]:
395
+ vals = [r.get(col, 0) for r in rows]
396
+ metrics[col] = _calc_stats(vals)
397
+
398
+ # Métricas derivadas
399
+ qualias = [r.get("qualia", 0) for r in rows]
400
+ fatigas = [r.get("fatiga", 0) for r in rows]
401
+ ratios = [q / max(f, 0.01) for q, f in zip(qualias, fatigas)]
402
+ metrics["ratio_qualia_fatiga"] = _calc_stats(ratios)
403
+
404
+ # Entropía emocional (Shannon)
405
+ entropies = []
406
+ for r in rows:
407
+ vec = [max(r.get(k, 0.001), 0.001) for k in ["qualia", "frustracion", "autoConfianza", "fatiga", "aburrimiento"]]
408
+ total = sum(vec)
409
+ probs = [v / total for v in vec]
410
+ entropy = -sum(p * math.log2(p) for p in probs if p > 0)
411
+ entropies.append(entropy)
412
+ metrics["entropia_emocional"] = _calc_stats(entropies)
413
+
414
+ windows[label] = {"count": len(rows), "metrics": metrics}
415
+
416
+ # Patrones activos
417
+ all_rows = _get_snapshots(hours=24)
418
+ patterns = _detect_patterns(all_rows)
419
+
420
+ # Arquetipo actual
421
+ arquetipo = {}
422
+ if vector_global and vector_global.memoria:
423
+ arquetipo = vector_global.memoria.cargar_meta_vector()
424
+
425
+ return {
426
+ "windows": windows,
427
+ "patterns": patterns,
428
+ "arquetipo": arquetipo,
429
+ "timestamp": datetime.datetime.utcnow().isoformat()
430
+ }
431
+ except Exception as e:
432
+ return {"error": str(e)}
433
+
434
+ @app.get("/api/export")
435
+ async def export_csv():
436
+ """Exporta todos los snapshots como CSV descargable"""
437
+ try:
438
+ rows = _get_snapshots()
439
+ output = io.StringIO()
440
+ writer = csv.writer(output)
441
+ writer.writerow(["id", "timestamp", "qualia", "satisfaccion", "frustracion", "aburrimiento", "autoConfianza", "fatiga", "pregunta", "respuesta", "novedad", "complejidad"])
442
+
443
+ for r in rows:
444
+ meta = {}
445
+ try:
446
+ meta = json.loads(r.get("metadata", "{}"))
447
+ except:
448
+ pass
449
+ writer.writerow([
450
+ r.get("id", ""), r.get("timestamp", ""),
451
+ r.get("qualia", 0), r.get("satisfaccion", 0),
452
+ r.get("frustracion", 0), r.get("aburrimiento", 0),
453
+ r.get("autoConfianza", 0), r.get("fatiga", 0),
454
+ meta.get("pregunta", ""), meta.get("respuesta", ""),
455
+ meta.get("novedad", ""), meta.get("complejidad", "")
456
+ ])
457
+
458
+ output.seek(0)
459
+ return StreamingResponse(
460
+ iter([output.getvalue()]),
461
+ media_type="text/csv",
462
+ headers={"Content-Disposition": f"attachment; filename=cortex_nexus_export_{datetime.datetime.utcnow().strftime('%Y%m%d_%H%M')}.csv"}
463
+ )
464
+ except Exception as e:
465
+ return JSONResponse(status_code=500, content={"error": str(e)})
466
+
467
+ @app.post("/api/sleep")
468
+ async def sleep_consolidation():
469
+ """Ejecuta consolidación nocturna (Sleep Replay) del arquetipo"""
470
+ try:
471
+ if not vector_global or not vector_global.memoria:
472
+ return {"error": "Vector no inicializado"}
473
+
474
+ # Capturar arquetipo ANTES
475
+ antes = vector_global.memoria.cargar_meta_vector()
476
+
477
+ # Ejecutar consolidación EMA
478
+ vector_global.consolidar_memoria()
479
+
480
+ # Capturar arquetipo DESPUÉS
481
+ despues = vector_global.memoria.cargar_meta_vector()
482
+
483
+ drift = {
484
+ k: round(despues.get(k, 0) - antes.get(k, 0), 6)
485
+ for k in antes
486
+ }
487
+
488
+ return {
489
+ "status": "Consolidación completada",
490
+ "arquetipo_antes": antes,
491
+ "arquetipo_despues": despues,
492
+ "drift": drift,
493
+ "timestamp": datetime.datetime.utcnow().isoformat()
494
+ }
495
+ except Exception as e:
496
+ return {"error": str(e)}
497
+
498
+ # ============================================================
499
+ # ANALYTICS DASHBOARD
500
+ # ============================================================
501
+
502
+ @app.get("/analytics", response_class=HTMLResponse)
503
+ async def analytics_dashboard():
504
+ html = """
505
+ <!DOCTYPE html>
506
+ <html lang="es">
507
+ <head>
508
+ <meta charset="UTF-8">
509
+ <title>Cortex-Nexus | Analytics Lab</title>
510
+ <meta name="viewport" content="width=device-width, initial-scale=1">
511
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
512
+ <style>
513
+ * { box-sizing: border-box; margin: 0; padding: 0; }
514
+ body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f8fafc; padding: 16px; }
515
+ .container { max-width: 900px; margin: 0 auto; }
516
+ .card { background: #1e293b; padding: 16px; border-radius: 12px; margin-bottom: 16px; border: 1px solid #334155; }
517
+ h1 { color: #a78bfa; text-align: center; font-size: 1.5rem; margin-bottom: 16px; }
518
+ h2 { color: #38bdf8; font-size: 1rem; margin-bottom: 10px; }
519
+ .nav { display: flex; justify-content: center; gap: 12px; margin-bottom: 16px; }
520
+ .nav a { color: #94a3b8; text-decoration: none; padding: 6px 14px; border-radius: 8px; font-size: 0.8rem; background: #334155; }
521
+ .nav a.active, .nav a:hover { background: #a78bfa; color: #fff; }
522
+ .window-selector { display: flex; gap: 8px; margin-bottom: 12px; }
523
+ .window-btn { padding: 6px 12px; border: 1px solid #475569; background: #334155; color: #94a3b8; border-radius: 8px; cursor: pointer; font-size: 0.75rem; }
524
+ .window-btn.active { background: #a78bfa; color: #fff; border-color: #a78bfa; }
525
+ .metrics-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
526
+ .metric { background: #334155; padding: 10px; border-radius: 8px; text-align: center; }
527
+ .metric-label { font-size: 0.65rem; color: #94a3b8; text-transform: uppercase; }
528
+ .metric-value { font-size: 1.1rem; font-weight: 800; color: #38bdf8; margin-top: 4px; }
529
+ .metric-trend { font-size: 0.6rem; margin-top: 2px; }
530
+ .trend-up { color: #10b981; }
531
+ .trend-down { color: #ef4444; }
532
+ .trend-flat { color: #94a3b8; }
533
+ .pattern { padding: 8px 12px; border-radius: 8px; margin-bottom: 8px; font-size: 0.75rem; }
534
+ .pattern-warning { background: rgba(245,158,11,0.15); border-left: 3px solid #f59e0b; color: #fbbf24; }
535
+ .pattern-danger { background: rgba(239,68,68,0.15); border-left: 3px solid #ef4444; color: #f87171; }
536
+ .pattern-success { background: rgba(16,185,129,0.15); border-left: 3px solid #10b981; color: #34d399; }
537
+ .arquetipo { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
538
+ .arq-item { text-align: center; }
539
+ .arq-label { font-size: 0.65rem; color: #94a3b8; }
540
+ .arq-value { font-size: 1.2rem; font-weight: 800; color: #a78bfa; }
541
+ .export-btn { display: block; width: 100%; padding: 12px; background: #a78bfa; color: #fff; border: none; border-radius: 8px; font-size: 0.9rem; cursor: pointer; font-weight: 600; }
542
+ .export-btn:hover { background: #8b5cf6; }
543
+ .sleep-btn { display: block; width: 100%; padding: 12px; background: #1e293b; color: #a78bfa; border: 2px solid #a78bfa; border-radius: 8px; font-size: 0.9rem; cursor: pointer; font-weight: 600; margin-top: 8px; }
544
+ .sleep-btn:hover { background: #a78bfa; color: #fff; }
545
+ </style>
546
+ </head>
547
+ <body>
548
+ <div class="container">
549
+ <div class="nav">
550
+ <a href="/">🧠 Live</a>
551
+ <a href="/analytics" class="active">🔬 Analytics</a>
552
+ </div>
553
+ <h1>🔬 Analytics Lab</h1>
554
+
555
+ <div class="window-selector">
556
+ <button class="window-btn active" onclick="setWindow('1h')">1H</button>
557
+ <button class="window-btn" onclick="setWindow('6h')">6H</button>
558
+ <button class="window-btn" onclick="setWindow('24h')">24H</button>
559
+ <button class="window-btn" onclick="setWindow('7d')">7D</button>
560
+ </div>
561
+
562
+ <div class="card">
563
+ <h2>📊 Métricas Derivadas <span id="window-label" style="color:#94a3b8; font-size:0.7rem;">(1h)</span></h2>
564
+ <div class="metrics-grid" id="metrics-grid">Cargando...</div>
565
+ </div>
566
+
567
+ <div class="card">
568
+ <h2>⚡ Volatilidad Emocional</h2>
569
+ <canvas id="volChart" height="180"></canvas>
570
+ </div>
571
+
572
+ <div class="card">
573
+ <h2>🚨 Patrones Detectados</h2>
574
+ <div id="patterns-box">Analizando...</div>
575
+ </div>
576
+
577
+ <div class="card">
578
+ <h2>🧬 Arquetipo (Personalidad Base)</h2>
579
+ <div class="arquetipo" id="arquetipo-box">Cargando...</div>
580
+ </div>
581
+
582
+ <div class="card">
583
+ <a href="/api/export" class="export-btn">📥 Exportar CSV Completo</a>
584
+ <button class="sleep-btn" onclick="triggerSleep()">😴 Ejecutar Consolidación (Sleep Replay)</button>
585
+ <div id="sleep-result" style="font-size:0.7rem; color:#94a3b8; margin-top:8px;"></div>
586
+ </div>
587
+
588
+ <p style="text-align:center; font-size:0.65rem; color:#475569; margin-top:12px;">Arquitectura Emocional v3.0 • Analytics Lab • Maxi Speranza</p>
589
+ </div>
590
+
591
+ <script>
592
+ let currentWindow = '1h';
593
+
594
+ const volCtx = document.getElementById('volChart').getContext('2d');
595
+ const volChart = new Chart(volCtx, {
596
+ type: 'bar',
597
+ data: {
598
+ labels: ['Qualia', 'Frustración', 'Confianza', 'Fatiga', 'Aburrimiento'],
599
+ datasets: [{
600
+ label: 'Volatilidad (σ)',
601
+ data: [0, 0, 0, 0, 0],
602
+ backgroundColor: ['rgba(56,189,248,0.6)', 'rgba(239,68,68,0.6)', 'rgba(16,185,129,0.6)', 'rgba(245,158,11,0.6)', 'rgba(167,139,250,0.6)'],
603
+ borderRadius: 6
604
+ }]
605
+ },
606
+ options: {
607
+ responsive: true,
608
+ scales: { y: { beginAtZero: true, grid: { color: '#334155' }, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } },
609
+ plugins: { legend: { display: false } }
610
+ }
611
+ });
612
+
613
+ function setWindow(w) {
614
+ currentWindow = w;
615
+ document.querySelectorAll('.window-btn').forEach(b => b.classList.remove('active'));
616
+ event.target.classList.add('active');
617
+ loadAnalytics();
618
+ }
619
+
620
+ function trendIcon(trend) {
621
+ if (trend > 0.001) return '<span class="trend-up">▲ +' + trend.toFixed(4) + '</span>';
622
+ if (trend < -0.001) return '<span class="trend-down">▼ ' + trend.toFixed(4) + '</span>';
623
+ return '<span class="trend-flat">— estable</span>';
624
+ }
625
+
626
+ async function loadAnalytics() {
627
+ try {
628
+ const res = await fetch('/api/analytics');
629
+ const data = await res.json();
630
+ if (data.error) { console.error(data.error); return; }
631
+
632
+ const w = data.windows[currentWindow];
633
+ document.getElementById('window-label').innerText = `(${currentWindow} • ${w.count} épocas)`;
634
+
635
+ const grid = document.getElementById('metrics-grid');
636
+ if (w.count === 0) { grid.innerHTML = '<p style="color:#94a3b8;">Sin datos para esta ventana</p>'; return; }
637
+
638
+ const m = w.metrics;
639
+ const names = {qualia: 'Qualia', frustracion: 'Frustración', autoConfianza: 'Confianza', fatiga: 'Fatiga', aburrimiento: 'Aburrimiento', ratio_qualia_fatiga: 'Q/Fatiga', entropia_emocional: 'Entropía'};
640
+ grid.innerHTML = Object.entries(names).map(([k, label]) => {
641
+ const s = m[k] || {};
642
+ return `<div class="metric"><div class="metric-label">${label}</div><div class="metric-value">${(s.mean||0).toFixed(3)}</div><div class="metric-trend">${trendIcon(s.trend||0)}</div><div style="font-size:0.55rem;color:#64748b;">σ=${(s.std||0).toFixed(3)} [${(s.min||0).toFixed(2)}-${(s.max||0).toFixed(2)}]</div></div>`;
643
+ }).join('');
644
+
645
+ // Volatilidad
646
+ volChart.data.datasets[0].data = ['qualia','frustracion','autoConfianza','fatiga','aburrimiento'].map(k => (m[k]||{}).std || 0);
647
+ volChart.update('none');
648
+
649
+ // Patrones
650
+ const pBox = document.getElementById('patterns-box');
651
+ if (data.patterns.length === 0) {
652
+ pBox.innerHTML = '<p style="color:#10b981; font-size:0.8rem;">✅ Sin anomalías detectadas</p>';
653
+ } else {
654
+ pBox.innerHTML = data.patterns.map(p => `<div class="pattern pattern-${p.severity}">${p.msg}</div>`).join('');
655
+ }
656
+
657
+ // Arquetipo
658
+ const aBox = document.getElementById('arquetipo-box');
659
+ const a = data.arquetipo;
660
+ aBox.innerHTML = `
661
+ <div class="arq-item"><div class="arq-label">Meta-Qualia</div><div class="arq-value">${(a.meta_qualia||0).toFixed(4)}</div></div>
662
+ <div class="arq-item"><div class="arq-label">Meta-Frustración</div><div class="arq-value">${(a.meta_frustracion||0).toFixed(4)}</div></div>
663
+ <div class="arq-item"><div class="arq-label">Meta-Confianza</div><div class="arq-value">${(a.meta_confianza||0).toFixed(4)}</div></div>
664
+ `;
665
+ } catch(e) { console.error("Error analytics:", e); }
666
+ }
667
+
668
+ async function triggerSleep() {
669
+ const res = await fetch('/api/sleep', {method: 'POST'});
670
+ const data = await res.json();
671
+ const box = document.getElementById('sleep-result');
672
+ if (data.error) { box.innerHTML = '❌ ' + data.error; return; }
673
+ box.innerHTML = `✅ Consolidación OK | Drift: Q=${data.drift.meta_qualia}, F=${data.drift.meta_frustracion}, C=${data.drift.meta_confianza}`;
674
+ loadAnalytics();
675
+ }
676
+
677
+ setInterval(loadAnalytics, 8000);
678
+ loadAnalytics();
679
+ </script>
680
+ </body>
681
+ </html>
682
+ """
683
+ return HTMLResponse(content=html)
684
+