const KEYS={daily:"mzjV7Daily",wins:"mzjV7Wins",meals:"mzjV7Meals",weights:"mzjV7Weights",settings:"mzjV7Settings",journal:"mzjV7Journal",dayOne:"mzjV8DayOne",futureLetter:"mzjV8FutureLetter",mission:"mzjV8Mission",workouts:"mzjV8Workouts"};
const defaults={name:"Kevin Wiltz",startDate:new Date().toISOString().split("T")[0],startingWeight:328,waterGoal:80,proteinGoal:100,movementGoal:30,sleepGoal:8};
const days=["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"];
const mealSuggestions={
balanced:{breakfast:["Greek yogurt and berries","Eggs and toast","Oatmeal and peanut butter","Cottage cheese and fruit","Egg and turkey wrap","Protein smoothie","Scrambled eggs and avocado"],lunch:["Chicken salad","Turkey wrap and fruit","Tuna and crackers","Chicken soup and half sandwich","Leftover protein and vegetables","Chicken and bean bowl","Cottage cheese plate"],dinner:["Baked chicken, green beans, potato","Salmon, vegetables, rice","Turkey meatballs and vegetables","Beef and vegetable stir-fry","Pork tenderloin and sweet potato","Turkey chili and salad","Shrimp tacos and slaw"],snack:["Cheese stick and apple","Protein shake","Greek yogurt","Cottage cheese","Turkey roll-ups","Egg and fruit","Small handful of nuts"]},
highProtein:{breakfast:["3 eggs and turkey sausage","Greek yogurt with protein powder","Protein shake and egg","Cottage cheese and fruit","Turkey breakfast sandwich","Protein oatmeal","Eggs and chicken sausage"],lunch:["Chicken breast salad","Tuna and eggs","Turkey burger and salad","Chicken and bean bowl","Turkey and cottage cheese plate","Salmon pouch and crackers","Roast beef wrap"],dinner:["Chicken and broccoli","Salmon and asparagus","Lean steak and vegetables","Turkey chili","Shrimp stir-fry","Pork tenderloin","Baked cod and vegetables"],snack:["Protein shake","Greek yogurt","Cottage cheese","Turkey roll-ups","Two eggs","Cheese and turkey","Edamame"]},
simple:{breakfast:["Greek yogurt and banana","Eggs and toast","Protein shake","Cottage cheese and fruit","Oatmeal and peanut butter","Turkey sausage and fruit","Breakfast sandwich"],lunch:["Rotisserie chicken and salad","Turkey sandwich and carrots","Tuna packet and crackers","Soup and half sandwich","Frozen grilled chicken and vegetables","Deli turkey wrap","Cottage cheese and fruit"],dinner:["Rotisserie chicken and vegetables","Frozen salmon and rice","Turkey burger and salad","Chicken sausage and vegetables","Prepared chicken and sweet potato","Low-sodium chili","Shrimp and frozen vegetables"],snack:["Protein shake","Cheese stick","Greek yogurt","Apple and peanut butter","Hard-boiled egg","Cottage cheese cup","Nut pack"]},
gentle:{breakfast:["Plain oatmeal and banana","Greek yogurt and berries","Scrambled egg and toast","Cottage cheese and peaches","Protein shake sipped slowly","Cream of wheat","Egg whites and toast"],lunch:["Chicken noodle soup","Turkey sandwich on toast","Chicken and rice","Tuna and crackers","Cottage cheese and banana","Vegetable soup with chicken","Plain turkey wrap"],dinner:["Baked chicken, rice, carrots","Baked fish and mashed potatoes","Turkey meatloaf and green beans","Chicken soup and crackers","Turkey burger and sweet potato","Scrambled eggs and toast","Baked cod and rice"],snack:["Banana","Applesauce","Greek yogurt","Crackers and cheese","Protein shake","Cottage cheese","Toast and peanut butter"]}
};
function todayString(){const n=new Date(),o=n.getTimezoneOffset();return new Date(n.getTime()-o*60000).toISOString().split("T")[0]}
function read(k,f){try{return JSON.parse(localStorage.getItem(k))??f}catch{return f}}
function write(k,v){
localStorage.setItem(k,JSON.stringify(v));
try{window.MZJFoundation?.recordChange(k,v)}catch(err){console.warn("Foundation journal update failed",err)}
}
function settings(){return {...defaults,...read(KEYS.settings,{})}}
function esc(v=""){return String(v).replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""")}
function dl(name,rows){const csv=rows.map(r=>r.map(v=>`"${String(v??"").replaceAll('"','""')}"`).join(",")).join("\n"),b=new Blob([csv],{type:"text/csv"}),u=URL.createObjectURL(b),a=document.createElement("a");a.href=u;a.download=name;a.click();URL.revokeObjectURL(u)}
function pct(v,g){return Math.max(0,Math.min(100,g?(Number(v)/Number(g))*100:0))}
function setGreeting(){const h=new Date().getHours(),w=h<12?"Good morning":h<18?"Good afternoon":"Good evening",s=settings();const greetingEl=document.getElementById("greeting");if(greetingEl)greetingEl.textContent=`${w}, ${s.name}`;const todayEl=document.getElementById("todayLabel");if(todayEl)todayEl.textContent=new Date().toLocaleDateString(undefined,{weekday:"short",month:"short",day:"numeric"});const start=new Date(s.startDate+"T00:00:00"),daysSince=Math.max(0,Math.floor((new Date()-start)/86400000));const weekEl=document.getElementById("journeyWeek");if(weekEl)weekEl.textContent=Math.floor(daysSince/7)+1}
function loadToday(){const d=read(KEYS.daily,[]).find(x=>x.date===todayString());if(!d)return;["water","protein","movement","sleep","mood","energy","appetite","dose"].forEach(id=>{if(document.getElementById(id)&&d[id]!==undefined)document.getElementById(id).value=d[id]});document.getElementById("shotTaken").checked=!!d.shotTaken}
function renderToday(){
const s=settings(),d=read(KEYS.daily,[]).find(x=>x.date===todayString())||{};
const setText=(id,value)=>{const el=document.getElementById(id);if(el)el.textContent=value;};
const setWidth=(id,value)=>{const el=document.getElementById(id);if(el)el.style.width=value;};
setText("waterValue",`${d.water||0} / ${s.waterGoal} oz`);
setText("proteinValue",`${d.protein||0} / ${s.proteinGoal} g`);
setText("movementValue",`${d.movement||0} / ${s.movementGoal} min`);
setText("sleepValue",`${d.sleep||0} / ${s.sleepGoal} hr`);
setText("shotValue",d.shotTaken?"Complete":"Not logged");
const doseField=document.getElementById("dose");
setText("currentDose",d.dose||doseField?.value||"2.5 mg");
setWidth("waterMeter",`${pct(d.water,s.waterGoal)}%`);
setWidth("proteinMeter",`${pct(d.protein,s.proteinGoal)}%`);
setWidth("movementMeter",`${pct(d.movement,s.movementGoal)}%`);
setWidth("sleepMeter",`${pct(d.sleep,s.sleepGoal)}%`);
setWidth("shotMeter",d.shotTaken?"100%":"0%");
}
dailyForm.addEventListener("submit",e=>{e.preventDefault();const entry={date:todayString(),water:+water.value||0,protein:+protein.value||0,movement:+movement.value||0,sleep:+sleep.value||0,mood:mood.value,energy:energy.value,appetite:appetite.value,shotTaken:shotTaken.checked,dose:dose.value};const arr=read(KEYS.daily,[]),i=arr.findIndex(x=>x.date===entry.date);i>=0?arr[i]=entry:arr.push(entry);arr.sort((a,b)=>new Date(a.date)-new Date(b.date));write(KEYS.daily,arr);renderAll();renderMilestones();alert("Today was saved.")});
let selectedWins=new Set();document.querySelectorAll("[data-win]").forEach(b=>b.onclick=()=>{selectedWins.has(b.dataset.win)?(selectedWins.delete(b.dataset.win),b.classList.remove("selected")):(selectedWins.add(b.dataset.win),b.classList.add("selected"))});
saveWinBtn.onclick=()=>{const custom=customWin.value.trim(),arr=read(KEYS.wins,[]);let today=arr.find(x=>x.date===todayString());if(!today){today={date:todayString(),wins:[]};arr.push(today)}today.wins=[...new Set([...today.wins,...selectedWins,...(custom?[custom]:[])])];write(KEYS.wins,arr);selectedWins.clear();document.querySelectorAll("[data-win]").forEach(b=>b.classList.remove("selected"));customWin.value="";renderWins()};
function renderWins(){const t=read(KEYS.wins,[]).find(x=>x.date===todayString());winsList.innerHTML=t?.wins?.map(w=>`⭐ ${esc(w)}`).join("")||""}
function blankMeals(){return days.map(day=>({day,breakfast:"",lunch:"",dinner:"",snack:""}))}
function renderMeals(plan=read(KEYS.meals,blankMeals())){mealCarousel.innerHTML=plan.map((m,i)=>`
"}
exportDataBtn.onclick=()=>{const d=read(KEYS.daily,[]);dl("my-zepbound-journey-data.csv",[["Date","Water","Protein","Movement","Sleep","Mood","Energy","Appetite","Shot","Dose"],...d.map(x=>[x.date,x.water,x.protein,x.movement,x.sleep,x.mood,x.energy,x.appetite,x.shotTaken?"Yes":"No",x.dose])])}
weightDate.value=todayString();weightForm.addEventListener("submit",e=>{e.preventDefault();const date=weightDate.value,w=+weight.value;if(!date||!w)return;const arr=read(KEYS.weights,[]);arr.push({date,weight:w});arr.sort((a,b)=>new Date(a.date)-new Date(b.date));write(KEYS.weights,arr);weight.value="";renderProgress()})
function renderProgress(){const s=settings(),arr=read(KEYS.weights,[]);startWeight.textContent=`${(+s.startingWeight).toFixed(1)} lb`;if(!arr.length){currentWeight.textContent="—";weightChange.textContent="—";drawChart([]);return}const c=arr.at(-1).weight,ch=s.startingWeight-c;currentWeight.textContent=`${c.toFixed(1)} lb`;weightChange.textContent=ch>=0?`${ch.toFixed(1)} lb down`:`${Math.abs(ch).toFixed(1)} lb up`;drawChart(arr)}
function drawChart(arr){const c=weightChart,x=c.getContext("2d"),w=c.width,h=c.height;x.clearRect(0,0,w,h);x.fillStyle="#fff";x.fillRect(0,0,w,h);if(!arr.length){x.fillStyle="#6b7280";x.font="20px system-ui";x.textAlign="center";x.fillText("Add a weight entry whenever you are ready.",w/2,h/2);return}const p={l:60,r:30,t:25,b:50},v=arr.map(a=>a.weight);let mn=Math.min(...v)-3,mx=Math.max(...v)+3;const X=i=>p.l+(arr.length===1?(w-p.l-p.r)/2:i*(w-p.l-p.r)/(arr.length-1)),Y=n=>p.t+(mx-n)*(h-p.t-p.b)/(mx-mn);x.strokeStyle="#e6ebf3";for(let i=0;i<=4;i++){const py=p.t+i*(h-p.t-p.b)/4;x.beginPath();x.moveTo(p.l,py);x.lineTo(w-p.r,py);x.stroke()}const g=x.createLinearGradient(0,0,w,0);g.addColorStop(0,"#2563eb");g.addColorStop(1,"#8b5cf6");x.strokeStyle=g;x.lineWidth=5;x.beginPath();arr.forEach((a,i)=>i?x.lineTo(X(i),Y(a.weight)):x.moveTo(X(i),Y(a.weight)));x.stroke()}
function navigateToView(viewId,{targetId="",smooth=true}={}){
if(!viewId)return false;
const view=document.getElementById(viewId);
if(!view)return false;
document.getElementById("moreMenuDialog")?.close?.();
document.querySelectorAll(".view").forEach(item=>item.classList.toggle("active",item===view));
document.querySelectorAll(".nav-item[data-view]").forEach(button=>button.classList.toggle("active",button.dataset.view===viewId));
window.scrollTo({top:0,behavior:smooth?"smooth":"auto"});
if(targetId){
window.setTimeout(()=>document.getElementById(targetId)?.scrollIntoView({behavior:smooth?"smooth":"auto",block:"start"}),220);
}
return true;
}
window.mzjNavigate=navigateToView;
document.querySelectorAll(".nav-item[data-view]").forEach(button=>{
button.addEventListener("click",()=>navigateToView(button.dataset.view,{targetId:button.dataset.target||""}));
});
settingsBtn.onclick=()=>{const s=settings();settingName.value=s.name;settingStartDate.value=s.startDate;settingStartWeight.value=s.startingWeight;settingWaterGoal.value=s.waterGoal;settingProteinGoal.value=s.proteinGoal;settingMovementGoal.value=s.movementGoal;settingSleepGoal.value=s.sleepGoal;settingsDialog.showModal()}
saveSettingsBtn.onclick=()=>{write(KEYS.settings,{name:settingName.value.trim()||"Kevin Wiltz",startDate:settingStartDate.value||todayString(),startingWeight:+settingStartWeight.value||328,waterGoal:+settingWaterGoal.value||80,proteinGoal:+settingProteinGoal.value||100,movementGoal:+settingMovementGoal.value||30,sleepGoal:+settingSleepGoal.value||8});settingsDialog.close();renderAll()}
function renderAll(){setGreeting();renderToday();renderWins();renderMeals();renderJournal();renderStory();renderProgress()}
const missions=[
{icon:"💧",title:"Water first.",text:"Keep your water nearby and take steady sips throughout the day."},
{icon:"🥩",title:"Protein first.",text:"Start meals with a comfortable protein choice before moving to the rest."},
{icon:"👟",title:"Move for ten.",text:"Ten comfortable minutes count. The goal is to keep your body involved."},
{icon:"🌙",title:"Protect your rest.",text:"Give yourself a calmer wind-down and a reasonable bedtime tonight."},
{icon:"❤️",title:"Be kind to yourself.",text:"A hard day is still part of the journey. Choose the next helpful step."}
];
const routines=[
{id:"gymA",category:"gym",name:"Gym A — Lower Body & Cardio",badge:"Gym",exercises:[
{name:"Stationary Bike",weight:"",sets:1,reps:"10 min"},{name:"Seated Leg Curl",weight:"",sets:2,reps:10},{name:"Leg Extension",weight:"",sets:2,reps:10},{name:"Supported Calf Raise",weight:"",sets:2,reps:12},{name:"Gentle Core Brace",weight:"",sets:2,reps:10}]},
{id:"gymB",category:"gym",name:"Gym B — Shoulder-Friendly Upper Body",badge:"Gym",exercises:[
{name:"Recumbent Bike Warm-up",weight:"",sets:1,reps:"8 min"},{name:"Seated Row — Comfortable Range",weight:"",sets:2,reps:10},{name:"Biceps Curl Machine",weight:"",sets:2,reps:10},{name:"Triceps Pressdown — Light",weight:"",sets:2,reps:10},{name:"Pallof Press — Light",weight:"",sets:2,reps:8}]},
{id:"gentle",category:"gentle",name:"Gentle Day — Keep Momentum",badge:"Gentle",exercises:[
{name:"Easy Bike or Walk",weight:"",sets:1,reps:"10 min"},{name:"Sit-to-Stand — Comfortable Height",weight:"",sets:2,reps:8},{name:"Heel Raises with Support",weight:"",sets:2,reps:10},{name:"Gentle Marching",weight:"",sets:2,reps:"30 sec"},{name:"Easy Stretching",weight:"",sets:1,reps:"5 min"}]},
{id:"home",category:"home",name:"Home Backup Workout",badge:"Home",exercises:[
{name:"Chair Sit-to-Stand",weight:"",sets:2,reps:8},{name:"Wall Push-up — Only if Pain-Free",weight:"",sets:2,reps:8},{name:"Standing Heel Raise",weight:"",sets:2,reps:10},{name:"Seated Knee Extension",weight:"",sets:2,reps:10},{name:"Indoor Walk",weight:"",sets:1,reps:"10 min"}]}
];
const milestoneDefinitions=[
{id:"began",icon:"🌅",title:"The Journey Begins",text:"Completed the Day One welcome.",test:()=>!!read(KEYS.dayOne,null)?.begun},
{id:"firstInjection",icon:"💉",title:"First Injection",text:"Logged your first Zepbound injection.",test:()=>read(KEYS.daily,[]).some(d=>d.shotTaken)},
{id:"firstJournal",icon:"📖",title:"First Journal Entry",text:"Saved the first page of your story.",test:()=>read(KEYS.journal,[]).length>=1},
{id:"futureLetter",icon:"❤️",title:"Letter to Future Me",text:"Captured why you started.",test:()=>!!read(KEYS.futureLetter,"").trim()},
{id:"firstWorkout",icon:"🏋️",title:"First Workout Logged",text:"Completed your first workout entry.",test:()=>read(KEYS.workouts,[]).length>=1},
{id:"threeWorkouts",icon:"💪",title:"Building Strength",text:"Logged three workouts.",test:()=>read(KEYS.workouts,[]).length>=3},
{id:"sevenCheckins",icon:"🔥",title:"Showing Up",text:"Completed seven daily check-ins.",test:()=>read(KEYS.daily,[]).length>=7},
{id:"tenWins",icon:"⭐",title:"Ten Wins",text:"Recorded ten personal victories.",test:()=>read(KEYS.wins,[]).flatMap(w=>w.wins||[]).length>=10},
{id:"tenPounds",icon:"🎉",title:"Ten Pounds",text:"Reached a ten-pound change when you chose to look.",test:()=>{const s=settings(),a=read(KEYS.weights,[]);return !!(a.length&&s.startingWeight-a.at(-1).weight>=10)}}
];
function initDayOne(){
const overlay=document.getElementById("dayOneOverlay");
const state=read(KEYS.dayOne,null);
if(overlay && !state?.begun) overlay.classList.remove("hidden");
}
document.getElementById("beginJourneyBtn")?.addEventListener("click",()=>{
write(KEYS.dayOne,{begun:true,date:todayString(),dose:"2.5 mg"});
document.getElementById("dayOneOverlay")?.classList.add("hidden");
renderMilestones();
});
function getDailyMission(){const n=Math.floor(new Date(todayString()+"T00:00:00").getTime()/86400000);return missions[Math.abs(n)%missions.length]}
function renderMission(){const m=getDailyMission(),s=read(KEYS.mission,{}),done=s.date===todayString()&&s.complete;missionIcon.textContent=done?"✓":m.icon;missionTitle.textContent=done?"Mission complete.":m.title;missionText.textContent=done?"You followed through on today’s focus. That matters.":m.text;completeMissionBtn.textContent=done?"Completed":"Mark complete";document.querySelector(".mission-card").classList.toggle("complete",done)}
completeMissionBtn.addEventListener("click",()=>{const s=read(KEYS.mission,{}),done=!(s.date===todayString()&&s.complete);write(KEYS.mission,{date:todayString(),complete:done});renderMission()});
document.querySelectorAll("[data-quick]").forEach(b=>b.addEventListener("click",()=>{document.querySelectorAll("[data-quick='mood']").forEach(x=>x.classList.remove("selected"));b.classList.add("selected");mood.value=b.dataset.value;quickCheckMessage.textContent=`Mood set to ${b.dataset.value}. Save the full check-in whenever you’re ready.`}));
function addQuickValue(field,amount){const el=document.getElementById(field);el.value=(Number(el.value)||0)+amount;quickCheckMessage.textContent=`Added ${amount}${field==="water"?" oz":field==="protein"?" g":" min"}. Save today when finished.`}
quickWaterBtn.addEventListener("click",()=>addQuickValue("water",8));quickProteinBtn.addEventListener("click",()=>addQuickValue("protein",20));quickMoveBtn.addEventListener("click",()=>addQuickValue("movement",10));
function loadFutureLetter(){futureLetter.value=read(KEYS.futureLetter,"")}
saveFutureLetterBtn.addEventListener("click",()=>{write(KEYS.futureLetter,futureLetter.value.trim());futureLetterSaved.classList.remove("hidden");setTimeout(()=>futureLetterSaved.classList.add("hidden"),2500);renderMilestones()});
toggleFutureLetterBtn.addEventListener("click",()=>{const h=futureLetter.classList.toggle("hidden");toggleFutureLetterBtn.textContent=h?"Show letter":"Hide letter"});
function lastExerciseSetting(name){for(const w of [...read(KEYS.workouts,[])].reverse()){const f=(w.exercises||[]).find(e=>e.name.toLowerCase()===name.toLowerCase());if(f)return f}return null}
function exerciseRowTemplate(e={name:"",weight:"",sets:2,reps:10}){const last=e.name?lastExerciseSetting(e.name):null;return `
`}
function bindExerciseRows(){document.querySelectorAll(".remove-exercise").forEach(b=>b.onclick=()=>b.closest(".exercise-row").remove())}
function addExerciseRow(e){exerciseRows.insertAdjacentHTML("beforeend",exerciseRowTemplate(e));bindExerciseRows()}
addExerciseBtn.addEventListener("click",()=>addExerciseRow());
clearWorkoutBtn.addEventListener("click",()=>{workoutForm.reset();workoutDate.value=todayString();exerciseRows.innerHTML="";addExerciseRow();workoutHeading.textContent="Start a workout"});
function startRoutine(id){const r=routines.find(x=>x.id===id);if(!r)return;workoutName.value=r.name;workoutDate.value=todayString();exerciseRows.innerHTML="";r.exercises.forEach(addExerciseRow);workoutHeading.textContent=r.name;document.querySelector('[data-view="exerciseView"]').click();setTimeout(()=>workoutHeading.scrollIntoView({behavior:"smooth",block:"start"}),100)}
window.startRoutine=startRoutine;
function renderRoutines(){const f=routineFilter.value||"all";routineCards.innerHTML=routines.filter(r=>f==="all"||r.category===f).map(r=>`${r.badge}
${r.name}
${r.exercises.map(e=>`
${e.name} — ${e.sets} × ${e.reps}
`).join("")}
`).join("")}
routineFilter.addEventListener("change",renderRoutines);
workoutForm.addEventListener("submit",ev=>{ev.preventDefault();const exercises=[...document.querySelectorAll(".exercise-row")].map(row=>({name:row.querySelector(".ex-name").value.trim(),weight:Number(row.querySelector(".ex-weight").value)||0,sets:Number(row.querySelector(".ex-sets").value)||1,reps:row.querySelector(".ex-reps").value.trim()})).filter(e=>e.name);if(!exercises.length)return alert("Add at least one exercise.");const w={id:crypto.randomUUID?crypto.randomUUID():String(Date.now()),date:workoutDate.value||todayString(),name:workoutName.value.trim()||"Gym Workout",feeling:workoutFeeling.value,exercises,notes:workoutNotes.value.trim()};const all=read(KEYS.workouts,[]);all.push(w);all.sort((a,b)=>new Date(a.date)-new Date(b.date));write(KEYS.workouts,all);workoutForm.reset();workoutDate.value=todayString();exerciseRows.innerHTML="";addExerciseRow();renderWorkouts();renderMilestones();alert("Workout complete. Great job today, Kevin.")});
function startOfWeek(date=new Date()){const d=new Date(date),day=d.getDay();d.setHours(0,0,0,0);d.setDate(d.getDate()-day);return d}
function renderWorkouts(){const all=[...read(KEYS.workouts,[])].reverse(),week=startOfWeek(),weekly=all.filter(w=>new Date(w.date+"T00:00:00")>=week).length;gymVisitsWeek.textContent=`${weekly} / 2–3`;totalWorkouts.textContent=all.length;lastWorkoutDate.textContent=all.length?new Date(all[0].date+"T00:00:00").toLocaleDateString(undefined,{month:"short",day:"numeric"}):"—";workoutHistory.innerHTML=all.length?all.map(w=>`
${Math.round(item.calories)} cal • ${nutritionRound(item.protein)}g protein • ${nutritionRound(item.carbs)}g carbs • ${nutritionRound(item.sugar)}g sugar • ${nutritionRound(item.fiber)}g fiber • ${nutritionRound(item.fat)}g fat • ${Math.round(item.sodium)}mg sodium
`).join("")}`
}).join("");
wrap.querySelectorAll("[data-nutrition-id]").forEach(b=>b.addEventListener("click",()=>{nutritionEntries=nutritionEntries.filter(e=>e.id!==b.dataset.nutritionId);saveNutrition()}))
}
function initNutrition(){
const date=document.getElementById("nutritionDate");if(!date)return;
date.value=todayString();date.addEventListener("change",renderNutrition);
document.getElementById("nutritionFoodSearch").addEventListener("input",e=>renderNutritionSearch(e.target.value));
["nutritionAmount","nutritionUnit","nutritionCalories","nutritionProtein","nutritionCarbs","nutritionSugar","nutritionFiber","nutritionFat","nutritionSodium"].forEach(id=>{
const field=document.getElementById(id);
["input","change","keyup","blur"].forEach(eventName=>field.addEventListener(eventName,nutritionPreview));
});
document.getElementById("nutritionFoodForm")?.addEventListener("input",nutritionPreview);
document.getElementById("nutritionFoodForm")?.addEventListener("change",nutritionPreview);
document.getElementById("nutritionAmount").addEventListener("keydown",event=>{
if(event.key==="Enter"){
event.preventDefault();
nutritionPreview();
document.getElementById("nutritionFoodForm").requestSubmit();
}
});
document.getElementById("nutritionFoodForm")?.addEventListener("submit",e=>{
e.preventDefault();
const amount=Number(document.getElementById("nutritionAmount").value)||1;
const servingAmount=Number(document.getElementById("nutritionServingAmount").value)||1;
const multiplier=amount/servingAmount;
const entry={id:crypto.randomUUID?crypto.randomUUID():String(Date.now()),date:nutritionSelectedDate(),name:document.getElementById("nutritionFoodName").value.trim(),mealType:document.getElementById("nutritionMealType").value,amount,unit:document.getElementById("nutritionUnit").value,servingAmount,servingUnit:document.getElementById("nutritionServingUnit").value};
nutritionNutrients.forEach(k=>{const id="nutrition"+k[0].toUpperCase()+k.slice(1);entry[k]=nutritionRound(Number(document.getElementById(id).value)*multiplier,k==="calories"||k==="sodium"?0:1)});
nutritionEntries.push(entry);saveNutrition();e.target.reset();
document.getElementById("nutritionAmount").value=1;
setSelectValue("nutritionUnit","serving");
setNutritionServingBasis(1,"serving");
nutritionNutrients.forEach(k=>document.getElementById("nutrition"+k[0].toUpperCase()+k.slice(1)).value=0);
renderNutrition();
nutritionPreview()
});
document.getElementById("nutritionClearDayBtn")?.addEventListener("click",()=>{const d=nutritionSelectedDate();if(confirm(`Clear all food logged for ${d}?`)){nutritionEntries=nutritionEntries.filter(e=>e.date!==d);saveNutrition()}});
document.getElementById("nutritionCopyProteinBtn").addEventListener("click",()=>{const total=nutritionRound(nutritionTotals().protein);document.getElementById("protein").value=total;quickCheckMessage.textContent=`Nutrition total copied: ${total} g protein. Save today when finished.`;document.querySelector('[data-view="homeView"]').click();setTimeout(()=>document.getElementById("protein").scrollIntoView({behavior:"smooth",block:"center"}),200)});
renderNutritionSearch();renderNutrition();nutritionPreview()
}
initNutrition();
/* Version 9.0 Nutrition Coach */
const NUTRITION_GOALS_KEY="mzjV9NutritionGoals";
const NUTRITION_FAVORITES_KEY="mzjV9NutritionFavorites";
const GROCERY_KEY="mzjV9Grocery";
let nutritionGoals=read(NUTRITION_GOALS_KEY,{calories:1900,protein:130,fiber:30});
let nutritionFavorites=read(NUTRITION_FAVORITES_KEY,[]);
let groceryItems=read(GROCERY_KEY,[]);
function clampPercent(value,goal){return Math.max(0,Math.min(100,goal?value/goal*100:0))}
function uniqueByName(items){
const seen=new Set();
return items.filter(item=>{
const key=(item.name||"").toLowerCase();
if(!key||seen.has(key))return false;
seen.add(key);return true
})
}
function nutritionTodayEntries(){return nutritionEntries.filter(e=>e.date===todayString())}
function totalsForDate(date){
return nutritionEntries.filter(e=>e.date===date).reduce((sum,e)=>{
nutritionNutrients.forEach(k=>sum[k]+=Number(e[k])||0);return sum
},{calories:0,protein:0,carbs:0,sugar:0,fiber:0,fat:0,sodium:0})
}
function renderGoalProgress(){
if(!document.getElementById("goalCaloriesText"))return;
const totals=nutritionTotals();
document.getElementById("goalCaloriesText").textContent=`${Math.round(totals.calories).toLocaleString()} / ${nutritionGoals.calories.toLocaleString()}`;
document.getElementById("goalProteinText").textContent=`${nutritionRound(totals.protein)} / ${nutritionGoals.protein} g`;
document.getElementById("goalFiberText").textContent=`${nutritionRound(totals.fiber)} / ${nutritionGoals.fiber} g`;
document.getElementById("goalCaloriesMeter").style.width=`${clampPercent(totals.calories,nutritionGoals.calories)}%`;
document.getElementById("goalProteinMeter").style.width=`${clampPercent(totals.protein,nutritionGoals.protein)}%`;
document.getElementById("goalFiberMeter").style.width=`${clampPercent(totals.fiber,nutritionGoals.fiber)}%`
}
function renderMealTotals(){
document.querySelectorAll(".nutrition-meal-section").forEach(section=>{
const title=section.querySelector("h4")?.textContent;
const items=nutritionForDate().filter(e=>e.mealType===title);
const totals=items.reduce((s,e)=>{nutritionNutrients.forEach(k=>s[k]+=Number(e[k])||0);return s},{calories:0,protein:0,carbs:0,sugar:0,fiber:0,fat:0,sodium:0});
let summary=section.querySelector(".meal-total-summary");
if(!summary){summary=document.createElement("p");summary.className="meal-total-summary";section.querySelector("h4").after(summary)}
summary.textContent=`${Math.round(totals.calories)} calories • ${nutritionRound(totals.protein)} g protein • ${nutritionRound(totals.carbs)} g carbs`
});
document.querySelectorAll(".nutrition-meal-row").forEach(row=>{
const id=row.querySelector("[data-nutrition-id]")?.dataset.nutritionId;
if(!id||row.querySelector(".nutrition-favorite"))return;
const button=document.createElement("button");
button.type="button";button.className="nutrition-favorite";
button.textContent=nutritionFavorites.some(f=>f.sourceId===id)?"★":"☆";
button.title="Save as favorite";
button.addEventListener("click",()=>{
const entry=nutritionEntries.find(e=>e.id===id);if(!entry)return;
const existing=nutritionFavorites.findIndex(f=>f.name===entry.name&&f.amount===entry.amount&&f.unit===entry.unit);
if(existing>=0)nutritionFavorites.splice(existing,1);
else nutritionFavorites.unshift({...entry,sourceId:id,id:"fav-"+Date.now(),date:undefined});
write(NUTRITION_FAVORITES_KEY,nutritionFavorites);
renderNutritionCoach()
});
row.querySelector("div").prepend(button)
})
}
function prefillNutrition(entry){
document.getElementById("nutritionFoodName").value=entry.name;
document.getElementById("nutritionMealType").value=entry.mealType||"Breakfast";
document.getElementById("nutritionAmount").value=entry.amount||entry.servingAmount||1;
setSelectValue("nutritionUnit",entry.unit||entry.servingUnit||"serving");
setNutritionServingBasis(entry.servingAmount||entry.amount||1,entry.servingUnit||entry.unit||"serving");
const multiplier=(entry.amount||1)/(entry.servingAmount||entry.amount||1);
nutritionNutrients.forEach(k=>{
const perBasis=multiplier?Number(entry[k]||0)/multiplier:Number(entry[k]||0);
document.getElementById("nutrition"+k[0].toUpperCase()+k.slice(1)).value=nutritionRound(perBasis,k==="calories"||k==="sodium"?0:1)
});
nutritionPreview();
document.getElementById("nutritionFoodForm").scrollIntoView({behavior:"smooth",block:"center"})
}
function renderRecentFoods(){
const list=document.getElementById("recentFoodsList");if(!list)return;
const recent=uniqueByName([...nutritionEntries].reverse()).slice(0,8);
list.innerHTML=recent.map((e,i)=>``).join("");
document.getElementById("recentFoodsEmpty").classList.toggle("hidden",recent.length>0);
list.querySelectorAll("[data-recent-index]").forEach(b=>b.addEventListener("click",()=>prefillNutrition(recent[Number(b.dataset.recentIndex)])))
}
function renderFavorites(){
const list=document.getElementById("favoriteFoodsList");if(!list)return;
list.innerHTML=nutritionFavorites.slice(0,10).map((e,i)=>``).join("");
document.getElementById("favoriteFoodsEmpty").classList.toggle("hidden",nutritionFavorites.length>0);
list.querySelectorAll("[data-favorite-index]").forEach(b=>b.addEventListener("click",()=>prefillNutrition(nutritionFavorites[Number(b.dataset.favoriteIndex)])))
}
function renderProteinCoach(){
const total=nutritionTotals().protein;
const remaining=Math.max(0,nutritionGoals.protein-total);
document.getElementById("proteinCoachHeading").textContent=remaining>0?`${nutritionRound(remaining)} g still available to reach your goal`:"Protein goal reached";
document.getElementById("proteinCoachMessage").textContent=remaining>0
?`You have logged ${nutritionRound(total)} g. Choose a familiar protein food or continue with your planned meals.`
:`You have logged ${nutritionRound(total)} g of protein today. Nice work—keep meals comfortable and balanced.`;
const suggestions=NUTRITION_FOODS.filter(f=>f.protein>=12).sort((a,b)=>b.protein-a.protein).slice(0,6);
const wrap=document.getElementById("proteinCoachSuggestions");
wrap.innerHTML=suggestions.map((f,i)=>``).join("");
wrap.querySelectorAll("[data-protein-index]").forEach(b=>b.addEventListener("click",()=>{
const f=suggestions[Number(b.dataset.proteinIndex)];
prefillNutrition({...f,amount:f.servingAmount,unit:f.servingUnit,mealType:"Snack"})
}))
}
function renderWeeklyNutrition(){
const wrap=document.getElementById("weeklyNutritionGrid");if(!wrap)return;
const days=[];
for(let i=6;i>=0;i--){
const d=new Date();d.setHours(0,0,0,0);d.setDate(d.getDate()-i);
const date=d.toISOString().slice(0,10),totals=totalsForDate(date);
days.push({date,label:d.toLocaleDateString(undefined,{weekday:"short"}),totals})
}
wrap.innerHTML=days.map(day=>`${day.label}${Math.round(day.totals.calories)} cal${nutritionRound(day.totals.protein)}g protein${nutritionRound(day.totals.fiber)}g fiber`).join("")
}
function renderGrocery(){
const wrap=document.getElementById("groceryList");if(!wrap)return;
wrap.innerHTML=groceryItems.map((item,i)=>``).join("");
wrap.querySelectorAll("[data-grocery-check]").forEach(input=>input.addEventListener("change",()=>{
groceryItems[Number(input.dataset.groceryCheck)].checked=input.checked;write(GROCERY_KEY,groceryItems);renderGrocery()
}));
wrap.querySelectorAll("[data-grocery-remove]").forEach(button=>button.addEventListener("click",()=>{
groceryItems.splice(Number(button.dataset.groceryRemove),1);write(GROCERY_KEY,groceryItems);renderGrocery()
}))
}
function renderHomeNutrition(){
const totals=totalsForDate(todayString());
const meals=new Set(nutritionTodayEntries().map(e=>e.mealType)).size;
const setText=(id,value)=>{const el=document.getElementById(id);if(el)el.textContent=value;};
const setWidth=(id,value)=>{const el=document.getElementById(id);if(el)el.style.width=value;};
setText("homeCalories",Math.round(totals.calories).toLocaleString());
setText("homeProtein",`${nutritionRound(totals.protein)} g`);
setText("homeFiber",`${nutritionRound(totals.fiber)} g`);
setText("homeMealsLogged",meals);
setText("homeCaloriesGoal",`of ${nutritionGoals.calories.toLocaleString()}`);
setText("homeProteinGoal",`of ${nutritionGoals.protein} g`);
setText("homeFiberGoal",`of ${nutritionGoals.fiber} g`);
setWidth("homeCaloriesMeter",`${clampPercent(totals.calories,nutritionGoals.calories)}%`);
setWidth("homeProteinMeter",`${clampPercent(totals.protein,nutritionGoals.protein)}%`);
setWidth("homeFiberMeter",`${clampPercent(totals.fiber,nutritionGoals.fiber)}%`);
setWidth("homeMealsMeter",`${Math.min(100,meals/3*100)}%`);
const proteinRemaining=Math.max(0,nutritionGoals.protein-totals.protein);
let title="Your next good choice",message="Log your first meal when you are ready.";
if(totals.protein>=nutritionGoals.protein){title="Protein goal reached";message="You reached today’s protein target. Nice work."}
else if(totals.calories>0&&proteinRemaining>30){title="Protein can lead the next meal";message=`About ${nutritionRound(proteinRemaining)} g remains toward today’s protein goal.`}
else if(totals.calories>0){title="You are building a solid day";message=`You are within ${nutritionRound(proteinRemaining)} g of your protein goal. Keep going one choice at a time.`}
setText("dailyCoachTitle",title);setText("dailyCoachMessage",message);
}
function renderNutritionCoach(){
renderGoalProgress();renderMealTotals();renderRecentFoods();renderFavorites();renderProteinCoach();renderWeeklyNutrition();renderGrocery();renderHomeNutrition()
}
function initNutritionCoach(){
const goalsDialog=document.getElementById("nutritionGoalsDialog");
document.getElementById("nutritionGoalSettingsBtn")?.addEventListener("click",()=>{
document.getElementById("goalCaloriesInput").value=nutritionGoals.calories;
document.getElementById("goalProteinInput").value=nutritionGoals.protein;
document.getElementById("goalFiberInput").value=nutritionGoals.fiber;
goalsDialog.showModal()
});
document.getElementById("saveNutritionGoalsBtn")?.addEventListener("click",()=>{
nutritionGoals={
calories:Number(document.getElementById("goalCaloriesInput").value)||1900,
protein:Number(document.getElementById("goalProteinInput").value)||130,
fiber:Number(document.getElementById("goalFiberInput").value)||30
};
write(NUTRITION_GOALS_KEY,nutritionGoals);goalsDialog.close();renderNutritionCoach()
});
document.getElementById("groceryForm")?.addEventListener("submit",e=>{
e.preventDefault();const input=document.getElementById("groceryInput");const text=input.value.trim();
if(text){groceryItems.push({text,checked:false});write(GROCERY_KEY,groceryItems);input.value="";renderGrocery()}
});
document.getElementById("clearGroceryBtn")?.addEventListener("click",()=>{
groceryItems=groceryItems.filter(i=>!i.checked);write(GROCERY_KEY,groceryItems);renderGrocery()
});
document.getElementById("openNutritionBtn")?.addEventListener("click",()=>document.querySelector('[data-view="mealsView"]').click());
document.getElementById("nutritionDate")?.addEventListener("change",renderNutritionCoach);
document.getElementById("nutritionFoodForm")?.addEventListener("submit",()=>setTimeout(renderNutritionCoach,0));
document.getElementById("nutritionClearDayBtn")?.addEventListener("click",()=>setTimeout(renderNutritionCoach,0));
renderNutritionCoach()
}
initNutritionCoach();
/* Version 9.0.1 Automatic Food Matching */
const FOOD_ALIASES={
"cabbage":"cabbage, raw",
"raw cabbage":"cabbage, raw",
"cooked cabbage":"cabbage, cooked",
"boiled cabbage":"cabbage, cooked",
"steamed cabbage":"cabbage, cooked",
"broccoli":"broccoli, cooked",
"steamed broccoli":"broccoli, cooked",
"broccoli steamed":"broccoli, cooked",
"eggs":"egg, large",
"egg":"egg, large",
"bacon":"bacon, pork, thick-cut",
"chicken":"chicken breast, cooked",
"chicken breast":"chicken breast, cooked",
"rice":"rice, white, cooked",
"brown rice":"rice, brown, cooked",
"oatmeal":"oatmeal, cooked",
"apple":"apple, medium",
"banana":"banana, medium"
};
function normalizeFoodText(value){return String(value||"").toLowerCase().replace(/[^\w\s]/g," ").replace(/\s+/g," ").trim()}
function findFoodMatch(query){
const normalized=normalizeFoodText(query); if(!normalized)return null;
const aliasTarget=FOOD_ALIASES[normalized];
if(aliasTarget){const aliased=NUTRITION_FOODS.find(food=>normalizeFoodText(food.name)===normalizeFoodText(aliasTarget));if(aliased)return aliased}
const exact=NUTRITION_FOODS.find(food=>normalizeFoodText(food.name)===normalized);if(exact)return exact;
const starts=NUTRITION_FOODS.filter(food=>normalizeFoodText(food.name).startsWith(normalized));if(starts.length===1)return starts[0];
const contains=NUTRITION_FOODS.filter(food=>normalizeFoodText(food.name).includes(normalized));if(contains.length===1)return contains[0];
const words=normalized.split(" ").filter(Boolean);
const scored=NUTRITION_FOODS.map(food=>{const haystack=normalizeFoodText(food.name);return {food,score:words.reduce((t,w)=>t+(haystack.includes(w)?1:0),0)}}).filter(x=>x.score>0).sort((a,b)=>b.score-a.score);
if(scored.length&&(scored.length===1||scored[0].score>scored[1].score))return scored[0].food;
return null
}
function applyFoodMatch(food){
if(!food)return false;
document.getElementById("nutritionFoodName").value=food.name;
setNutritionServingBasis(food.servingAmount||1,food.servingUnit||"serving");
document.getElementById("nutritionAmount").value=food.servingAmount||1;
setSelectValue("nutritionUnit",food.servingUnit||"serving");
nutritionNutrients.forEach(key=>{document.getElementById("nutrition"+key[0].toUpperCase()+key.slice(1)).value=food[key]??0});
const status=document.getElementById("nutritionFoodMatchStatus");
if(status){status.textContent=`Matched: ${food.name} — nutrition values loaded.`;status.classList.add("matched");status.classList.remove("not-matched")}
nutritionPreview(); return true
}
function autoMatchTypedFood(showFailure=true){
const input=document.getElementById("nutritionFoodName"),query=input?.value.trim();if(!query)return false;
const match=findFoodMatch(query);if(match)return applyFoodMatch(match);
if(showFailure){const status=document.getElementById("nutritionFoodMatchStatus");if(status){status.textContent="No matching food was found. Choose a suggestion or enter the nutrition values manually.";status.classList.remove("matched");status.classList.add("not-matched")}}
return false
}
function initAutomaticFoodMatching(){
const input=document.getElementById("nutritionFoodName");if(!input)return;
let mobileMatchTimer;
input.addEventListener("input",()=>{clearTimeout(mobileMatchTimer);mobileMatchTimer=setTimeout(()=>autoMatchTypedFood(false),650)});
input.addEventListener("change",()=>autoMatchTypedFood(true));
input.addEventListener("blur",()=>autoMatchTypedFood(true));
input.addEventListener("keydown",event=>{if(event.key==="Enter"||event.key==="Tab")autoMatchTypedFood(true)});
["nutritionAmount","nutritionUnit","nutritionMealType"].forEach(id=>document.getElementById(id)?.addEventListener("focus",()=>autoMatchTypedFood(true)));
document.getElementById("nutritionFoodForm")?.addEventListener("submit",event=>{
const nutrientTotal=nutritionNutrients.reduce((sum,key)=>sum+(Number(document.getElementById("nutrition"+key[0].toUpperCase()+key.slice(1))?.value)||0),0);
if(nutrientTotal===0&&input.value.trim()){const matched=autoMatchTypedFood(true);if(matched){event.preventDefault();setTimeout(()=>document.getElementById("nutritionFoodForm").requestSubmit(),0)}}
},true)
}
initAutomaticFoodMatching();
/* Version 9.0.3 robust typed-food matching */
(function(){
const input=document.getElementById("nutritionFoodName");
const amount=document.getElementById("nutritionAmount");
const findButton=document.getElementById("findTypedFoodBtn");
if(!input)return;
let timer=null;
function showStatus(message,state){
const status=document.getElementById("nutritionFoodMatchStatus");
if(!status)return;
status.textContent=message;
status.classList.remove("matched","not-matched");
if(state)status.classList.add(state);
}
function normalizeV903(value){
return String(value||"").toLowerCase().replace(/[^\w\s]/g," ").replace(/\s+/g," ").trim();
}
function findV903Match(query){
const normalized=normalizeV903(query);
if(!normalized)return null;
const aliases={
"cabbage":"cabbage, raw",
"raw cabbage":"cabbage, raw",
"cooked cabbage":"cabbage, cooked",
"boiled cabbage":"cabbage, cooked",
"steamed cabbage":"cabbage, cooked"
};
const target=aliases[normalized]||normalized;
return NUTRITION_FOODS.find(food=>normalizeV903(food.name)===normalizeV903(target))
|| NUTRITION_FOODS.find(food=>normalizeV903(food.name).startsWith(normalized))
|| NUTRITION_FOODS.find(food=>normalizeV903(food.name).includes(normalized))
|| null;
}
function applyV903Match(){
const query=input.value.trim();
if(!query)return false;
const food=findV903Match(query);
if(!food){
showStatus(`"${query}" is not in the food list yet. Choose a suggestion or enter nutrition manually.`,"not-matched");
return false;
}
input.value=food.name;
document.getElementById("nutritionAmount").value=food.servingAmount||1;
if(typeof setSelectValue==="function"){
setSelectValue("nutritionUnit",food.servingUnit||"serving");
}else{
const unit=document.getElementById("nutritionUnit");
if(unit)unit.value=food.servingUnit||"serving";
}
if(typeof setNutritionServingBasis==="function"){
setNutritionServingBasis(food.servingAmount||1,food.servingUnit||"serving");
}
nutritionNutrients.forEach(key=>{
const field=document.getElementById("nutrition"+key[0].toUpperCase()+key.slice(1));
if(field)field.value=food[key]??0;
});
showStatus(`Matched: ${food.name} — nutrition values loaded.`,"matched");
if(typeof nutritionPreview==="function")nutritionPreview();
return true;
}
input.addEventListener("input",()=>{
clearTimeout(timer);
timer=setTimeout(applyV903Match,450);
});
input.addEventListener("blur",applyV903Match);
input.addEventListener("change",applyV903Match);
input.addEventListener("keydown",event=>{
if(event.key==="Tab" || event.key==="Enter"){
applyV903Match();
}
});
if(amount){
amount.addEventListener("focus",applyV903Match);
amount.addEventListener("pointerdown",applyV903Match);
}
if(findButton)findButton.addEventListener("click",applyV903Match);
})();
/* Version 10 — smart food search and phone-first nutrition entry */
const V10_OFF_SEARCH_URL="https://world.openfoodfacts.org/cgi/search.pl";
let v10OnlineFoods=[];
function v10Number(value){
const n=Number(value);
return Number.isFinite(n)?n:0;
}
function v10Text(value){return String(value||"").trim()}
function v10Nutrient(product,key){
const n=product?.nutriments||{};
return v10Number(n[`${key}_100g`] ?? n[key]);
}
function v10ProductToFood(product){
const name=v10Text(product.product_name_en||product.product_name||product.generic_name_en||product.generic_name);
if(!name)return null;
const brand=v10Text(product.brands);
return {
name:brand?`${name} — ${brand}`:name,
servingLabel:"100 g",
servingAmount:100,
servingUnit:"g",
calories:Math.round(v10Nutrient(product,"energy-kcal")),
protein:v10Nutrient(product,"proteins"),
carbs:v10Nutrient(product,"carbohydrates"),
sugar:v10Nutrient(product,"sugars"),
fiber:v10Nutrient(product,"fiber"),
fat:v10Nutrient(product,"fat"),
sodium:Math.round(v10Nutrient(product,"sodium")*1000),
image:v10Text(product.image_front_small_url||product.image_small_url),
source:"Open Food Facts"
};
}
function v10FoodHasNutrition(food){
return [food.calories,food.protein,food.carbs,food.fat,food.fiber].some(v=>Number(v)>0);
}
function v10ApplyFood(food){
if(!food)return;
document.getElementById("nutritionFoodName").value=food.name;
document.getElementById("nutritionAmount").value=food.servingAmount||1;
setSelectValue("nutritionUnit",food.servingUnit||"serving");
setNutritionServingBasis(food.servingAmount||1,food.servingUnit||"serving");
nutritionNutrients.forEach(k=>{
const field=document.getElementById("nutrition"+k[0].toUpperCase()+k.slice(1));
if(field)field.value=food[k]??0;
});
const status=document.getElementById("nutritionFoodMatchStatus");
if(status){
status.textContent=`Selected: ${food.name}. Change the amount, then add it.`;
status.classList.add("matched");status.classList.remove("not-matched");
}
nutritionPreview();
document.getElementById("nutritionAmount")?.focus({preventScroll:true});
document.getElementById("nutritionFoodForm")?.scrollIntoView({behavior:"smooth",block:"start"});
}
function v10FoodCard(food,index,online=false){
const image=food.image?``:`🍽️`;
return ``;
}
function v10BindFoodCards(container){
container.querySelectorAll("[data-v10-local]").forEach(btn=>btn.addEventListener("click",()=>v10ApplyFood(NUTRITION_FOODS[Number(btn.dataset.v10Local)])));
container.querySelectorAll("[data-v10-online]").forEach(btn=>btn.addEventListener("click",()=>v10ApplyFood(v10OnlineFoods[Number(btn.dataset.v10Online)])));
}
function v10RenderLocal(query=""){
const wrap=document.getElementById("nutritionFoodResults");if(!wrap)return;
const q=query.trim().toLowerCase();
if(!q){wrap.innerHTML="";return}
const matches=NUTRITION_FOODS.filter(f=>f.name.toLowerCase().includes(q)||(f.aliases||[]).some(a=>a.includes(q))).slice(0,8);
wrap.innerHTML=matches.length?`
Quick local matches
${matches.map(f=>v10FoodCard(f,NUTRITION_FOODS.indexOf(f),false)).join("")}`:"";
v10BindFoodCards(wrap);
}
async function v10SearchOnline(){
const input=document.getElementById("nutritionFoodSearch");
const query=input.value.trim();
const status=document.getElementById("onlineFoodSearchStatus");
const wrap=document.getElementById("onlineFoodResults");
if(query.length<2){status.textContent="Type at least two letters first.";input.focus();return}
status.textContent=`Searching the larger food database for “${query}”…`;
wrap.innerHTML='
`:""}`;
}
let photoViewerWeek=null;
let photoViewerObjectUrl="";
function photoEntriesWithImages(){return photoProgressEntries.filter(e=>e.photoKey).sort((a,b)=>Number(a.week)-Number(b.week));}
function closePhotoViewer(){
const viewer=document.getElementById("photoViewer");
if(!viewer)return;
viewer.hidden=true;viewer.setAttribute("aria-hidden","true");document.body.classList.remove("photo-viewer-open");
if(photoViewerObjectUrl){URL.revokeObjectURL(photoViewerObjectUrl);photoViewerObjectUrl="";}
photoViewerWeek=null;
}
async function openPhotoViewer(week){
const entry=photoProgressEntries.find(e=>Number(e.week)===Number(week));
if(!entry?.photoKey){alert(`No photo is saved for Week ${week}.`);return;}
const viewer=document.getElementById("photoViewer"),wrap=document.getElementById("photoViewerImageWrap"),details=document.getElementById("photoViewerDetails");
if(!viewer||!wrap||!details)return;
try{
const blob=await getProgressPhoto(entry.photoKey);
if(!blob){alert("The saved photo could not be found on this device.");return;}
if(photoViewerObjectUrl)URL.revokeObjectURL(photoViewerObjectUrl);
photoViewerObjectUrl=URL.createObjectURL(blob);photoViewerWeek=Number(entry.week);
document.getElementById("photoViewerTitle").textContent=`Week ${entry.week}`;
wrap.innerHTML=``;
details.innerHTML=`
Date${formatDate(entry.date)}
Weight${entry.weight?esc(entry.weight)+" lb":"—"}
Dose${esc(entry.dose||"—")}
Notes
${esc(entry.notes||"No notes added.")}
`;
const entries=photoEntriesWithImages(),index=entries.findIndex(e=>Number(e.week)===photoViewerWeek);
document.getElementById("previousPhotoBtn").disabled=index<=0;
document.getElementById("nextPhotoBtn").disabled=index<0||index>=entries.length-1;
viewer.hidden=false;viewer.setAttribute("aria-hidden","false");document.body.classList.add("photo-viewer-open");
}catch(err){console.error(err);alert("The photo could not be opened.");}
}
function movePhotoViewer(direction){
const entries=photoEntriesWithImages(),index=entries.findIndex(e=>Number(e.week)===Number(photoViewerWeek));
const next=entries[index+direction];if(next)openPhotoViewer(next.week);
}
function initPhotoViewer(){
document.getElementById("closePhotoViewerBtn")?.addEventListener("click",closePhotoViewer);
document.querySelectorAll("[data-close-photo-viewer]").forEach(el=>el.addEventListener("click",closePhotoViewer));
document.getElementById("previousPhotoBtn")?.addEventListener("click",()=>movePhotoViewer(-1));
document.getElementById("nextPhotoBtn")?.addEventListener("click",()=>movePhotoViewer(1));
document.getElementById("editViewedPhotoBtn")?.addEventListener("click",()=>{const week=photoViewerWeek;closePhotoViewer();if(week!==null)loadPhotoEntry(week);});
document.addEventListener("keydown",e=>{if(document.getElementById("photoViewer")?.hidden!==false)return;if(e.key==="Escape")closePhotoViewer();if(e.key==="ArrowLeft")movePhotoViewer(-1);if(e.key==="ArrowRight")movePhotoViewer(1);});
}
function initPhotoProgress(){
const form=document.getElementById("photoWeekForm");
if(!form)return;
// Use explicit element references. Safari/iPhone PWAs do not reliably create
// global JavaScript variables from element IDs.
const fileInput=document.getElementById("photoFileInput");
const weekInput=document.getElementById("photoWeek");
const dateInput=document.getElementById("photoDate");
const weightInput=document.getElementById("photoWeight");
const doseInput=document.getElementById("photoDose");
const notesInput=document.getElementById("photoNotes");
const copyLabelBtn=document.getElementById("copyPhotoLabelBtn");
const clearFormBtn=document.getElementById("clearPhotoFormBtn");
const compareA=document.getElementById("comparePhotoA");
const compareB=document.getElementById("comparePhotoB");
if(!fileInput||!weekInput||!dateInput||!weightInput||!doseInput||!notesInput){
console.error("Photo form could not initialize because a required field is missing.");
setPhotoUploadStatus("The photo form did not load correctly. Refresh the app.","error");
return;
}
weekInput.value=suggestedPhotoWeek();
dateInput.value=todayString();
const todayDaily=read(KEYS.daily,[]).find(x=>x.date===todayString());
if(todayDaily?.dose)doseInput.value=todayDaily.dose;
const weights=read(KEYS.weights,[]);
if(weights.length)weightInput.value=weights[weights.length-1].weight||"";
const handleChosenPhoto=()=>{
const file=(fileInput.files&&fileInput.files[0])||window.__pendingProgressPhoto;
if(!file){showSelectedPhoto(null);return;}
const imageLike=(file.type&&file.type.startsWith("image/"))||/\.(heic|heif|jpg|jpeg|png|webp)$/i.test(file.name||"");
if(!imageLike){
alert("Please choose a photo.");
fileInput.value="";
showSelectedPhoto(null);
return;
}
if(file.size>35*1024*1024){
alert("That photo is larger than 35 MB. Choose a smaller image.");
fileInput.value="";
showSelectedPhoto(null);
return;
}
showSelectedPhoto(file);
};
// Keep the real file input visible. iOS Safari and installed PWAs can block
// synthetic clicks or labels that target visually hidden file inputs.
// A direct tap on this native control is the most reliable approach.
fileInput.addEventListener("change",handleChosenPhoto);
fileInput.addEventListener("input",handleChosenPhoto);
document.getElementById("removeSelectedPhotoBtn")?.addEventListener("click",()=>{
fileInput.value="";
window.__pendingProgressPhoto=null;
showSelectedPhoto(null);
});
form.addEventListener("submit",async e=>{
e.preventDefault();
const week=Number(weekInput.value);
if(!week||!dateInput.value){alert("Enter a week number and date.");return;}
const existing=photoProgressEntries.find(x=>Number(x.week)===week);
let photoKey=existing?.photoKey||"";
const saveBtn=document.getElementById("savePhotoWeekBtn");
try{
if(saveBtn){saveBtn.disabled=true;saveBtn.textContent="Saving photo…";}
if(!selectedPhotoFile && window.__pendingProgressPhoto) selectedPhotoFile=window.__pendingProgressPhoto;
if(selectedPhotoFile){
setPhotoUploadStatus("Saving photo securely on this device…","loading");
photoKey=`week-${week}`;
await putProgressPhoto(photoKey,selectedPhotoFile);
const testBlob=await getProgressPhoto(photoKey);
if(!testBlob)throw new Error("Photo verification failed");
}
const entry={
week,
date:dateInput.value,
weight:weightInput.value?Number(weightInput.value):null,
dose:doseInput.value,
notes:notesInput.value.trim(),
photoKey,
updatedAt:new Date().toISOString()
};
const i=photoProgressEntries.findIndex(x=>Number(x.week)===week);
if(i>=0)photoProgressEntries[i]=entry;else photoProgressEntries.push(entry);
write(PHOTO_PROGRESS_KEY,photoProgressEntries);
await renderPhotoProgress();
if(photoKey){
setPhotoUploadStatus("Photo saved successfully. It now appears in your weekly gallery.","ready");
await openPhotoViewer(week);
}else{
alert(`Week ${week} saved. Add a photo anytime by editing this week.`);
}
}catch(err){
console.error(err);
setPhotoUploadStatus("The photo could not be saved. Try choosing a JPEG or screenshot copy.","error");
alert("The photo could not be saved. Try again. If it still fails, choose a screenshot or JPEG copy of the photo.");
}finally{
if(saveBtn){saveBtn.disabled=false;saveBtn.textContent="Save week";}
}
});
copyLabelBtn?.addEventListener("click",()=>{
const entry={week:Number(weekInput.value||suggestedPhotoWeek()),date:dateInput.value||todayString(),weight:weightInput.value,dose:doseInput.value,notes:notesInput.value.trim()};
navigator.clipboard.writeText(photoEntryLabel(entry)).then(()=>alert("Photo label copied. Paste it into Markup or your photo editor."));
});
clearFormBtn?.addEventListener("click",()=>{
form.reset();
fileInput.value="";
window.__pendingProgressPhoto=null;
showSelectedPhoto(null);
weekInput.value=suggestedPhotoWeek();
dateInput.value=todayString();
doseInput.value="2.5 mg";
});
compareA?.addEventListener("change",renderPhotoComparison);
compareB?.addEventListener("change",renderPhotoComparison);
renderPhotoProgress();
}
initPhotoViewer();
initPhotoProgress();
/* ===== Version 12.0 — Sprint 1 navigation + dashboard bridge ===== */
(function v12SprintOne(){
function openView(viewId){
return navigateToView(viewId);
}
function refreshSnapshot(){
try{
const week=document.getElementById('journeyWeek')?.textContent||'1';
const dose=document.getElementById('currentDose')?.textContent||'2.5 mg';
const water=document.getElementById('waterValue')?.textContent||'0 / 80 oz';
const [currentWater,waterGoal]=(water.match(/[\d.]+/g)||['0','80']);
const currentWeight=document.getElementById('currentWeight')?.textContent||'—';
const weightChange=document.getElementById('weightChange')?.textContent||'Add a weigh-in';
const set=(id,value)=>{const el=document.getElementById(id);if(el)el.textContent=value;};
set('v12Week',week);set('v12Dose',dose);set('v12CurrentWeight',currentWeight);set('v12WeightChange',weightChange);
set('v12Water',`${currentWater} oz`);set('v12WaterGoal',`of ${waterGoal} oz`);
const meter=document.getElementById('v12WaterMeter');if(meter)meter.style.width=`${Math.min(100,(Number(currentWater)/Math.max(1,Number(waterGoal)))*100)}%`;
}catch(err){console.warn('Version 12 snapshot refresh skipped',err);}
}
window.addEventListener('DOMContentLoaded',()=>{
const more=document.getElementById('moreMenuDialog');
document.getElementById('moreNavBtn')?.addEventListener('click',()=>more?.showModal());
document.getElementById('closeMoreMenuBtn')?.addEventListener('click',()=>more?.close());
document.querySelectorAll('[data-v12-view]').forEach(btn=>btn.addEventListener('click',()=>{more?.close();openView(btn.dataset.v12View);}));
// Version 12.0.1: make the four dashboard shortcuts direct and reliable.
// Each shortcut opens the correct page and then moves the user to the useful control.
const openAndFocus=(viewId,targetId)=>{
more?.close();
openView(viewId);
window.setTimeout(()=>{
const target=document.getElementById(targetId);
if(target){target.scrollIntoView({behavior:'smooth',block:'start'});}
},180);
};
document.getElementById('v12LogFoodBtn')?.addEventListener('click',()=>openAndFocus('mealsView','nutritionLogForm'));
document.getElementById('v12WeighInBtn')?.addEventListener('click',()=>openAndFocus('progressView','weightForm'));
document.getElementById('v12WeeklyPhotoBtn')?.addEventListener('click',()=>openAndFocus('photosView','photoFileInput'));
document.getElementById('v12ExerciseBtn')?.addEventListener('click',()=>openAndFocus('exerciseView','exerciseRows'));
document.getElementById('v12SettingsBtn')?.addEventListener('click',()=>{more?.close();document.getElementById('settingsBtn')?.click();});
refreshSnapshot();setTimeout(refreshSnapshot,400);setInterval(refreshSnapshot,1500);
});
})();
/* Version 12.1.1 — repair initialization chain and add direct injection logging */
(function initDoseInjectionEntry(){
const form=document.getElementById("doseInjectionForm");
if(!form)return;
const dateInput=document.getElementById("doseInjectionDate");
const doseInput=document.getElementById("doseInjectionDose");
const message=document.getElementById("doseInjectionMessage");
dateInput.value=todayString();
const latest=doseFindShots?.()[0];
if(latest?.dose)doseInput.value=latest.dose;
form.addEventListener("submit",event=>{
event.preventDefault();
const date=dateInput.value||todayString();
const entries=read(KEYS.daily,[]);
let entry=entries.find(item=>item.date===date);
if(!entry){entry={date,water:0,protein:0,movement:0,sleep:0,mood:"",energy:"",appetite:"",shotTaken:true,dose:doseInput.value};entries.push(entry)}
else{entry.shotTaken=true;entry.dose=doseInput.value}
entries.sort((x,y)=>new Date(x.date)-new Date(y.date));
write(KEYS.daily,entries);
if(date===todayString()){loadToday();renderToday()}
renderDoseEffectiveness();
if(message)message.textContent=`Injection saved for ${formatDate(date)} at ${doseInput.value}. You can now log effectiveness below.`;
const effectForm=document.getElementById("doseEffectivenessForm");
effectForm?.scrollIntoView({behavior:"smooth",block:"start"});
});
})();
// ============================================================
// Version 13.0 Cloud Foundation Development
// Cloudflare Pages frontend + Cloudflare Worker/D1/R2 backend
// ============================================================
(function(){
const APP_VERSION="13.0.0-dev.11";
const CHANNEL="Development";
const META_KEY="mzjV13FoundationMeta";
const JOURNAL_KEY="mzjV13ChangeJournal";
const ERROR_KEY="mzjV13Errors";
const CONFIG_KEY="mzjV13CloudConfig";
const CLOCK_KEY="mzjV13CloudClock";
const BACKUP_FORMAT="my-zepbound-journey-backup";
const BACKUP_SCHEMA=2;
const knownPrefixes=["mzjV7","mzjV8","mzjV9","mzjV10","mzjV11","mzjV12","mzjV13","zepboundProcess"];
let isRestoring=false,isApplyingCloud=false,syncPromise=null;
function now(){return new Date().toISOString()}
function uuid(){return crypto.randomUUID?crypto.randomUUID():`${Date.now()}-${Math.random().toString(16).slice(2)}`}
function safeParse(value,fallback=null){try{return JSON.parse(value)}catch{return fallback}}
function simpleHash(text){let h=2166136261;for(let i=0;i>>0).toString(16).padStart(8,"0")}
function appKeys(){return Object.keys(localStorage).filter(k=>knownPrefixes.some(p=>k.startsWith(p))&&!([META_KEY,JOURNAL_KEY,ERROR_KEY,CONFIG_KEY,CLOCK_KEY].includes(k))).sort()}
function getConfig(){return safeParse(localStorage.getItem(CONFIG_KEY),{})||{}}
function saveConfig(c){localStorage.setItem(CONFIG_KEY,JSON.stringify(c))}
function getClock(){return safeParse(localStorage.getItem(CLOCK_KEY),{})||{}}
function setClock(key,stamp){const c=getClock();c[key]=stamp;localStorage.setItem(CLOCK_KEY,JSON.stringify(c))}
function getMeta(){
const saved=safeParse(localStorage.getItem(META_KEY),{})||{};
return {schemaVersion:2,installationId:saved.installationId||uuid(),firstFoundationRun:saved.firstFoundationRun||now(),lastStartedAt:now(),lastBackupAt:saved.lastBackupAt||null,lastRestoreAt:saved.lastRestoreAt||null,lastChangeAt:saved.lastChangeAt||null,cloud:{provider:"Cloudflare",status:"not-configured",lastSyncAt:null,lastError:null},...saved,appVersion:APP_VERSION,channel:CHANNEL};
}
function saveMeta(patch={}){const current=getMeta();const meta={...current,...patch,cloud:{...current.cloud,...(patch.cloud||{})}};localStorage.setItem(META_KEY,JSON.stringify(meta));return meta}
saveMeta();
function recordError(error,context="app"){
const list=safeParse(localStorage.getItem(ERROR_KEY),[])||[];
list.unshift({id:uuid(),at:now(),context,message:String(error?.message||error),stack:String(error?.stack||"").slice(0,1500)});
localStorage.setItem(ERROR_KEY,JSON.stringify(list.slice(0,25)));
}
window.addEventListener("error",e=>recordError(e.error||e.message,"window"));
window.addEventListener("unhandledrejection",e=>recordError(e.reason,"promise"));
function queueItem(item){
const journal=safeParse(localStorage.getItem(JOURNAL_KEY),[])||[];
const filtered=journal.filter(x=>!(x.kind===item.kind&&x.key===item.key&&x.status==="pending"));
filtered.push({...item,id:uuid(),updatedAt:now(),deviceId:getMeta().installationId,status:"pending"});
localStorage.setItem(JOURNAL_KEY,JSON.stringify(filtered.slice(-1000)));
saveMeta({lastChangeAt:now()});updateFoundationBadges();scheduleSync();
}
function recordChange(key,value){
if(isRestoring||isApplyingCloud||[META_KEY,JOURNAL_KEY,ERROR_KEY,CONFIG_KEY,CLOCK_KEY].includes(key))return;
const stamp=now();setClock(key,stamp);
queueItem({kind:"record",key,operation:"upsert",value,checksum:simpleHash(JSON.stringify(value))});
}
function recordPhotoChange(key,blob,operation="upsert"){
if(isRestoring||isApplyingCloud)return;
queueItem({kind:"photo",key,operation,contentType:blob?.type||null});
}
function configured(){const c=getConfig();return !!(c.apiUrl&&c.token)}
function apiUrl(path){return `${getConfig().apiUrl.replace(/\/$/,"")}${path}`}
async function api(path,options={}){
const c=getConfig();if(!configured())throw new Error("Cloud sync is not configured.");
const headers=new Headers(options.headers||{});headers.set("Authorization",`Bearer ${c.token}`);
if(options.body&&!(options.body instanceof Blob)&&!(options.body instanceof FormData)&&!headers.has("Content-Type"))headers.set("Content-Type","application/json");
const response=await fetch(apiUrl(path),{...options,headers});
if(!response.ok){let message=`Cloud request failed (${response.status})`;try{message=(await response.json()).error||message}catch{}throw new Error(message)}
if(response.status===204)return null;const type=response.headers.get("content-type")||"";return type.includes("json")?response.json():response.blob();
}
async function testConnection(){const r=await api("/api/health");return r?.ok===true}
async function uploadPending(){
let journal=safeParse(localStorage.getItem(JOURNAL_KEY),[])||[];
for(const item of journal.filter(x=>x.status==="pending")){
if(item.kind==="record"){
const value=safeParse(localStorage.getItem(item.key),null);
await api("/api/records",{method:"POST",body:JSON.stringify({key:item.key,value,updatedAt:item.updatedAt,deviceId:item.deviceId,deleted:item.operation==="delete"})});
}else if(item.kind==="photo"){
if(item.operation==="delete")await api(`/api/photos/${encodeURIComponent(item.key)}`,{method:"DELETE"});
else {const blob=await getProgressPhoto(item.key);if(blob)await api(`/api/photos/${encodeURIComponent(item.key)}`,{method:"PUT",headers:{"Content-Type":blob.type||"application/octet-stream","X-Updated-At":item.updatedAt},body:blob});}
}
journal=(safeParse(localStorage.getItem(JOURNAL_KEY),[])||[]).map(x=>x.id===item.id?{...x,status:"synced",syncedAt:now()}:x);
localStorage.setItem(JOURNAL_KEY,JSON.stringify(journal.slice(-1000)));
}
}
async function pullRecords(){
const result=await api("/api/records");const records=result?.records||[];const clock=getClock();
isApplyingCloud=true;
try{
for(const r of records){const localStamp=clock[r.key]||"";if(!localStamp||r.updated_at>localStamp){if(r.deleted)localStorage.removeItem(r.key);else localStorage.setItem(r.key,JSON.stringify(r.value));setClock(r.key,r.updated_at)}}
}finally{isApplyingCloud=false}
}
async function pullPhotos(){
const result=await api("/api/photos");const photos=result?.photos||[];const meta=getMeta();const photoClock=meta.photoClock||{};
isApplyingCloud=true;
try{for(const p of photos){if(!photoClock[p.key]||p.updated_at>photoClock[p.key]){const blob=await api(`/api/photos/${encodeURIComponent(p.key)}`);await putProgressPhoto(p.key,blob);photoClock[p.key]=p.updated_at}}}
finally{isApplyingCloud=false;saveMeta({photoClock})}
}
async function syncNow({quiet=false}={}){
if(syncPromise)return syncPromise;
syncPromise=(async()=>{
if(!configured()){if(!quiet)openCloudSetup();return false}
if(!navigator.onLine){saveMeta({cloud:{status:"offline"}});updateFoundationBadges();return false}
const btn=document.getElementById("v13SyncBtn");if(btn){btn.disabled=true;btn.textContent="Syncing…"}
saveMeta({cloud:{status:"syncing",lastError:null}});updateFoundationBadges();
try{await uploadPending();await pullRecords();await pullPhotos();saveMeta({cloud:{status:"synced",lastSyncAt:now(),lastError:null}});updateFoundationBadges();if(!quiet){renderAll?.();renderMilestones?.();alert("Laptop and phone data are synchronized.")}return true}
catch(err){recordError(err,"cloud-sync");saveMeta({cloud:{status:"error",lastError:err.message}});updateFoundationBadges();if(!quiet)alert(`Sync could not finish. Your information is still saved on this device.\n\n${err.message}`);return false}
finally{if(btn){btn.disabled=false;btn.textContent="Sync now"}syncPromise=null}
})();return syncPromise;
}
let syncTimer;function scheduleSync(){clearTimeout(syncTimer);if(configured()&&navigator.onLine)syncTimer=setTimeout(()=>syncNow({quiet:true}),1200)}
function openPhotos(){return openPhotoDb()}
async function getAllPhotos(){try{const db=await openPhotos();const result=await new Promise((resolve,reject)=>{const tx=db.transaction(PHOTO_DB_STORE,"readonly"),store=tx.objectStore(PHOTO_DB_STORE),keysReq=store.getAllKeys(),valuesReq=store.getAll();tx.oncomplete=async()=>{try{const out=[];for(let i=0;ireject(tx.error);tx.onabort=()=>reject(tx.error)});db.close();return result}catch(err){recordError(err,"backup-photos");return []}}
function blobToDataURL(blob){return new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=()=>reject(r.error);r.readAsDataURL(blob)})}
function dataURLToBlob(url){const [head,data]=url.split(","),type=(head.match(/data:(.*?);/)||[])[1]||"application/octet-stream",bytes=atob(data),arr=new Uint8Array(bytes.length);for(let i=0;istorage[k]=safeParse(localStorage.getItem(k),localStorage.getItem(k)));const photos=await getAllPhotos();const backup={format:BACKUP_FORMAT,schemaVersion:BACKUP_SCHEMA,createdAt:now(),app:{name:"My Zepbound Journey",version:APP_VERSION,channel:CHANNEL},source:{installationId:getMeta().installationId,userAgent:navigator.userAgent},integrity:{localStorageKeys:Object.keys(storage).length,photoCount:photos.length},storage,photos};backup.integrity.checksum=simpleHash(JSON.stringify({storage,photos:photos.map(p=>({key:p.key,type:p.type,size:p.data.length}))}));const blob=new Blob([JSON.stringify(backup,null,2)],{type:"application/json"}),url=URL.createObjectURL(blob),a=document.createElement("a");a.href=url;a.download=`My_Zepbound_Journey_Backup_${todayString()}.json`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);saveMeta({lastBackupAt:backup.createdAt});updateFoundationBadges();alert(`Backup created successfully.\n\n${Object.keys(storage).length} data sections\n${photos.length} saved photos`)}catch(err){recordError(err,"backup");alert("The backup could not be created. Open Diagnostics for details.")}finally{if(button){button.disabled=false;button.textContent="Create complete backup"}}}
function normalizeBackup(raw,file){
if(raw&&raw.format===BACKUP_FORMAT)return raw;
if(raw&&typeof raw==="object"&&!Array.isArray(raw)){
const allowed=Object.keys(raw).filter(k=>knownPrefixes.some(p=>k.startsWith(p)));
if(allowed.length){
const storage={};for(const k of allowed)storage[k]=safeParse(raw[k],raw[k]);
return {format:BACKUP_FORMAT,schemaVersion:2,createdAt:file?.lastModified?new Date(file.lastModified).toISOString():now(),app:{name:"My Zepbound Journey",version:"12.2 recovered backup",channel:"Legacy import"},source:{description:"Converted from Version 12.2 browser-storage backup"},integrity:{localStorageKeys:allowed.length,photoCount:0},storage,photos:[]};
}
}
return raw;
}
function validateBackup(b){const errors=[];if(!b||b.format!==BACKUP_FORMAT)errors.push("This is not a recognized My Zepbound Journey backup.");if(![1,2].includes(b?.schemaVersion))errors.push("The backup format version is not supported.");if(!b?.storage||typeof b.storage!=="object")errors.push("The backup has no data section.");if(!Array.isArray(b?.photos))errors.push("The photo section is invalid.");return errors}
async function restoreBackup(file){if(!file)return;try{const raw=JSON.parse(await file.text()),backup=normalizeBackup(raw,file),errors=validateBackup(backup);if(errors.length)throw new Error(errors.join("\n"));if(!confirm(`Restore this backup?\n\nCreated: ${new Date(backup.createdAt).toLocaleString()}\nData sections: ${Object.keys(backup.storage).length}\nPhotos: ${backup.photos.length}\n\nA safety backup will be downloaded first.`))return;await createBackup();isRestoring=true;appKeys().forEach(k=>localStorage.removeItem(k));Object.entries(backup.storage).forEach(([k,v])=>localStorage.setItem(k,typeof v==="string"?v:JSON.stringify(v)));const db=await openPhotos();await new Promise((resolve,reject)=>{const tx=db.transaction(PHOTO_DB_STORE,"readwrite");tx.objectStore(PHOTO_DB_STORE).clear();tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)});db.close();for(const photo of backup.photos)await putProgressPhoto(photo.key,dataURLToBlob(photo.data));isRestoring=false;saveMeta({lastRestoreAt:now(),restoredFrom:backup.createdAt});for(const k of Object.keys(backup.storage))recordChange(k,backup.storage[k]);alert("Restore completed. The app will reload.");location.reload()}catch(err){isRestoring=false;recordError(err,"restore");alert(`Restore stopped safely.\n\n${err.message}`)}}
function bytesUsed(){let n=0;for(const k of Object.keys(localStorage))n+=k.length+(localStorage.getItem(k)||"").length;return n*2}
function humanBytes(n){if(n<1024)return `${n} B`;if(n<1048576)return `${(n/1024).toFixed(1)} KB`;return `${(n/1048576).toFixed(1)} MB`}
async function diagnosticSnapshot(){const meta=getMeta(),journal=safeParse(localStorage.getItem(JOURNAL_KEY),[])||[],errors=safeParse(localStorage.getItem(ERROR_KEY),[])||[],photos=await getAllPhotos();return {version:APP_VERSION,channel:CHANNEL,online:navigator.onLine,configured:configured(),cloudStatus:meta.cloud?.status,lastSync:meta.cloud?.lastSyncAt,lastError:meta.cloud?.lastError,pendingChanges:journal.filter(x=>x.status==="pending").length,dataSections:appKeys().length,photos:photos.length,localStorage:humanBytes(bytesUsed()),serviceWorker:!!navigator.serviceWorker?.controller,errors}}
async function renderDiagnostics(){const d=await diagnosticSnapshot(),grid=document.getElementById("v13DiagGrid"),errors=document.getElementById("v13ErrorList");if(!grid)return;const rows=[["App version",d.version],["Release channel",d.channel],["Connection",d.online?"Online":"Offline"],["Cloud configured",d.configured?"Yes":"No"],["Cloud status",d.cloudStatus||"Not configured"],["Pending changes",d.pendingChanges],["Data sections",d.dataSections],["Saved photos",d.photos],["Local storage",d.localStorage],["Last synchronization",d.lastSync?new Date(d.lastSync).toLocaleString():"Never"],["Offline cache",d.serviceWorker?"Active":"Waiting"]];grid.innerHTML=rows.map(([a,b])=>`${a}${esc(b)}`).join("");errors.innerHTML=d.errors.length?d.errors.slice(0,5).map(e=>`