Open the week-data .json exported from Takt (Week & PT → “Week data (JSON)”).
| Meal · item (protected) | Status | kcal | P g | C g | F g |
|---|
No medications yet — add them in Standard day → Medication plan, and each dose will appear here to tick off.
"); plannedDoses().forEach(({med,t})=>{ const key=medKey(med.id,t); const rec=day.meds[key]||{}; const row=document.createElement("div"); row.className="row"; row.style.padding="5px 0"; let badge=""; if(rec.status==="taken"){ const late=rec.t&&(tmin(rec.t)-tmin(t))>30; badge='taken '+(rec.t||"")+''+(late?' late':""); } else if(rec.status==="skipped") badge='skipped'; else if(rec.status==="planned"&&rec.t&&rec.t!==t) badge='planned '+rec.t+""; row.innerHTML=''+t+''+esc(med.name)+ ' '+esc(med.dose)+" "+badge+""; const state=rec.status||"planned"; const sel=document.createElement("select"); sel.className="medsel"; sel.setAttribute("aria-label",med.name+" "+t+" status"); sel.innerHTML=''; sel.value=state; sel.onchange=()=>{ // Default a fresh "taken" to the dose's own scheduled time (t), not the current clock // time — the user: "if I say taken, use the planned time." The time input right below still // lets them override to when they actually took it, if that differed. if(sel.value==="taken") day.meds[key]={status:"taken",t}; else if(sel.value==="skipped") day.meds[key]={status:"skipped"}; else delete day.meds[key]; // planned at the scheduled time save(); render(); }; row.appendChild(sel); if(state!=="skipped"){ const ti=document.createElement("input"); ti.type="time"; ti.value=rec.t||t; ti.style.width="120px"; ti.title=state==="taken"?"actual time taken":"planned time for this dose today"; ti.onchange=()=>{ if(state==="taken") day.meds[key]={status:"taken",t:ti.value}; else if(ti.value===t) delete day.meds[key]; else day.meds[key]={status:"planned",t:ti.value}; save(); render(); }; row.appendChild(ti); } mc.appendChild(row); }); const allDoses=plannedDoses(); if(allDoses.length && allDoses.some(({med,t})=>{ const r=day.meds[medKey(med.id,t)]; return !r||r.status!=="taken"; })){ const allMedB=document.createElement("button"); allMedB.className="btn small"; allMedB.style.marginTop="10px"; allMedB.textContent="Mark all taken as planned"; allMedB.title="Mark every not-yet-taken dose today as taken, at its own scheduled time"; allMedB.onclick=()=>{ allDoses.forEach(({med,t})=>{ const key=medKey(med.id,t); const r=day.meds[key]; if(!r||r.status!=="taken") day.meds[key]={status:"taken",t}; }); save(); render(); }; mc.appendChild(allMedB); } // Reset every dose's time back to its scheduled (default) time. Only offered when at least one // dose actually has a changed time. Planned doses simply drop their override; doses already marked // Taken keep their status but have their recorded time replaced, so that case asks first. const custom=plannedDoses().filter(({med,t})=>{ const r=day.meds[medKey(med.id,t)]; return r&&r.status!=="skipped"&&r.t&&r.t!==t; }); if(custom.length){ const rb=document.createElement("button"); rb.className="btn ghost small"; rb.style.margin="8px 0"; rb.textContent="↺ Set times to default ("+custom.length+" changed)"; rb.title="Put every medication time back to its scheduled time"; rb.onclick=()=>{ const nTaken=custom.filter(({med,t})=>day.meds[medKey(med.id,t)].status==="taken").length; if(nTaken && !confirm(nTaken+" dose(s) marked Taken have a different actual time recorded. Replace with the scheduled time?")) return; custom.forEach(({med,t})=>{ const k=medKey(med.id,t), r=day.meds[k]; if(r.status==="taken") day.meds[k]={status:"taken",t}; else delete day.meds[k]; }); save(); render(); }; mc.appendChild(rb); } mc.appendChild(noteInput("Medication note (side effects, off periods, changes)…",day.medNote,v=>day.medNote=v)); m.appendChild(mc); // meals const mealsHead=document.createElement("h2"); mealsHead.textContent="Nutrition"; mealsHead.style.margin="18px 0 4px"; m.appendChild(mealsHead); // Day-level version of each meal card's own "Same as standard" button: one tap logs every meal // slot (breakfast, mid-morning, lunch, afternoon snack, dinner, end of day…) that has a standard // as eaten, in one go — for the common day that went exactly to plan across the board, rather // than tapping "Same as standard"/flipping Planned→Actual once per meal. Today's meals are // usually already auto-filled with the standard items but left as "Planned" (not yet counted), // so most days this only needs to flip their status, not overwrite anything. const slotsWithStd=MEAL_SLOTS.filter(sl=>(S.template.meals[sl.id]||[]).length); const mealSameAsStd=sl=>{ const cur=(day.meals[sl.id]&&day.meals[sl.id].items)||[]; const std=S.template.meals[sl.id]||[]; if(cur.length!==std.length) return false; return cur.every((it,i)=>{ const s=std[i]; return it.n===s.n && it.g===s.g && Math.abs((it.kcal||0)-(s.kcal||0))<0.05 && Math.abs((it.p||0)-(s.p||0))<0.05 && Math.abs((it.c||0)-(s.c||0))<0.05 && Math.abs((it.f||0)-(s.f||0))<0.05; }); }; const notYetCounted=slotsWithStd.filter(sl=>{ const meal=day.meals[sl.id]; return !(meal&&(meal.st==="act"||meal.st==="upd")); }); if(notYetCounted.length){ const allB=document.createElement("button"); allB.className="btn small"; allB.style.marginBottom="8px"; allB.textContent="Mark all meals as standard"; allB.title="Log every meal with a standard set up as eaten today — one tap"; allB.onclick=()=>{ // Only meals that already have entries genuinely differing from the standard need a // confirmation before being overwritten — a meal that's empty, or already carries exactly // the standard items (just still marked Planned, as a fresh day usually is), needs none. const differing=notYetCounted.filter(sl=>{ const meal=day.meals[sl.id]; return meal && meal.items && meal.items.length && !mealSameAsStd(sl); }); let targets=notYetCounted; if(differing.length){ const names=differing.map(sl=>sl.label.toLowerCase()).join(", "); if(!confirm(differing.length+" meal(s) — "+names+" — have entries that differ from your standard. Replace them with your standard meals too?")){ targets=notYetCounted.filter(sl=>!differing.includes(sl)); } } targets.forEach(sl=>{ const meal=day.meals[sl.id]||(day.meals[sl.id]={items:[]}); meal.items=JSON.parse(JSON.stringify(S.template.meals[sl.id])); meal.st="act"; delete meal.stAutoFromCorr; }); save(); render(); }; m.appendChild(allB); } MEAL_SLOTS.forEach(sl=>{ const meal=day.meals[sl.id]||(day.meals[sl.id]={items:[]}); const t=mealTotals(meal); const macCounted=meal.st==="act"||meal.st==="upd"; const corrItems=(meal.items||[]).filter(x=>x.s==="pt-corr"); const pureItems=(meal.items||[]).filter(x=>x.s!=="pt-corr"); const preCorrT=corrItems.length?mealTotals({items:pureItems}):null; const c=document.createElement("div"); c.className="card"; c.innerHTML='| Time | What | Why |
|---|---|---|
| "+esc(p.time)+" | "+esc(p.what)+" | "+esc(p.why)+" |
No targets set yet — enter them on the Standard day tab.
'); } else if(!st.nDays){ tg.insertAdjacentHTML("beforeend",'No food logged this week yet — averages will appear here.
'); } else { const cg=document.createElement("div"); cg.className="totgrid"; [["kcal","Calories","",r0],["p","Protein","g",r1],["c","Carbohydrates","g",r1],["f","Fat","g",r1]].forEach(([k,lbl,unit,rf])=>{ const avg=st.avg[k], t3=T2[k]; const box=document.createElement("div"); box.className="totbox"; const pct=t3?Math.round(avg/t3*100):null; const delta=t3?rf(avg-t3):null; box.innerHTML=''+lbl+(unit?" ("+unit+")":"")+""+ 'Daily averages over '+st.nDays+' logged day'+(st.nDays===1?"":"s")+' vs the agreed targets.
'); } m.appendChild(tg); // comments const cc=document.createElement("div"); cc.className="card grid2"; const mkTa=(label,val,cb)=>{ const d=document.createElement("div"); d.innerHTML="1. Send your PT the review file (or the week-data JSON plus the separate takt-pt-review.html). 2. They adjust macros, set targets, comment, save a PDF, and download a return file. 3. Import that file below — it stores their verified figures, targets, comments and review stamp.
"; const rtRow=document.createElement("div"); rtRow.className="row"; const exH=document.createElement("button"); exH.className="btn"; exH.textContent="PT review file (HTML)"; // Stamp the week as "sent to PT" BEFORE building the file, so the file the PT receives already // shows this week as sent; undone if the download didn't actually happen. const stampSent=()=>{ const w=W(curWeek), prev={a:w.sentToPtAt||null,b:!!w.sentBackfilled}; w.sentToPtAt=todayKey(); w.sentBackfilled=false; return ()=>{ w.sentToPtAt=prev.a; w.sentBackfilled=prev.b; }; }; exH.onclick=async()=>{ const undo=stampSent(); const data=reviewerHTML(buildWeekExport(curWeek)); const ptName=fileOwnerPrefix()+"PT review ("+weekLabelForFilename(curWeek)+") ("+fileDateStr(todayKey())+") (v"+nextFileVersion("ptReview")+").html"; let sentOk=false; try{ sentOk=!!(await fileSave(ptName,data)); } catch(e){ try{ sentOk=!!(await fileSave("takt-week-"+curWeek+".json",JSON.stringify(buildWeekExport(curWeek),null,1))); alert("HTML downloads aren't enabled here, so I saved the week DATA file instead. Send it with the takt-pt-review.html file (from the chat) — your PT opens that file and loads this JSON into it."); } catch(e2){} } if(sentOk){ save(); render(); } else undo(); }; const exJ=document.createElement("button"); exJ.className="btn ghost"; exJ.textContent="Week data (JSON)"; exJ.onclick=async()=>{ const undo=stampSent(); let ok=false; try{ ok=!!(await fileSave("takt-week-"+curWeek+".json",JSON.stringify(buildWeekExport(curWeek),null,1))); }catch(e){ alert("Download failed — try again, or use the PT review file (HTML) button instead."); } if(ok){ save(); render(); } else undo(); }; rtRow.append(exH,exJ); rt.appendChild(rtRow); const impD=document.createElement("details"); impD.innerHTML='"+esc(fmtDay(from))+" – "+esc(fmtDay(today))+" · tap a chart for values
"); const pick=(fn,ks)=>(ks||keys).map(k=>{const d=S.days[k];const v=d?fn(d):null;return {x:k,y:(v==null||isNaN(v))?null:v};}); const sec=t=>m.insertAdjacentHTML("beforeend","No macro targets set yet — enter them on the Standard day tab.
'); // ---- Biometrics ---- sec("Biometrics"); const bpMean=(d,key)=>{ const rs=bpReadings(d); return rs.length?rs.reduce((a,r)=>a+r[key],0)/rs.length:null; }; // the 7-day average needs the 6 days before the range too, or its first points average too little const wtLead=pick(d=>d.bio.wt,keysFrom(addDays(from,-6))), wt=wtLead.slice(6), wtAvg=smooth(wtLead,7).slice(6); const sys=pick(d=>bpMean(d,"sys")), dia=pick(d=>bpMean(d,"dia")); const bpSum=(()=>{ const ix=sys.map((p,i)=>i).filter(i=>sys[i].y!=null&&dia[i].y!=null); if(!ix.length) return ""; const z=ix[ix.length-1], av=a=>Math.round(ix.reduce((t,i)=>t+a[i].y,0)/ix.length); return "Latest "+Math.round(sys[z].y)+"/"+Math.round(dia[z].y)+" ("+fmtDay(keys[z])+") · avg "+av(sys)+"/"+av(dia)+" over "+ix.length+" day"+(ix.length===1?"":"s"); })(); const hr=pick(d=>d.bio.hr), steps=pick(d=>d.train.steps), sleep=pick(d=>d.bio.sleepH), bf=pick(d=>d.bio.bf), spo2=pick(d=>d.bio.spo2); const bio=[ ["Weight (kg)","line",[{name:"Weight",cvar:"--s1",pts:wt},{name:"7-day avg",cvar:"--s2",pts:wtAvg,dash:true}],"kg",{key:"wt",summary:trendLatest(wt,"kg",1)}], ["Blood pressure (mmHg)","line",[{name:"Systolic",cvar:"--s1",pts:sys},{name:"Diastolic",cvar:"--s2",pts:dia}],"mmHg",{key:"bp",summary:bpSum}], ["Resting heart rate (bpm)","line",[{name:"Resting HR",cvar:"--s1",pts:hr}],"bpm",{key:"hr",summary:trendLatest(hr,"bpm",0)}], ["Steps per day","bar",[{name:"Steps",cvar:"--s1",pts:steps}],"steps",{key:"steps",summary:trendAvg(steps,"steps/day",0)}], ["Sleep (hours)","bar",[{name:"Sleep",cvar:"--s1",pts:sleep}],"h",{key:"sleep",summary:trendAvg(sleep,"h/night",1)}], ["Body fat (%)","line",[{name:"Body fat",cvar:"--s1",pts:bf}],"%",{key:"bf",summary:trendLatest(bf,"%",1)}], ["SpO₂ (%)","line",[{name:"SpO₂",cvar:"--s1",pts:spo2}],"%",{key:"spo2",summary:trendLatest(spo2,"%",0)}] ]; const missing=[]; bio.forEach(([t,kind,ser,unit,o])=>{ if(ser[0].pts.some(p=>p.y!=null)) m.appendChild(chartCard(t,kind,ser,unit,o)); else missing.push(t.replace(/ \(.*\)$/,"")); }); if(missing.length) m.insertAdjacentHTML("beforeend","Not logged in this range: "+esc(missing.join(", "))+".
"); // ---- Training volume per week ---- sec("Training volume per week"); const lastWk=isoWeekKey(today); let firstWk=isoWeekKey(from); if(weekDates(firstWk)[0]>weekDates(weekShift(lastWk,-3))[0]) firstWk=weekShift(lastWk,-3); const tw=trainingByWeek(firstWk,lastWk); const wkPts=f=>tw.map(w=>({x:w.week,y:f(w)})); const done=tw.slice(0,-1).filter(w=>w.mins!=null), cur=tw[tw.length-1]; let tSum=""; if(done.length){ tSum="Avg "+fmtNum(done.reduce((t,w)=>t+w.mins,0)/done.length,0)+" min/week · "+ fmtNum(done.reduce((t,w)=>t+w.sessions,0)/done.length,1)+(cur.planned?" of "+cur.planned+" planned":"")+" sessions"; } if(cur.mins!=null) tSum+=(tSum?" · ":"")+"this week so far "+cur.mins+" min, "+cur.sessions+" session"+(cur.sessions===1?"":"s"); const wkTip=wk=>{ const ds=weekDates(wk); return "Wk "+wk.slice(-2)+" ("+fmtDay(ds[0])+" – "+fmtDay(ds[6])+")"; }; m.appendChild(chartCard("Training minutes per week","bar",[ {name:"Minutes",cvar:"--s1",pts:wkPts(w=>w.mins)}, {name:"Sessions",unit:"",pts:wkPts(w=>w.sessions),hide:true}, {name:"Planned",unit:"",pts:wkPts(w=>w.mins==null?null:w.planned),hide:true} ],"min",{key:"train",summary:tSum,xLabel:wk=>"Wk "+wk.slice(-2),tipLabel:wkTip})); } function smooth(pts,n){ // a value for every day with a reading somewhere in its window, so weigh-ins every other day // still draw a continuous average line return pts.map((p,i)=>{ const win=pts.slice(Math.max(0,i-n+1),i+1).map(q=>q.y).filter(v=>v!=null); return {x:p.x,y:win.length?win.reduce((a,b)=>a+b,0)/win.length:null}; }); } // series: [{name,cvar,pts:[{x,y}],dash?,step?,hide?,unit?}] — series[0] defines the x axis; on a bar // chart only series[0] is bars and the rest are overlay lines. `hide` series appear only in the // tooltip and table view. opts: {key, summary, xLabel(x), tipLabel(x)}. function chartCard(title,kind,series,unit,opts){ opts=opts||{}; const card=document.createElement("div"); card.className="card chart"; if(opts.key) card.dataset.k=opts.key; const drawn=series.filter(s=>!s.hide); const has=drawn.some(s=>s.pts.some(p=>p.y!=null)); card.innerHTML='| "+(opts.tipLabel?"Week":"Date")+" | "+series.map(s=>""+esc(s.name)+" | ").join("")+"
|---|---|
| "+esc(lab(r.x))+" | "+r.vals.map(v=>""+(v==null?"–":fmtNum(r1(v)))+" | ").join("")+"
You signed in with a temporary password. Choose your own before you continue — at least "+PW_MIN+" characters. Nothing else opens until you have.
"; const row=document.createElement("div"); row.className="row"; const a=document.createElement("input"); a.type="password"; a.id="pwNew"; a.placeholder="New password"; a.autocomplete="new-password"; a.className="grow"; const b=document.createElement("input"); b.type="password"; b.id="pwAgain"; b.placeholder="New password again"; b.autocomplete="new-password"; b.className="grow"; const go=document.createElement("button"); go.className="btn small"; go.id="pwSave"; go.textContent="Save password"; const msg=document.createElement("p"); msg.className="hint"; msg.id="pwMsg"; msg.style.color="var(--crit)"; go.onclick=async()=>{ msg.textContent=""; if(a.value.lengthSigned in as "+esc((a&&a.name)||(sbSession()||{}).email||"")+" — a coach account. Your clients' logs open here.
"; who.appendChild(acctSignOutRow()); m.appendChild(who); renderCoachClients(m); // v55 if(coachState.links==null&&!coachState.busy){ coachLoadLinks().then(render); } const inv=document.createElement("div"); inv.className="card"; inv.id="coachInvites"; inv.innerHTML="No clients or invites yet.
"); links.forEach(l=>{ const row=document.createElement("div"); row.className="row"; row.dataset.link=l.link_id; row.dataset.status=l.status; const t=document.createElement("span"); t.className="grow"; if(l.status==="active") t.innerHTML=""+esc(l.other_name)+" · since "+esc(coachDate(l.started_at))+" · shares "+esc(coachShares(l))+""; else if(l.status==="invited") t.innerHTML="Invite "+esc(l.invite_code||"")+" · unused · valid until "+esc(coachDate(l.invite_expires_at))+""; else if(l.status==="expired") t.innerHTML="Invite expired "+esc(coachDate(l.invite_expires_at))+""; else t.innerHTML=""+(l.started_at?esc(l.other_name)+" · ended "+esc(coachDate(l.ended_at))+(l.ended_by_me?" by you":" by the client"):"Invite cancelled "+esc(coachDate(l.ended_at)))+""; row.appendChild(t); if(l.status==="active"||l.status==="invited"){ const b=document.createElement("button"); b.className="btn ghost small"; b.textContent=l.status==="active"?"End link":"Cancel invite"; b.onclick=()=>coachEnd(l,l.status==="active"?"End your link with "+l.other_name+"? You will no longer see their log. They can link again with a new code.":"Cancel this invite code?"); row.appendChild(b); } list.appendChild(row); }); m.appendChild(list); } /* ---- The client's side: the Coach card on the Data tab ---- */ function coachLinkCard(){ const card=document.createElement("div"); card.className="card"; card.id="coachCard"; card.innerHTML="Not linked to a coach.
"); const f=document.createElement("div"); f.id="coachJoin"; f.style.marginTop="10px"; f.innerHTML=""+(cur?"Linking to a different coach ends the link with "+esc(cur.other_name)+".":"Your coach gives you a code.")+" They always see your food, weight and daily training totals; tick what else to share.
"+ ""+ ""+esc(String(t.by||"Your coach"))+" changed your daily targets: "+esc(bits.join(" · "))+"."+(t.reason?" “"+esc(String(t.reason))+"”":"")+"
"; const b=document.createElement("button"); b.className="btn small"; b.id="tgSeen"; b.textContent="Got it"; b.onclick=async()=>{ b.disabled=true; try{ await sbRpc("mark_targets_seen"); }catch(e){ b.disabled=false; alert("Couldn't reach the cloud — try again when you're online."); return; } S.targetsMeta.seen=true; save(); render(); }; c.appendChild(b); return c; } // The Standard day's target editor while a coach owns the targets: the coach's name, read-only. function tgLockedBy(){ const l=coachLinkedNow(); return l&&sbSignedIn()&&acctLogSyncOk()?l.other_name:null; } /* ================= v55: the coach screen — clients, their week, targets and food ================= */ const coachWeekStart=k=>weekDates(isoWeekKey(k))[0]; async function coachLoadClients(){ coachView.loading=true; try{ coachView.clients=(await sbRpc("coach_clients_overview"))||[]; coachView.err=""; } catch(e){ coachView.err=e.code==="offline"?"Offline — clients can't be loaded.":"Couldn't load clients: "+(e.message||e.code); } coachView.loading=false; } async function coachLoadClient(){ const c=coachView.sel; if(!c) return; coachView.loading=true; coachView.msg=""; try{ coachView.summary=((await sbRpc("weekly_summary",{p_client:c.client_id, p_week_start:coachView.week}))||[])[0]||null; coachView.target=((await sbRpc("macro_target_on",{p_client:c.client_id, p_day:todayKey()}))||[])[0]||null; coachView.log=(await sbRpc("coach_day_log",{p_client:c.client_id, p_from:coachView.day, p_to:coachView.day}))||[]; }catch(e){ coachView.msg="Couldn't load: "+(e.message||e.code); } coachView.loading=false; } function renderCoachClients(m){ if(coachView.clients==null&&!coachView.loading){ coachLoadClients().then(render); } const card=document.createElement("div"); card.className="card"; card.id="coachClients"; card.innerHTML="No clients yet — create an invite code below.
"); list.forEach(c=>{ const row=document.createElement("div"); row.className="row coachClient"; row.dataset.client=c.client_id; const flags=[c.no_log_3_days?"no log for 3 days":null, c.under_target_days>=3?"under target "+c.under_target_days+" of the last 7 days":null, c.unseen_target?"target change not yet seen":null].filter(Boolean); const b=document.createElement("button"); b.className="btn ghost small"+(coachView.sel&&coachView.sel.client_id===c.client_id?" on":""); b.textContent=c.client_name; b.onclick=()=>{ coachView.sel=c; coachView.week=coachWeekStart(todayKey()); coachView.day=todayKey(); coachView.summary=null; coachView.log=null; coachView.target=null; render(); coachLoadClient().then(render); }; const f=document.createElement("span"); f.className="grow hint coachFlags"; f.innerHTML=flags.length?flags.map(x=>''+esc(x)+"").join(" "):"no flags"+(c.last_logged?" · last logged "+esc(fmtNice(String(c.last_logged).slice(0,10))):""); row.append(b,f); card.appendChild(row); }); m.appendChild(card); if(coachView.sel) renderCoachClient(m); } function renderCoachClient(m){ const c=coachView.sel; const head=document.createElement("div"); head.className="card"; head.id="coachClientHead"; head.innerHTML=""+esc(coachView.msg)+"
":"")+(coachView.loading?"Loading…
":""); m.appendChild(head); // Weekly summary, Sunday to Saturday const ws=document.createElement("div"); ws.className="card"; ws.id="coachWeek"; const wd=weekDates(isoWeekKey(coachView.week)); ws.innerHTML="| Days logged | "+S2.days_logged+" of "+S2.days_counted+" |
| Average kcal | "+nz(S2.avg_kcal)+(S2.avg_target_kcal!=null?" target "+nz(S2.avg_target_kcal)+"":"")+" |
| Average protein | "+nz(S2.avg_protein_g," g")+(S2.avg_target_protein_g!=null?" target "+nz(S2.avg_target_protein_g," g")+"":"")+" |
| Average carbs · fat | "+nz(S2.avg_carbs_g," g")+" · "+nz(S2.avg_fat_g," g")+" |
| Training | "+nz(S2.training_sessions)+" sessions · "+nz(S2.training_minutes," min")+" · volume "+nz(S2.training_volume)+" |
| Weight | "+nz(S2.weight_last," kg")+(S2.weight_change!=null?" ("+(+S2.weight_change>0?"+":"")+esc(String(+S2.weight_change))+" kg vs last week)":"")+" |
"+ (T?"Now: "+nz(T.kcal," kcal")+" · protein "+nz(T.protein_g," g")+" · carbs "+nz(T.carbs_g," g")+" · fat "+nz(T.fat_g," g")+" — set by "+esc(T.set_by)+(T.by_coach&&T.seen_at==null?" · not yet seen":""):"No target set yet.")+"
"; const fr=document.createElement("div"); fr.className="row"; const mk=(id,ph,v)=>{ const i=document.createElement("input"); i.type="number"; i.id=id; i.placeholder=ph; i.title=ph; i.style.width="90px"; if(v!=null) i.value=+v; return i; }; const tk=mk("ctKcal","kcal",T&&T.kcal), tp=mk("ctP","protein g",T&&T.protein_g), tcb=mk("ctC","carbs g",T&&T.carbs_g), tf=mk("ctF","fat g",T&&T.fat_g); const from=document.createElement("input"); from.type="date"; from.id="ctFrom"; from.value=todayKey(); from.min=todayKey(); from.title="takes effect from"; const why=document.createElement("input"); why.id="ctWhy"; why.className="grow"; why.placeholder="Reason (the client sees it)"; why.maxLength=500; const go=document.createElement("button"); go.className="btn small"; go.id="ctSave"; go.textContent="Set targets"; const msg=document.createElement("p"); msg.className="hint"; msg.id="ctMsg"; msg.style.color="var(--crit)"; go.onclick=async()=>{ const v=k=>k.value===""?null:+k.value, t={kcal:v(tk),p:v(tp),c:v(tcb),f:v(tf)}; if(!why.value.trim()){ msg.textContent="Give a reason — the client sees it."; return; } if(!tgValid(t)){ msg.textContent="Use kcal 800–6000, protein 0–400 g, carbs 0–1000 g, fat 0–400 g."; return; } go.disabled=true; const r=await sbFetch("/rest/v1/macro_targets",{method:"POST",headers:{"Prefer":"return=minimal"},body:JSON.stringify({client_id:c.client_id, link_id:c.link_id, kcal:t.kcal, protein_g:t.p, carbs_g:t.c, fat_g:t.f, effective_from:from.value||todayKey(), reason:why.value.trim()})}).catch(e=>({ok:false, json:async()=>({message:e.message})})); if(!r.ok){ const j=await r.json().catch(()=>({})); go.disabled=false; msg.textContent=j.message||"Couldn't set the targets."; return; } await coachLoadClient(); await coachLoadClients(); render(); }; fr.append(tk,tp,tcb,tf,from,why,go); tc.append(fr,msg); m.appendChild(tc); // The daily log: read-only except the four macro figures of each food item const lc=document.createElement("div"); lc.className="card"; lc.id="coachLog"; lc.innerHTML="Nothing logged (or before the date the client shares from).
"); L.filter(r=>r.kind==="food").forEach(r=>{ const row=document.createElement("div"); row.className="row coachFood"; row.dataset.entry=r.entry_id; row.innerHTML=""+esc(r.meal)+" "+esc(r.food)+(r.amount!=null?" "+esc(String(+r.amount))+" g":"")+ (r.corrected_by_coach?" corrected":"")+""; if(r.correctable){ const ins=[["kcal","kcal"],["protein_g","P g"],["carbs_g","C g"],["fat_g","F g"]].map(([k,ph])=>{ const i=document.createElement("input"); i.type="number"; i.step="0.1"; i.dataset.k=k; i.placeholder=ph; i.title=ph; i.style.width="72px"; i.value=+r[k]; return i; }); const sv=document.createElement("button"); sv.className="btn small"; sv.textContent="Save"; sv.onclick=async()=>{ const v=Object.fromEntries(ins.map(i=>[i.dataset.k, i.value===""?NaN:+i.value])); if(Object.values(v).some(x=>!Number.isFinite(x)||x<0)||v.kcal>20000||v.protein_g>2000||v.carbs_g>2000||v.fat_g>2000){ alert("kcal 0–20000, protein, carbs and fat 0–2000 g."); return; } sv.disabled=true; try{ await sbRpc("coach_correct_food",{p_entry:r.entry_id, p_kcal:v.kcal, p_protein_g:v.protein_g, p_carbs_g:v.carbs_g, p_fat_g:v.fat_g}); } catch(e){ sv.disabled=false; alert("Not saved: "+(e.message||e.code)); return; } await coachLoadClient(); await coachLoadClients(); render(); }; row.append(...ins,sv); } else row.insertAdjacentHTML("beforeend",""+r0(+r.kcal)+" kcal · P "+r1(+r.protein_g)+" · C "+r1(+r.carbs_g)+" · F "+r1(+r.fat_g)+""); lc.appendChild(row); }); const rd=L.filter(r=>r.kind==="reading"), ds=L.filter(r=>r.kind==="dose"); if(rd.length) lc.insertAdjacentHTML("beforeend",""+rd.map(r=>esc(r.metric.replace(/_/g," "))+" "+esc(String(+r.value))+" "+esc(r.unit)).join(" · ")+"
"); if(ds.length) lc.insertAdjacentHTML("beforeend",""+ds.map(r=>esc(r.medication)+(r.dose?" "+esc(r.dose):"")+" — "+esc(r.status)).join(" · ")+"
"); m.appendChild(lc); } /* ---- main2.js ---- */ /* ================= GUIDE ================= */ // Personal guide sections come from the user's own data (S.profile.guide — imported from their // personal-settings file, or a restored backup), so they are untrusted HTML here: rebuilt element by // element from an allowlist — no scripts, styles, event handlers or non-https links survive. const GUIDE_TAGS=new Set(["P","B","I","EM","STRONG","BR","UL","OL","LI","TABLE","THEAD","TBODY","TR","TH","TD","DIV","SPAN","A"]); const GUIDE_CLASSES=new Set(["cite","tablewrap","num","hint","warnbox"]); function safeGuideHTML(html){ const tpl=document.createElement("template"); tpl.innerHTML=String(html||""); const out=document.createDocumentFragment(); const copy=(src,dst)=>{ src.childNodes.forEach(n=>{ if(n.nodeType===3){ dst.appendChild(document.createTextNode(n.textContent)); return; } if(n.nodeType!==1) return; const tag=n.tagName.toUpperCase(); if(tag==="SCRIPT"||tag==="STYLE"||tag==="TEMPLATE") return; if(!GUIDE_TAGS.has(tag)){ copy(n,dst); return; } // unknown element: keep its text only const el=document.createElement(tag.toLowerCase()); const cls=(n.getAttribute("class")||"").split(/\s+/).filter(c=>GUIDE_CLASSES.has(c)); if(cls.length) el.className=cls.join(" "); if(tag==="A"){ const href=n.getAttribute("href")||""; if(/^https:\/\//i.test(href)){ el.href=href; el.target="_blank"; el.rel="noopener noreferrer"; } } copy(n,el); dst.appendChild(el); }); }; copy(tpl.content,out); return out; } // v50: the Timing guide holds only what the user wrote (S.profile.guide, from their personal-settings // file). Up to v49 it also carried the app's own section on protein timing, with general-guidance // figures — timing advice the app must not give. The food-database note moved to Data → My foods. function renderGuide(m){ const h=document.createElement("div"); h.innerHTML=""+(artifactNS? "Data is saved into this page itself on claude.ai — it follows the link across your devices, and your PT sees the same data on the shared link (they need edit access and a claude.ai sign-in to enter macros).": (sbEnabled()? "Standalone version with cloud sync. Once you sign in below, your log is kept in your own cloud account and is the same on every device you sign in on. A copy is also kept in this browser so the app opens and saves offline — changes made offline sync when you're back online. Backups are still worth downloading after each PT review. Note: this copy and the claude.ai app do not sync with each other.": IS_LOCAL? "Standalone version. Data lives in this browser on this computer (browser storage). It survives closing the browser and restarting the computer — but clearing this browser's site data erases it, and it does not follow you to another browser or device. Download a backup after each PT review; a backup restores everything. Note: this copy and the claude.ai app do not sync with each other — pick one as your main log, or move data between them with backup files.": "This view cannot save to the page — data is kept in this browser only. Make regular backups below."))+ "
"+nDays+" day(s) stored · entries older than ~1 year are pruned automatically."+(IS_LOCAL?"":" A copy is also cached in this browser.")+"
"; m.appendChild(info); if(sbEnabled()) m.appendChild(sbCard()); if(sbSignedIn()&&acctLogSyncOk()) m.appendChild(coachLinkCard()); // v53 if(sbSignedIn()) m.appendChild(hsCard()); if(sbSignedIn()) m.appendChild(sbSnapshotsCard()); m.appendChild(personalCard()); const ex=document.createElement("div"); ex.className="card"; ex.innerHTML="Paste a backup below, or pick a file. Restoring replaces all current data.
'; const fi=document.createElement("input"); fi.type="file"; fi.accept=".json,application/json"; const ta=document.createElement("textarea"); ta.placeholder='{"version":1,…}'; const rb=document.createElement("button"); rb.className="btn danger small"; rb.textContent="Restore (replace all data)"; fi.onchange=()=>{ const f=fi.files[0]; if(!f) return; f.text().then(t=>ta.value=t); }; rb.onclick=()=>{ let obj; try{ obj=JSON.parse(ta.value); }catch(e){ alert("That isn't valid JSON."); return; } // typeof null==="object" in JS, so the old check here (typeof obj.days!=="object") let // {"version":1,"days":null} through — ensureTemplate() then crashed uncaught reading a // property off null, bricking the live session from the one UI path meant for disaster // recovery. Require days to actually be a (non-array) object. if(!obj||obj.version!==1||!obj.days||typeof obj.days!=="object"||Array.isArray(obj.days)){ alert("Not a Takt backup (version 1 expected)."); return; } if(!confirm("Replace ALL current data with this backup?")) return; const prev=S; try{ S=normalizeState(obj); ensureTemplate(); ensureMedPlan(); migrateMedKeys(); ensurePtTracking(); }catch(e){ // Something in the file was still malformed enough to crash setup — roll back rather than // leave S half-replaced with a broken store and no way to recover without a reload. S=prev; alert("That file doesn't look like a valid Takt backup — nothing was changed."); return; } save(); render(); }; ib.append(fi,ta,rb); imp.appendChild(ib); ex.appendChild(imp); m.appendChild(ex); // Automatic local backup folder (File System Access API) — the user's request: point backups at a // real folder on disk their Google Drive desktop app already syncs, so they reach Drive with zero // manual step, instead of relying on them remembering to move each download over themselves. const fsc=document.createElement("div"); fsc.className="card"; fsc.innerHTML="Not available in this browser — needs Chrome or Edge on a desktop computer. Backups will still download as regular files here.
"); } m.appendChild(fsc); // custom foods const cf=document.createElement("div"); cf.className="card"; cf.innerHTML="The built-in food list holds ~110 common foods (German staples included) with approximate per-100 g values compiled from standard nutrition references, so treat every figure as an estimate. Your PT verifies weekly figures in Week & PT. Public databases for reference: USDA FoodData Central, Open Food Facts and Germany\'s Bundeslebensmittelschlüssel.
'; const list=document.createElement("div"); (S.customFoods||[]).forEach((f,ix)=>{ const r2=document.createElement("div"); r2.className="row"; r2.style.padding="3px 0"; r2.innerHTML=''+esc(f.n)+''+esc(f.kcal)+" kcal · P "+esc(f.p)+" · C "+esc(f.c)+" · F "+esc(f.f)+" /100 g"; const x=document.createElement("button"); x.className="xbtn"; x.textContent="✕"; x.setAttribute("aria-label","remove custom food"); x.onclick=()=>{ S.customFoods.splice(ix,1); save(); render(); }; r2.appendChild(x); list.appendChild(r2); }); cf.appendChild(list); const ar=document.createElement("div"); ar.className="row"; ar.style.marginTop="6px"; ar.innerHTML=''+ ''+ ''+ ''+ ''; const ab=document.createElement("button"); ab.className="btn small"; ab.textContent="Add food"; ab.onclick=()=>{ const v=q=>ar.querySelector('[data-f="'+q+'"]').value; if(!v("n")) return; S.customFoods.push({n:v("n"),kcal:+v("kcal")||0,p:+v("p")||0,c:+v("c")||0,f:+v("f")||0}); save(); render(); }; ar.appendChild(ab); cf.appendChild(ar); m.appendChild(cf); const sh=document.createElement("div"); sh.className="card"; sh.innerHTML="1. Each week, open Week & PT and download the PT review file (HTML) — email it to your PT.
"+
"2. They open it in any browser, verify/adjust the macros, set targets, comment, save a PDF, and send you back the small return file.
"+
"3. Import the return file in Week & PT — their verified figures, targets, comments and review stamp are stored.
1. Sharing this page's link with your PT (edit access) is optional — your data syncs here either way; it just lets them see your live log directly.
"+
"2. Each week, open Week & PT and download the PT review file (HTML) — send it to your PT. They open it in any browser, verify or correct each meal's macros, set next week's targets, comment, save a PDF, and send back a small return file.
"+
"3. Import that return file in Week & PT — their verified meal corrections, targets, comments and review stamp are stored automatically. The auto-generated weekly summary can also be copied into email or WhatsApp if they prefer reading it there.