GlucoTrac

Current Glucose
--
mmol/L
Active Insulin
--
units IOB
💊
GlucoTrac
Select an action from the menu ←

🩸 Log Glucose

📊 Calculate insulin dose based on this reading →

💉 Log Insulin

🍽️ Log Meal

🧮 Insulin Calculator

🔮 Predict Glucose Impact

const password = document.getElementById('login-password').value; try { const result = await api('/login', 'POST', { email, password }); if (result.success && result.data?.token) { localStorage.setItem('auth_token', result.data.token); localStorage.setItem('user', JSON.stringify(result.data.user)); showScreen('dashboard-screen'); } else { showError(result.message || 'Login failed. Please check your credentials.'); } } catch (err) { showError('Connection error. Please try again.'); } }); // Register form document.getElementById('register-form').addEventListener('submit', async (e) => { e.preventDefault(); hideMessages(); const name = document.getElementById('register-name').value; const email = document.getElementById('register-email').value; const password = document.getElementById('register-password').value; const password_confirmation = document.getElementById('register-confirm').value; const diabetes_type = document.getElementById('register-diabetes-type').value; if (password !== password_confirmation) { showError('Passwords do not match.'); return; } try { const result = await api('/register', 'POST', { name, email, password, password_confirmation, diabetes_type }); if (result.success && result.data?.token) { localStorage.setItem('auth_token', result.data.token); localStorage.setItem('user', JSON.stringify(result.data.user)); showScreen('dashboard-screen'); } else { showError(result.message || 'Registration failed. Please try again.'); } } catch (err) { showError('Connection error. Please try again.'); } }); // Logout document.getElementById('logout-btn').addEventListener('click', async () => { try { await api('/logout', 'POST'); } catch (e) {} localStorage.removeItem('auth_token'); localStorage.removeItem('user'); showScreen('auth-screen'); }); // Check auth on load - TEMPORARILY DISABLED (login bypassed) // document.addEventListener('DOMContentLoaded', () => { // const token = localStorage.getItem('auth_token'); // if (token) { // api('/user').then(result => { // if (result.data) { // showScreen('dashboard-screen'); // } else { // localStorage.removeItem('auth_token'); // localStorage.removeItem('user'); // } // }).catch(() => {}); // } // }); // PWA Install Prompt - Use the early-captured prompt const installAppBtn = document.getElementById('install-app-btn'); const installBanner = document.getElementById('install-banner'); const iosModal = document.getElementById('ios-install-modal'); // Detect iOS const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const isStandalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone; console.log('PWA Debug:', { isIOS, isStandalone, hasEarlyPrompt: !!window.deferredInstallPrompt, userAgent: navigator.userAgent }); // Hide install button if already installed as PWA if (isStandalone) { installAppBtn.style.display = 'none'; console.log('App already installed, hiding install button'); } // Also listen for the event in case it fires later window.addEventListener('beforeinstallprompt', (e) => { console.log('beforeinstallprompt fired (late listener)!'); e.preventDefault(); window.deferredInstallPrompt = e; installBanner?.classList.add('show'); }); // Handle install button click installAppBtn.addEventListener('click', async () => { const prompt = window.deferredInstallPrompt; console.log('Install button clicked. iOS:', isIOS, 'hasPrompt:', !!prompt); if (isIOS) { // Show iOS instructions modal iosModal.classList.add('show'); } else if (prompt) { // Trigger the native Android install prompt try { prompt.prompt(); const { outcome } = await prompt.userChoice; console.log('Install outcome:', outcome); if (outcome === 'accepted') { installAppBtn.style.display = 'none'; installBanner?.classList.remove('show'); } window.deferredInstallPrompt = null; } catch (err) { console.error('Install prompt error:', err); } } else { // No prompt available - tell user to use browser menu // This can happen on first visit before Chrome decides to offer install alert('To install GlucoTrac:\n\n1. Tap the browser menu (⋮) at the top right\n2. Select "Install app" or "Add to Home screen"\n3. Tap "Install"\n\nIf you don\'t see this option, try visiting the app again tomorrow.'); } }); // Install banner button document.getElementById('install-btn')?.addEventListener('click', async () => { const prompt = window.deferredInstallPrompt; if (prompt) { prompt.prompt(); const { outcome } = await prompt.userChoice; if (outcome === 'accepted') { installBanner?.classList.remove('show'); installAppBtn.style.display = 'none'; } window.deferredInstallPrompt = null; } }); document.getElementById('install-dismiss')?.addEventListener('click', () => { installBanner?.classList.remove('show'); }); // iOS modal close document.getElementById('ios-close-btn')?.addEventListener('click', () => { iosModal.classList.remove('show'); }); iosModal.addEventListener('click', (e) => { if (e.target === iosModal) { iosModal.classList.remove('show'); } }); // Check on load document.addEventListener('DOMContentLoaded', () => { if (isIOS && !isStandalone) { installAppBtn.style.display = 'flex'; } }); // Hide UI when app is installed window.addEventListener('appinstalled', () => { installAppBtn.style.display = 'none'; installBanner.classList.remove('show'); deferredPrompt = null; }); // ============================================================ // FEATURE MODALS FUNCTIONALITY // ============================================================ // Helper: Show modal function showModal(modalId) { const el = document.getElementById(modalId); if (el) el.classList.add('show'); } // Helper: Hide modal function hideModal(modalId) { const el = document.getElementById(modalId); if (el) el.classList.remove('show'); } // Helper: Show inline panel (replaces showModal for 5 action panels) function showPanel(panelId) { // Hide all panels + empty state document.querySelectorAll('.panel-section').forEach(p => p.classList.remove('active')); document.getElementById('panel-empty').style.display = 'none'; // Deactivate all sidebar buttons document.querySelectorAll('.sb-btn').forEach(b => b.classList.remove('active')); // Show requested panel const panel = document.getElementById(panelId); if (panel) panel.classList.add('active'); // Highlight matching sidebar button const btnMap = { 'panel-glucose': 'btn-log-glucose', 'panel-insulin': 'btn-log-insulin', 'panel-meal': 'btn-log-meal', 'panel-correction': 'btn-correction', 'panel-predict': 'btn-predict' }; const btn = document.getElementById(btnMap[panelId]); if (btn) btn.classList.add('active'); } // Helper: Close active panel (returns to empty state) function closeActivePanel() { document.querySelectorAll('.panel-section').forEach(p => p.classList.remove('active')); document.querySelectorAll('.sb-btn').forEach(b => b.classList.remove('active')); document.getElementById('panel-empty').style.display = ''; } // Panel close buttons document.querySelectorAll('[data-close-panel]').forEach(btn => { btn.addEventListener('click', closeActivePanel); }); // Sidebar button → panel map const sidebarPanelMap = { 'btn-log-glucose': 'panel-glucose', 'btn-log-insulin': 'panel-insulin', 'btn-log-meal': 'panel-meal', 'btn-correction': 'panel-correction', 'btn-predict': 'panel-predict' }; Object.entries(sidebarPanelMap).forEach(([btnId, panelId]) => { const btn = document.getElementById(btnId); if (btn) { btn.addEventListener('click', () => { // Toggle: click active button collapses panel if (btn.classList.contains('active')) { closeActivePanel(); } else { showPanel(panelId); } }); } }); // Helper: Get current date/time in local format function getCurrentDateTime() { const now = new Date(); const date = now.toISOString().split('T')[0]; const time = now.toTimeString().slice(0, 5); return { date, time }; } // Helper: Show toast message function showToast(message, type = 'success') { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%); background: ${type === 'success' ? '#4ade80' : '#ef4444'}; color: ${type === 'success' ? '#0a0a0a' : '#fff'}; padding: 1rem 1.5rem; border-radius: 8px; font-weight: 600; z-index: 999; animation: fadeIn 0.3s; `; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } // Close modal buttons document.querySelectorAll('[data-close-modal]').forEach(btn => { btn.addEventListener('click', () => { btn.closest('.modal-overlay').classList.remove('show'); }); }); // Close modal on overlay click document.querySelectorAll('.modal-overlay').forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('show'); } }); }); // ============================================================ // LOG GLUCOSE // ============================================================ // Note: btn-log-glucose click is handled by sidebarPanelMap above. // This listener pre-fills the date/time when the panel opens. document.getElementById('btn-log-glucose').addEventListener('click', () => { const { date, time } = getCurrentDateTime(); document.getElementById('glucose-date').value = date; document.getElementById('glucose-time').value = time; }); document.getElementById('save-glucose').addEventListener('click', () => { const value = parseFloat(document.getElementById('glucose-value').value); const date = document.getElementById('glucose-date').value; const time = document.getElementById('glucose-time').value; const context = document.getElementById('glucose-context').value; const notes = document.getElementById('glucose-notes').value; if (!value || value < 20 || value > 600) { showToast('Please enter a valid glucose reading (20-600)', 'error'); return; } // Save to localStorage const readings = JSON.parse(localStorage.getItem('glucoseReadings') || '[]'); readings.unshift({ id: Date.now(), value, date, time, context, notes, timestamp: new Date(`${date}T${time}`).getTime() }); localStorage.setItem('glucoseReadings', JSON.stringify(readings)); // Update dashboard updateDashboardGlucose(value); // Clear form and stay open (or close) document.getElementById('glucose-value').value = ''; document.getElementById('glucose-notes').value = ''; showToast('Glucose reading saved!'); }); // Update dashboard with latest glucose function updateDashboardGlucose(value) { const display = document.querySelector('.stat-card .value.glucose-good, .stat-card .value.glucose-warning, .stat-card .value.glucose-danger'); if (display) { display.textContent = value; display.className = 'value ' + getGlucoseClass(value); } } function getGlucoseClass(value) { if (value < 70) return 'glucose-danger'; if (value > 180) return 'glucose-warning'; if (value > 250) return 'glucose-danger'; return 'glucose-good'; } // Switch from glucose panel to correction panel document.getElementById('open-correction-from-glucose').addEventListener('click', () => { const glucose = document.getElementById('glucose-value').value; showPanel('panel-correction'); if (glucose) { document.getElementById('correction-glucose').value = glucose; } }); // ============================================================ // CORRECTION CALCULATOR // ============================================================ document.getElementById('calculate-correction').addEventListener('click', () => { const carbs = parseFloat(document.getElementById('correction-carbs').value) || 0; const icr = parseFloat(document.getElementById('correction-icr').value) || 10; const currentGlucose = parseFloat(document.getElementById('correction-glucose').value) || 0; const targetGlucose = parseFloat(document.getElementById('correction-target').value) || 100; const isf = parseFloat(document.getElementById('correction-isf').value) || 50; // Calculate carb coverage const carbDose = carbs / icr; // Calculate correction dose let correctionDose = 0; if (currentGlucose > targetGlucose) { correctionDose = (currentGlucose - targetGlucose) / isf; } // Total dose (rounded to nearest 0.5) const totalDose = Math.round((carbDose + correctionDose) * 2) / 2; // Display results document.getElementById('carb-dose').textContent = carbDose.toFixed(1); document.getElementById('correction-dose').textContent = correctionDose.toFixed(1); document.getElementById('correction-total').textContent = totalDose.toFixed(1); document.getElementById('correction-result').style.display = 'block'; }); // ============================================================ // LOG INSULIN // ============================================================ // btn-log-insulin handled by sidebarPanelMap; pre-fill on click document.getElementById('btn-log-insulin').addEventListener('click', () => { const { date, time } = getCurrentDateTime(); document.getElementById('insulin-date').value = date; document.getElementById('insulin-time').value = time; }); document.getElementById('save-insulin').addEventListener('click', () => { const type = document.getElementById('insulin-type').value; const units = parseFloat(document.getElementById('insulin-units').value); const date = document.getElementById('insulin-date').value; const time = document.getElementById('insulin-time').value; const site = document.getElementById('insulin-site').value; const notes = document.getElementById('insulin-notes').value; if (!units || units <= 0) { showToast('Please enter valid insulin units', 'error'); return; } // Save to localStorage const logs = JSON.parse(localStorage.getItem('insulinLogs') || '[]'); logs.unshift({ id: Date.now(), type, units, date, time, site, notes, timestamp: new Date(`${date}T${time}`).getTime() }); localStorage.setItem('insulinLogs', JSON.stringify(logs)); // Update IOB on dashboard updateIOB(); document.getElementById('insulin-units').value = ''; document.getElementById('insulin-notes').value = ''; showToast('Insulin injection logged!'); }); // Calculate IOB (Insulin on Board) function updateIOB() { const logs = JSON.parse(localStorage.getItem('insulinLogs') || '[]'); const now = Date.now(); const fourHoursAgo = now - (4 * 60 * 60 * 1000); let iob = 0; logs.forEach(log => { if (log.timestamp > fourHoursAgo && (log.type === 'rapid' || log.type === 'short')) { const hoursAgo = (now - log.timestamp) / (60 * 60 * 1000); // Simple linear decay over 4 hours const remaining = Math.max(0, 1 - (hoursAgo / 4)); iob += log.units * remaining; } }); const iobDisplay = document.querySelectorAll('.stat-card')[1]?.querySelector('.value'); if (iobDisplay) { iobDisplay.textContent = iob.toFixed(1); } } // ============================================================ // LOG MEAL // ============================================================ let currentMealItems = []; let searchTimeout = null; // Common foods database (fallback) const commonFoods = [ { name: 'White Rice (cooked, 1 cup)', carbs: 45, protein: 4, fat: 0, calories: 200 }, { name: 'Brown Rice (cooked, 1 cup)', carbs: 45, protein: 5, fat: 2, calories: 220 }, { name: 'White Bread (1 slice)', carbs: 13, protein: 2, fat: 1, calories: 70 }, { name: 'Whole Wheat Bread (1 slice)', carbs: 12, protein: 4, fat: 1, calories: 80 }, { name: 'Banana (medium)', carbs: 27, protein: 1, fat: 0, calories: 105 }, { name: 'Apple (medium)', carbs: 25, protein: 0, fat: 0, calories: 95 }, { name: 'Orange (medium)', carbs: 15, protein: 1, fat: 0, calories: 62 }, { name: 'Chicken Breast (100g)', carbs: 0, protein: 31, fat: 4, calories: 165 }, { name: 'Egg (large)', carbs: 1, protein: 6, fat: 5, calories: 70 }, { name: 'Milk (1 cup)', carbs: 12, protein: 8, fat: 8, calories: 150 }, { name: 'Pasta (cooked, 1 cup)', carbs: 43, protein: 8, fat: 1, calories: 220 }, { name: 'Potato (medium, baked)', carbs: 37, protein: 4, fat: 0, calories: 160 }, { name: 'Sweet Potato (medium)', carbs: 24, protein: 2, fat: 0, calories: 103 }, { name: 'Pizza (1 slice)', carbs: 36, protein: 12, fat: 10, calories: 285 }, { name: 'Hamburger (regular)', carbs: 31, protein: 17, fat: 14, calories: 320 }, { name: 'French Fries (medium)', carbs: 48, protein: 4, fat: 16, calories: 365 }, { name: 'Coca-Cola (330ml)', carbs: 35, protein: 0, fat: 0, calories: 140 }, { name: 'Orange Juice (1 cup)', carbs: 26, protein: 2, fat: 0, calories: 110 }, { name: 'Yogurt (plain, 1 cup)', carbs: 12, protein: 12, fat: 0, calories: 100 }, { name: 'Oatmeal (cooked, 1 cup)', carbs: 27, protein: 5, fat: 3, calories: 150 }, { name: 'Cereal (1 cup)', carbs: 24, protein: 3, fat: 1, calories: 120 }, { name: 'Cheese (1 oz)', carbs: 0, protein: 7, fat: 9, calories: 110 }, { name: 'Beans (cooked, 1 cup)', carbs: 40, protein: 15, fat: 1, calories: 225 }, { name: 'Corn (1 cup)', carbs: 31, protein: 5, fat: 1, calories: 130 }, { name: 'Ketchup (1 tbsp)', carbs: 4, protein: 0, fat: 0, calories: 20 }, { name: 'Mayonnaise (1 tbsp)', carbs: 0, protein: 0, fat: 10, calories: 90 }, { name: 'BBQ Sauce (2 tbsp)', carbs: 13, protein: 0, fat: 0, calories: 50 }, { name: 'Honey (1 tbsp)', carbs: 17, protein: 0, fat: 0, calories: 64 }, { name: 'Sugar (1 tsp)', carbs: 4, protein: 0, fat: 0, calories: 16 }, { name: 'Soda (can)', carbs: 39, protein: 0, fat: 0, calories: 150 }, ]; // btn-log-meal handled by sidebarPanelMap; pre-fill on click document.getElementById('btn-log-meal').addEventListener('click', () => { const { date, time } = getCurrentDateTime(); document.getElementById('meal-date').value = date; document.getElementById('meal-time').value = time; currentMealItems = []; renderMealItems(); }); // Meal search document.getElementById('meal-search').addEventListener('input', (e) => { clearTimeout(searchTimeout); const query = e.target.value.trim().toLowerCase(); if (query.length < 2) { document.getElementById('meal-search-results').classList.remove('show'); return; } searchTimeout = setTimeout(async () => { // First search local database let results = commonFoods.filter(f => f.name.toLowerCase().includes(query) ).slice(0, 5); // Try to fetch from Open Food Facts try { const response = await fetch(`https://world.openfoodfacts.org/cgi/search.pl?search_terms=${encodeURIComponent(query)}&search_simple=1&action=process&json=1&page_size=5`); const data = await response.json(); if (data.products && data.products.length > 0) { const apiResults = data.products.map(p => ({ name: p.product_name || 'Unknown', carbs: Math.round(p.nutriments?.carbohydrates_100g || 0), protein: Math.round(p.nutriments?.proteins_100g || 0), fat: Math.round(p.nutriments?.fat_100g || 0), calories: Math.round(p.nutriments?.['energy-kcal_100g'] || 0), serving: '100g' })).filter(p => p.name !== 'Unknown'); results = [...apiResults, ...results].slice(0, 8); } } catch (err) { console.log('API search failed, using local results'); } renderSearchResults(results); }, 300); }); function renderSearchResults(results) { const container = document.getElementById('meal-search-results'); if (results.length === 0) { container.classList.remove('show'); return; } container.innerHTML = results.map((item, index) => `
${item.name}
Carbs: ${item.carbs}g | Protein: ${item.protein}g | Fat: ${item.fat}g | ${item.calories} kcal
`).join(''); container.classList.add('show'); // Add click handlers container.querySelectorAll('.meal-search-item').forEach((el, index) => { el.addEventListener('click', () => { currentMealItems.push(results[index]); renderMealItems(); document.getElementById('meal-search').value = ''; container.classList.remove('show'); }); }); } // Hide search results when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.meal-search-container')) { document.getElementById('meal-search-results').classList.remove('show'); } }); function renderMealItems() { const container = document.getElementById('meal-items'); const totalsSection = document.getElementById('meal-totals'); if (currentMealItems.length === 0) { container.innerHTML = '

No items added yet

'; totalsSection.style.display = 'none'; return; } container.innerHTML = currentMealItems.map((item, index) => `
${item.name}
C: ${item.carbs}g | P: ${item.protein}g | F: ${item.fat}g | ${item.calories} kcal
`).join(''); // Add remove handlers container.querySelectorAll('.meal-item-remove').forEach(btn => { btn.addEventListener('click', () => { currentMealItems.splice(parseInt(btn.dataset.index), 1); renderMealItems(); }); }); // Calculate totals const totals = currentMealItems.reduce((acc, item) => ({ carbs: acc.carbs + item.carbs, protein: acc.protein + item.protein, fat: acc.fat + item.fat, calories: acc.calories + item.calories }), { carbs: 0, protein: 0, fat: 0, calories: 0 }); document.getElementById('total-carbs').textContent = totals.carbs + 'g'; document.getElementById('total-protein').textContent = totals.protein + 'g'; document.getElementById('total-fat').textContent = totals.fat + 'g'; document.getElementById('total-calories').textContent = totals.calories; totalsSection.style.display = 'block'; } // Add custom food document.getElementById('add-custom-meal').addEventListener('click', () => { showModal('modal-custom-food'); }); document.getElementById('add-custom-food-item').addEventListener('click', () => { const name = document.getElementById('custom-food-name').value.trim(); const serving = document.getElementById('custom-food-serving').value.trim(); const carbs = parseFloat(document.getElementById('custom-food-carbs').value) || 0; const protein = parseFloat(document.getElementById('custom-food-protein').value) || 0; const fat = parseFloat(document.getElementById('custom-food-fat').value) || 0; const calories = parseFloat(document.getElementById('custom-food-calories').value) || 0; if (!name) { showToast('Please enter a food name', 'error'); return; } currentMealItems.push({ name: serving ? `${name} (${serving})` : name, carbs, protein, fat, calories }); // Clear form document.getElementById('custom-food-name').value = ''; document.getElementById('custom-food-serving').value = ''; document.getElementById('custom-food-carbs').value = ''; document.getElementById('custom-food-protein').value = ''; document.getElementById('custom-food-fat').value = ''; document.getElementById('custom-food-calories').value = ''; hideModal('modal-custom-food'); renderMealItems(); }); // Save meal document.getElementById('save-meal').addEventListener('click', () => { if (currentMealItems.length === 0) { showToast('Please add at least one food item', 'error'); return; } const date = document.getElementById('meal-date').value; const time = document.getElementById('meal-time').value; const mealType = document.getElementById('meal-type').value; const totals = currentMealItems.reduce((acc, item) => ({ carbs: acc.carbs + item.carbs, protein: acc.protein + item.protein, fat: acc.fat + item.fat, calories: acc.calories + item.calories }), { carbs: 0, protein: 0, fat: 0, calories: 0 }); // Save to localStorage const meals = JSON.parse(localStorage.getItem('mealLogs') || '[]'); meals.unshift({ id: Date.now(), date, time, mealType, items: currentMealItems, totals, timestamp: new Date(`${date}T${time}`).getTime() }); localStorage.setItem('mealLogs', JSON.stringify(meals)); currentMealItems = []; renderMealItems(); showToast(`Meal logged! Total carbs: ${totals.carbs}g`); }); // ============================================================ // PREDICT IMPACT // ============================================================ // btn-predict handled by sidebarPanelMap; pre-fill on click document.getElementById('btn-predict').addEventListener('click', () => { const readings = JSON.parse(localStorage.getItem('glucoseReadings') || '[]'); if (readings.length > 0) { const mmol = (readings[0].value / 18).toFixed(1); document.getElementById('predict-glucose').value = mmol; } const meals = JSON.parse(localStorage.getItem('mealLogs') || '[]'); if (meals.length > 0 && (Date.now() - meals[0].timestamp) < 3600000) { document.getElementById('predict-carbs').value = meals[0].totals.carbs; } document.getElementById('prediction-results').style.display = 'none'; }); document.getElementById('run-prediction').addEventListener('click', () => { const currentGlucose = parseFloat(document.getElementById('predict-glucose').value) || 5.6; const carbs = parseFloat(document.getElementById('predict-carbs').value) || 0; const insulin = parseFloat(document.getElementById('predict-insulin').value) || 0; const icr = parseFloat(document.getElementById('predict-icr').value) || 10; const isf = parseFloat(document.getElementById('predict-isf').value) || 2.8; // Simple prediction model // Carbs typically peak at 1-2 hours // Rapid insulin peaks at 1-2 hours const carbsNeeded = carbs / icr; const insulinEffect = insulin * isf; const carbEffect = carbs * 0.22; // ~0.22 mmol/L per gram carb // Generate hourly predictions const predictions = []; for (let hour = 0; hour <= 4; hour++) { let glucose = currentGlucose; // Carb absorption curve (peaks at 1.5 hours) const carbsAbsorbed = hour === 0 ? 0 : hour <= 1.5 ? (hour / 1.5) * carbEffect : hour <= 3 ? carbEffect * (1 - (hour - 1.5) / 3) : carbEffect * 0.1; // Insulin absorption curve (peaks at 1.5 hours for rapid) const insulinActive = hour === 0 ? 0 : hour <= 1.5 ? (hour / 1.5) * insulinEffect : hour <= 4 ? insulinEffect * (1 - (hour - 1.5) / 4) : 0; glucose = currentGlucose + carbsAbsorbed - insulinActive; predictions.push({ hour, glucose: parseFloat(glucose.toFixed(1)) }); } // Find peak const peak = predictions.reduce((max, p) => p.glucose > max.glucose ? p : max, predictions[0]); // Render chart const chart = document.getElementById('prediction-chart'); const maxGlucose = Math.max(...predictions.map(p => p.glucose), 11); chart.innerHTML = predictions.map(p => { const height = (p.glucose / maxGlucose) * 100; const isPeak = p === peak; const isHigh = p.glucose > 10; return `
`; }).join(''); // Update info document.getElementById('peak-glucose').textContent = peak.glucose + ' mmol/L'; document.getElementById('peak-time').textContent = `~${peak.hour} hour${peak.hour !== 1 ? 's' : ''} after eating`; document.getElementById('glucose-2hr').textContent = predictions[2].glucose + ' mmol/L'; document.getElementById('prediction-results').style.display = 'block'; }); // ============================================================ // NAVIGATION // ============================================================ document.getElementById('nav-home').addEventListener('click', () => { document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.getElementById('nav-home').classList.add('active'); }); document.getElementById('nav-history').addEventListener('click', () => { document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.getElementById('nav-history').classList.add('active'); loadHistory('glucose'); showModal('modal-history'); }); document.getElementById('nav-reports').addEventListener('click', () => { document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.getElementById('nav-reports').classList.add('active'); loadReports(); showModal('modal-reports'); }); document.getElementById('nav-settings').addEventListener('click', () => { document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.getElementById('nav-settings').classList.add('active'); loadSettings(); showModal('modal-settings'); }); // Reset nav on modal close document.querySelectorAll('.modal-overlay').forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal || e.target.closest('[data-close-modal]')) { document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.getElementById('nav-home').classList.add('active'); } }); }); // ============================================================ // HISTORY // ============================================================ let currentHistoryType = 'glucose'; document.querySelectorAll('.history-tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.history-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); loadHistory(tab.dataset.historyType); }); }); function loadHistory(type) { currentHistoryType = type; const container = document.getElementById('history-list'); let items = []; if (type === 'glucose') { items = JSON.parse(localStorage.getItem('glucoseReadings') || '[]'); container.innerHTML = items.length ? items.map((item, idx) => { const glucoseClass = item.value < 70 ? 'glucose-low' : (item.value > 180 ? 'glucose-high' : ''); return `
${(item.value / 18).toFixed(1)} mmol/L
📅 ${formatDate(item.date)} at ${item.time}
${item.context ? `Context: ${item.context}` : ''} ${item.notes ? ` • ${item.notes}` : ''}
`; }).join('') : '
No glucose readings yet. Start logging!
'; } else if (type === 'insulin') { items = JSON.parse(localStorage.getItem('insulinLogs') || '[]'); container.innerHTML = items.length ? items.map((item, idx) => `
${item.units} units
📅 ${formatDate(item.date)} at ${item.time}
Type: ${item.type} • Site: ${item.site} ${item.notes ? ` • ${item.notes}` : ''}
`).join('') : '
No insulin logs yet. Start logging!
'; } else if (type === 'meals') { items = JSON.parse(localStorage.getItem('mealLogs') || '[]'); container.innerHTML = items.length ? items.map((item, idx) => `
${item.totals.carbs}g carbs
📅 ${formatDate(item.date)} at ${item.time}
${item.mealType} • ${item.items.length} items • ${item.totals.calories} kcal
`).join('') : '
No meals logged yet. Start logging!
'; } // Add delete handlers container.querySelectorAll('.history-delete').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); deleteHistoryItem(btn.dataset.type, parseInt(btn.dataset.id)); }); }); } function deleteHistoryItem(type, id) { if (!confirm('Delete this entry?')) return; const storageKey = type === 'glucose' ? 'glucoseReadings' : type === 'insulin' ? 'insulinLogs' : 'mealLogs'; let items = JSON.parse(localStorage.getItem(storageKey) || '[]'); items = items.filter(item => item.id !== id); localStorage.setItem(storageKey, JSON.stringify(items)); loadHistory(type); showToast('Entry deleted'); } function formatDate(dateStr) { const date = new Date(dateStr); const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); if (dateStr === today.toISOString().split('T')[0]) return 'Today'; if (dateStr === yesterday.toISOString().split('T')[0]) return 'Yesterday'; return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } // ============================================================ // REPORTS & ANALYSIS // ============================================================ function loadReports() { const readings = JSON.parse(localStorage.getItem('glucoseReadings') || '[]'); const meals = JSON.parse(localStorage.getItem('mealLogs') || '[]'); const settings = JSON.parse(localStorage.getItem('glucoSettings') || '{}'); const lowThreshold = settings.low || 70; const highThreshold = settings.high || 180; // Last 7 days filter const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); const recentReadings = readings.filter(r => r.timestamp > sevenDaysAgo); // Calculate stats if (recentReadings.length > 0) { const avg = Math.round(recentReadings.reduce((sum, r) => sum + r.value, 0) / recentReadings.length); const highCount = recentReadings.filter(r => r.value > highThreshold).length; const lowCount = recentReadings.filter(r => r.value < lowThreshold).length; document.querySelector('#report-avg .stat-value').textContent = avg; document.querySelector('#report-avg').className = 'stat-box ' + (avg < lowThreshold ? 'danger' : avg > highThreshold ? 'warning' : 'good'); document.querySelector('#report-high .stat-value').textContent = highCount; document.querySelector('#report-high').className = 'stat-box ' + (highCount > 3 ? 'warning' : ''); document.querySelector('#report-low .stat-value').textContent = lowCount; document.querySelector('#report-low').className = 'stat-box ' + (lowCount > 0 ? 'danger' : ''); // Render chart renderGlucoseChart(recentReadings.slice(0, 30), lowThreshold, highThreshold); // Calculate time in range const inRange = recentReadings.filter(r => r.value >= lowThreshold && r.value <= highThreshold).length; const total = recentReadings.length; const lowPercent = (lowCount / total * 100).toFixed(0); const highPercent = ((recentReadings.length - inRange - lowCount) / total * 100).toFixed(0); const inRangePercent = (inRange / total * 100).toFixed(0); document.querySelector('.range-segment.low').style.width = lowPercent + '%'; document.querySelector('.range-segment.in-range').style.width = inRangePercent + '%'; document.querySelector('.range-segment.high').style.width = highPercent + '%'; } // Smart analysis runSmartAnalysis(readings, meals, settings); // Meal impact analysis analyzeMealImpact(readings, meals); } function renderGlucoseChart(readings, low, high) { const chart = document.getElementById('chart-line'); if (readings.length === 0) { chart.innerHTML = '

No data yet

'; return; } const maxGlucose = Math.max(...readings.map(r => r.value), 250); const minGlucose = Math.min(...readings.map(r => r.value), 50); const range = maxGlucose - minGlucose; // Set target range indicator const targetRange = document.getElementById('target-range-indicator'); const lowPos = ((low - minGlucose) / range) * 100; const highPos = ((high - minGlucose) / range) * 100; targetRange.style.bottom = lowPos + '%'; targetRange.style.height = (highPos - lowPos) + '%'; // Reverse to show newest on right const sortedReadings = [...readings].sort((a, b) => a.timestamp - b.timestamp); chart.innerHTML = sortedReadings.map(r => { const height = ((r.value - minGlucose) / range) * 100; const className = r.value < low ? 'low' : (r.value > high ? 'high' : ''); return `
`; }).join(''); } // ============================================================ // SMART ANALYSIS - Pre/Post Meal Analysis // ============================================================ function runSmartAnalysis(readings, meals, settings) { const container = document.getElementById('smart-analysis'); const suggestions = []; if (readings.length < 5) { container.innerHTML = '

Log at least 5 glucose readings to enable smart analysis.

'; return; } const icr = settings.icr || 10; const isf = settings.isf || 50; const lowThreshold = settings.low || 70; const highThreshold = settings.high || 180; // Analyze recent trends const recentReadings = readings.slice(0, 20); const avgGlucose = recentReadings.reduce((sum, r) => sum + r.value, 0) / recentReadings.length; const highCount = recentReadings.filter(r => r.value > highThreshold).length; const lowCount = recentReadings.filter(r => r.value < lowThreshold).length; // Pattern detection if (highCount > recentReadings.length * 0.4) { // More than 40% high readings const suggestedICR = Math.max(5, Math.round((icr - 1) * 10) / 10); suggestions.push({ title: '⚠️ Frequent High Readings Detected', message: `${Math.round(highCount / recentReadings.length * 100)}% of recent readings are above ${(highThreshold/18).toFixed(1)} mmol/L.`, suggestion: `Consider discussing with your doctor about adjusting your ICR from ${icr} to ${suggestedICR} (more insulin per carb).` }); } if (lowCount > recentReadings.length * 0.2) { // More than 20% low readings const suggestedICR = Math.round((icr + 1) * 10) / 10; suggestions.push({ title: '🚨 Frequent Low Readings Detected', message: `${Math.round(lowCount / recentReadings.length * 100)}% of recent readings are below ${(lowThreshold/18).toFixed(1)} mmol/L.`, suggestion: `Consider discussing with your doctor about adjusting your ICR from ${icr} to ${suggestedICR} (less insulin per carb).` }); } // Time-based patterns const morningReadings = readings.filter(r => { const hour = parseInt(r.time?.split(':')[0] || 0); return hour >= 5 && hour < 10; }).slice(0, 10); if (morningReadings.length >= 3) { const morningAvg = morningReadings.reduce((sum, r) => sum + r.value, 0) / morningReadings.length; if (morningAvg > 150) { suggestions.push({ title: '🌅 Dawn Phenomenon Detected', message: `Your average morning glucose is ${(morningAvg/18).toFixed(1)} mmol/L.`, suggestion: 'Consider discussing basal insulin timing or dawn phenomenon management with your doctor.' }); } } // Render suggestions if (suggestions.length === 0) { container.innerHTML = '

✅ Your glucose patterns look stable! Keep up the good work.

'; } else { container.innerHTML = suggestions.map(s => `
${s.title}

${s.message}

${s.suggestion}
`).join(''); } } function analyzeMealImpact(readings, meals) { const container = document.getElementById('meal-analysis'); if (meals.length < 3 || readings.length < 6) { container.innerHTML = '

Log more meals and glucose readings to see meal impact analysis.

'; return; } const settings = JSON.parse(localStorage.getItem('glucoSettings') || '{}'); const mealImpacts = []; // Analyze each meal meals.slice(0, 10).forEach(meal => { // Find pre-meal glucose (within 30 min before) const preMeal = readings.find(r => { const diff = meal.timestamp - r.timestamp; return diff > 0 && diff < 30 * 60 * 1000; }); // Find post-meal glucose (1.5-3 hours after) const postMeal = readings.find(r => { const diff = r.timestamp - meal.timestamp; return diff > 90 * 60 * 1000 && diff < 180 * 60 * 1000; }); if (preMeal && postMeal) { const impact = postMeal.value - preMeal.value; mealImpacts.push({ meal, preMeal: preMeal.value, postMeal: postMeal.value, impact, carbRatio: meal.totals.carbs > 0 ? impact / meal.totals.carbs : 0 }); } }); if (mealImpacts.length === 0) { container.innerHTML = '

No matching pre/post meal glucose pairs found. Try logging glucose 30 min before and 2 hours after meals.

'; return; } // Calculate averages const avgImpact = mealImpacts.reduce((sum, m) => sum + m.impact, 0) / mealImpacts.length; const avgImpactMmol = (avgImpact / 18).toFixed(1); const avgCarbRatio = mealImpacts.reduce((sum, m) => sum + m.carbRatio, 0) / mealImpacts.length; const avgCarbRatioMmol = (avgCarbRatio / 18).toFixed(2); let analysis = `
📊 Meal Impact Summary (${mealImpacts.length} meals analyzed)

Average glucose rise after meals: ${avgImpact > 0 ? '+' : ''}${avgImpactMmol} mmol/L

Average impact per gram of carb: ${avgCarbRatioMmol} mmol/L

`; if (avgImpact > 80) { analysis += `
Your post-meal spikes are high. Consider: eating slower, reducing carbs per meal, or discussing ICR with your doctor.
`; } else if (avgImpact < 30) { analysis += `
✅ Great post-meal control! Your insulin dosing appears well-matched.
`; } analysis += '
'; // Show individual meal impacts analysis += mealImpacts.slice(0, 5).map(m => { const preMealMmol = (m.preMeal / 18).toFixed(1); const postMealMmol = (m.postMeal / 18).toFixed(1); const impactMmol = (m.impact / 18).toFixed(1); return `

${m.meal.mealType} on ${formatDate(m.meal.date)}

${preMealMmol} → ${postMealMmol} mmol/L ( ${m.impact > 0 ? '+' : ''}${impactMmol} ) | ${m.meal.totals.carbs}g carbs

`; }).join(''); container.innerHTML = analysis; } // ============================================================ // SETTINGS // ============================================================ function loadSettings() { const settings = JSON.parse(localStorage.getItem('glucoSettings') || '{}'); document.getElementById('setting-age').value = settings.age || ''; document.getElementById('setting-weight').value = settings.weight || ''; document.getElementById('setting-weight-unit').value = settings.weightUnit || 'kg'; document.getElementById('setting-isf').value = settings.isf || ''; document.getElementById('setting-icr').value = settings.icr || ''; document.getElementById('setting-target').value = settings.target || ''; document.getElementById('setting-low').value = settings.low || '70'; document.getElementById('setting-high').value = settings.high || '180'; } document.getElementById('save-settings').addEventListener('click', () => { const settings = { age: parseInt(document.getElementById('setting-age').value) || null, weight: parseFloat(document.getElementById('setting-weight').value) || null, weightUnit: document.getElementById('setting-weight-unit').value, isf: parseFloat(document.getElementById('setting-isf').value) || 50, icr: parseFloat(document.getElementById('setting-icr').value) || 10, target: parseInt(document.getElementById('setting-target').value) || 100, low: parseInt(document.getElementById('setting-low').value) || 70, high: parseInt(document.getElementById('setting-high').value) || 180 }; localStorage.setItem('glucoSettings', JSON.stringify(settings)); // Update correction calculator defaults document.getElementById('correction-icr').value = settings.icr; document.getElementById('correction-isf').value = settings.isf; document.getElementById('correction-target').value = settings.target; // Update prediction defaults document.getElementById('predict-icr').value = settings.icr; document.getElementById('predict-isf').value = settings.isf; hideModal('modal-settings'); showToast('Settings saved!'); }); document.getElementById('export-data').addEventListener('click', () => { const data = { exportDate: new Date().toISOString(), settings: JSON.parse(localStorage.getItem('glucoSettings') || '{}'), glucoseReadings: JSON.parse(localStorage.getItem('glucoseReadings') || '[]'), insulinLogs: JSON.parse(localStorage.getItem('insulinLogs') || '[]'), mealLogs: JSON.parse(localStorage.getItem('mealLogs') || '[]') }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `glucotrac-export-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); showToast('Data exported!'); }); document.getElementById('clear-data').addEventListener('click', () => { if (!confirm('Are you sure you want to delete ALL logged data? This cannot be undone.')) return; if (!confirm('This will permanently delete all glucose readings, insulin logs, and meals. Continue?')) return; localStorage.removeItem('glucoseReadings'); localStorage.removeItem('insulinLogs'); localStorage.removeItem('mealLogs'); showToast('All data cleared'); loadSettings(); }); // ============================================================ // INITIALIZE // ============================================================ document.addEventListener('DOMContentLoaded', () => { // Load settings and apply to forms const settings = JSON.parse(localStorage.getItem('glucoSettings') || '{}'); if (settings.icr) { document.getElementById('correction-icr').value = settings.icr; document.getElementById('predict-icr').value = settings.icr; } if (settings.isf) { document.getElementById('correction-isf').value = settings.isf; document.getElementById('predict-isf').value = settings.isf; } if (settings.target) { document.getElementById('correction-target').value = settings.target; } // Load last glucose reading const readings = JSON.parse(localStorage.getItem('glucoseReadings') || '[]'); if (readings.length > 0) { updateDashboardGlucose(readings[0].value); } // Calculate IOB updateIOB(); });