Paso 1 de 2
👤
Contanos sobre vos
Usamos estos datos para personalizar
tu experiencia con CarbLens.
Sexo
Tus datos
Edad
años
Altura
cm
Peso
kg
Paso 2 de 2
🎯
Ajuste fino
Estos valores se usan para calcular
tu dosis de insulina con precisión.
Ratio de insulina
Cuántos gramos de carbohidratos cubre 1 unidad de insulina. Tu médico o diabetólogo te indica este valor.
g / UI
💡 Valor típico: 8 – 15 g/UI
Factor de sensibilidad
Cuántos mg/dL baja tu glucosa con 1 unidad de insulina de corrección.
mg/dL
💡 Valor típico: 30 – 80 mg/dL
Glucosa objetivo
Tu nivel de glucosa meta al momento de comer. La corrección apunta a este valor.
mg/dL
💡 Valor típico: 80 – 120 mg/dL

Hola, carblensapp 👋

¿Qué vas a comer hoy?

CA
🩸 Glucosa actual
-- mg/dL
mg/dL
Tendencia:
Objetivo: 120 mg/dL

Escaneá tu comida

La IA identifica cada alimento y calcula
los hidratos al instante

🔍 0 escaneos de prueba restantes
📷
Abrir Cámara
Tips para mejores resultados
☀️
Buena iluminación para identificar mejor los alimentos
📐
Incluí todo el plato en el encuadre
🍽️
Si hay varios platos, analizá por separado
Verificá siempre el etiquetado del packaging
🍽️
Encuadrá tu comida aquí
🔍 Identificando alimentos...
CarbLens
🍽️
💉 Dosis total recomendada
UI
🌾
g HC
🍽️
Comida
📊
0
Corrección
🌾 Ajustar hidratos
Si la IA se equivocó
g HC
Glucosa
--
mg/dL
Objetivo
120
mg/dL
Ratio
1:10
HC/UI
🤖
Análisis completo IA
Tocá para ver detalles
⚠️

Valores estimativos. Siempre consultá con tu médico o diabetólogo antes de ajustar tu dosis de insulina.

Historial
CarbLens
Mi perfil
Tus datos se usan para calcular la dosis de insulina
CA
carblensapp
Datos personales
Sexo
Edad
Altura (cm)
Peso (kg)
Parámetros de insulina
Ratio HC / Insulina
Gramos de HC que cubre 1 UI de insulina
g / UI
Sensibilidad
Cuánto baja la glucosa 1 UI (mg/dL)
mg/dL
Glucosa objetivo
Glucosa meta al momento de comer (mg/dL)
mg/dL
Sensor de glucosa
📡 Sensor de glucosa continuo
No conectado
Plataforma / Fuente de datos
📷
Escanear
🕐
Historial
👤
Perfil
'; var modal = document.getElementById('pdfModal'); if (modal) modal.remove(); var win = window.open('', '_blank'); if (win) { win.document.write(html); win.document.close(); setTimeout(function() { win.print(); }, 800); } } function calcDose(carbs, glucose, glucTarget, ratio) { const g = parseFloat(glucose) || 0; const gt = parseFloat(glucTarget) || 120; const r = parseFloat(ratio) || 10; const mealDose = carbs > 0 ? carbs / r : 0; const sens = parseFloat(state.sensitivity || profile.sensibilidad) || 50; const corrDose = (g > 0 && g > gt) ? (g - gt) / sens : 0; const total = mealDose + corrDose; return { meal: Math.round(mealDose * 10) / 10, correction: Math.round(corrDose * 10) / 10, total: Math.round(total * 10) / 10 }; } function renderResultDose(carbs, glucose, glucTarget, ratio) { const dose = calcDose(carbs, glucose, glucTarget, ratio); const sens = state.sensitivity || profile.sensibilidad || 50; const trend = profile.sensorTrend || 4; const trendEmoji = { 1:'⬆️⬆️', 2:'⬆️', 3:'↗️', 4:'➡️', 5:'↘️', 6:'⬇️', 7:'⬇️⬇️' }; const trendLabel = trendEmoji[trend] || '➡️'; // IOB const lastBolusTime = profile.lastBolusTime || null; const lastBolusUnits = profile.lastBolusUnits || 0; let iob = 0; if (lastBolusTime) { const minAgo = Math.round((Date.now() - lastBolusTime) / 60000); if (minAgo < 240) iob = Math.round(lastBolusUnits * Math.max(0, (240 - minAgo) / 240) * 10) / 10; } // Cálculo simple: bolo comida + corrección const hypo = glucose && glucose < 70; const total = dose.total; const meal = dose.meal; const corr = dose.correction; const tAdj = 0; const preBolo = glucose > 160 ? 25 : glucose > 120 ? 15 : glucose > 90 ? 10 : 0; const immUI = total; const extUI = 0; const extHrs = 0; // Hero card const heroCard = document.getElementById('resultHeroCard'); const hypoDiv = document.getElementById('resultHypoWarning'); const normDiv = document.getElementById('resultDoseNormal'); if (hypo) { if (heroCard) heroCard.style.background = 'linear-gradient(135deg,#dc2626,#991b1b)'; if (hypoDiv) hypoDiv.style.display = 'block'; if (normDiv) normDiv.style.display = 'none'; } else { if (hypoDiv) hypoDiv.style.display = 'none'; if (normDiv) normDiv.style.display = 'block'; const totalEl = document.getElementById('resultDoseTotal'); if (totalEl) totalEl.textContent = total > 0 ? total.toFixed(1) : '0'; // Pre-bolo chip const preChip = document.getElementById('resultPreboloChip'); const preTxt = document.getElementById('resultPreboloText'); if (preChip && preTxt) { if (preBolo > 0) { preChip.style.display = 'inline-flex'; preTxt.textContent = 'Aplicá ' + preBolo + ' min antes'; } else { preChip.style.display = 'inline-flex'; preTxt.textContent = 'Aplicá al momento de comer'; } } } // Mini cards const hcEl = document.getElementById('resultHCVal'); const mealEl = document.getElementById('resultDoseMeal'); const corrEl = document.getElementById('resultDoseCorr'); if (hcEl) hcEl.textContent = carbs > 0 ? carbs + 'g' : '--'; if (mealEl) mealEl.textContent = meal > 0 ? meal.toFixed(1) + ' UI' : '--'; if (corrEl) corrEl.textContent = corr > 0 ? '+' + corr.toFixed(1) + ' UI' : '0'; // Trend card const trendCard = document.getElementById('resultTrendCard'); const trendIcon = document.getElementById('resultTrendIcon'); const trendAdjEl = document.getElementById('resultTrendAdj'); if (tAdj !== 0 && trendCard) { trendCard.style.display = 'flex'; if (trendIcon) trendIcon.textContent = trendLabel; if (trendAdjEl) trendAdjEl.textContent = (tAdj > 0 ? '+' : '') + tAdj.toFixed(1) + ' UI'; } // Dual bolus card const dualCard = document.getElementById('resultDualCard'); if (extUI > 0 && dualCard) { dualCard.style.display = 'block'; const immEl = document.getElementById('resultImmediateUI'); const extEl = document.getElementById('resultExtendedUI'); const lblEl = document.getElementById('resultExtendedLbl'); if (immEl) immEl.textContent = immUI.toFixed(1) + ' UI'; if (extEl) extEl.textContent = extUI.toFixed(1) + ' UI'; if (lblEl) lblEl.textContent = 'En ' + extHrs + 'h'; } // IOB row — oculto, no se muestra const iobRow = document.getElementById('resultIOBRow'); if (iobRow) iobRow.style.display = 'none'; // Glucose chips const gEl = document.getElementById('resultGlucoseVal'); const oEl = document.getElementById('resultGlucoseObj'); const rEl = document.getElementById('resultRatioVal'); if (gEl) { gEl.textContent = glucose || '--'; gEl.className = 'result-glucose-chip-val' + (glucose > 180 ? ' high' : glucose < 70 ? ' low' : ' ok'); } if (oEl) oEl.textContent = glucTarget; if (rEl) rEl.textContent = '1:' + ratio; } function toggleAIDetail() { const el = document.getElementById('aiResponseText'); const chv = document.getElementById('resultAIChevron'); if (!el) return; const open = el.style.display !== 'none'; el.style.display = open ? 'none' : 'block'; if (chv) chv.textContent = open ? '↓' : '↑'; } function recalcFromResult() { const carbs = parseFloat(document.getElementById('manualCarbs').value) || 0; const glucose = profile.glucosaActual || null; const glucTarget = profile.glucosaObj || 120; const ratio = state.insulinRatio || profile.ratio || 10; renderResultDose(carbs, glucose, glucTarget, ratio); // Update glucose chips const gEl = document.getElementById('resultGlucoseVal'); if (gEl) { gEl.textContent = glucose || '--'; gEl.className = 'result-glucose-chip-val' + (glucose > 180 ? ' high' : glucose < 70 ? ' low' : ' ok'); } } function calcManualInsulin() { const carbs = parseFloat(document.getElementById('manualCarbs').value) || 0; const glucose = parseFloat(document.getElementById('manualGlucose').value) || 0; const ratio = state.insulinRatio || 10; const sensitivity = state.sensitivity || 50; const glucTarget = state.glucTarget || 120; const mealDose = carbs / ratio; const corrDose = glucose > glucTarget ? (glucose - glucTarget) / sensitivity : 0; const total = Math.round(mealDose + corrDose); const doseEl = document.getElementById('insulinDoseNum'); const labelEl = document.getElementById('insulinDoseLabel'); if (doseEl) doseEl.textContent = carbs > 0 ? total : '—'; if (labelEl) { if (carbs <= 0) labelEl.textContent = 'Ingresá los hidratos para calcular'; else if (corrDose > 0) labelEl.textContent = `${Math.round(mealDose*10)/10} UI comida + ${Math.round(corrDose*10)/10} UI corrección`; else labelEl.textContent = `${Math.round(mealDose*10)/10} UI para ${carbs}g HC`; } } function selectFood(i) { const wasSelected = state.selectedFood === i; state.selectedFood = wasSelected ? null : i; document.querySelectorAll('.f-label').forEach((l, li) => { l.classList.toggle('selected', !wasSelected && li === i); }); document.querySelectorAll('.food-row').forEach((r, ri) => { r.classList.toggle('selected', !wasSelected && ri === i); }); if (!wasSelected) { const row = document.getElementById('row-' + i); if (row) row.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } } // ═══ HOME PROGRESS ═══ function updateHomeProgress() { const goalEl = document.getElementById('f-goal'); const goal = goalEl ? (parseInt(goalEl.value) || 225) : (profile.carbGoal || 225); const pct = Math.min((state.todayCarbs / goal) * 100, 100); const rem = Math.max(goal - state.todayCarbs, 0); document.getElementById('home-carbs').textContent = `${state.todayCarbs}g`; document.getElementById('home-kcal').textContent = state.todayKcal; document.getElementById('home-remaining').textContent = `${rem}g restantes`; document.getElementById('home-bar').style.width = `${pct}%`; document.getElementById('home-bar').style.background = pct >= 100 ? '#DC2626' : pct >= 80 ? '#D97706' : '#4A7C59'; } // ═══ HISTORY ═══ function renderHistory() { const container = document.getElementById('historyList'); if (!container) return; var historyToShow = isPremiumUser() ? state.history : state.history.slice(0, 10); var isLimited = !isPremiumUser() && state.history.length > 10; if (!historyToShow || historyToShow.length === 0) { container.innerHTML = '
' + '
🍽️
' + '
Sin análisis aún
' + '
Escaneá tu primera comida
' + '
'; return; } container.innerHTML = historyToShow.map((h, idx) => { const imgStyle = h.image ? 'background-image:url(' + h.image + ');background-size:cover;background-position:center;' : ''; const imgContent = h.image ? '' : '🍽️'; const hc = h.totalCarbs || 0; const dose = h.dose ? Number(h.dose).toFixed(1) : null; const glucose = h.glucose || null; const glucClass = glucose ? (glucose > 180 ? 'high' : glucose < 70 ? 'low' : 'ok') : 'ok'; const glucColor = glucClass === 'high' ? '#dc2626' : glucClass === 'low' ? '#ea580c' : 'var(--green)'; const hcChip = '
' + '
' + (hc > 0 ? hc + 'g' : '--') + '
' + '
HC
'; const doseChip = '
' + '
' + (dose ? dose + ' UI' : '--') + '
' + '
Dosis
'; const glucChip = '
' + '
' + (glucose ? glucose : '--') + '
' + '
Glucosa
'; const preview = (h.text || '').split('\n').filter(l => l.trim()).slice(0,2).join(' · '); return '
' + '
' + '
' + imgContent + '
' + '
' + '
' + (h.date || '') + '
' + '
' + hcChip + doseChip + glucChip + '
' + '
' + preview + '
' + '
' + '' + '
' + (h.text || '').replace(/' + '
'; }).join(''); if (isLimited) { container.innerHTML += '
🔒
Historial completo con Premium
Activá Premium para ver todo el historial del último mes y descargar PDF para tu médico.
'; } // Mostrar/ocultar botón PDF según plan var pdfWrap = document.getElementById('pdfBtnWrap'); if (pdfWrap) pdfWrap.style.display = (isPremiumUser() && historyToShow.length > 0) ? 'block' : 'none'; } function toggleHistoryDetail(idx) { const el = document.getElementById('hdetail-' + idx); if (!el) return; el.classList.toggle('open'); const btn = el.previousElementSibling.querySelector('.history-expand-btn'); if (btn) btn.textContent = el.classList.contains('open') ? 'Cerrar ↑' : 'Ver análisis completo ↓'; } function deleteHistory(id) { state.history = state.history.filter(h => h.id !== id); saveHistory(); renderHistory(); } function clearHistory() { if (confirm('¿Borrar todo el historial?')) { state.history = []; saveHistory(); renderHistory(); } } // ═══ PROFILE ═══ // ══ AVATAR ══ var AVATAR_COLORS = ['#2D6A4F','#1D4ED8','#7C3AED','#B45309','#BE185D','#0F766E','#9F1239','#1E40AF']; function getAvatarColor(name) { var idx = (name || 'U').charCodeAt(0) % AVATAR_COLORS.length; return AVATAR_COLORS[idx]; } function getInitials(name) { var parts = (name || 'U').trim().split(' '); if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase(); return (parts[0] || 'U').substring(0, 2).toUpperCase(); } function renderAvatar(containerId, size) { var el = document.getElementById(containerId); if (!el) return; var avatarUrl = localStorage.getItem('cl_avatar'); if (avatarUrl) { el.innerHTML = ''; } else { var name = profile.nombre || 'U'; var initials = getInitials(name); var color = getAvatarColor(name); var fontSize = Math.round(size * 0.35); el.innerHTML = '
' + initials + '
'; } } async function handleAvatarUpload(input) { var file = input.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = async function(e) { // Comprimir a thumbnail circular var img = new Image(); img.onload = async function() { var canvas = document.createElement('canvas'); var size = 200; canvas.width = size; canvas.height = size; var ctx = canvas.getContext('2d'); // Recortar al centro var s = Math.min(img.width, img.height); var sx = (img.width - s) / 2; var sy = (img.height - s) / 2; ctx.drawImage(img, sx, sy, s, s, 0, 0, size, size); var dataUrl = canvas.toDataURL('image/jpeg', 0.82); localStorage.setItem('cl_avatar', dataUrl); // Guardar en Supabase si hay userId if (currentUserId) { try { await sbCall('save_profile', { user_id: currentUserId, avatar_url: dataUrl }); } catch(e) {} } renderAvatar('profileAvatarCircle', 88); renderAvatar('homeAvatar', 44); }; img.src = e.target.result; }; reader.readAsDataURL(file); } function renderProfile() { // Renderizar avatar renderAvatar('profileAvatarCircle', 88); renderAvatar('homeAvatar', 44); // Mostrar email en perfil var emailEl = document.getElementById('profileEmailDisplay'); if (emailEl && profile.email) emailEl.textContent = profile.email; var badge = document.getElementById('premiumBadge'); if (badge) { if (isPremiumUser()) { badge.innerHTML = '
⭐ PREMIUM
'; } else { badge.innerHTML = '
FREE → Mejorar ⭐
'; } } // Llenar campos editables del perfil var sex = document.getElementById('p-sex'); var age = document.getElementById('p-age'); var height = document.getElementById('p-height'); var weight = document.getElementById('p-weight'); var ratio = document.getElementById('p-ratio'); var sens = document.getElementById('p-sens'); var goal = document.getElementById('p-goal'); if (sex && profile.sexo) sex.value = profile.sexo; if (age && profile.edad) age.value = profile.edad; if (height && profile.altura) height.value = profile.altura; if (weight && profile.peso) weight.value = profile.peso; if (ratio && profile.ratio) ratio.value = profile.ratio; if (sens && profile.sensibilidad) sens.value = profile.sensibilidad; if (goal && profile.glucosaObj) goal.value = profile.glucosaObj; } function resetToday() { if (confirm('¿Reiniciar los totales del día?')) { state.todayCarbs = 0; state.todayKcal = 0; updateHomeProgress(); renderProfile(); } } let isEditing = false; function toggleEdit() { isEditing = !isEditing; const btn = document.getElementById('editBtn'); const fields = ['name', 'goal', 'cal', 'ratio']; fields.forEach(f => { const v = document.getElementById('f-' + f + '-v'); const i = document.getElementById('f-' + f); if (v && i) { v.style.display = isEditing ? 'none' : 'block'; i.style.display = isEditing ? 'block' : 'none'; } }); if (!isEditing) { // Save const name = document.getElementById('f-name').value; const goal = document.getElementById('f-goal').value; const cal = document.getElementById('f-cal').value; const ratio= document.getElementById('f-ratio').value; document.getElementById('f-name-v').textContent = name; document.getElementById('f-goal-v').textContent = goal + 'g'; document.getElementById('f-cal-v').textContent = cal + ' kcal'; document.getElementById('f-ratio-v').textContent= ratio + 'g'; document.getElementById('avatarName').textContent = name; document.getElementById('avatarLetter').textContent = name.charAt(0).toUpperCase(); btn.textContent = '✏️ Editar'; btn.classList.remove('saving'); updateHomeProgress(); // Sync insulin ratio state.insulinRatio = parseFloat(ratio) || 10; } else { btn.textContent = '✅ Guardar'; btn.classList.add('saving'); } } function updateAvatar(val) { document.getElementById('avatarLetter').textContent = (val || 'U').charAt(0).toUpperCase(); } let isDiabetic = false; function toggleDiabetic() { isDiabetic = !isDiabetic; document.getElementById('diabeticToggle').classList.toggle('on', isDiabetic); document.getElementById('insulinField').style.display = isDiabetic ? 'block' : 'none'; document.getElementById('diabeticBadge').style.display = isDiabetic ? 'flex' : 'none'; } // ═══ INSULIN ═══ function renderInsulinCard(totalCarbs) { const ratio = state.insulinRatio; const dose = totalCarbs / ratio; const doseRounded = Math.round(dose); const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; set('ibCarbs', `${totalCarbs}g`); set('ibRatio', `÷ ${ratio}`); set('ibDose', `${doseRounded} UI`); set('insulinRatioDisplay', `1 UI cada ${ratio}g HC`); if (ratio) { set('insulinDoseNum', doseRounded); set('insulinDoseLabel', `= ${totalCarbs}g HC ÷ ${ratio} (tu ratio)`); } else { set('insulinDoseNum', '—'); set('insulinDoseLabel', 'Configurá tu ratio para ver la dosis'); } } function openRatioModal() { state.pendingRatio = state.insulinRatio; document.getElementById('ratioCustomInput').value = state.insulinRatio; highlightPreset(state.insulinRatio); document.getElementById('ratioModal').classList.add('open'); } function closeRatioModal() { document.getElementById('ratioModal').classList.remove('open'); } function closeRatioOnOverlay(e) { if (e.target === document.getElementById('ratioModal')) closeRatioModal(); } function selectRatioPreset(val) { state.pendingRatio = val; document.getElementById('ratioCustomInput').value = val; highlightPreset(val); } function highlightPreset(val) { document.querySelectorAll('.ratio-opt').forEach(o => o.classList.remove('selected')); const presets = [5, 8, 10, 12, 15, 20]; const idx = presets.indexOf(Number(val)); if (idx !== -1) { document.querySelectorAll('.ratio-opt')[idx].classList.add('selected'); } } function onCustomRatioInput(val) { state.pendingRatio = parseFloat(val) || 10; highlightPreset(val); } function saveRatio() { const val = parseFloat(document.getElementById('ratioCustomInput').value); if (!val || val <= 0) { alert('Ingresá un ratio válido (mayor a 0)'); return; } state.insulinRatio = val; closeRatioModal(); // Recalculate with current meal if (state.currentMeal) { const totalCarbs = state.currentMeal.foods.reduce((s, f) => s + f.carbs, 0); renderInsulinCard(totalCarbs); } // Update profile field document.getElementById('f-ratio').value = val; document.getElementById('f-ratio-v').textContent = val + 'g'; } // ═══ INIT ═══ // (se ejecuta desde DOMContentLoaded) // ══ SENSOR / LIBRELINKUP ══ function onSensorPlatformChange() { const platform = document.getElementById('sensorPlatform').value; const creds = document.getElementById('sensorCredentialsBlock'); const soon = document.getElementById('sensorComingSoon'); const emailLabel = document.getElementById('sensorEmailLabel'); creds.style.display = 'none'; soon.style.display = 'none'; const available = ['librelinkup']; const coming = ['nightscout', 'dexcom', 'medtrum', 'medtronic', 'omnipod', 'eversense']; if (available.includes(platform)) { creds.style.display = 'block'; // Update label and region options based on platform if (platform === 'librelinkup') { if (emailLabel) emailLabel.textContent = 'Email de LibreLinkUp'; } // Pre-fill saved credentials if (profile.sensorEmail) document.getElementById('sensorEmail').value = profile.sensorEmail; if (profile.sensorPassword) document.getElementById('sensorPassword').value = profile.sensorPassword; if (profile.sensorRegion) document.getElementById('sensorRegion').value = profile.sensorRegion; } else if (coming.includes(platform)) { soon.style.display = 'block'; // Show platform-specific message const names = { nightscout: 'Nightscout', dexcom: 'Dexcom Share', medtrum: 'Medtrum EasyView', medtronic: 'Medtronic', omnipod: 'Omnipod', eversense: 'Eversense' }; const soonEl = document.getElementById('sensorComingSoon'); if (soonEl) soonEl.innerHTML = '🔜 La integración con ' + (names[platform]||platform) + ' estará disponible próximamente'; } } function renderSensorCard() { const platform = profile.sensorPlatform || ''; const connected = profile.sensorConnected || false; const platSelect = document.getElementById('sensorPlatform'); if (platSelect) platSelect.value = platform; onSensorPlatformChange(); const dot = document.getElementById('sensorStatusDot'); const txt = document.getElementById('sensorStatusText'); const val = document.getElementById('sensorStatusVal'); const disconnBtn = document.getElementById('sensorDisconnectBtn'); const lastRead = document.getElementById('sensorLastRead'); if (connected && profile.sensorGlucose) { dot.className = 'sensor-status-dot connected'; const platformNames = { librelinkup: 'FreeStyle Libre', nightscout: 'Nightscout', dexcom: 'Dexcom' }; txt.textContent = 'Conectado · ' + (platformNames[profile.sensorPlatform] || profile.sensorPlatform); val.textContent = profile.sensorGlucose + ' mg/dL'; if (disconnBtn) disconnBtn.style.display = 'block'; if (lastRead && profile.sensorLastRead) { lastRead.textContent = 'Última lectura: ' + profile.sensorLastRead; } } else { dot.className = 'sensor-status-dot'; txt.textContent = 'No conectado'; val.textContent = ''; if (disconnBtn) disconnBtn.style.display = 'none'; } } async function connectSensor() { const email = document.getElementById('sensorEmail').value.trim(); const password = document.getElementById('sensorPassword').value.trim(); const region = document.getElementById('sensorRegion').value; if (!email || !password) { alert('Ingresá tu email y contraseña de LibreLinkUp'); return; } const btn = document.getElementById('sensorConnectBtn'); btn.disabled = true; btn.innerHTML = '
Conectando...'; try { const glucose = await fetchLibreLinkUp(email, password, region); // Save credentials and result profile.sensorPlatform = 'librelinkup'; profile.sensorEmail = email; profile.sensorPassword = password; profile.sensorRegion = region; profile.sensorConnected = true; profile.sensorGlucose = glucose.value; profile.sensorTrend = glucose.trend || 0; profile.sensorLastRead = new Date().toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' }); // Update glucose widget automatically profile.glucosaActual = glucose.value; state.currentGlucose = glucose.value; saveProfile(); updateGlucoseWidget(); renderSensorCard(); btn.disabled = false; btn.innerHTML = '📡 Conectar sensor'; alert('✅ Sensor conectado. Glucosa actual: ' + glucose.value + ' mg/dL'); } catch (err) { btn.disabled = false; btn.innerHTML = '📡 Conectar sensor'; alert('❌ Error al conectar: ' + err.message); console.error('Sensor error:', err); } } async function fetchLibreLinkUp(email, password, region) { // Direct calls through our Cloudflare Worker proxy // Worker sets the correct LibreLinkUp headers server-side const PROXY = 'https://llu-proxy.carlosvera1986.workers.dev/proxy'; // ── Step 1: Login ── const loginRes = await fetch(PROXY + '/' + region + '/llu/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); if (!loginRes.ok && loginRes.status !== 200) { const t = await loginRes.text(); throw new Error('Proxy error ' + loginRes.status + ': ' + t.substring(0,100)); } let loginData; try { loginData = await loginRes.json(); } catch(e) { const t = await loginRes.text(); throw new Error('Login response no es JSON: ' + t.substring(0,100)); } console.log('Login status:', loginData?.status, 'redirect:', loginData?.data?.redirect); // Region redirect if (loginData?.data?.redirect === true) { const regionMap = { 'ae':'api-ae.libreview.io','ap':'api-ap.libreview.io','au':'api-au.libreview.io', 'ca':'api-ca.libreview.io','de':'api-de.libreview.io','eu':'api-eu.libreview.io', 'fr':'api-fr.libreview.io','jp':'api-jp.libreview.io','us':'api-us.libreview.io', 'la':'api-la.libreview.io' }; const newHost = regionMap[loginData.data.region] || ('api-' + loginData.data.region + '.libreview.io'); console.log('Redirect to:', newHost); return fetchLibreLinkUp(email, password, newHost); } const token = loginData?.data?.authTicket?.token; if (!token) { const s = loginData?.status; const msg = loginData?.error || ''; throw new Error(s === 2 || s === 401 || msg.toLowerCase().includes('password') ? 'Email o contraseña incorrectos en LibreLinkUp.' : 'Login sin token: ' + JSON.stringify(loginData).substring(0,150)); } // ── Step 2: Get connection ID ── const connRes = await fetch(PROXY + '/' + region + '/llu/connections', { headers: { 'Authorization': 'Bearer ' + token } }); const connData = await connRes.json(); const connections = connData?.data || []; if (connections.length === 0) throw new Error('No hay conexiones en LibreLinkUp.'); const connectionId = connections[0].id; console.log('Connection ID:', connectionId); // ── Step 3: Get real-time graph ── const graphRes = await fetch(PROXY + '/' + region + '/llu/connections/' + connectionId + '/graph', { headers: { 'Authorization': 'Bearer ' + token } }); const graphData = await graphRes.json(); console.log('Graph keys:', Object.keys(graphData?.data || {})); const current = graphData?.data?.connection?.glucoseMeasurement; const graphArr = graphData?.data?.graphData || []; const lastInArr = graphArr.length > 0 ? graphArr[graphArr.length - 1] : null; let rawValue, rawTrend; if (current?.Value) { rawValue = current.Value; rawTrend = current.TrendArrow || 0; console.log('Current:', rawValue, current.Timestamp); } else if (lastInArr?.Value) { rawValue = lastInArr.Value; rawTrend = lastInArr.TrendArrow || 0; console.log('Last graph entry:', rawValue, lastInArr.Timestamp); } else { throw new Error('Sin lectura: ' + JSON.stringify(graphData).substring(0,200)); } const glucoseNum = Number(rawValue); if (isNaN(glucoseNum) || glucoseNum < 40 || glucoseNum > 500) { throw new Error('Valor inválido: ' + rawValue); } return { value: glucoseNum, trend: Number(rawTrend) || 0 }; } async function refreshSensorGlucose() { if (!profile.sensorConnected || !profile.sensorEmail) return; try { const glucose = await fetchLibreLinkUp(profile.sensorEmail, profile.sensorPassword, profile.sensorRegion); profile.sensorGlucose = glucose.value; profile.sensorTrend = glucose.trend || 0; profile.sensorLastRead = new Date().toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' }); profile.glucosaActual = glucose.value; state.currentGlucose = glucose.value; saveProfile(); updateGlucoseWidget(); } catch(e) { console.log('Sensor refresh failed:', e.message); } } function disconnectSensor() { if (!confirm('¿Desconectar el sensor?')) return; profile.sensorPlatform = ''; profile.sensorEmail = ''; profile.sensorPassword = ''; profile.sensorConnected = false; profile.sensorGlucose = null; saveProfile(); renderSensorCard(); // Reset platform selector const sel = document.getElementById('sensorPlatform'); if (sel) sel.value = ''; document.getElementById('sensorCredentialsBlock').style.display = 'none'; } // ══ ONBOARDING SENSOR ══ function obSensorPlatformChange() { const platform = document.getElementById('ob-sensorPlatform').value; const creds = document.getElementById('ob-sensorCredentials'); const soon = document.getElementById('ob-sensorComingSoon'); creds.style.display = 'none'; soon.style.display = 'none'; if (platform === 'librelinkup') { creds.style.display = 'block'; if (profile.sensorEmail) document.getElementById('ob-sensorEmail').value = profile.sensorEmail; if (profile.sensorPassword) document.getElementById('ob-sensorPassword').value = profile.sensorPassword; if (profile.sensorRegion) document.getElementById('ob-sensorRegion').value = profile.sensorRegion; } else if (platform && platform !== '') { soon.style.display = 'block'; } } async function obConnectSensor() { const email = document.getElementById('ob-sensorEmail').value.trim(); const password = document.getElementById('ob-sensorPassword').value.trim(); const region = document.getElementById('ob-sensorRegion').value; if (!email || !password) { alert('Ingresá tu email y contraseña de LibreLinkUp'); return; } const btn = document.getElementById('ob-sensorConnectBtn'); btn.disabled = true; btn.innerHTML = '
Conectando...'; try { const glucose = await fetchLibreLinkUp(email, password, region); // Save to profile profile.sensorPlatform = 'librelinkup'; profile.sensorEmail = email; profile.sensorPassword = password; profile.sensorRegion = region; profile.sensorConnected = true; profile.sensorGlucose = glucose.value; profile.sensorTrend = glucose.trend || 0; profile.sensorLastRead = new Date().toLocaleTimeString('es-AR', { hour:'2-digit', minute:'2-digit' }); profile.glucosaActual = glucose.value; // Update onboarding status row const dot = document.getElementById('ob-sensorStatusDot'); const txt = document.getElementById('ob-sensorStatusText'); const val = document.getElementById('ob-sensorStatusVal'); const last = document.getElementById('ob-sensorLastRead'); if (dot) { dot.className = 'sensor-status-dot connected'; } if (txt) { txt.textContent = '✅ Conectado · FreeStyle Libre'; } if (val) { val.textContent = glucose.value + ' mg/dL'; } if (last) { last.textContent = 'Lectura exacta del sensor · ' + profile.sensorLastRead; } btn.disabled = false; btn.innerHTML = '✅ Sensor conectado'; btn.style.background = '#16a34a'; } catch(err) { btn.disabled = false; btn.innerHTML = '📡 Intentar de nuevo'; alert('❌ Error al conectar: ' + err.message); console.error('Sensor ob error:', err); } } function obSensorContinue() { // Save whatever platform was selected even if not connected const platform = document.getElementById('ob-sensorPlatform').value; if (platform) profile.sensorPlatform = platform; goTo('s-final'); } // Show correct screen on load — checkAutoLogin maneja la redirección // ══ PWA INSTALL PROMPT ══ let deferredPrompt; window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; // Show install banner after 3 seconds setTimeout(showInstallBanner, 3000); }); function showInstallBanner() { if (!deferredPrompt) return; const banner = document.getElementById('installBanner'); if (banner) banner.style.display = 'flex'; } function installPWA() { const banner = document.getElementById('installBanner'); if (banner) banner.style.display = 'none'; if (deferredPrompt) { deferredPrompt.prompt(); deferredPrompt.userChoice.then(() => { deferredPrompt = null; }); } } function dismissInstallBanner() { const banner = document.getElementById('installBanner'); if (banner) banner.style.display = 'none'; } // ══ GLUCOSE WIDGET ══ function toggleGlucoseEdit() { /* no-op — input always visible */ } function saveGlucose() { const val = parseInt(document.getElementById('glucoseInputHome').value); if (!val || val < 40 || val > 500) { alert('Valor entre 40 y 500'); return; } profile.glucosaActual = val; profile.glucosaTrend = selectedTrend; state.currentGlucose = val; saveProfile(); updateGlucoseWidget(); document.getElementById('glucoseInputHome').value = ''; document.getElementById('glucoseInputHome').blur(); } function editGlucose() { var inputRow = document.getElementById('glucoseInputRow'); var editRow = document.getElementById('glucoseEditRow'); if (inputRow) inputRow.style.display = 'flex'; if (editRow) editRow.style.display = 'none'; var inp = document.getElementById('glucoseInputHome'); if (inp) { inp.value = profile.glucosaActual || ''; inp.focus(); } } var selectedTrend = 0; function selectTrend(t) { selectedTrend = t; var ids = ['trendBtn-2','trendBtn-1','trendBtn-0','trendBtn-1p','trendBtn-2p']; var vals = [-2,-1,0,1,2]; ids.forEach(function(id,i) { var btn = document.getElementById(id); if (btn) btn.classList.toggle('active', vals[i] === t); }); } function liveGlucosePreview(val) { // Show live status as user types const v = parseInt(val); const pill = document.getElementById('glucoseStatus'); if (!pill || isNaN(v)) return; if (v < 70) { pill.className='gw-status-pill low'; pill.textContent='⚠️ HIPO'; } else if (v <= 180) { pill.className='gw-status-pill ok'; pill.textContent='✓ OK'; } else { pill.className='gw-status-pill high'; pill.textContent='↑ ALTA'; } } function updateGlucoseWidget() { const val = profile.glucosaActual; const obj = profile.glucosaObj || 120; // Use manual trend (from selector) or sensor trend // Manual trend: -2=⬇ -1=↘ 0=➡ 1=↗ 2=⬆ // Sensor trend (LibreLinkUp): 1=⬆⬆ 2=⬆ 3=↗ 4=➡ 5=↘ 6=⬇ 7=⬇⬇ const manualTrend = (profile.glucosaTrend !== undefined && profile.glucosaTrend !== null) ? profile.glucosaTrend : null; const sensorTrend = profile.sensorTrend || 0; const manualArrow = { '-2':'↓', '-1':'↘', '0':'→', '1':'↗', '2':'↑' }; const sensorArrow = { 1:'↑↑', 2:'↑', 3:'↗', 4:'→', 5:'↘', 6:'↓', 7:'↓↓' }; let trendArrow = ''; if (manualTrend !== null) { trendArrow = manualArrow[String(manualTrend)] || '→'; } else if (sensorTrend) { trendArrow = sensorArrow[sensorTrend] || ''; } const valEl = document.getElementById('glucoseValDisplay'); const statusEl = document.getElementById('glucoseStatus'); const objEl = document.getElementById('glucoseObjDisplay'); const trendEl = document.getElementById('glucoseTrend'); const inputRow = document.getElementById('glucoseInputRow'); if (objEl) objEl.textContent = obj; if (trendEl) { trendEl.textContent = trendArrow; trendEl.className = 'gw-trend' + (trendArrow ? ' visible' : ''); } if (!val) { if (valEl) { valEl.textContent = '--'; valEl.className = 'gw-val'; } if (statusEl) { statusEl.textContent = ''; statusEl.className = 'gw-status-pill'; } if (inputRow) inputRow.style.display = 'flex'; return; } if (valEl) valEl.textContent = val; if (statusEl) { if (val < 70) { statusEl.className='gw-status-pill low'; statusEl.textContent='⚠️ HIPO'; } else if (val <= 180){ statusEl.className='gw-status-pill ok'; statusEl.textContent='✓ OK'; } else { statusEl.className='gw-status-pill high'; statusEl.textContent='↑ ALTA'; } } // Hide input row once value is set — show edit link instead if (inputRow) inputRow.style.display = 'none'; const editRow = document.getElementById('glucoseEditRow'); if (editRow) editRow.style.display = 'flex'; updateIOBWidget(); } function updateIOBWidget() { const iobWidget = document.getElementById('iobWidget'); if (!iobWidget) return; const lastBolusTime = profile.lastBolusTime || null; const lastBolusUnits = profile.lastBolusUnits || 0; if (!lastBolusTime || lastBolusUnits <= 0) { iobWidget.style.display = 'none'; return; } const minAgo = Math.round((Date.now() - lastBolusTime) / 60000); const totalMin = 240; // Novorapid duration if (minAgo >= totalMin) { iobWidget.style.display = 'none'; return; } const iob = Math.round(lastBolusUnits * Math.max(0, (totalMin - minAgo) / totalMin) * 10) / 10; const minRemaining = totalMin - minAgo; const hh = Math.floor(minRemaining / 60); const mm = minRemaining % 60; const timeStr = hh > 0 ? hh + 'h ' + mm + 'm' : mm + 'm'; const pct = minAgo / totalMin; // 0=fresh, 1=done const dashOffset = Math.round(113 * (1 - (1 - pct))); // fill from 0 to 113 iobWidget.style.display = 'flex'; const iobValEl = document.getElementById('iobVal'); const iobSubEl = document.getElementById('iobSub'); const iobTimerEl = document.getElementById('iobTimerText'); const iobCircle = document.getElementById('iobProgressCircle'); if (iobValEl) iobValEl.textContent = iob.toFixed(1); if (iobSubEl) iobSubEl.textContent = 'Último bolo hace ' + (minAgo < 60 ? minAgo + ' min' : Math.floor(minAgo/60) + 'h ' + (minAgo%60) + 'm'); if (iobTimerEl) iobTimerEl.textContent = timeStr; if (iobCircle) iobCircle.setAttribute('stroke-dashoffset', 113 - Math.round(113 * (1 - pct))); } // ── VALUE PROPS: see vpCurrentSlide + vpNext below ── // ── SPLASH AUTO-ADVANCE (if returning user) ── function initSplashAnim() { // Nothing to auto-advance — user must tap } // ══ VALUE PROPS CAROUSEL ══ var vpCurrentSlide = 1; var vpTouchStartX = 0; var vpTouchStartY = 0; var vpIsDragging = false; function vpGoTo(n) { if (n < 1 || n > 3) return; vpCurrentSlide = n; var track = document.getElementById('vpTrack'); if (track) { track.style.transition = 'transform .35s cubic-bezier(.4,0,.2,1)'; track.style.transform = 'translateX(-' + (n-1) * 100 + '%)'; } for (var i = 1; i <= 3; i++) { var dot = document.getElementById('vp-dot-' + i); if (dot) dot.classList.toggle('active', i === n); } var hint = document.getElementById('vpSwipeHint'); var btn = document.getElementById('vpNextBtn'); if (hint) hint.style.display = n < 3 ? 'flex' : 'none'; if (btn) btn.style.display = n === 3 ? 'block' : 'none'; } function vpNext() { if (vpCurrentSlide < 3) { vpGoTo(vpCurrentSlide + 1); } else { vpCurrentSlide = 1; vpGoTo(1); goTo('s-trans1'); } } function vpInitTouch() { var wrap = document.getElementById('vpTrackWrap'); if (!wrap || wrap._vpReady) return; wrap._vpReady = true; wrap.addEventListener('touchstart', function(e) { vpTouchStartX = e.touches[0].clientX; vpTouchStartY = e.touches[0].clientY; vpIsDragging = false; }, {passive:true}); wrap.addEventListener('touchmove', function(e) { var dx = e.touches[0].clientX - vpTouchStartX; var dy = e.touches[0].clientY - vpTouchStartY; if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 8) { vpIsDragging = true; e.preventDefault(); var track = document.getElementById('vpTrack'); if (track) { var baseOffset = (vpCurrentSlide - 1) * window.innerWidth; track.style.transition = 'none'; track.style.transform = 'translateX(' + (-baseOffset + dx) + 'px)'; } } }, {passive:false}); wrap.addEventListener('touchend', function(e) { var dx = e.changedTouches[0].clientX - vpTouchStartX; var track = document.getElementById('vpTrack'); if (track) track.style.transition = ''; if (vpIsDragging && Math.abs(dx) > 40) { if (dx < 0 && vpCurrentSlide < 3) vpGoTo(vpCurrentSlide + 1); else if (dx > 0 && vpCurrentSlide > 1) vpGoTo(vpCurrentSlide - 1); else vpGoTo(vpCurrentSlide); } else { vpGoTo(vpCurrentSlide); } vpIsDragging = false; }, {passive:true}); } document.addEventListener('DOMContentLoaded', function() { setTimeout(startCarousel, 100); // Verificar token PRIMERO — antes de checkAutoLogin var fullUrl = window.location.href; var hasToken = new URLSearchParams(window.location.search).get('verify') || (window.location.hash ? new URLSearchParams(window.location.hash.slice(1)).get('verify') : null) || (fullUrl.match(/[?&#]verify=([a-f0-9]{32,})/) || [])[1]; if (hasToken) { // Hay token — verificar cuenta y mostrar login, NO redirigir var splash = document.getElementById('appLoadingSplash'); if (splash) splash.style.display = 'none'; checkVerifyTokenInURL(); return; // no continuar con checkAutoLogin } checkAutoLogin(); window._autoLoginDone = true; }); // Fallback: ejecutar con window.onload si DOMContentLoaded no captó el token window.addEventListener('load', function() { var _hasToken = new URLSearchParams(window.location.search).get('verify') || (window.location.hash ? new URLSearchParams(window.location.hash.slice(1)).get('verify') : null); if (_hasToken) { var _splash = document.getElementById('appLoadingSplash'); if (_splash) _splash.style.display = 'none'; // Solo ejecutar si el modal no está ya abierto var modal = document.getElementById('authModal'); if (!modal || modal.style.display === 'none' || modal.style.display === '') { checkVerifyTokenInURL(); } } else { // Sin token — asegurarse que checkAutoLogin se ejecutó if (!window._autoLoginDone) checkAutoLogin(); } });
🚀
Activá CarbLens Premium
Usaste tus 10 escaneos de prueba gratuitos
0 / 10
escaneos de prueba utilizados
📷 Escaneos ILIMITADOS
💉 Corrección automática por glucosa
📊 Historial completo x 90 días
📄 Exportar reportes para tu médico — 7, 15 y 30 días
Múltiples ratios por horario
✅ Ya pagué — activar Premium
Instalá CarbLens
Agregala a tu pantalla de inicio
Crear cuenta
Empezá a gestionar tu insulina con IA
+54
¿Ya tenés cuenta? Ingresá
📬
Verificá tu email
Te enviamos un link a
1️⃣
Abrí tu casilla de email y buscá el mensaje de CarbLens
2️⃣
Hacé clic en "Verificar mi cuenta"
3️⃣
Vas a ingresar automáticamente y empezar el setup
¿No llegó el email? Reenviar
← Usar otro email
Bienvenido de vuelta
Ingresá con tu cuenta CarbLens
¿No tenés cuenta? Registrate