Current Glucose
--
mmol/L
Active Insulin
--
units IOB
💊
GlucoTrac
Select an action from the menu ←
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();
});