-- wx
#--
Track Detected
Tap to start race night
--TEMP
--HUMID
--DEW PT
--DENSITY ALT
--WIND
RACE NIGHT LOADING...
Step 1 of 7 — Division
WELCOME TO BLACK BOOK
What do you race? Hunter will tune your setup sheet to your exact class.
Step 2 of 7 — Chassis
Step 3 of 7 — Front Suspension
Some chassis ship torsion and can be converted to coil springs. This changes your setup sheet.
Step 4 of 7 — Weight
Step 5 of 7 — Engine
Step 9 of 10 — Geometry
J-ladder, panhard height, torsion bar, birdcage. If nothing changed, tap Skip.
Step 10 of 10 — Known Characteristics
Step 6 of 10 — Drivetrain
Auto-detected from your engine. Confirm or change below.
Step 7 of 10 — Car Specs
These help Hunter compute stagger, tire pressure, and handling changes.
Step 7 of 10 — Car Specs
These save once per car.
Step 8 of 10 — Clutch
For chain-drive cars. Skip if direct/gear drive.
PIVOT — Platform Integrity
Still as builder delivered? Log structural changes so pickup measurements stay trustworthy — builder hole charts are reference only.
PIVOT — Pickup Points
PIVOT — Shock Baseline
Log shock clicks after geometry — springs and pickup points set the platform first.
New Car
Hunter Can
Crew Chief Dashboard
TONIGHT'S TRACK
Detecting via GPS...
TEMP
--
°F
WIND
--
MPH
DENSITY ALT
--
FEET
--
-2k
8k
0
HUMID
--
%
DEW PT
--
°F
TRACK EST
--
-- EVAP
SURF
--
EST
ACQUIRING GPS...
BARO
--
ELEVATION
--
DEW SPREAD
--
WIND
--
SURFACE & TRACK SCIENCE
TRACK CONDITION INTELLIGENCE
TRACK ANALYSIS TOOLS
AI CREW CHIEF · REPORT + RECOMMENDATIONS
PIT CALCS
PRE-RACE CHECKLIST
PROGRESS
0 / 0
📷 Camera Tools
AI Vision · Measurement · Analysis
TOOL 01 LIVE
Camera Guide
PRE-FLIGHT · STABILITY CHECK · AI QUALITY GATE
01
📸
Get the shot right first time
Guide overlay + gyro stability check + Claude vision pre-flight. Never waste an API call on a blurry photo.
WORKS OFFLINE · ALL TIERS
TOOL 02 LIVE
Alignment Tool
TOE · CAMBER · ±0.05° · BLACK BOOK MEMBERS
02
🎯
No laser. No strings. No shop.
Toe + Camber from two marker boards and your phone camera. Replaces $630 in Longacre tools. Works on micro sprints where string cannot reach.
ArUco MARKERS · RUNS IN BROWSER · NO SERVER
TOOL 03 LIVE
Setup Photo Log
AI VISION · SEASON HISTORY · WARRIOR+
03
📸
Snap your suspension every race night
Hunter sees what you see. Build a visual record across the season — the data moat no competitor has.
Log a Race Night
Session Type
Track
Finish P#
Start P#
Cars
Best Lap
Track Conditions
Condition Start
Condition End
Surface Temp (°F)
Ambient Temp (°F)
Tire Pressures (PSI cold)
LF
RF
LR
RR
Stagger (in)
Gear Ratio
Geometry
TOE FRONT (in)
TOE REAR (in)
BUMP STEER LF
BUMP STEER RF
ACKERMAN (%)
PINION ANGLE (°)
TRAIL ARM L (in)
TRAIL ARM R (in)
NOSE HEIGHT (in)
SPOILER (°)
ROLL CENTER F (in)
ROLL CENTER R (in)
Shocks — Compression (clicks from full soft)
LF
RF
LR
RR
Shocks — Rebound (clicks from full soft)
LF REB
RF REB
LR REB
RR REB
Shock Travel (cm after session)
LF TRAVEL
RF TRAVEL
LR TRAVEL
RR TRAVEL
Shock Info
BRAND / MODEL
GAS PRESSURE (psi)
Tire Temps (I / M / O per corner)
LF
INSIDE
MIDDLE
OUTSIDE
RF
INSIDE
MIDDLE
OUTSIDE
LR
INSIDE
MIDDLE
OUTSIDE
RR
INSIDE
MIDDLE
OUTSIDE
Hot Pressures (psi after session)
LF HOT
RF HOT
LR HOT
RR HOT
Engine Data
WATER (°F)
OIL (°F)
OIL PSI
PEAK RPM
MAIN JET
TIMING (°)
PLUG COLOR
CHT/EGT (°F)
Notes
Setup Notes
Jetting Notes
Data Logger Upload
Logger Source
📂 Drop file or tap to browse
Attach to selected track session above
.CSV .JSON .TXT .LOG
◆ HUNTER'S ANALYSIS
Recent Races
Loading history...
YOUR DATA
Heads up — Inkwell is not a CPA, tax preparer, attorney, financial advisor, or licensed insurance agent. Numbers and recommendations on this site are estimates from data you logged. Prices, tip percentages, mileage deductions, ink valuations, gas prices, venue locations, and earnings are user-submitted or AI-generated and may be inaccurate. Verify with a qualified professional before relying. No CPA-client, attorney-client, or advisory relationship is created by use of this site. By using Inkwell you agree to the Terms. © 2026 G10be — Amarillo, Texas.
');vw.document.close();}} function deleteMyDoc(idx){var docs=JSON.parse(localStorage.getItem("bb_mydocs")||"[]");docs.splice(idx,1);localStorage.setItem("bb_mydocs",JSON.stringify(docs));loadMyDocs();toast("Removed")} var _GEAR_SLOTS=[["RACING LICENSE","license"],["HANS CERT","hans"],["HELMET","helmet"],["SUIT","suit"],["SHOES","shoes"],["RIB PROTECTOR","ribs"],["INSURANCE","insurance"],["TECH CARD","tech_card"],["TRANSPONDER","transponder"],["MEMBERSHIP","membership"],["MEDICAL","medical"],["OTHER","other"]]; function buildGearDocs(){var grid=document.getElementById("gear-docs-grid");if(!grid)return;grid.innerHTML="";var data=JSON.parse(localStorage.getItem("bb_geardocs")||"{}");_GEAR_SLOTS.forEach(function(sl){var tile=document.createElement("div");tile.style.cssText="background:var(--dark2);border:1px solid rgba(208,25,14,.12);padding:10px 8px;text-align:center;cursor:pointer;position:relative;min-height:64px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px";if(data[sl[1]]){var img=document.createElement("img");img.src=data[sl[1]];img.style.cssText="max-width:100%;max-height:44px;object-fit:cover";tile.appendChild(img);var vb=document.createElement("div");vb.style.cssText="font-family:var(--mono);font-size:7px;color:var(--amber);letter-spacing:1px";vb.textContent="TAP TO REPLACE";tile.appendChild(vb);}else{var plus=document.createElement("div");plus.style.cssText="font-family:var(--head);font-size:22px;font-weight:900;color:rgba(208,25,14,.25)";plus.textContent="+";tile.appendChild(plus);}var lbl=document.createElement("div");lbl.style.cssText="font-family:var(--mono);font-size:8px;color:var(--muted);letter-spacing:1px";lbl.textContent=sl[0];tile.appendChild(lbl);tile.onclick=(function(key){return function(){var inp=document.createElement("input");inp.type="file";inp.accept="image/*";inp.capture="environment";inp.style.display="none";inp.onchange=function(){if(!inp.files||!inp.files[0])return;var fr=new FileReader();fr.onload=function(){var d2=JSON.parse(localStorage.getItem("bb_geardocs")||"{}");d2[key]=fr.result;localStorage.setItem("bb_geardocs",JSON.stringify(d2));if(S.token&&S.cur&&S.cur.id)_cloudSave('geardocs',S.cur.id,d2);buildGearDocs();toast("Saved");};fr.readAsDataURL(inp.files[0]);};document.body.appendChild(inp);inp.click();setTimeout(function(){if(inp.parentNode)document.body.removeChild(inp);},60000);};})(sl[1]);grid.appendChild(tile);});} function viewGearDoc(key){var data=JSON.parse(localStorage.getItem("bb_geardocs")||"{}");if(!data[key])return;var w=window.open("","_blank");if(w){w.document.write(''+key+'');w.document.close();}} function loadWeather(){if(typeof _isDemoMode==='function'&&_isDemoMode()){if(typeof _applyDemoWeather==='function')_applyDemoWeather(true);return;}var WX="https://zmrouoqututfndplboyc.supabase.co/functions/v1/bb-weather";navigator.geolocation.getCurrentPosition(function(pos){fetch(WX+"?lat="+pos.coords.latitude+"&lon="+pos.coords.longitude).then(function(r){return r.json()}).then(function(d){if(d.temp===undefined)return;var T=Math.round(d.temp||0),H=Math.round(d.humidity||0),W=Math.round(d.wind_speed||0),DA=Math.round(d.density_altitude||0),DP=Math.round(d.dewpoint||0);var wT=$("wT"),wH=$("wH"),wD=$("wD"),wDA=$("wDA"),wW=$("wW");if(wT)wT.textContent=T+"°";if(wH)wH.textContent=H+"%";if(wD)wD.textContent=DP+"°";if(wDA)wDA.textContent=DA.toLocaleString();if(wW)wW.textContent=W+"mph";S.wx={temp:T,humidity:H,wind_speed:W,density_altitude:DA,dewpoint:DP,wind_dir:d.wind_dir||"N",barometric_pressure:d.barometric_pressure||29.92,location:d.location};if(typeof _syncSurfaceTempToWx==='function')_syncSurfaceTempToWx();var rn=$("rn-banner");if(rn&&d.location){$("rn-track").textContent=d.location;rn.classList.add("on")}}).catch(function(){})},function(){},{timeout:10000});setInterval(loadWeather,300000)} function geoCheck(){if(typeof _isDemoMode==='function'&&_isDemoMode()){if(typeof _applyDemoWeather==='function')_applyDemoWeather(true);if(typeof _setDemoTrackWhenReady==='function')_setDemoTrackWhenReady();return;}loadWeather();_gpsDetectTrack();} function _gpsDetectTrack(){try{if(!navigator.geolocation)return;navigator.geolocation.getCurrentPosition(function(pos){var lat=pos.coords.latitude,lon=pos.coords.longitude;if(!S.tracks||!S.tracks.length){setTimeout(_gpsDetectTrack,4000);return;}var best=null,bestD=999;for(var i=0;i Export > CSV."; bcard.appendChild(bt);bcard.appendChild(bn);res.appendChild(bcard); return; } res.textContent="Reading "+file.name+"..."; if(ext==="drk"||ext==="xdrk"){ file.arrayBuffer().then(function(buf){ var session=_logParseDRKBuf(buf); if(!session||!session.rows||!session.rows.length){res.textContent="Could not parse DRK. Try CSV export.";return;} var chs=(session.parsed&&session.parsed.channels)||[]; for(var i=0;i=0){window._lastGForce.lat_max=chs[i].stats.max;window._lastGForce.lat_avg=chs[i].stats.avg;} if(n.indexOf("longitudinal")>=0){window._lastGForce.brake=Math.abs(chs[i].stats.min);window._lastGForce.accel=chs[i].stats.max;} } _logSess=session; _logInitDrkExpDraft(file.name); _logRenderDrkSummary(session,file.name,_logDrkSummaryMeta(session.parsed,{saveStatus:"Saving to logger DB\u2026",saved:false})); if(typeof _logEnsureWxForDrkSave==="function"){_logEnsureWxForDrkSave().then(function(){_logRenderDrkSummary(session,file.name,_logDrkSummaryMeta(session.parsed,{saveStatus:"Saving to logger DB\u2026",saved:false}));});} if(session.parsed&&typeof _logSaveDrkToBbLogger==="function"){ _logSaveDrkToBbLogger(session.parsed,file.name).then(function(r){ if(r&&r.skipped){ if(!S.user||!S.user.user_id){ _logRenderDrkSummary(session,file.name,_logDrkSummaryMeta(session.parsed,{saveStatus:"Parsed \u2014 sign in to cloud-save",saved:false})); toast("DRK parsed \u2014 sign in to save to cloud"); } return; } if(r&&r.ok&&r.body&&r.body.id){ var _cLbl=S.cur&&S.cur.id?" #"+(S.cur.car_number||"")+" "+(S.cur.name||"").trim():""; var _tLbl=S.curTrack?" @ "+(S.curTrack.short||S.curTrack.name):""; toast("DRK saved"+_cLbl+_tLbl+" \u2714"); _logRenderDrkSummary(session,file.name,_logDrkSummaryMeta(session.parsed,{saved:true,saveStatus:"Saved to logger DB \u00b7 "+r.body.id.slice(0,8),saveId:r.body.id})); if(typeof _logReadDrkExperimentCapture==="function"){var _cap=_logReadDrkExperimentCapture();if(_cap)_logAppendDrkExperimentRun(_cap,r.body.id);} console.log("[BB] bb-logger-save ok",r.body.id); } else if(r&&r.body&&r.body.error){toast("DRK cloud save: "+r.body.error);_logRenderDrkSummary(session,file.name,_logDrkSummaryMeta(session.parsed,{saveStatus:"Cloud save failed: "+r.body.error,saved:false}));console.warn("[BB] bb-logger-save",r);} }).catch(function(e){toast("DRK cloud save failed");_logRenderDrkSummary(session,file.name,_logDrkSummaryMeta(session.parsed,{saveStatus:"Cloud save failed",saved:false}));console.warn("[BB] bb-logger-save failed",e);}); } }).catch(function(){res.textContent="DRK read failed";}); return; } var reader=new FileReader(); reader.onload=function(e){ var text=e.target.result,session=null; if(ext==="json"){try{session=_logParseJSON(JSON.parse(text));}catch(er){session=null;}} else if(ext==="gpx"){session=_logParseGPX(text);} else if(ext==="vbo"){session=_logParseVBO(text);} else{session=_logParseCSV(text,file.name);} if(!session||!session.rows||!session.rows.length){ res.textContent="Could not parse file. Try exporting as CSV from your logger software."; return; } _logSess=session; _logRender(session,file.name); }; reader.readAsText(file); } var _logCH={ time:["time","elapsed time","time (s)","timestamp","session time","t","stime","utc","interval"], lap:["lap","lap number","lapcount","lap count","lap #"], laptime:["laptime","lap time","lap_time","lap duration","ltime"], speed:["speed","gps speed","vehicle speed","speed (mph)","speed (kph)","gps_speed","groundspeed","v"], rpm:["rpm","engine rpm","engine speed","motor rpm","revs","engine_rpm"], throttle:["throttle","tps","throttle pos","throttle (%)","throttle position","accel"], brake:["brake","brake pressure","brake (%)","brake pos"], lat_g:["lateral g","lateral_g","lat g","g_lat","lateral accel","lateral"], lon_g:["longitudinal g","long_g","g_lon","linear g"], lat:["latitude","lat","gps lat","gps_lat"], lon:["longitude","lon","lng","gps lon","gps_lon"], water_temp:["water temp","water_temp","coolant","coolant_temp","ect"], gear:["gear","gear position","current gear"] }; function _logParseCSV(text,fname){ var lines=text.replace(/\r\n/g,"\n").replace(/\r/g,"\n").split("\n"); var delim=","; var i,j,cells; for(i=0;i(lines[i].match(/,/g)||[]).length){delim=";";break;}} var headerRow=-1,maxCols=0; for(i=0;i=3&&isNaN(parseFloat(cells[0].trim()))&&cells[0].trim().length>0&&cells.length>maxCols){maxCols=cells.length;headerRow=i;} } if(headerRow<0)return null; var headers=lines[headerRow].split(delim).map(function(x){return x.trim().replace(/["']/g,"").toLowerCase();}); var cols={}; Object.keys(_logCH).forEach(function(ch){ _logCH[ch].forEach(function(alias){ if(cols[ch]===undefined){ var idx=headers.indexOf(alias); if(idx<0){for(j=0;j=0){idx=j;break;}}} if(idx>=0)cols[ch]=idx; } }); }); var rows=[]; for(i=headerRow+1;i0)rows.push(row); } var brand="Generic CSV",fn=(fname||"").toLowerCase(); if(fn.indexOf("aim")>=0||fn.indexOf("mychron")>=0||headers.indexOf("beacon")>=0)brand="AiM / MyChron"; else if(headers.indexOf("heading")>=0||fn.indexOf("trackaddict")>=0)brand="TrackAddict"; else if(headers.indexOf("lapcount")>=0||headers.indexOf("interval")>=0)brand="RaceCapture"; else if(fn.indexOf("harry")>=0)brand="Harry\u2019s LapTimer"; return {brand:brand,headers:headers,cols:cols,rows:rows}; } function _logParseVBO(text){ var lines=text.replace(/\r\n/g,"\n").split("\n"),inData=false,headers=[],rows=[],i; for(i=0;i]*>/g,re2=/<\/trkpt>/g; var trkptRe=/]*lat="([^"]+)"[^>]*lon="([^"]+)"/; var speedRe=/([^<]+)<\/speed>/; var chunks=text.split("([^<]+)<\/speed>/); var lat=latMatch?parseFloat(latMatch[1]):0; var lon=lonMatch?parseFloat(lonMatch[1]):0; var spd=spdMatch?parseFloat(spdMatch[1])*3.6*0.621371:0; rows.push({lat:lat,lon:lon,speed:spd}); } return rows.length?{brand:"GPX",headers:["lat","lon","speed"],cols:{},rows:rows}:null; } function _logParseJSON(obj){ if(Array.isArray(obj)&&obj.length&&typeof obj[0]==="object")return{brand:"JSON",headers:Object.keys(obj[0]),cols:{},rows:obj}; var rows=obj.laps||obj.data||obj.sessions||[]; return rows.length?{brand:"JSON",headers:rows.length?Object.keys(rows[0]):[],cols:{},rows:rows}:null; } function _logAnalyze(session){ var rows=session.rows,an={laps:[],maxSpeed:0,maxRPM:0,maxLatG:0,avgThrottle:0,channels:[]}; var thSum=0,thCnt=0,prevLap=-1,lapStart=0; rows.forEach(function(row){ if(row.speed!=null)an.maxSpeed=Math.max(an.maxSpeed,row.speed); if(row.rpm!=null)an.maxRPM=Math.max(an.maxRPM,row.rpm); if(row.lat_g!=null)an.maxLatG=Math.max(an.maxLatG,Math.abs(row.lat_g)); if(row.throttle!=null){thSum+=row.throttle;thCnt++;} if(row.laptime!=null&&row.laptime>0&&row.laptime<600){if(!an.laps.length||an.laps[an.laps.length-1]!==row.laptime)an.laps.push(row.laptime);} else if(row.lap!=null&&row.lap!==prevLap&&row.lap>0){if(row.time!=null&&lapStart>0){var lt=row.time-lapStart;if(lt>5&<<600)an.laps.push(Math.round(lt*1000)/1000);}lapStart=row.time||0;prevLap=row.lap;} }); an.avgThrottle=thCnt?Math.round(thSum/thCnt*10)/10:0; var lbl={speed:"Speed",rpm:"RPM",throttle:"Throttle",brake:"Brake",lat_g:"Lateral G",lon_g:"Long G",water_temp:"H2O Temp",gear:"Gear"}; Object.keys(session.cols||{}).forEach(function(ch){if(lbl[ch])an.channels.push(lbl[ch]);}); return an; } function _fmtLap(s){ if(s>60){var m=Math.floor(s/60);var sec=s%60;return m+":"+(sec<10?"0":"")+sec.toFixed(2);} return s.toFixed(3)+"s"; } function _logSVG(laps){ if(!laps||laps.length<2)return ""; var mn=Math.min.apply(null,laps),mx=Math.max.apply(null,laps),rng=mx-mn||1; var W=280,H=52,pad=4,bw=Math.max(3,Math.floor((W-pad*2)/laps.length)-2); var out=""; laps.forEach(function(lt,i){ var bh=Math.max(4,Math.round(((mx-lt)/rng)*(H-pad*2-10))); var x=pad+i*(bw+2),y=H-pad-bh; var col=lt===mn?"#C8960A":"rgba(255,255,255,.1)"; out+=""; if(lt===mn)out+=""+_fmtLap(lt)+""; }); return out+""; } function _logRender(session,fname){ var res=document.getElementById("logger-result");if(!res)return; var an=_logAnalyze(session);_logSess.an=an;res.innerHTML=""; var card=document.createElement("div");card.style.cssText="background:var(--dark2);border:1px solid rgba(245,166,35,.2);padding:14px"; var hdr=document.createElement("div");hdr.style.cssText="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px"; var hl=document.createElement("div"); var hn=document.createElement("div");hn.style.cssText="font-family:var(--head);font-size:15px;font-weight:900;color:var(--amber)";hn.textContent=session.brand.toUpperCase(); var hf=document.createElement("div");hf.style.cssText="font-family:var(--mono);font-size:8px;color:var(--muted);margin-top:2px";hf.textContent=fname+" \u2014 "+session.rows.length.toLocaleString()+" pts"; hl.appendChild(hn);hl.appendChild(hf); var hr2=document.createElement("div");hr2.style.cssText="font-family:var(--mono);font-size:8px;color:var(--muted);text-align:right;max-width:120px";hr2.textContent=an.channels.join(" \u2022 "); hdr.appendChild(hl);hdr.appendChild(hr2);card.appendChild(hdr); var stats=[ ["BEST LAP",an.laps.length?_fmtLap(Math.min.apply(null,an.laps)):"--"], ["LAPS",an.laps.length||"--"], ["MAX SPEED",an.maxSpeed?Math.round(an.maxSpeed)+" mph":"--"], ["MAX RPM",an.maxRPM?Math.round(an.maxRPM/100)*100:"--"], ["AVG THROT",an.avgThrottle?an.avgThrottle+"%":"--"], ["MAX LAT G",an.maxLatG?an.maxLatG.toFixed(2)+"g":"--"] ]; var grid=document.createElement("div");grid.style.cssText="display:grid;grid-template-columns:repeat(3,1fr);gap:5px;margin-bottom:10px"; stats.forEach(function(s){ var d=document.createElement("div");d.style.cssText="background:var(--dark);padding:8px 4px;text-align:center"; var lbl=document.createElement("div");lbl.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);letter-spacing:1px";lbl.textContent=s[0]; var v=document.createElement("div");v.style.cssText="font-family:var(--mono);font-size:14px;color:var(--white);font-weight:700;margin-top:2px";v.textContent=s[1]; d.appendChild(lbl);d.appendChild(v);grid.appendChild(d); }); card.appendChild(grid); if(an.laps.length>1){ var cw=document.createElement("div");cw.style.marginBottom="10px"; var cl=document.createElement("div");cl.style.cssText="font-family:var(--mono);font-size:8px;color:var(--muted);letter-spacing:1px;margin-bottom:4px"; cl.textContent="LAP TIMES \u2014 "+an.laps.length+" LAPS (GOLD = BEST)"; cw.appendChild(cl); var svg=document.createElement("div");svg.innerHTML=_logSVG(an.laps); cw.appendChild(svg);card.appendChild(cw); } var btns=document.createElement("div");btns.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:8px"; var bh=document.createElement("button");bh.style.cssText="padding:10px;font-family:var(--head);font-size:13px;font-weight:900;letter-spacing:1px;background:rgba(200,150,10,.08);border:1px solid rgba(200,150,10,.25);color:var(--gold);cursor:pointer";bh.textContent="\u25C6 ASK HUNTER";bh.onclick=_logAskHunter; var bs=document.createElement("button");bs.style.cssText="padding:10px;font-family:var(--head);font-size:13px;font-weight:900;letter-spacing:1px;background:rgba(44,200,50,.06);border:1px solid rgba(44,200,50,.2);color:#4CAF50;cursor:pointer";bs.textContent="\uD83D\uDCBE SAVE SESSION";bs.onclick=_logSave; btns.appendChild(bh);btns.appendChild(bs);card.appendChild(btns); res.appendChild(card); } function _logAskHunter(){ if(!_logSess||!_logSess.an){toast("No session loaded");return;} var an=_logSess.an; var msg="I uploaded a data logger session. " +(an.laps.length?"Best lap "+_fmtLap(Math.min.apply(null,an.laps))+", "+an.laps.length+" laps. ":"No lap data. ") +(an.maxSpeed?"Max speed "+Math.round(an.maxSpeed)+"mph. ":"") +(an.maxRPM?"Peak RPM "+Math.round(an.maxRPM/100)*100+". ":"") +(an.avgThrottle?"Avg throttle "+an.avgThrottle+"%. ":"") +(an.maxLatG?"Max lateral G "+an.maxLatG.toFixed(2)+". ":"") +"What does this tell you about what the car is doing?"; switchTab("hunter"); setTimeout(function(){addMsg("u",msg);sendToHunter(msg);},300); } function _logSave(){ if(!_logSess||!_logSess.an){toast("No session loaded");return;} if(!S.token||!S.cur){toast("Sign in to save sessions");return;} var an=_logSess.an; var entry={date:new Date().toISOString().split("T")[0],track:S.curTrack?S.curTrack.name:"Unknown",brand:_logSess.brand,laps:an.laps,best_lap:an.laps.length?Math.min.apply(null,an.laps):null,max_speed:an.maxSpeed,max_rpm:an.maxRPM,avg_throttle:an.avgThrottle,max_lat_g:an.maxLatG,channels:an.channels,setup_snapshot:Object.assign({},_su)}; fetch(AU+"?action=save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:S.token,car_id:S.cur.id,data_type:"logger_session",data:entry})}).then(function(r){return r.json();}).then(function(d){toast(d.success?"Session saved":"Save failed");}).catch(function(){toast("Save failed");}); } // ─── MyRacePass Integration Functions ──────────────────────────────────────── // Appended before `if("serviceWorker" in navigator)` in bb-merged-final.html // Uses allorigins.win as CORS proxy to fetch public MRP mobile pages (no API key needed) // Attribution: "Powered by MyRacePass" shown on all MRP-sourced data var MRP_PROXY = 'https://api.allorigins.win/get?url='; var MRP_BASE = 'https://m.myracepass.com'; var _mrpCache = {}; // ── Core fetch + text-strip utility ───────────────────────────────────────── async function mrpFetch(path) { var url = MRP_BASE + path; if (_mrpCache[url] && Date.now() - _mrpCache[url].ts < 120000) return _mrpCache[url].text; try { var r = await fetch(MRP_PROXY + encodeURIComponent(url)); var j = await r.json(); var raw = j.contents || ''; // Strip scripts, styles, nav boilerplate; extract readable text blocks var cleaned = raw .replace(//gi, '') .replace(//gi, '') .replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/©/g,'©'); _mrpCache[url] = { text: cleaned, ts: Date.now() }; return cleaned; } catch(e) { return ''; } } // ── Parse track schedule page → find tonight's or next event ──────────────── async function mrpGetTonightEvent(mrpTrackId) { var html = await mrpFetch('/tracks/' + mrpTrackId + '/schedule'); if (!html) return null; // Pull all event IDs from schedule links var eventLinks = [...html.matchAll(/href="\/events\/(\d+)"/g)].map(m => parseInt(m[1])); var uniqueIds = [...new Set(eventLinks)]; // Extract plain text and find events with their dates var text = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' '); // Look for today's date in multiple formats var now = new Date(); var months = ['January','February','March','April','May','June','July','August','September','October','November','December']; var todayFull = months[now.getMonth()] + ' ' + now.getDate() + ', ' + now.getFullYear(); var todayShort = (now.getMonth()+1) + '/' + now.getDate() + '/' + now.getFullYear(); // Is there a "Next Event" block? var nextEventIdx = text.indexOf('Next Event'); var nextBlock = nextEventIdx !== -1 ? text.slice(nextEventIdx, nextEventIdx + 400) : ''; var nextIdMatch = html.match(/href="\/events\/(\d+)"[^>]*>[^<]*Details/); var nextId = nextIdMatch ? parseInt(nextIdMatch[1]) : (uniqueIds[0] || null); // Check if next event is today var isTonight = nextBlock.includes(todayFull) || nextBlock.includes(todayShort) || nextBlock.includes((now.getMonth()+1) + '/' + now.getDate() + '/'); if (!nextId) return null; // Extract next event name var nameMatch = text.match(/Next Event\s+\w+\s+[\w,\s]+20\d\d\s+[^\n]+?([A-Z][A-Za-z &'-]{5,80}(?:Round|Series|Night|Race|Classic|Spring|Summer|Fall|Championship|Invitational|Shootout|Nationals|[#\d]){0,1}[^]*?)\s+Details/); var eventName = ''; if (nextBlock) { // Event name is usually after the date line var nameSearch = nextBlock.replace(/Next Event\s+/,'').replace(/\w+,\s+\w+\s+\d+,\s+\d{4}\s+/,'').replace(/\w+ Speedway\s+/,'').replace(/Pits Open[\s\S]*/,'').trim(); eventName = nameSearch.split(/\s{2,}/)[0] || ''; } return { eventId: nextId, eventName: eventName.trim(), isTonight: isTonight, trackId: mrpTrackId, url: 'https://www.myracepass.com/events/' + nextId }; } // ── Parse event detail page → classes + times ──────────────────────────────── async function mrpGetEventDetail(eventId) { var html = await mrpFetch('/events/' + eventId); if (!html) return null; var text = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' '); // Find classes block: text between "classes" marker and "EVENT TIMES" or "Pits Open" var classesStart = text.search(/\bclasses\b/i); var timesStart = text.search(/Pits Open|EVENT TIMES|Gates Open/i); var classesRaw = ''; if (classesStart !== -1 && timesStart > classesStart) { classesRaw = text.slice(classesStart + 7, timesStart); } else if (classesStart !== -1) { classesRaw = text.slice(classesStart + 7, classesStart + 300); } var classes = classesRaw.split(/[,\n]+/).map(s => s.replace(/\s+/g,' ').trim()).filter(s => s.length > 1 && s.length < 60 && !/^\d+$/.test(s)); // Extract times var timePatterns = [ ['pitsOpen', /Pits Open\s+([\d:]+\s*[AP]M)/i], ['gatesOpen', /Gates Open\s+([\d:]+\s*[AP]M)/i], ['hotLaps', /Hot Laps\s+(?:At\s+)?([\d:]+\s*[AP]M)/i], ['raceStart', /Racing Starts\s+([\d:]+\s*[AP]M)/i], ]; var times = {}; timePatterns.forEach(function(p) { var m = text.match(p[1]); if (m) times[p[0]] = m[1].trim(); }); // Event title + date var dateMatch = text.match(/(\d+\/\d+\/\d{4})\s+[-–]\s+([^]+?)(?:Home|Entries|Races|EVENT)/); var eventDate = dateMatch ? dateMatch[1] : ''; var trackName = dateMatch ? dateMatch[2].trim() : ''; // Event name (second "details" block) var evNameMatch = text.match(/EVENT INFORMATION\s+([^\n]+?)(?:classes|Pits Open|EVENT TIMES)/i); var eventName = evNameMatch ? evNameMatch[1].trim() : ''; return { eventId, eventDate, trackName, eventName, classes, times }; } // ── Parse event races page → race format + results ─────────────────────────── async function mrpGetEventRaces(eventId) { var html = await mrpFetch('/events/' + eventId + '/races'); if (!html) return null; var text = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' '); // Each class shows as "CLASS NAME (pending)" or "CLASS NAME X Heat, Y Result" // Format: "COOK OUT CADET (pending)" or "COOK OUT CADET Heat 1 Results Heat 2 Results" var classMatches = [...text.matchAll(/([A-Z][A-Z &'\-\/0-9]{2,50})\s+(\(pending\)|(?:(?:Heat|Dash|Consi|B-Main|A-Main|Feature)\s+\d*\s*(?:Results|Lineup)?[,\s]*)+)/g)]; var classes = classMatches.map(function(m) { var name = m[1].trim(); var status = m[2].trim(); var isPending = status.includes('pending'); var heats = (status.match(/Heat/gi) || []).length; var hasDash = /Dash/i.test(status); var hasConsi = /Consi|B-Main/i.test(status); var hasFeature = /A-Main|Feature/i.test(status); var hasResults = /Results/i.test(status); return { name, isPending, heats, hasDash, hasConsi, hasFeature, hasResults }; }); // Also pull any format hints from "pending" class list at bottom var pendingBlock = text.match(/classes\s+([\s\S]+?)(?:Build your brand|Events|$)/i); var pendingClasses = []; if (pendingBlock) { pendingClasses = pendingBlock[1].split(/\n/).map(s => s.replace(/\(pending\)/i,'').replace(/\s+/g,' ').trim()).filter(s => s.length > 2 && s.length < 60); } return { classes, pendingClasses }; } // ── Find today's event across all MRP tracks near user ────────────────────── async function mrpFindNearbyEvent() { if (!S.curTrack) return null; // Lookup mrp_track_id from bb_tracks REST (not in compact class data) try { var res = await fetch('https://zmrouoqututfndplboyc.supabase.co/rest/v1/bb_tracks?select=mrp_track_id&name=ilike.'+encodeURIComponent(S.curTrack.name)+'&limit=1', {headers:{'apikey':_AK,'Authorization':'Bearer '+_AK}}); var rows = await res.json(); var mrpId = rows&&rows[0]&&rows[0].mrp_track_id; if (!mrpId) return null; S.curTrack.mrp_track_id = mrpId; // cache on track object return await mrpGetTonightEvent(mrpId); } catch(e) { return null; } } // ── Build MRP section UI in Race tab ───────────────────────────────────────── var _mrpEventData = null; async function buildMRPSection(container) { var mrpId = S.curTrack && S.curTrack.mrp_track_id; if (!mrpId) return; // Header row with MRP branding var hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:8px'; hdr.innerHTML = '
MyRacePass
' + '
POWERED BY MYRACEPASS.COM
'; var box = document.createElement('div'); box.id = 'mrp-box'; box.style.cssText = 'background:var(--dark2);border:1px solid rgba(208,25,14,.1);padding:14px;margin-bottom:12px;position:relative'; box.appendChild(hdr); var status = document.createElement('div'); status.id = 'mrp-status'; status.style.cssText = 'font-family:var(--mono);font-size:9px;color:var(--muted);letter-spacing:1px'; status.textContent = 'LOADING...'; box.appendChild(status); container.appendChild(box); // Async load try { var ev = await mrpGetTonightEvent(mrpId); if (!ev) { status.textContent = 'No upcoming events found on MyRacePass'; return; } _mrpEventData = ev; var detail = await mrpGetEventDetail(ev.eventId); var races = await mrpGetEventRaces(ev.eventId); // Store MRP data where Hunter can see it S._mrpEvent = ev; if (detail) { S._mrpTimes = detail.times || {}; S._mrpClasses = detail.classes || []; S._raceCarCount = (races && races.length) || null; if (detail.times && detail.times.raceStart) { var _rst = detail.times.raceStart.match(/(\d+):?(\d*)\s*(AM|PM)/i); if (_rst) { var _rh = parseInt(_rst[1]); if (/PM/i.test(_rst[3]) && _rh < 12) _rh += 12; S._raceStartHour = _rh; } } if (detail.times && detail.times.hotLaps) { var _hlt = detail.times.hotLaps.match(/(\d+):?(\d*)\s*(AM|PM)/i); if (_hlt) { var _hlh = parseInt(_hlt[1]); if (/PM/i.test(_hlt[3]) && _hlh < 12) _hlh += 12; S._hotLapsHour = _hlh; } } } box.innerHTML = ''; box.appendChild(hdr); renderMRPEvent(box, ev, detail, races); } catch(e) { status.textContent = 'Could not load MyRacePass data'; } } function renderMRPEvent(box, ev, detail, races) { // Tonight badge or "Next Event" badge var isTonight = ev.isTonight; var badgeEl = document.createElement('div'); badgeEl.style.cssText = 'display:inline-block;font-family:var(--mono);font-size:8px;letter-spacing:2px;padding:3px 10px;margin-bottom:10px;' + (isTonight ? 'background:rgba(208,25,14,.1);border:1px solid rgba(208,25,14,.3);color:var(--red)' : 'background:rgba(200,150,10,.08);border:1px solid rgba(200,150,10,.2);color:var(--amber)'); badgeEl.textContent = isTonight ? '● TONIGHT' : 'NEXT EVENT'; box.appendChild(badgeEl); // Event name if (detail && detail.eventName) { var evName = document.createElement('div'); evName.style.cssText = 'font-family:var(--head);font-weight:900;font-size:18px;color:var(--white);margin-bottom:6px;line-height:1.2'; evName.textContent = detail.eventName || ev.eventName || 'Race Night'; box.appendChild(evName); } // Date if (detail && detail.eventDate) { var evDate = document.createElement('div'); evDate.style.cssText = 'font-family:var(--mono);font-size:9px;color:var(--muted);letter-spacing:1px;margin-bottom:10px'; evDate.textContent = detail.eventDate; box.appendChild(evDate); } // Times row if (detail && detail.times && Object.keys(detail.times).length) { var timesEl = document.createElement('div'); timesEl.style.cssText = 'display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px'; var timeLabels = { pitsOpen:'PITS', gatesOpen:'GATES', hotLaps:'HOT LAPS', raceStart:'RACING' }; Object.entries(detail.times).forEach(function([k,v]) { var t = document.createElement('div'); t.style.cssText = 'text-align:center;background:var(--dark);border:1px solid var(--dark4);padding:6px 10px;flex:1;min-width:60px'; t.innerHTML = '
' + (timeLabels[k]||k.toUpperCase()) + '
' + '
' + v + '
'; timesEl.appendChild(t); }); box.appendChild(timesEl); } // Classes racing tonight var classList = (detail && detail.classes && detail.classes.length) ? detail.classes : (races && races.pendingClasses && races.pendingClasses.length) ? races.pendingClasses : []; if (classList.length) { var clsHdr = document.createElement('div'); clsHdr.style.cssText = 'font-family:var(--mono);font-size:8px;letter-spacing:2px;color:var(--muted);margin-bottom:6px'; clsHdr.textContent = 'CLASSES RACING'; box.appendChild(clsHdr); var pillRow = document.createElement('div'); pillRow.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px'; classList.forEach(function(cls) { var p = document.createElement('div'); p.style.cssText = 'font-family:var(--mono);font-size:8px;padding:3px 8px;background:var(--dark);border:1px solid var(--dark4);color:var(--white);letter-spacing:.5px'; p.textContent = cls; pillRow.appendChild(p); }); box.appendChild(pillRow); } // Race format section: show heat/dash/consi per class if races are posted if (races && races.classes && races.classes.length) { var fmtHdr = document.createElement('div'); fmtHdr.style.cssText = 'font-family:var(--mono);font-size:8px;letter-spacing:2px;color:var(--muted);margin-bottom:6px'; fmtHdr.textContent = 'RACE FORMAT'; box.appendChild(fmtHdr); var hasAnyFormat = races.classes.some(function(c) { return !c.isPending; }); if (!hasAnyFormat) { var pendingNote = document.createElement('div'); pendingNote.style.cssText = 'font-size:11px;color:var(--muted);margin-bottom:8px'; pendingNote.textContent = 'Lineups not yet posted — check back closer to race time.'; box.appendChild(pendingNote); } else { // Auto-fill format button — infer heats/consi from first class with data var refClass = races.classes.find(function(c) { return !c.isPending; }); if (refClass) { var fillBtn = document.createElement('button'); fillBtn.style.cssText = 'font-family:var(--mono);font-size:9px;color:var(--amber);background:none;border:1px solid rgba(200,150,10,.2);padding:4px 12px;cursor:pointer;letter-spacing:1px;margin-bottom:8px'; fillBtn.textContent = 'AUTO-FILL FORMAT FROM MRP'; fillBtn.onclick = function() { var h = document.getElementById('fmt-heats'); var d = document.getElementById('fmt-dash'); var c = document.getElementById('fmt-consi'); if (h) h.value = refClass.heats || 2; if (d) d.value = refClass.hasDash ? 1 : 0; if (c) c.value = refClass.hasConsi ? 1 : 0; fillBtn.textContent = '✓ FORMAT FILLED'; fillBtn.style.color = 'var(--green)'; fillBtn.style.borderColor = 'rgba(45,184,127,.3)'; }; box.appendChild(fillBtn); } // Per-class race status races.classes.slice(0, 6).forEach(function(cls) { var row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--dark4)'; var label = document.createElement('div'); label.style.cssText = 'font-family:var(--mono);font-size:9px;color:var(--white)'; label.textContent = cls.name; var tags = document.createElement('div'); tags.style.cssText = 'display:flex;gap:4px'; var addTag = function(text, color) { var t = document.createElement('span'); t.style.cssText = 'font-family:var(--mono);font-size:7px;padding:2px 5px;background:' + color + ';letter-spacing:.5px'; t.textContent = text; tags.appendChild(t); }; if (cls.isPending) { addTag('PENDING', 'rgba(154,144,128,.15)'); } else { if (cls.heats) addTag('H×' + cls.heats, 'rgba(200,150,10,.1)'); if (cls.hasDash) addTag('DASH', 'rgba(74,144,226,.1)'); if (cls.hasConsi) addTag('CONSI', 'rgba(154,144,128,.1)'); addTag(cls.hasResults ? '✓ RESULTS' : 'RUNNING', cls.hasResults ? 'rgba(45,184,127,.1)' : 'rgba(208,25,14,.1)'); } row.appendChild(label); row.appendChild(tags); box.appendChild(row); }); } } // Action buttons row var btnRow = document.createElement('div'); btnRow.style.cssText = 'display:flex;gap:6px;margin-top:14px'; var viewBtn = document.createElement('a'); viewBtn.href = ev.url; viewBtn.target = '_blank'; viewBtn.rel = 'noopener'; viewBtn.style.cssText = 'flex:1;text-align:center;padding:10px;background:rgba(208,25,14,.08);border:1px solid rgba(208,25,14,.2);color:var(--red);font-family:var(--head);font-weight:900;font-size:12px;letter-spacing:.1em;text-decoration:none;text-transform:uppercase'; viewBtn.textContent = 'View on MyRacePass'; btnRow.appendChild(viewBtn); if (detail && detail.times && detail.times.raceStart) { var resultsBtn = document.createElement('a'); resultsBtn.href = ev.url + '/races'; resultsBtn.target = '_blank'; resultsBtn.rel = 'noopener'; resultsBtn.style.cssText = 'flex:1;text-align:center;padding:10px;background:rgba(45,184,127,.06);border:1px solid rgba(45,184,127,.2);color:var(--green);font-family:var(--head);font-weight:900;font-size:12px;letter-spacing:.1em;text-decoration:none;text-transform:uppercase'; resultsBtn.textContent = 'Lineups & Results'; btnRow.appendChild(resultsBtn); } box.appendChild(btnRow); // MRP attribution footer var attr = document.createElement('div'); attr.style.cssText = 'font-family:var(--mono);font-size:7px;color:rgba(154,144,128,.35);letter-spacing:1px;margin-top:10px;text-align:right'; attr.textContent = 'DATA PROVIDED BY MYRACEPASS.COM'; box.appendChild(attr); // Refresh button (small) var refreshBtn = document.createElement('button'); refreshBtn.style.cssText = 'position:absolute;top:10px;right:10px;font-family:var(--mono);font-size:8px;color:var(--muted);background:none;border:none;cursor:pointer;letter-spacing:1px'; refreshBtn.textContent = '↻'; refreshBtn.title = 'Refresh MRP data'; refreshBtn.onclick = function() { _mrpCache = {}; // bust cache box.innerHTML = ''; var h2 = document.createElement('div'); h2.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:8px'; h2.innerHTML = '
MyRacePass
' + '
POWERED BY MYRACEPASS.COM
'; box.appendChild(h2); var st = document.createElement('div'); st.style.cssText = 'font-family:var(--mono);font-size:9px;color:var(--muted);letter-spacing:1px'; st.textContent = 'REFRESHING...'; box.appendChild(st); buildMRPSection.refreshing = true; mrpGetTonightEvent(S.curTrack.mrp_track_id).then(function(newEv) { if (!newEv) { st.textContent = 'No events found'; return; } _mrpEventData = newEv; Promise.all([mrpGetEventDetail(newEv.eventId), mrpGetEventRaces(newEv.eventId)]).then(function([det,rac]) { box.innerHTML = ''; box.appendChild(h2); renderMRPEvent(box, newEv, det, rac); }); }); }; box.appendChild(refreshBtn); } // ── Latest Results for this track ──────────────────────────────────────────── async function mrpGetLatestResults(mrpTrackId) { var html = await mrpFetch('/tracks/' + mrpTrackId + '/schedule'); if (!html) return []; var text = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' '); // Find events with "Results" link var resultsPattern = /(\w+,\s+\w+\s+\d+,\s+\d{4})\s+([^\n]+?)\s+Results/g; var found = []; var m; var seen = {}; while ((m = resultsPattern.exec(text)) !== null) { var dateStr = m[1]; var eventText = m[2].trim(); if (!seen[dateStr]) { seen[dateStr] = true; // Find event ID near this position in the raw HTML var rawIdx = html.search(new RegExp(dateStr.replace(/[.*+?^${}()|[\]\\]/g,'\\if("serviceWorker" in navigator)'))); var nearIds = [...html.slice(Math.max(0,rawIdx-100),rawIdx+200).matchAll(/\/events\/(\d+)/g)].map(mm=>parseInt(mm[1])); if (nearIds[0]) found.push({ date: dateStr, name: eventText, eventId: nearIds[0], url: 'https://www.myracepass.com/events/' + nearIds[0] }); } if (found.length >= 5) break; } return found; } // ── Search for a driver across MRP ─────────────────────────────────────────── async function mrpSearchDriver(carNum) { // Use MRP's public driver search (no auth needed) // Drivers list their car number — we can link to driver search results var searchUrl = 'https://www.myracepass.com/drivers?search=' + encodeURIComponent(carNum); return searchUrl; } // ─── Offline / Service Worker Registration + IndexedDB Cache ───────────────── // Injected before `if("serviceWorker" in navigator)` in bb-merged-final.html // Provides: offline app shell, cached class/track data, cached Hunter responses, // offline data entry queue that syncs when back online. // ── IndexedDB wrapper ──────────────────────────────────────────────────────── var _IDB = null; function idbOpen() { if (_IDB) return Promise.resolve(_IDB); return new Promise(function(res, rej) { var req = indexedDB.open('bb_offline', 2); req.onupgradeneeded = function(e) { var db = e.target.result; if (!db.objectStoreNames.contains('cache')) db.createObjectStore('cache', { keyPath: 'key' }); if (!db.objectStoreNames.contains('queue')) db.createObjectStore('queue', { keyPath: 'id', autoIncrement: true }); if (!db.objectStoreNames.contains('hunter')) db.createObjectStore('hunter', { keyPath: 'key' }); }; req.onsuccess = function(e) { _IDB = e.target.result; res(_IDB); }; req.onerror = function(e) { rej(e); }; }); } function idbSet(store, key, val) { return idbOpen().then(function(db) { return new Promise(function(res, rej) { var tx = db.transaction(store, 'readwrite'); tx.objectStore(store).put({ key: key, val: val, ts: Date.now() }); tx.oncomplete = res; tx.onerror = rej; }); }); } function idbGet(store, key) { return idbOpen().then(function(db) { return new Promise(function(res, rej) { var req = db.transaction(store, 'readonly').objectStore(store).get(key); req.onsuccess = function(e) { res(e.target.result ? e.target.result.val : null); }; req.onerror = rej; }); }); } function idbEnqueue(store, val) { return idbOpen().then(function(db) { return new Promise(function(res, rej) { var tx = db.transaction(store, 'readwrite'); var req = tx.objectStore(store).add({ val: val, ts: Date.now() }); req.onsuccess = function(e) { res(e.target.result); }; tx.onerror = rej; }); }); } function idbGetAll(store) { return idbOpen().then(function(db) { return new Promise(function(res, rej) { var req = db.transaction(store, 'readonly').objectStore(store).getAll(); req.onsuccess = function(e) { res(e.target.result || []); }; req.onerror = rej; }); }); } function idbDelete(store, id) { return idbOpen().then(function(db) { return new Promise(function(res, rej) { var tx = db.transaction(store, 'readwrite'); tx.objectStore(store).delete(id); tx.oncomplete = res; tx.onerror = rej; }); }); } // ── Offline status detection ───────────────────────────────────────────────── var _bbOnline = navigator.onLine; function _setOnlineStatus(online) { _bbOnline = online; var banner = document.getElementById('offline-banner'); if (!banner) { banner = document.createElement('div'); banner.id = 'offline-banner'; banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:9999;background:var(--dark2,#141311);border-bottom:1px solid rgba(200,150,10,.3);padding:6px 16px;font-family:var(--mono,"Share Tech Mono",monospace);font-size:9px;letter-spacing:2px;color:var(--amber,#F5A623);text-align:center;display:none;transition:all .3s'; banner.textContent = '⚡ OFFLINE — HUNTER ALGORITHM ACTIVE · DATA SAVES LOCALLY · SYNCS WHEN CONNECTED'; document.body && document.body.prepend(banner); } banner.style.display = online ? 'none' : 'block'; if (online) { _syncOfflineQueue(); } } window.addEventListener('online', function() { _setOnlineStatus(true); }); window.addEventListener('offline', function() { _setOnlineStatus(false); }); _setOnlineStatus(navigator.onLine); // ── Cache class + track data from bb-classes on load ───────────────────────── var _origLoadClasses = null; function _patchLoadClasses() { if (typeof loadClasses !== 'function') { setTimeout(_patchLoadClasses, 300); return; } var _orig = loadClasses; window.loadClasses = async function() { if (!_bbOnline) { // Serve from IndexedDB cache var cached = await idbGet('cache', 'bb_classes'); if (cached) { try { // Replay the class data exactly as the live fetch would var d = JSON.parse(cached); S.classData = d.divisions || []; if (typeof populateClassDropdown === 'function') populateClassDropdown(); S.tracks = (d.tracks||[]).map(function(t){ return{name:t[0],short:t[1],lat:t[2],lon:t[3],size:t[4],banking:t[5],surface:t[6],city:t[7],state:t[8]}; }); if (S.cur && typeof showSetup === 'function') showSetup(S.cur); console.log('[BB offline] Class/track data served from cache'); return; } catch(e) {} } console.log('[BB offline] No cached class data — some features unavailable'); return; } // Online: fetch normally then cache result try { var r = await fetch(CLU); var d = await r.json(); await idbSet('cache', 'bb_classes', JSON.stringify(d)); // Also inject into live state manually (re-call original logic) S.classData = d.divisions || []; if (typeof populateClassDropdown === 'function') populateClassDropdown(); S.tracks = (d.tracks||[]).map(function(t){ return{name:t[0],short:t[1],lat:t[2],lon:t[3],size:t[4],banking:t[5],surface:t[6],city:t[7],state:t[8]}; }); if (S.cur && typeof showSetup === 'function') showSetup(S.cur); } catch(e) { console.warn('[BB] loadClasses failed, trying cache'); window.loadClasses = _orig; _orig(); } }; } setTimeout(_patchLoadClasses, 100); // ── Hunter response caching ─────────────────────────────────────────────────── // Cache key: track_slug + class_name + da_band (rounded to nearest 500ft) function hunterCacheKey(ctx) { var t = S.curTrack ? S.curTrack.name.toLowerCase().replace(/\W+/g,'-') : 'notrack'; var c = (S.cur && (S.cur.name||S.cur.class_name)||'nocls').toLowerCase().replace(/\W+/g,'-'); var da = Math.round(((S.weather&&S.weather.density_altitude)||0) / 500) * 500; return 'hunter:' + t + ':' + c + ':da' + da; } function hunterCacheSave(query, response) { var key = hunterCacheKey({}); idbSet('hunter', key, JSON.stringify({ q: query, r: response, ts: Date.now() })).catch(function(){}); } async function hunterCacheLoad(query) { var key = hunterCacheKey({}); var raw = await idbGet('hunter', key).catch(function(){return null;}); if (!raw) return null; try { var obj = JSON.parse(raw); // Expire after 4 hours if (Date.now() - obj.ts > 4*3600*1000) return null; return obj; } catch(e) { return null; } } // ── Offline data entry queue ────────────────────────────────────────────────── // Call this instead of direct Supabase saves when offline async function bbOfflineSave(type, data) { await idbEnqueue('queue', { type: type, data: data }); if (typeof toast === 'function') toast('Saved offline — will sync when connected'); } async function _syncOfflineQueue() { var items = await idbGetAll('queue').catch(function(){ return []; }); if (!items.length) return; var synced = 0; for (var i = 0; i < items.length; i++) { var item = items[i]; try { var ok = await _syncItem(item.val); if (ok) { await idbDelete('queue', item.id); synced++; } } catch(e) {} } if (synced > 0 && typeof toast === 'function') toast(synced + ' offline item' + (synced>1?'s':'') + ' synced'); } async function _syncItem(item) { var ANON = AK || ''; var SB_URL = SB || 'https://zmrouoqututfndplboyc.supabase.co'; if (item.type === 'setup_save') { var r = await fetch(SB_URL + '/rest/v1/bb_setups', { method: 'POST', headers: { 'apikey': ANON, 'Authorization': 'Bearer ' + ANON, 'Content-Type': 'application/json', 'Prefer': 'resolution=merge-duplicates' }, body: JSON.stringify(item.data) }); return r.ok; } if (item.type === 'log_save') { var r2 = await fetch(SB_URL + '/rest/v1/bb_logs', { method: 'POST', headers: { 'apikey': ANON, 'Authorization': 'Bearer ' + ANON, 'Content-Type': 'application/json', 'Prefer': 'resolution=merge-duplicates' }, body: JSON.stringify(item.data) }); return r2.ok; } return false; // unknown type — leave in queue } // ── Patch Hunter API calls to use cache when offline ───────────────────────── function _patchHunterAPI() { // Find all fetch calls to HNTR endpoint and wrap them // We do this by patching window.fetch when offline var _origFetch = window.fetch; window.fetch = async function(url, opts) { var urlStr = (url || '').toString(); // If it's a Hunter API call and we're offline if (!_bbOnline && (urlStr.includes('hunter') || urlStr.includes('HNTR'))) { // Try algo first var q = ''; try { var body = JSON.parse((opts && opts.body) || '{}'); q = body.query || body.question || body.prompt || ''; } catch(e){} var algoResp = (typeof hunterAlgo === 'function') ? hunterAlgo(q) : null; if (algoResp) { // Return synthetic Response with algo answer var payload = JSON.stringify({ response: algoResp.answer + '\n\n' + algoResp.action, offline: true, cached: false }); return new Response(payload, { status: 200, headers: { 'Content-Type': 'application/json' } }); } // Try cache var cached = await hunterCacheLoad(q).catch(function(){return null;}); if (cached) { var age = Math.round((Date.now() - cached.ts) / 60000); var payload2 = JSON.stringify({ response: '[CACHED ' + age + 'min ago]\n\n' + cached.r, offline: true, cached: true }); return new Response(payload2, { status: 200, headers: { 'Content-Type': 'application/json' } }); } // Nothing — return graceful error var payload3 = JSON.stringify({ response: 'No signal. Hunter is running in algorithm mode — ask about setup, stagger, tire pressure, or what to change between heats.', offline: true, cached: false }); return new Response(payload3, { status: 200, headers: { 'Content-Type': 'application/json' } }); } // Online: normal fetch, but cache Hunter responses var resp = await _origFetch.apply(this, arguments); if (_bbOnline && urlStr.includes('hunter') && resp.ok) { resp.clone().json().then(function(d) { var q2 = ''; try { var b2 = JSON.parse((opts && opts.body) || '{}'); q2 = b2.query||b2.question||b2.prompt||''; } catch(e){} hunterCacheSave(q2, d.response || d.answer || ''); }).catch(function(){}); } return resp; }; } setTimeout(_patchHunterAPI, 50); // ── Service worker registration ─────────────────────────────────────────────── // The inline SW handles app shell caching var _SW_SCRIPT = ` var CACHE='bb-v1'; var SHELL=[location.pathname, location.href]; self.addEventListener('install', function(e) { e.waitUntil( caches.open(CACHE).then(function(c) { return c.addAll(SHELL); }).catch(function(){}) ); self.skipWaiting(); }); self.addEventListener('activate', function(e) { e.waitUntil( caches.keys().then(function(keys) { return Promise.all(keys.filter(function(k){return k!==CACHE;}).map(function(k){return caches.delete(k);})); }) ); self.clients.claim(); }); self.addEventListener('fetch', function(e) { var url = e.request.url; // Only cache-first for same-origin page requests, not API calls if (e.request.method !== 'GET') return; if (url.includes('supabase.co') || url.includes('allorigins') || url.includes('myracepass')) return; e.respondWith( caches.match(e.request).then(function(cached) { var fetchPromise = fetch(e.request).then(function(resp) { if (resp.ok && url.includes(location.hostname)) { var clone = resp.clone(); caches.open(CACHE).then(function(c){c.put(e.request, clone);}); } return resp; }).catch(function(){ return cached; }); return cached || fetchPromise; }) ); }); `; // ─── Hunter Algorithm Brain ─────────────────────────────────────────────────── // Handles ~90% of Hunter queries with deterministic physics/engineering logic. // Returns null when the query needs Claude AI (complex, novel, or narrative). // All formulas derived from dirt track engineering principles. // Injected before `if("serviceWorker" in navigator)` in bb-merged-final.html // ── Intent detection keywords ──────────────────────────────────────────────── var _HK = { stagger: /stagger|stag\b/i, tight: /tight|push|understeer|plowing|won't turn|can'?t turn|front tight|entry tight|mid tight|exit tight/i, loose: /loose|free|oversteer|spinning|swap|slid|slipping|wiggle|loose[\- ]in|loose[\- ]off/i, tire: /tire|tyre|pressure|psi|rubber/i, tireprep: /tire prep|prep|soften|harden|treat|wipe|roll|soak|durometer|duro\b|compound/i, spring: /spring|rate|coil|corner weight/i, shock: /shock|rebound|compression|valv/i, bar: /sway|torsion|bar|anti[\- ]roll/i, bite: /bite|wedge|panhard|j[\- ]bar|birdcage|rear weight/i, camber: /camber|caster|toe\b/i, da: /density altitude|d\.?a\.?|altitude|air|jetting|jet\b|carb/i, temp: /temp|hot|cold|heat|cool/i, rain: /rain|wet|mud|water|storm|slick track|sealed/i, fuel: /fuel|gas|lap|distance|how many laps/i, history: /last time|last week|before|previous|compared|diff/i, tonight: /tonight|should i run|what setup|recommend|start|where should/i, heat: /heat|between heat|consi|b[\- ]main|feature|main event/i, phase: /hot lap|qualify|warm up|practice/i, result: /finish|position|won|lost|moved up|fell back|gained|dropped|started|finish/i, setup: /setup|set up|what do i change|what should i change|adjust/i, gear: /gear|sprocket|fdr|final drive|rollout|axle tooth|driver tooth/i, rules: /minimum weight|weight minimum|legal weight|spec tire|sealed|lo206 rules|clone rules|can i jet|jetting/i, pyro: /pyrometer|pyro|inner.*outer|hot spot|tire temp|read my tire/i, }; function _kartClassKey() { var k = typeof _bestClassMatch === 'function' ? _bestClassMatch() : ''; if (k && KART_RULES[k]) return k; var cls = (S.cur && (S.cur.class || S.cur.car_class || S.cur.name || '')) || ''; var lc = cls.toLowerCase(); if (/bandolero/.test(lc)) return 'Bandolero'; if (/quarter/.test(lc)) return 'Quarter Midget'; if (/outlaw/.test(lc)) return 'Outlaw Kart'; if (/clone.*jun|junior.*clone/.test(lc)) return 'Kart Clone Junior'; if (/clone/.test(lc)) return 'Kart Clone'; if (/206.*jun|junior.*206/.test(lc)) return 'Kart LO206 Junior'; if (/206.*sport|sportsman/.test(lc)) return 'Kart LO206 Sportsman'; if (/206|briggs|lo206/.test(lc)) return 'Kart LO206 Senior'; return 'Kart LO206 Senior'; } function _kartTrackKey(sz) { var l = (sz || '').toLowerCase(); if (/1\/10|0\.1/.test(l)) return '1/10'; if (/1\/8|0\.125|eighth/.test(l)) return '1/8'; if (/1\/6|sixth/.test(l)) return '1/6'; if (/1\/5|0\.2|fifth/.test(l)) return '1/5'; if (/1\/4|quarter/.test(l)) return '1/4'; if (/3\/8/.test(l)) return '3/8'; return ''; } function hunterKartGear(q, ctx) { if (_algoCarType() !== 'kart') return null; var k = _kartClassKey(); var gd = GEAR_DB[k]; if (!gd) return null; var track = ctx.track || S.curTrack || {}; var sz = (track.size || track.sz || '') + ' ' + (q || ''); var tk = _kartTrackKey(sz); if (!tk) { return { algo: true, answer: k + ' — RPM ' + gd.rpm[0] + '-' + gd.rpm[1], detail: gd.note, action: 'Pick track size: 1/10, 1/8, 1/6, 1/5, or 1/4 mile. Chain: 10T driver typical.', confidence: 'HIGH' }; } var rng = gd.tracks[tk]; if (!rng) return null; var fdrLo = rng[0].toFixed(2), fdrHi = rng[1].toFixed(2); var cur = (S.cur && S.cur.drive_sprocket && S.cur.driven_sprocket) ? (S.cur.driven_sprocket / S.cur.drive_sprocket).toFixed(2) : null; return { algo: true, answer: k + ' on ' + tk + ' mile — FDR ' + fdrLo + ' to ' + fdrHi, detail: gd.note + (cur ? ' · You are at ' + cur + ' FDR now.' : ''), action: '10T driver typical. If you hit the limiter before the flag stand, go 1 tooth taller on the axle. Never bounce the limiter in junior classes.', confidence: 'HIGH', }; } function hunterKartRules(q) { if (_algoCarType() !== 'kart') return null; var k = _kartClassKey(); var kr = KART_RULES[k]; if (!kr) return null; var l = (q || '').toLowerCase(); if (/jet|carb|fuel/.test(l)) { return { algo: true, answer: kr.sealed ? 'SEALED — no jetting' : 'Open class — check local fuel/carb rules', detail: kr.engine, action: kr.sealed ? 'LO206/Clone is sealed. Carb jetting is not a setup tool — tire, seat, and rear width are.' : 'Outlaw — verify fuel type and restrictor with tech.', confidence: 'HIGH' }; } return { algo: true, answer: k + ' — min ' + kr.weightMin + ' lb w/driver', detail: 'Left side target ' + kr.leftPct + ' · ' + kr.specTire, action: 'SEALED: ' + (kr.sealed ? 'yes' : 'no') + '\nRPM band: ' + kr.rpm + '\nMove the seat before you add lead. Seat IS the ballast on a kart.', confidence: 'HIGH', }; } function hunterPyrometer(q) { if (_algoCarType() !== 'kart') return null; var carId = S.cur ? (S.cur.id || 'local') : 'demo'; var tD = null; try { tD = JSON.parse(localStorage.getItem('bb_tire_' + carId) || 'null'); } catch (e) {} if (!tD) return { algo: true, answer: 'No tire log yet', detail: 'Log pyrometer in Extras → Tire Log', action: 'Inner / center / outer °F on each corner after hot laps. I read it from your garage automatically.', confidence: 'MEDIUM' }; var issues = [], ok = []; ['rr','lr','rf','lf'].forEach(function(p) { var t = tD[p]; if (!t) return; var inn = t.pyro_in, mid = t.pyro_mid, out = t.pyro_out; if (inn == null && mid == null && out == null) return; var vals = [inn, mid, out].filter(function(v){ return v != null; }); if (!vals.length) return; var spread = Math.max.apply(null, vals) - Math.min.apply(null, vals); var label = (t.pos || p.toUpperCase()); if (spread > 25) issues.push(label + ': ' + spread + '°F spread — check pressure/camber'); else if (out != null && inn != null && out > inn + 15) issues.push(label + ': outside hot — less pressure or camber'); else if (inn != null && out != null && inn > out + 15) issues.push(label + ': inside hot — too much camber'); else ok.push(label + ': ' + vals.join('/') + '°F balanced'); }); if (issues.length) return { algo: true, answer: 'PYRO — fix ' + issues[0], detail: ok.join(' · '), action: issues.join('\n') + '\n\nOne change, re-run, re-check.', confidence: 'HIGH' }; if (ok.length) return { algo: true, answer: 'PYRO looks balanced', detail: ok.join(' · '), action: 'Compare RR vs LR — RR hotter on dirt is normal. If RR is much hotter, you may be overworking the right rear on exit.', confidence: 'HIGH' }; return { algo: true, answer: 'Log pyrometer readings', detail: 'Tire Log → inner/center/outer °F per corner', action: 'After hot laps, before you change anything.', confidence: 'MEDIUM' }; } // ── Track size → stagger baseline table (inches) ───────────────────────────── // Based on track radius geometry. More banking = add 1-2". Slick = subtract 1". var _STAGGER_BASE = { '1/4': { min:2, rec:3, max:5 }, '3/8': { min:3, rec:5, max:7 }, '1/2': { min:4, rec:6, max:9 }, '5/8': { min:5, rec:7, max:10 }, '3/4': { min:6, rec:8, max:12 }, '1': { min:7, rec:10, max:14 }, 'mile': { min:8, rec:11, max:15 }, }; // ── DA tire pressure adjustment table (psi delta per 1000ft DA) ─────────────── // Higher DA = less air density = tire runs cooler = can run slightly higher pressure // But heat buildup reduced, so typically -0.5 psi per 2000ft DA var _DA_TIRE_FACTOR = -0.25; // psi per 1000ft DA above sea level baseline // ── Stagger algorithm ──────────────────────────────────────────────────────── // ── Division + Wind + Wing helpers for all Hunter algos ────────────────────── function _algoCarType() { var cls = (S.cur && (S.cur.class || S.cur.car_class || S.cur.name || S.cur.class_name || '')) || ''; var lc = cls.toLowerCase(); if (/kart|lo.?206|clone|quarter.mid/i.test(lc)) return 'kart'; if (/micro|600|now600|restricted.micro/i.test(lc)) return 'micro'; if (/sprint|midget|305|360|410|usac/i.test(lc)) return 'sprint'; if (/late.model|lm|604|602|crate|super late|pro late|limited late/i.test(lc)) return 'latemodel'; if (/modified|mod|b.mod|sport.mod|mod.lite|imca|ump/i.test(lc)) return 'modified'; if (/stock|bomber|hobby|pure|compact|street/i.test(lc)) return 'stock'; return 'generic'; } function _isSealedEngine() { var cls = (S.cur && (S.cur.name || S.cur.class_name || S.cur.class || '')) || ''; return /602|604|crate|sealed|spec|lo.?206|clone/i.test(cls); } function _isRestrictedKart() { var cls = (S.cur && (S.cur.name || S.cur.class_name || S.cur.class || '')) || ''; return /lo.?206|clone|restricted|junior|cadet|kid|sportsman/i.test(cls); } function _isRulesLimited() { var ct = _algoCarType(); return ct === 'stock' || ct === 'kart' || /imca/i.test((S.cur && (S.cur.name || S.cur.class_name || '')) || ''); } function _isWingCar() { var cls = (S.cur && (S.cur.name || S.cur.class_name || S.cur.class || '')) || ''; return /wing/i.test(cls) && !/non.wing|non-wing/i.test(cls); } function _windAero() { var wx = S.wx || S.weather || {}; var wind = Math.round(wx.wind_speed || wx.windMph || 0); var dir = (wx.wind_dir || wx.windDir || 'N').toUpperCase(); if (wind < 5) return null; var angle = {N:0,NNE:22,NE:45,ENE:67,E:90,ESE:112,SE:135,SSE:157,S:180,SSW:202,SW:225,WSW:247,W:270,WNW:292,NW:315,NNW:337}[dir] || 0; var e1_headwind = Math.cos(angle * Math.PI / 180); var e2_headwind = -e1_headwind; var crosswind = Math.abs(Math.sin(angle * Math.PI / 180)); return { wind: wind, dir: dir, angle: angle, e1_headwind: e1_headwind, e2_headwind: e2_headwind, crosswind: crosswind, e1_effect: e1_headwind > 0.3 ? 'headwind' : e1_headwind < -0.3 ? 'tailwind' : 'crosswind', e2_effect: e2_headwind > 0.3 ? 'headwind' : e2_headwind < -0.3 ? 'tailwind' : 'crosswind', }; } function hunterStagger(ctx) { if (_algoCarType() === 'kart') { var rw = (_su && _su.rear_track_width_mm) || 44; var ax = (_su && _su.axle_stiffness) || 'medium'; return { algo: true, answer: 'KART ROTATION: rear track width, not inch stagger', detail: 'Rear width ' + rw + 'mm · Axle: ' + ax, action: 'Widen rear 2-5mm to free the kart. Narrow 2-5mm to tighten. LO206 left side 56-62% — move seat before adding lead.', confidence: 'HIGH', }; } var tgt = _resolveStaggerTarget({ track: (ctx && ctx.track) || S.curTrack, carType: _algoCarType() }); if (tgt.kart) { return { algo: true, answer: 'KART ROTATION: rear track width, not inch stagger', detail: tgt.note || '', action: 'Use Garage setup for rear track width (mm) and axle stiffness.', confidence: 'HIGH', }; } var wingWind = ''; if (_isWingCar() && typeof _windAero === 'function') { var aero = _windAero(); if (aero && aero.crosswind > 0.7) wingWind = '\nWIND ON WING: Crosswind — wing acts as sail; stagger tuned for tonight.'; else if (aero && (aero.e1_effect === 'headwind' || aero.e2_effect === 'headwind')) wingWind = '\nWIND ON WING: Headwind adds aero grip — less stagger than calm air.'; } return { algo: true, answer: 'STAGGER: ' + tgt.rec + '" tonight (class ' + tgt.rear[0] + '–' + tgt.rear[1] + '")', detail: tgt.notes.join(' · '), action: 'Measure cold in pits. Class target ' + tgt.rec + '" hot (RR−LR). Source: ' + (tgt.source === 'bb-classes' ? tgt.className + ' bb-classes' : 'matrix fallback') + (tgt.anchor ? ' · See ' + tgt.anchor.mfr + ' setup sheet.' : '') + wingWind, confidence: tgt.source === 'bb-classes' ? 'HIGH — class rules on file' : (S.curTrack ? 'MEDIUM — pick exact class in Garage' : 'MEDIUM'), }; } // ── Tight/understeer diagnosis ─────────────────────────────────────────────── function hunterTight(q, ctx) { var where = /entry|turn in|corner entry/i.test(q) ? 'entry' : /exit|drive off|off corner|acceleration/i.test(q) ? 'exit' : /center|mid|middle|middle of/i.test(q) ? 'center' : 'all'; var setup = (ctx && ctx.setup) || (S.cur && S.cur.setup) || {}; var ct = _algoCarType(); // Kart: no suspension if (ct === 'kart') { var isR = _isRestrictedKart(); var kF = [];var kE = ''; var sn = isR ? '\nRESTRICTED: Every fix must CARRY speed, not scrub it. Small changes only.' : ''; if (where==='entry'||where==='all'){kF.push({change:'Move seat forward 15-20mm',why:'More front weight for entry bite'});kF.push({change:'Increase caster 0.5-1°',why:'More front grip turning in'+(isR?' — too much scrubs speed on straight':'')});kE='Tight on entry = not enough front weight.';} if (where==='center'||where==='all'){kF.push({change:'Softer rear axle',why:'More flex = rear lifts inside tire to rotate'});kF.push({change:'Raise seat 10mm',why:'Higher CG = more weight transfer'});kE=kE||'Tight in center = chassis too stiff.';} if (where==='exit'||where==='all'){kF.push({change:'Move seat back 10mm',why:'Plants rear under throttle'});kF.push({change:'Widen rear track 5mm/side',why:'More rear grip off corner'});kE=kE||'Tight on exit = rear not hooking.';} return {algo:true,answer:'TIGHT '+where.toUpperCase()+' (KART'+(isR?' RESTRICTED':'')+'): '+kF.slice(0,2).map(function(f){return f.change;}).join(' / '),detail:kE,action:kF.slice(0,3).map(function(f,i){return(i+1)+'. '+f.change+' — '+f.why;}).join('\n')+sn,confidence:'HIGH'}; } // Stock: rules-limited if (ct === 'stock') { var sF = [];var sE = ''; if (where==='entry'||where==='all'){sF.push({change:'Lower LF pressure 1 psi',why:'More front contact patch'});sF.push({change:'Add front weight if rules allow',why:'More nose weight = entry grip'});sE='Tight on entry — tire pressure and weight are your main tools.';} if (where==='center'||where==='all'){sF.push({change:'Increase stagger 0.5-1"',why:'More rotation'});sE=sE||'Tight in center — stagger and weight.';} if (where==='exit'||where==='all'){sF.push({change:'Reduce RR pressure 1 psi',why:'More RR grip off corner'});sE=sE||'Tight on exit — tire pressure is your best tool.';} return {algo:true,answer:'TIGHT '+where.toUpperCase()+' (STOCK): '+sF.slice(0,2).map(function(f){return f.change;}).join(' / '),detail:sE+' Check class rules.',action:sF.slice(0,3).map(function(f,i){return(i+1)+'. '+f.change+' — '+f.why;}).join('\n'),confidence:'HIGH'}; } var fixes = []; var explain = ''; if (where === 'entry' || where === 'all') { fixes.push({ change: 'Reduce front spring rate (LF/RF) 25–50 lbs/in', why: 'Lets front roll more on entry' }); fixes.push({ change: 'Add LF stagger (loosen LF tire pressure 1–2 psi)', why: 'More front bite on entry' }); fixes.push({ change: 'Loosen front sway bar one hole or disconnect', why: 'More front body roll = more front grip' }); explain = 'Tight on entry = front tires can\'t rotate fast enough relative to rears. Car needs more front grip or less front resistance.'; } if (where === 'center' || where === 'all') { fixes.push({ change: 'Add 1/4 turn LR bite (raise LR birdcage or panhard bar left)', why: 'More rear grip through center' }); fixes.push({ change: 'Increase stagger 1"', why: 'More stagger helps rotation through center' }); explain = explain || 'Tight in center = car won\'t rotate. Need to free up the rear or add more stagger.'; } if (where === 'exit' || where === 'all') { fixes.push({ change: 'Add LR bite (wedge)', why: 'Plants left rear under acceleration' }); fixes.push({ change: 'Increase RR spring rate 25 lbs/in', why: 'Controls RR roll on acceleration' }); explain = explain || 'Tight on exit = rear not hooking up. Add rear bite and check stagger.'; } // Wing car + wind awareness var wingNote = ''; if (_isWingCar()) { var aero = _windAero(); if (aero) { var rEnd = /end.?2|turn.?3|turn.?4|t3|t4/i.test(q) ? 2 : 1; var eff = rEnd === 1 ? aero.e1_effect : aero.e2_effect; if (eff === 'headwind') { wingNote = '\n\nWING + WIND: '+aero.wind+'mph '+aero.dir+' headwind into End '+rEnd+'. Extra downforce pushes you to apex. This is AERO TIGHT — try less wing angle before changing suspension.'; fixes.unshift({change:'Reduce wing angle 2-3°',why:'Headwind = excess front downforce'}); } else if (eff === 'tailwind') { wingNote = '\n\nWING + WIND: Tailwind into End '+rEnd+' = no aero help. This IS chassis tight. Suspension changes will help.'; } else if (aero.crosswind > 0.7) { wingNote = '\n\nWING + WIND: '+aero.wind+'mph crosswind. Wing acts as sail — car gets dragged wide.'; } } } return { algo: true, answer: 'TIGHT ' + where.toUpperCase() + ': ' + fixes.slice(0,2).map(f=>f.change).join(' / '), detail: explain, action: fixes.slice(0,3).map((f,i)=>(i+1)+'. '+f.change+' — '+f.why).join('\n') + wingNote, confidence: 'HIGH', }; } // ── Loose/oversteer diagnosis ───────────────────────────────────────────────── function hunterLoose(q, ctx) { var where = /entry|turn in|corner entry/i.test(q) ? 'entry' : /exit|drive off|off corner|accelerat/i.test(q) ? 'exit' : /center|mid|middle/i.test(q) ? 'center' : 'all'; var ct = _algoCarType(); if (ct === 'kart') { var isR = _isRestrictedKart(); var kF = [];var kE = ''; var sn = isR ? '\nRESTRICTED: A loose kart scrubs speed through SLIDING. Fix the cause, don\'t just add grip.' : ''; if (where==='entry'||where==='all'){kF.push({change:'Move seat back 10-15mm',why:'More rear weight'});kF.push({change:'Reduce caster 0.5°',why:'Less front grip turning in'});kE='Loose on entry = rear is light.';} if (where==='center'||where==='all'){kF.push({change:'Stiffer rear axle',why:'Less flex = less rear slide'});kF.push({change:'Lower seat 10mm',why:'Lower CG = more stable'});kE=kE||'Loose in center = too much rotation.';} if (where==='exit'||where==='all'){kF.push({change:'Widen rear track 5mm/side',why:'More mechanical rear grip'});kF.push({change:'Add RR pressure '+(isR?'0.25':'0.5')+' psi',why:'Stiffer sidewall under throttle'});kE=kE||'Loose on exit = rear not planting.';} return {algo:true,answer:'LOOSE '+where.toUpperCase()+' (KART'+(isR?' RESTRICTED':'')+'): '+kF.slice(0,2).map(function(f){return f.change;}).join(' / '),detail:kE,action:kF.slice(0,3).map(function(f,i){return(i+1)+'. '+f.change+' — '+f.why;}).join('\n')+sn,confidence:'HIGH'}; } if (ct === 'stock') { var sF = [];var sE = ''; if (where==='entry'||where==='all'){sF.push({change:'Add RF pressure 1 psi',why:'Less front grip = calmer entry'});sE='Loose on entry — tire pressure and weight.';} if (where==='center'||where==='all'){sF.push({change:'Reduce stagger 0.5"',why:'Less rotation'});sE=sE||'Loose in center — reduce stagger.';} if (where==='exit'||where==='all'){sF.push({change:'Add RR pressure 1 psi',why:'More RR stiffness'});sE=sE||'Loose on exit — tire pressure is your tool.';} return {algo:true,answer:'LOOSE '+where.toUpperCase()+' (STOCK): '+sF.slice(0,2).map(function(f){return f.change;}).join(' / '),detail:sE+' Check class rules.',action:sF.slice(0,3).map(function(f,i){return(i+1)+'. '+f.change+' — '+f.why;}).join('\n'),confidence:'HIGH'}; } var fixes = []; var explain = ''; if (where === 'entry' || where === 'all') { fixes.push({ change: 'Increase front spring rate 25–50 lbs/in', why: 'Resists front roll on entry' }); fixes.push({ change: 'Tighten front sway bar one hole', why: 'Less front roll = more stable entry' }); fixes.push({ change: 'Add RF stagger (raise RF tire pressure 1 psi)', why: 'Tightens entry rotation' }); explain = 'Loose on entry = rear coming around before the front has grip. Stiffen front or tighten rotation.'; } if (where === 'center' || where === 'all') { fixes.push({ change: 'Reduce stagger 1"', why: 'Less stagger = less rotation tendency' }); fixes.push({ change: 'Remove 1/4 turn LR bite', why: 'Reduces rear roll steer through center' }); explain = explain || 'Loose in center = too much rotation. Reduce stagger and check rear geometry.'; } if (where === 'exit' || where === 'all') { fixes.push({ change: 'Remove LR bite (lower wedge)', why: 'Reduces rear lift under throttle' }); fixes.push({ change: 'Reduce RR stagger or add RR pressure 1 psi', why: 'Grounds RR harder under power' }); fixes.push({ change: 'Increase RR rebound (shock)', why: 'Controls rear squat on acceleration' }); explain = explain || 'Loose off exit = rear stepping out under power. Remove bite and control rear rebound.'; } // Wing car + wind awareness var wingNote = ''; if (_isWingCar()) { var aero = _windAero(); if (aero) { var rEnd = /end.?2|turn.?3|turn.?4|t3|t4/i.test(q) ? 2 : 1; var eff = rEnd === 1 ? aero.e1_effect : aero.e2_effect; if (eff === 'tailwind') { wingNote = '\n\nWING + WIND: '+aero.wind+'mph '+aero.dir+' tailwind into End '+rEnd+'. Less downforce = rear is light. This is AERO LOOSE — try more wing angle before adding wedge.'; fixes.unshift({change:'Increase wing angle 2-3°',why:'Tailwind = less effective downforce'}); } else if (eff === 'headwind' && (where==='exit'||where==='all')) { wingNote = '\n\nWING + WIND: Headwind loads wing on entry but UNLOADS on exit — that snap causes exit oversteer. Reduce wing 1° or add RR rebound.'; } else if (aero.crosswind > 0.7) { wingNote = '\n\nWING + WIND: '+aero.wind+'mph crosswind. Wing catches side wind like a sail.'; } } } return { algo: true, answer: 'LOOSE ' + where.toUpperCase() + ': ' + fixes.slice(0,2).map(f=>f.change).join(' / '), detail: explain, action: fixes.slice(0,3).map((f,i)=>(i+1)+'. '+f.change+' — '+f.why).join('\n') + wingNote, confidence: 'HIGH', }; } // ── Tire pressure algorithm ─────────────────────────────────────────────────── function hunterTires(q, ctx) { var da = parseFloat((ctx && ctx.da) || (S.weather && S.weather.density_altitude) || 0); var temp = parseFloat((ctx && ctx.temp) || (S.weather && S.weather.temp) || 70); var surf = ((ctx && ctx.track && ctx.track.surface) || (S.curTrack && S.curTrack.surface) || '').toLowerCase(); var cls = (S.cur && S.cur.name) || ''; // Base pressures by class type — division-specific var ct = _algoCarType(); var base, daScale; if (ct === 'kart') { base = {lf:5.5,rf:5.5,lr:6.5,rr:6.5}; daScale = 0.5; } else if (ct === 'micro') { base = {lf:7,rf:7,lr:8,rr:8}; daScale = 0.8; } else if (ct === 'latemodel' || ct === 'modified') { base = {lf:12,rf:12,lr:12,rr:12}; daScale = 1.0; } else if (ct === 'stock') { base = {lf:14,rf:14,lr:14,rr:14}; daScale = 1.0; } else { base = {lf:11,rf:11,lr:10,rr:10}; daScale = 1.0; } // DA adjustment — scaled by class var daAdj = (da / 1000) * _DA_TIRE_FACTOR * daScale; // Temp adjustment: cold air = tires swell less = slightly lower hot pressure var tempAdj = temp < 50 ? -0.5 : temp > 85 ? +0.5 : 0; // Surface adjustment var surfAdj = /hard|dry|black/i.test(surf) ? +1 : /tacky|cushy|rough/i.test(surf) ? -1 : 0; var psi = { lf: Math.round((base.lf + daAdj + tempAdj + surfAdj) * 2) / 2, rf: Math.round((base.rf + daAdj + tempAdj + surfAdj) * 2) / 2, lr: Math.round((base.lr + daAdj + tempAdj) * 2) / 2, rr: Math.round((base.rr + daAdj + tempAdj) * 2) / 2, }; var notes = []; if (Math.abs(daAdj) > 0.2) notes.push('DA ' + Math.round(da) + 'ft → ' + (daAdj>0?'+':'')+daAdj.toFixed(1)+' psi adj'); if (tempAdj !== 0) notes.push(Math.round(temp)+'°F → '+(tempAdj>0?'+':'')+tempAdj+' psi adj'); if (surfAdj !== 0) notes.push(surf+' surface → '+(surfAdj>0?'+':'')+surfAdj+' psi adj'); return { algo: true, answer: 'TIRE PRESSURES: LF '+psi.lf+' · RF '+psi.rf+' · LR '+psi.lr+' · RR '+psi.rr+' psi', detail: 'Cold-set (before racing). Check hot after 2–3 laps.' + (notes.length ? '\n'+notes.join(' · ') : ''), action: 'Set LF='+psi.lf+' RF='+psi.rf+' LR='+psi.lr+' RR='+psi.rr+' psi cold.\nAfter hot laps: target 2–3 psi gain on each corner.', confidence: da > 0 ? 'HIGH' : 'MEDIUM — no DA data, using temp/surface only', }; } // ── DA/jetting algorithm ────────────────────────────────────────────────────── function hunterDA(ctx) { var da = parseFloat((ctx && ctx.da) || (S.weather && S.weather.density_altitude) || 0); var temp = parseFloat((ctx && ctx.temp) || (S.weather && S.weather.temp) || 70); if (!da && !temp) return null; var fuelSys = (S.cur && (S.cur.fuel_system || S.cur.fuelSystem)) || ''; if (!fuelSys) { return { algo: true, answer: 'DA ' + Math.round(da) + 'ft — BUT I NEED TO KNOW YOUR FUEL SYSTEM', detail: 'Carb and injection get completely different DA advice.', action: 'Go to your car profile (Garage > tap your car > Engine section) and set CARB or INJECTION.\n\nAt ' + Math.round(da) + 'ft DA, a carb car needs ' + (da > 3000 ? 'aggressive jetting changes' : 'minor jetting tweaks') + '. An EFI car needs fuel map adjustments. Wrong advice = lost power or engine damage.', confidence: 'BLOCKED — set fuel system first', }; } var isEFI = /efi|inject|fi\b|throttle.body/i.test(fuelSys); var timingDelta = Math.round((da - 1000) / 2000 * -1); var sealed = _isSealedEngine(); if (isEFI) { var fuelPct = da > 5000 ? -3 : da > 3000 ? -2 : da > 1500 ? -1 : 0; return { algo: true, answer: 'DA '+Math.round(da)+'ft (EFI): Fuel table '+(fuelPct!==0?(fuelPct>0?'+':'')+fuelPct+'%':'no change'), detail: 'EFI detected. Higher DA = less O2 = pull fuel to match.\nTemp: '+Math.round(temp)+'F DA: '+Math.round(da)+'ft', action: [ 'EFI fuel table: '+(fuelPct!==0?'lean '+Math.abs(fuelPct)+'% across main cells':'no change needed'), sealed ? 'Timing: SEALED ENGINE — not adjustable.' : (timingDelta !== 0 ? 'Ignition timing: '+(timingDelta<0?'retard':'advance')+' '+Math.abs(timingDelta)+'deg' : 'Timing: no change'), ].join('\n'), confidence: 'HIGH', }; } // Carburetor — class-aware baselines var ct = _algoCarType(); var baseJet = ct === 'kart' ? 38 : ct === 'micro' ? 58 : 78; var jetScale = ct === 'kart' ? 1.5 : ct === 'micro' ? 1.2 : 1.0; var jetDelta = Math.round((da - 1000) / 1000 * -2 * jetScale); var recJet = baseJet + jetDelta; // Fuel mixture (if EFI): leaner at high DA var mixture = da > 5000 ? 'lean 2% from baseline' : da > 3000 ? 'lean 1% from baseline' : 'run baseline'; return { algo: true, answer: 'DA '+Math.round(da)+'ft: Main jet ~'+recJet+(jetDelta!==0?' ('+( jetDelta>0?'+':'')+jetDelta+' from baseline)':''), detail: 'Density altitude affects fuel/air ratio. Higher DA = less oxygen = richer mixture = go leaner.\n' + 'Temp: '+Math.round(temp)+'°F · DA: '+Math.round(da)+'ft', action: [ (ct==='kart'?'Kart':'Carburetor')+' main jet: ' + recJet + (jetDelta!==0?' (change '+Math.abs(jetDelta)+' size'+(Math.abs(jetDelta)>1?'s':'')+' '+(jetDelta<0?'leaner':'richer')+')':''), sealed ? 'Timing: SEALED ENGINE — not adjustable. Focus on jetting and air filter.' : (timingDelta !== 0 ? 'Ignition timing: '+(timingDelta<0?'retard':'advance')+' '+Math.abs(timingDelta)+'° from baseline' : 'Timing: no change needed'), ct==='kart' ? 'Kart tip: small engines lose power fast at high DA. Check air filter.' : 'Fuel mixture: '+mixture, ].join('\n'), confidence: 'HIGH', }; } // ── Rain/slick track algorithm ──────────────────────────────────────────────── function hunterRain(ctx) { var ct = _algoCarType(); var advice; if (ct === 'kart') { advice = ['1. Drop tire pressures 0.5 psi all around','2. Move seat back 15-20mm — more rear weight','3. Stiffer rear axle — less flex on wet','4. Reduce caster 1°','5. Widen rear track 5-10mm','6. Smooth throttle — wet dirt punishes aggression','7. Run high side where water pools less']; } else if (ct === 'stock') { advice = ['1. Drop tire pressures 1-2 psi all around','2. Add stagger 0.5-1"','3. Move weight left if rules allow','4. Smooth driving — no sudden inputs','5. Check for standing water in corners','6. Reduce rear pressure 0.5 more than fronts']; } else { advice = ['1. Increase stagger 1-2"','2. Drop ALL tire pressures 2 psi','3. Add LR bite (raise LR birdcage or add wedge)','4. Remove front sway bar or disconnect','5. Lower front spring rates '+(ct==='micro'?'15':'25')+' lbs/in','6. Raise front ride height slightly','7. Softer RR shock compression']; } return {algo:true, answer:'WET TRACK'+(ct!=='generic'?' ('+ct.toUpperCase()+')':'')+': Setup changes for rain/slick', detail:'Wet or freshly sealed track changes grip dramatically. Rear bite becomes critical.', action:advice.join('\n'), confidence:'HIGH'}; } // ── Between-heats algorithm ─────────────────────────────────────────────────── function hunterBetweenHeats(q, ctx) { // Try to extract start/finish positions var startMatch = q.match(/start(?:ed)?\s*(?:in|from|at|on|p)?\s*(\d+)(?:st|nd|rd|th)?/i); var finishMatch = q.match(/finish(?:ed)?\s*(?:in|on|at|p)?\s*(\d+)(?:st|nd|rd|th)?/i) || q.match(/(?:finish|came|run)\s+(\d+)(?:st|nd|rd|th)/i); var start = startMatch ? parseInt(startMatch[1]) : null; var finish = finishMatch ? parseInt(finishMatch[1]) : null; var isTight = _HK.tight.test(q); var isLoose = _HK.loose.test(q); var advice = []; var summary = ''; if (start !== null && finish !== null) { var delta = start - finish; // positive = moved up, negative = fell back if (delta > 0) { summary = 'Moved up ' + delta + ' spot' + (delta>1?'s':'') + ' — car working. Make small adjustments only.'; advice.push('Car is working. Resist the urge to make big changes.'); advice.push('Check tire wear — if even, your setup is dialed.'); if (isTight) advice.push('If still a little tight: try 1/4 turn more LR bite before feature.'); if (isLoose) advice.push('If still a little loose: +1 psi RR pressure, check stagger.'); } else if (delta < 0) { summary = 'Fell back ' + Math.abs(delta) + ' spot' + (Math.abs(delta)>1?'s':'') + ' — diagnose before feature.'; advice.push('Fell back — need to find the issue before the feature.'); if (isTight) { advice.push('Tight: loosen front sway bar 1 hole, add 1" stagger, drop LF pressure 1 psi'); } else if (isLoose) { advice.push('Loose: remove LR bite 1/4 turn, add RR pressure 1 psi, stiffen front bar'); } else { advice.push('Check tire temps — hot corner is overworked, cold corner has setup issue.'); } advice.push('Check for any damage: bent panhard, loose shock mount, tire rub.'); } else { summary = 'Held position — setup is neutral, execution is the factor.'; advice.push('Held position = car is setup-neutral. Work the restart and the slider.'); } } else if (isTight) { return hunterTight(q, ctx); } else if (isLoose) { return hunterLoose(q, ctx); } else { summary = 'Between heats: standard check routine.'; var ct2 = _algoCarType(); if (ct2 === 'kart') { var isR = _isRestrictedKart(); advice = [ '1. Check tire pressures — adjust in 0.25 psi increments' + (isR ? ' (restricted: this is your #1 tool)' : ''), '2. Check tire crown with straight edge — crowning evenly?', '3. Check chain tension and alignment', '4. Check axle and hub bolts — vibration loosens everything', '5. Check for mud packed in frame rails — extra weight kills restricted classes', '6. Feel tires by hand — even heat = good, hot spots = crown issue', ]; } else if (ct2 === 'stock') { advice = [ '1. Check all four tire pressures', '2. Verify stagger — tires grow with heat', '3. Check lug nuts — stock car wheels loosen on dirt', '4. Look for body damage or tire rub', '5. Check under car for hanging exhaust', '6. Drink water', ]; } else { advice = [ '1. Check all four tire temps (cross-reference with handling notes)', '2. Verify stagger front and rear — tires grow with heat', '3. Check lug nuts and any bodywork contact', '4. Add or remove 1/4 turn LR bite based on how rear felt', '5. Drink water — heat exhaustion is real', ]; } } return { algo: true, answer: summary, detail: start !== null ? 'Started P'+start+(finish?' · Finished P'+finish:'') : 'Between heats setup check', action: advice.join('\n'), confidence: 'HIGH', }; } // ── Tonight's setup recommendation ─────────────────────────────────────────── function hunterRoadTonight(ctx) { var track = S.curTrack; var cls = S.cur; if (!cls) return null; var wx = S.weather || S.wx || S.lastWeather || {}; var da = wx.density_altitude || wx.densityAltitude || 0; var temp = wx.temp || 70; var isMiata = _isSpecMiataClass(cls.class || cls.car_class || cls.name || ''); var bullets = []; if (track && track.name) { bullets.push('TRACK: ' + track.name + (track.size ? ' · ' + track.size : '') + ' · asphalt road course'); } else { bullets.push('Pick a road course (NTK Lewisville, Amarillo Motorsports Park) for track-specific notes.'); } if (isMiata) { bullets.push('SPEC MIATA: Scale cold — target cross 52–58%, left ~50–52%. Log corner weights before every session.'); bullets.push('TIRES: Cold LF/RF/LR/RR ~28–32 psi (spec tire). Hot target +2–4 psi — pyrometer if you have it.'); bullets.push('BRAKE BIAS: Start ~68% front; add rear if entry is stable but center pushes.'); } else { var rr = _roadRulesForCar(cls); bullets.push('ROAD KART: Rear track width (mm) + seat position rotate the kart — no inch stagger.'); if (rr && rr.gearing) bullets.push('GEAR: ' + rr.gearing); if (rr && rr.coldPsi) bullets.push('TIRES: Cold ' + rr.coldPsi + ' psi — check hot after 3 laps.'); if (rr && rr.leftPct) bullets.push('WEIGHT: Left side ' + rr.leftPct + ' typical for asphalt kart.'); } if (da > 3500) bullets.push('DA ' + Math.round(da) + 'ft — sealed kart/miata: focus on grip and tire temp, not jetting.'); if (temp < 55) bullets.push(Math.round(temp) + '°F — cold track: extra lap to build tire temp before pushing.'); else if (temp > 90) bullets.push(Math.round(temp) + '°F — manage tire deg; shorter stints, lower cold PSI if overheating.'); bullets.push('SESSION: One change at a time. Scale → hot laps → re-scale. Line over setup on road courses.'); return { algo: true, answer: 'TONIGHT' + (track && track.name ? ' AT ' + track.name.toUpperCase() : ' — ROAD COURSE'), detail: (isMiata ? 'Spec Miata' : (_roadProfileKey(cls.class || cls.car_class || '') || 'Road kart')) + ' · ' + Math.round(temp) + '°F · DA ' + Math.round(da) + 'ft', action: bullets.join('\n') + '\n\nUse Tools → Scale Sheet + Road Setup. No banking walk on asphalt.', confidence: track ? 'HIGH — road course profile' : 'MEDIUM — pick track for full read', }; } function hunterTonight(ctx) { var track = S.curTrack; var cls = S.cur; var wx = S.weather || S.lastWeather || {}; var hist = S.lastTrackSetup; if (_getCarDiscipline(cls) === 'road' || _isRoadTrack(track)) return hunterRoadTonight(ctx); if (!track || !cls) return null; // need context var da = wx.density_altitude || 0; var temp = wx.temp || 70; var sz = (track.size || track.sz || '3/8'); var stag = hunterStagger(ctx); var tire = hunterTires('tire', ctx); var daAdv = da > 500 ? hunterDA(ctx) : null; var bullets = [ stag.answer, tire.answer, daAdv ? daAdv.answer : null, ].filter(Boolean); // Walk data — if available, surface grip intel var walkNote = ''; if (S._walkSummary) { var ws = S._walkSummary; var e1g = ws.e1 && ws.e1.grip, e2g = ws.e2 && ws.e2.grip; if (e1g !== null && e2g !== null) { var gripDiff = e2g - e1g; if (Math.abs(gripDiff) >= 8) { walkNote = (gripDiff > 0 ? 'End 2 grippier' : 'End 1 grippier') + ' by ' + Math.abs(gripDiff) + ' pts — adjust line accordingly.'; bullets.push(walkNote); } } if (ws.e1 && ws.e1.dominant_tag) bullets.push('Track condition: ' + ws.e1.dominant_tag + (ws.e2 && ws.e2.dominant_tag && ws.e2.dominant_tag !== ws.e1.dominant_tag ? ' (E1) / ' + ws.e2.dominant_tag + ' (E2)' : '')); // Banking profiles var profiles = ws.banking_narrative || {}; var peaked = Object.keys(profiles).filter(function(k){return profiles[k].indexOf('peaked')>-1;}); if (peaked.length) bullets.push(peaked.join(' & ') + ' banking is peaked mid-corner — entry/exit lighter than apex.'); } // Wind-aware surface prediction var wx = S.weather || S.lastWeather || {}; var dew = wx.dewpoint || wx.dew_point || 0; var dewSpread = temp - dew; var wind = wx.wind_speed || wx.windMph || 0; var windDir = (wx.wind_dir || wx.windDir || 'N').toUpperCase(); var humid = wx.humidity || 0; var hour = new Date().getHours(); var raceHour = S._raceStartHour || (hour < 18 ? 20 : hour + 2); var tempDrop = Math.max(0, Math.round((raceHour - hour) * 2.5)); var featureDew = dewSpread - tempDrop; // NOW var surfNow = dewSpread < 5 ? 'Heavy/tacky — dew spread '+Math.round(dewSpread)+'°' : dewSpread < 10 ? 'Tacky, transitioning' : dewSpread > 18 || wind > 15 ? 'Dry/dusty' : 'Moderate'; bullets.push('NOW: ' + surfNow); // BY FEATURE var surfLater = featureDew < 3 ? 'BY FEATURE (~'+raceHour+':00): heavy/tacky. Free the car up.' : featureDew < 8 ? 'BY FEATURE (~'+raceHour+':00): rubber building. Car may tighten.' : 'BY FEATURE (~'+raceHour+':00): still dry/slick.'; if (hour >= 19 && humid > 70) surfLater += ' High humidity + post-sunset = heavier each session.'; bullets.push(surfLater); // Wind per corner if (wind > 5) { var windAngle = {N:0,NE:45,E:90,SE:135,S:180,SW:225,W:270,NW:315}[windDir] || 0; var exposed = (windAngle >= 315 || windAngle < 45) ? 'End 1' : (windAngle >= 135 && windAngle < 225) ? 'End 2' : (windAngle >= 45 && windAngle < 135) ? 'Front str' : 'Back str'; bullets.push('WIND '+Math.round(wind)+'mph '+windDir+': '+exposed+' dries first.'); } // Program position var ct = _algoCarType(); if (ct === 'kart' || ct === 'stock') bullets.push('PROGRAM: Your class runs early — track is raw, less rubber.'); else if (ct === 'sprint' || ct === 'latemodel') bullets.push('PROGRAM: Feature runs late — expect rubbered-up track by then.'); var histNote = ''; if (hist && hist.setup) { histNote = '\nLast visit: ' + (hist.trackName || track.name) + ' — comparing saved setup.'; } return { algo: true, answer: 'TONIGHT AT ' + track.name.toUpperCase(), detail: 'Class: ' + (cls.name||cls.class_name||'') + ' · Track: ' + sz + ' · DA: ' + Math.round(da) + 'ft · ' + Math.round(temp) + '°F' + (S._walkSummary ? ' · WALK DATA' : '') + histNote, action: bullets.join('\n') + '\n\nStart with your baseline. After hot laps, check tire temps and adjust.', confidence: S._walkSummary ? 'HIGH — walk data included' : 'HIGH', }; } // ── History/comparison algorithm ───────────────────────────────────────────── function hunterHistory(ctx) { var hist = S.lastTrackSetup; var track = S.curTrack; if (!hist || !hist.setup) { return { algo: true, answer: 'No saved setup found for ' + (track ? track.name : 'this track') + '.', detail: 'Set up tonight and save your setup after the feature for next time.', action: 'After racing: go to Garage → Setup → tap SAVE to record tonight\'s setup.', confidence: 'HIGH', }; } var diffs = []; var cur = (S.cur && S.cur.setup) || {}; var old = hist.setup; Object.keys(old).forEach(function(k) { var ov = parseFloat(old[k]); var nv = parseFloat(cur[k]); if (!isNaN(ov) && !isNaN(nv) && Math.abs(ov-nv) > 0.01) { diffs.push(k.toUpperCase().replace(/_/g,' ') + ': was ' + ov + ' → now ' + nv + ' (' + (nv>ov?'+':'') + (nv-ov).toFixed(2) + ')'); } }); return { algo: true, answer: 'COMPARED TO LAST VISIT: ' + diffs.length + ' change' + (diffs.length!==1?'s':''), detail: 'Last visit: ' + (hist.date||'') + (hist.result?' · Result: '+hist.result:''), action: diffs.length ? diffs.join('\n') : 'Setup identical to last visit.', confidence: 'HIGH', }; } // ── Fuel/lap calculation ────────────────────────────────────────────────────── function hunterFuel(q) { var lapMatch = q.match(/(\d+)[\s-]*lap/i); var mileMatch = q.match(/(\d+\.?\d*)[\s-]*mile/i); var laps = lapMatch ? parseInt(lapMatch[1]) : null; var miles = mileMatch ? parseFloat(mileMatch[1]): null; var trackMiles = (S.curTrack && parseFloat(S.curTrack.size)) || 0.375; if (laps && !miles) miles = laps * trackMiles; if (!miles) return null; var clsName = (S.cur && (S.cur.name || S.cur.class_name)) || ''; var ct = _algoCarType(); var mpg = ct==='kart' ? 8.0 : ct==='micro' ? 3.0 : /late model|lm/i.test(clsName) ? 1.4 : ct==='modified' ? 1.25 : ct==='stock' ? 1.6 : 1.0; // Weight penalty: heavier car burns more var wt = parseFloat((S.cur && S.cur.weight) || 0); if (wt > 2200) mpg *= 0.85; else if (wt > 1800) mpg *= 0.92; var fuel = Math.ceil(miles / mpg * 1.15); return { algo: true, answer: 'FUEL: ~' + fuel + ' gallons for ' + (laps ? laps + ' laps' : miles.toFixed(1) + ' miles'), detail: 'Based on ' + trackMiles + '-mile track at ~' + mpg + ' miles/gallon consumption.', action: 'Fill to ' + fuel + ' gallons before qualifying.\nCheck fuel cell vent is clear.\nIf green-to-checkered: can usually go 2 gallons lighter for weight savings.', confidence: 'MEDIUM', }; } // ── Tire Prep advisor ──────────────────────────────────────────────────────── function hunterTirePrep(q, ctx) { if (!_canDo('warrior')) return {algo:true, answer:'TIRE PREP ADVISOR', detail:'Tire prep analysis is a Warrior feature. Upgrade to get compound recommendations, target durometers, and prep timing based on your conditions.', action:'', confidence:'LOCKED'}; var temp = parseFloat((ctx && ctx.temp) || (S.wx && S.wx.temp) || 70); var da = parseFloat((ctx && ctx.da) || (S.wx && S.wx.density_altitude) || 0); var surf = ((ctx && ctx.track && ctx.track.surface) || (S.curTrack && S.curTrack.surface) || '').toLowerCase(); // Pull tire data from localStorage if available var carId = S.cur ? (S.cur.id || 'local') : 'demo'; var tireData = null; try { tireData = JSON.parse(localStorage.getItem('bb_tire_' + carId) || 'null'); } catch(e) {} // Try to extract durometer from query var duroMatch = q.match(/(\d{2,3})\s*(?:duro|durometer)/i); var duro = duroMatch ? parseInt(duroMatch[1]) : null; // Try to extract compound from query var compMatch = q.match(/(?:compound|running|on)\s+(\w+)/i); var compound = compMatch ? compMatch[1].toUpperCase() : null; // If we have tire log data, pull rears if (!duro && tireData) { var rr = tireData.rr || tireData.lr || {}; if (rr.dur_cur) duro = rr.dur_cur; if (!compound && rr.compound) compound = rr.compound.toUpperCase(); } // Surface condition mapping var surfHard = /hard|dry|black|slick|sealed/.test(surf); var surfHeavy = /heavy|wet|tacky|cushy|muddy/.test(surf); // Target durometer ranges — class-aware var ct = _algoCarType(); var isRestricted = ct === 'kart' && _isRestrictedKart(); var targetMin, targetMax, prepAdvice, timing; if (ct === 'kart') { if (isRestricted) { if (surfHard || temp > 85) { targetMin=45;targetMax=52;prepAdvice='RESTRICTED KART on hard/hot: softer end of spec range.';timing='Check rules on tire treatment — most restricted classes ban chemical prep. Pressure and crown management only.'; } else if (surfHeavy || temp < 50) { targetMin=50;targetMax=58;prepAdvice='RESTRICTED KART on heavy/cold: harder range. Cold + soft = chunking.';timing='Store tires at room temp 24h before.'; } else { targetMin=48;targetMax=54;prepAdvice='RESTRICTED KART mid-range.';timing='Room temp storage. No heat cycling on spec tires.'; } prepAdvice += '\nTIRE CROWN: Front tires need slight crown for turn-in. RR needs more crown than LR for free rotation. Run fronts 0.25 psi higher than rears to build crown. Check with straight edge — 1-2mm higher at center than edges.'; } else { if (surfHard || temp > 85) { targetMin=38;targetMax=45;prepAdvice='Open kart on hard/hot: go soft.';timing='Prep 2-3 days out. Kart tires respond faster to treatment.'; } else if (surfHeavy || temp < 50) { targetMin=48;targetMax=55;prepAdvice='Open kart heavy/cold: harder compound.';timing='Apply hardener 2 days out, cure in shade.'; } else { targetMin=42;targetMax=50;prepAdvice='Open kart mid-range.';timing='Prep 2 days before. Kart tires are thin — don\'t over-treat.'; } } } else { if (surfHard || temp > 85) { targetMin=35;targetMax=42;prepAdvice='Softer tire for grip on hard/hot surface.';timing='Prep 3-5 days before race. Wrap in black plastic, store in sun 24h before.'; } else if (surfHeavy || temp < 50) { targetMin=48;targetMax=55;prepAdvice='Harder tire for heavy/cold — soft rubber tears on tacky dirt.';timing='If treating to harden: apply 2-3 days out, cure in shade.'; } else { targetMin=42;targetMax=50;prepAdvice='Mid-range for typical conditions.';timing='Prep 2-3 days before race. Room temp storage, no direct sun.'; } } // DA affects rubber — REVERSED for restricted karts var daNote = ''; if (ct === 'kart' && isRestricted) { if (da > 5000) { daNote='High DA ('+Math.round(da)+'ft) — RESTRICTED: less power = less tire heat = go 1-2 pts HARDER. Engine can\'t work a soft tire.';targetMin+=1;targetMax+=1; } else if (da < 1000) { daNote='Low DA ('+Math.round(da)+'ft) — more power, more heat. Can go 1-2 pts softer.';targetMin-=1;targetMax-=1; } } else { if (da > 5000) { daNote='High DA ('+Math.round(da)+'ft) — less aero load. Go 2-3 pts softer.';targetMin-=2;targetMax-=2; } else if (da < 1000) { daNote='Low DA ('+Math.round(da)+'ft) — heavy air. Can run 1-2 pts harder.';targetMin+=1;targetMax+=1; } } // Build answer var answer = 'TARGET DURO: ' + targetMin + '-' + targetMax; var details = []; details.push(prepAdvice); if (duro) { var diff = duro - ((targetMin + targetMax) / 2); if (diff > 5) details.push('Current ' + duro + ' is HARD for these conditions — treat to soften or swap compound.'); else if (diff < -5) details.push('Current ' + duro + ' is SOFT — risk of chunking on ' + (surfHard ? 'hard surface' : 'this surface') + '. Go up a compound.'); else details.push('Current ' + duro + ' is in the window. Fine-tune with surface reads at the track.'); } if (compound) details.push('Running ' + compound + '.'); details.push(timing); if (daNote) details.push(daNote); details.push('Temp: ' + Math.round(temp) + '°F. Surface: ' + (surf || 'unknown') + '.'); var actions = []; actions.push('Check durometer on rears 1 hour before hot laps.'); actions.push('If treating: apply evenly, bag each tire separately.'); if (surfHard) actions.push('Hard track: consider a tire softener soak (follow your sanctioning body rules).'); if (surfHeavy) actions.push('Heavy track: fresh/hard tires survive better — save prepped softs for the feature.'); actions.push('Log your durometer in the Tire Log tab after every session.'); return { algo: true, answer: answer, detail: details.join('\n'), action: actions.join('\n'), confidence: duro ? 'HIGH' : 'MEDIUM', }; } // ── Master Hunter router ────────────────────────────────────────────────────── // Returns algo response or null (null = escalate to Claude AI) function hunterAlgo(query, ctx) { if (!query) return null; var q = query.toString(); ctx = ctx || {}; // Build context from global state var fullCtx = Object.assign({ track: S.curTrack, cls: S.cur, setup: S.cur && S.cur.setup, da: S.weather && S.weather.density_altitude, temp: S.weather && S.weather.temp, }, ctx); // Route by intent (order matters — most specific first) if (_HK.rain.test(q)) return hunterRain(fullCtx); if (_HK.pyro.test(q) && _algoCarType()==='kart') return hunterPyrometer(q); if (_HK.rules.test(q) && _algoCarType()==='kart') return hunterKartRules(q); if (_HK.gear.test(q) && _algoCarType()==='kart') return hunterKartGear(q, fullCtx); if (_HK.result.test(q) || _HK.heat.test(q)) return hunterBetweenHeats(q, fullCtx); if (_HK.history.test(q)) return hunterHistory(fullCtx); if (_HK.tight.test(q)) return hunterTight(q, fullCtx); if (_HK.loose.test(q)) return hunterLoose(q, fullCtx); if (_HK.stagger.test(q)) return hunterStagger(fullCtx); if (_HK.tireprep.test(q)) return hunterTirePrep(q, fullCtx); if (_HK.tire.test(q)) return hunterTires(q, fullCtx); if (_HK.da.test(q)) return hunterDA(fullCtx); if (_HK.fuel.test(q)) return hunterFuel(q); if (_HK.tonight.test(q) || _HK.setup.test(q)) return hunterTonight(fullCtx); return null; // Claude handles the rest } // ── Render an algo response in the Hunter UI ───────────────────────────────── function hunterAlgoRender(resp, container) { if (!resp || !resp.algo) return false; container.innerHTML = ''; var confColor = resp.confidence === 'HIGH' ? 'var(--green)' : 'var(--amber)'; var hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:10px'; hdr.innerHTML = '
' + (resp.answer||'') + '
' + '
'+resp.confidence+'
'; container.appendChild(hdr); if (resp.detail) { var det = document.createElement('div'); det.style.cssText = 'font-size:11px;color:var(--muted);margin-bottom:10px;line-height:1.5'; det.textContent = resp.detail; container.appendChild(det); } if (resp.action) { var act = document.createElement('div'); act.style.cssText = 'background:var(--dark);border-left:2px solid var(--green);padding:10px 12px;font-family:var(--mono);font-size:10px;line-height:1.8;white-space:pre-line;color:var(--white)'; act.textContent = resp.action; container.appendChild(act); } var footer = document.createElement('div'); footer.style.cssText = 'font-family:var(--mono);font-size:7px;color:rgba(154,144,128,.3);margin-top:8px;text-align:right;letter-spacing:1px'; footer.textContent = 'HUNTER ALGORITHM · NO AI USED'; container.appendChild(footer); return true; } // ── Hook into existing Hunter ask functions ─────────────────────────────────── // Wraps askHunter / askHunterRaceNight — tries algo first, falls back to Claude var _origAskHunter = null; function _wrapHunterFunctions() { // Wrap the main chat-style Hunter input handler var hunterInput = document.getElementById && document.getElementById('hunter-input'); if (hunterInput) { var sendBtn = document.getElementById('hunter-send'); if (sendBtn) { var _origClick = sendBtn.onclick; sendBtn.onclick = function() { var q = (hunterInput.value || '').trim(); if (!q) return; var algoResp = hunterAlgo(q); if (algoResp) { var out = document.getElementById('hunter-output') || document.getElementById('hunter-resp'); if (out) { hunterInput.value = ''; if (hunterAlgoRender(algoResp, out)) return; // handled } } if (_origClick) _origClick.call(this); }; } } // Override askHunterRaceNight if it exists (Race tab Hunter button) if (typeof askHunterRaceNight === 'function') { var _origRaceNight = askHunterRaceNight; window.askHunterRaceNight = function() { var ctx = { track: S.curTrack, cls: S.cur }; var wx = S.weather || {}; ctx.da = wx.density_altitude; ctx.temp = wx.temp; var algoResp = hunterTonight(ctx); if (algoResp) { // Find the output container in race tab var el = document.getElementById('hunter-race-output') || document.getElementById('race-content'); if (el) { var box = document.createElement('div'); box.style.cssText = 'background:var(--dark2);border:1px solid rgba(208,25,14,.1);padding:14px;margin-top:12px'; hunterAlgoRender(algoResp, box); el.appendChild(box); return; } } _origRaceNight.apply(this, arguments); }; } } // Wire up after DOM loads if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', _wrapHunterFunctions); } else { setTimeout(_wrapHunterFunctions, 500); } // ── Canvas Save — iOS Share API + fallback ────────────────────────────────── function _saveCanvas(canvas,filename,msg){ canvas.toBlob(function(blob){ if(!blob){toast('Could not generate image');return;} var file=new File([blob],filename,{type:'image/png'}); if(navigator.canShare&&navigator.canShare({files:[file]})){ navigator.share({files:[file],title:filename}).then(function(){toast(msg);}).catch(function(){ var url=URL.createObjectURL(blob);window.open(url,'_blank');toast('Long-press image to save'); }); } else { var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download=filename;a.click(); setTimeout(function(){URL.revokeObjectURL(url);},5000);toast(msg); } },'image/png'); } // ── Geofence Exit — Arrival Tracking + Departure Detection ────────────────── function _markTrackArrival(){ if(S._wasAtTrack)return;S._wasAtTrack=true;S._trackArrivalTs=Date.now(); if(!S._exitPollId){S._exitPollId=setInterval(_checkGeofenceExit,120000);} } function _checkGeofenceExit(){ if(!S._wasAtTrack||!S.curTrack)return; if(Date.now()-S._trackArrivalTs<300000)return; if(S._exitPromptShown)return; if(navigator.geolocation){ navigator.geolocation.getCurrentPosition(function(pos){ if(!S.curTrack||!S.curTrack.lat)return; var R=3959,dLat=(S.curTrack.lat-pos.coords.latitude)*Math.PI/180,dLon=(S.curTrack.lon-pos.coords.longitude)*Math.PI/180; var a2=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(pos.coords.latitude*Math.PI/180)*Math.cos(S.curTrack.lat*Math.PI/180)*Math.sin(dLon/2)*Math.sin(dLon/2); var dist=R*2*Math.atan2(Math.sqrt(a2),Math.sqrt(1-a2)); if(dist>3){S._wasAtTrack=false;S._exitPromptShown=true;if(S._exitPollId){clearInterval(S._exitPollId);S._exitPollId=null;}_showExitStoryPrompt();} },function(){},{timeout:5000}); } } function _showExitStoryPrompt(){ var isFan=(!S.cur||S._demo||_tl()===0); var m=document.createElement('div');m.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;overflow-y:auto;'; var box=document.createElement('div');box.style.cssText='background:var(--dark2);border:1px solid rgba(208,25,14,.3);border-top:3px solid '+(isFan?'var(--gold)':'var(--red)')+';max-width:380px;width:100%;padding:24px;text-align:center;'; var title=isFan?'GREAT NIGHT AT THE TRACK?':'HEADED HOME?'; box.innerHTML='
'+title+'
' +'
'+(isFan?'Save your story and make your card.':'Save your story before you forget.')+'
' +'' +'' +''; m.appendChild(box);m.onclick=function(e){if(e.target===m)m.remove();};document.body.appendChild(m); setTimeout(function(){ var xb=document.getElementById('exit-close-x');if(xb)xb.onclick=function(){m.remove();}; var sb=document.getElementById('exit-save');if(sb)sb.onclick=function(){var txt=(document.getElementById('exit-story')||{}).value||'';if(txt)localStorage.setItem('bb_exit_story_'+Date.now(),JSON.stringify({text:txt,track:S.curTrack?S.curTrack.name:'',ts:Date.now()}));toast('Story saved');m.remove();}; var sk=document.getElementById('exit-skip');if(sk)sk.onclick=function(){m.remove();}; },50); } // ── General Notes & Files ──────────────────────────────────────────────────── function _addGeneralNote(){ var title=prompt('Note title:');if(!title)return; var text=prompt('Note text:');if(!text)return; var notes=JSON.parse(localStorage.getItem('bb_general_notes')||'[]'); notes.push({type:'note',title:title,text:text,ts:Date.now()}); localStorage.setItem('bb_general_notes',JSON.stringify(notes)); if(S.token&&S.cur&&S.cur.id)_cloudSave('general_notes',S.cur.id,notes); _renderGeneralNotes();toast('Note saved'); } function _addGeneralFile(){ var inp=document.createElement('input');inp.type='file';inp.accept='image/*,application/pdf';inp.capture='environment';inp.style.display='none'; inp.onchange=function(){if(!inp.files||!inp.files[0])return; var title=prompt('What is this? (e.g. Tech card, receipt, contact)',inp.files[0].name);if(!title)title=inp.files[0].name; var fr=new FileReader();fr.onload=function(){ var notes=JSON.parse(localStorage.getItem('bb_general_notes')||'[]'); notes.push({type:'file',title:title,data:fr.result,ts:Date.now()}); localStorage.setItem('bb_general_notes',JSON.stringify(notes)); if(S.token&&S.cur&&S.cur.id)_cloudSave('general_notes',S.cur.id,notes); _renderGeneralNotes();toast('File saved'); };fr.readAsDataURL(inp.files[0]); };document.body.appendChild(inp);inp.click(); } function _renderGeneralNotes(){ var el=document.getElementById('general-notes-list');if(!el)return; var notes=JSON.parse(localStorage.getItem('bb_general_notes')||'[]'); if(!notes.length){el.innerHTML='
No notes yet.
';return;} el.innerHTML=''; notes.forEach(function(n,i){ var div=document.createElement('div'); div.style.cssText='background:var(--dark2);border:1px solid rgba(255,255,255,.04);padding:10px;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center'; var info=document.createElement('div');info.style.cssText='flex:1;cursor:pointer'; info.innerHTML='
'+n.title+'
'+(n.type==='note'?'NOTE':'FILE')+' — '+new Date(n.ts).toLocaleDateString()+'
'; if(n.type==='note'){info.onclick=function(){alert(n.title+':\n\n'+n.text);};} else{info.onclick=function(){var w=window.open();if(w){if(n.data.indexOf('data:image')===0){w.document.write('');}else{w.document.write('');}w.document.close();}};} var del=document.createElement('span');del.style.cssText='color:var(--dark4);cursor:pointer;font-size:16px;padding:8px';del.textContent='\u2715'; del.onclick=function(){notes.splice(i,1);localStorage.setItem('bb_general_notes',JSON.stringify(notes));if(S.token&&S.cur&&S.cur.id)_cloudSave('general_notes',S.cur.id,notes);_renderGeneralNotes();toast('Removed');}; div.appendChild(info);div.appendChild(del);el.appendChild(div); }); } // ── Track Rules save/load ──────────────────────────────────────────────────── function saveTrackRules(){ var ta=document.getElementById('track-rules-text');if(!ta)return; var text=ta.value.trim();if(!text){toast('Type or paste rules first');return;} var trackName=S.curTrack?S.curTrack.name:'general'; var slug=trackName.toLowerCase().replace(/[^a-z0-9]+/g,'-'); var key='bb_track_rules_'+slug; var existing=JSON.parse(localStorage.getItem(key)||'{}'); existing.text=text;existing.ts=Date.now();existing.track=trackName; localStorage.setItem(key,JSON.stringify(existing)); // Cloud sync per user if(S.token&&S.cur&&S.cur.id)_cloudSave('track_rules_'+slug,S.cur.id,existing); // Share to community pool — any racer at this track benefits if(S.token){ _sbUpsert('_rules_'+slug,JSON.stringify({text:text,track:trackName,ts:Date.now(),by:S.user?S.user.email:'anon'})); } toast('Rules saved & shared for '+trackName); var saved=document.getElementById('track-rules-saved'); if(saved){saved.style.display='block';saved.textContent='Saved '+text.length+' chars for '+trackName+' — shared with all racers at this track';} } function _loadTrackRulesText(){ var ta=document.getElementById('track-rules-text');if(!ta)return; var trackName=S.curTrack?S.curTrack.name:'general'; var slug=trackName.toLowerCase().replace(/[^a-z0-9]+/g,'-'); var key='bb_track_rules_'+slug; var saved=JSON.parse(localStorage.getItem(key)||'{}'); if(saved.text){ ta.value=saved.text; var el=document.getElementById('track-rules-saved'); if(el){el.style.display='block';el.textContent='Your rules for '+trackName+' ('+saved.text.length+' chars)';} } else { // Try community pool fetch(_SB+'/rest/v1/page_content?slug=eq._rules_'+slug+'&select=html',{headers:{apikey:_AK,Authorization:'Bearer '+_AK}}).then(function(r){return r.json();}).then(function(d){ if(d&&d[0]&&d[0].html){ try{var shared=JSON.parse(d[0].html);if(shared.text){ ta.value=shared.text;ta.placeholder='Community rules — edit and save to make them yours'; var el=document.getElementById('track-rules-saved'); if(el){el.style.display='block';el.textContent='Loaded from community (shared by another racer at '+trackName+')';} }}catch(e){} } }).catch(function(){}); } } // ═══ SETUP SHEET V2 — Spatial car layout ═══ function _carSVGv2(carType, hasWing) { var ct = carType || 'generic'; var ns = 'http://www.w3.org/2000/svg'; var svg = document.createElementNS(ns, 'svg'); svg.setAttribute('viewBox', '0 0 300 380'); svg.style.cssText = 'width:100%;max-width:300px;height:auto;display:block;margin:0 auto;'; function el(tag, a) { var e = document.createElementNS(ns, tag); for (var k in a) e.setAttribute(k, a[k]); return e; } function txt(x, y, t, sz, clr, anc) { var e = el('text', {x:x, y:y, 'text-anchor':anc||'middle', 'font-family':'Share Tech Mono', 'font-size':sz||'10', fill:clr||'#9A9080'}); e.textContent = t; return e; } // Center line svg.appendChild(el('line', {x1:150,y1:30,x2:150,y2:350,stroke:'#1e1c19','stroke-width':'1','stroke-dasharray':'4,4'})); // Tire dimensions by type var fw = ct==='kart'?18:ct==='micro'?20:24; var rw = ct==='kart'?20:ct==='micro'?24:30; var th = ct==='kart'?36:44; var fSp = ct==='kart'?90:ct==='stock'?100:110; var rSp = ct==='kart'?90:ct==='stock'?105:120; // Tires svg.appendChild(el('rect', {x:150-fSp/2-fw/2, y:48, width:fw, height:th, rx:3, fill:'#1E1C19', stroke:'#9A9080', 'stroke-width':'1.5'})); svg.appendChild(el('rect', {x:150+fSp/2-fw/2, y:48, width:fw, height:th, rx:3, fill:'#1E1C19', stroke:'#9A9080', 'stroke-width':'1.5'})); svg.appendChild(el('rect', {x:150-rSp/2-rw/2, y:275, width:rw, height:th+6, rx:3, fill:'#1E1C19', stroke:'#9A9080', 'stroke-width':'1.5'})); svg.appendChild(el('rect', {x:150+rSp/2-rw/2, y:275, width:rw, height:th+6, rx:3, fill:'#1E1C19', stroke:'#9A9080', 'stroke-width':'1.5'})); // Axles svg.appendChild(el('line', {x1:150-fSp/2,y1:68,x2:150+fSp/2,y2:68,stroke:'#282421','stroke-width':'2'})); svg.appendChild(el('line', {x1:150-rSp/2,y1:300,x2:150+rSp/2,y2:300,stroke:'#282421','stroke-width':'2'})); // Body shape per division var d; if (ct==='kart') d='M110,60 L110,320 M190,60 L190,320'; else if (ct==='sprint') d='M108,55 C108,45 192,45 192,55 L196,105 L196,275 L192,325 C192,335 108,335 108,325 L104,275 L104,105 Z'; else if (ct==='latemodel') d='M98,65 L94,95 L92,270 L96,318 L115,335 L185,335 L204,318 L208,270 L206,95 L202,65 L172,50 L128,50 Z'; else if (ct==='modified') d='M102,60 L98,95 L98,280 L102,322 L122,332 L178,332 L198,322 L202,280 L202,95 L198,60 L176,50 L124,50 Z'; else if (ct==='micro') d='M108,52 C108,42 192,42 192,52 L196,100 L196,280 L192,328 C192,338 108,338 108,328 L104,280 L104,100 Z'; else d='M102,62 L96,95 L94,270 L98,318 L118,338 L182,338 L202,318 L206,270 L204,95 L198,62 L176,50 L124,50 Z'; var body = el('path', {d:d, fill:'none', stroke:'rgba(208,25,14,.2)', 'stroke-width':'1.5'}); if (ct==='kart') { body.setAttribute('stroke', 'rgba(200,150,10,.15)'); body.setAttribute('stroke-width', '1'); } svg.appendChild(body); // Kart seat if (ct==='kart') { svg.appendChild(el('rect', {x:125,y:170,width:50,height:60,rx:6,fill:'rgba(200,150,10,.05)',stroke:'rgba(200,150,10,.2)','stroke-width':'1'})); svg.appendChild(txt(150,205,'SEAT','9','#C8960A')); } // Sprint nerf bars + wing if (ct==='sprint') { svg.appendChild(el('rect', {x:65,y:190,width:5,height:90,rx:2,fill:'none',stroke:'rgba(255,255,255,.08)','stroke-width':'1'})); svg.appendChild(el('rect', {x:230,y:190,width:5,height:90,rx:2,fill:'none',stroke:'rgba(255,255,255,.08)','stroke-width':'1'})); if (hasWing!==false) { svg.appendChild(el('rect', {x:60,y:18,width:180,height:12,rx:2,fill:'none',stroke:'rgba(200,150,10,.3)','stroke-width':'2'})); svg.appendChild(txt(150,15,'TOP WING','9','#C8960A')); } } // Late model spoiler if (ct==='latemodel') { svg.appendChild(el('rect', {x:96,y:335,width:108,height:8,rx:1,fill:'none',stroke:'rgba(200,150,10,.25)','stroke-width':'1.5'})); svg.appendChild(txt(150,355,'SPOILER','8','#C8960A')); } // Micro wing if (ct==='micro'&&hasWing) { svg.appendChild(el('rect', {x:85,y:28,width:130,height:8,rx:2,fill:'none',stroke:'rgba(200,150,10,.25)','stroke-width':'1.5'})); svg.appendChild(txt(150,25,'WING','8','#C8960A')); } // Corner labels svg.appendChild(txt(150-fSp/2, 44, 'LF', '11', '#9A9080')); svg.appendChild(txt(150+fSp/2, 44, 'RF', '11', '#9A9080')); svg.appendChild(txt(150-rSp/2, 272, 'LR', '11', '#9A9080')); svg.appendChild(txt(150+rSp/2, 272, 'RR', '11', '#9A9080')); // Live PSI values on the car var pC = '#F5A623'; svg.appendChild(txt(150-fSp/2, 102, String(_su.lf_psi||'--'), '13', pC)); svg.appendChild(txt(150+fSp/2, 102, String(_su.rf_psi||'--'), '13', pC)); svg.appendChild(txt(150-rSp/2, 335, String(_su.lr_psi||'--'), '13', pC)); svg.appendChild(txt(150+rSp/2, 335, String(_su.rr_psi||'--'), '13', pC)); // Center spine data var spY = 165; if (_su.stagger_r) { svg.appendChild(txt(150, spY, _su.stagger_r+'"', '14', '#C8960A')); svg.appendChild(txt(150, spY+12, 'STAGGER', '8', '#9A9080')); spY+=28; } if (S.cur && S.cur.weight) { svg.appendChild(txt(150, spY, S.cur.weight+'', '13', '#F2EDE6')); svg.appendChild(txt(150, spY+12, 'WEIGHT', '8', '#9A9080')); spY+=28; } if (_su.bite!==undefined&&_su.bite!==null) { svg.appendChild(txt(150, spY, String(_su.bite), '13', '#F5A623')); svg.appendChild(txt(150, spY+12, 'BITE', '8', '#9A9080')); } // Direction svg.appendChild(txt(150, 38, 'FRONT', '10', '#4a4540')); svg.appendChild(txt(150, 370, 'REAR', '10', '#4a4540')); svg.appendChild(el('path', {d:'M145,32 L150,26 L155,32', fill:'none', stroke:'#4a4540', 'stroke-width':'1.5'})); return svg; } function _buildRoadPlanCard(c, wrap, carType) { var isMiata = carType === 'specmiata' || _isSpecMiataClass(c.class || c.car_class || ''); var lbl = document.createElement('div'); lbl.style.cssText = 'font-family:Barlow Condensed;font-size:12px;font-weight:700;color:#6b8cff;letter-spacing:.12em;margin-top:2px;'; lbl.textContent = (isMiata ? 'SPEC MIATA' : 'ROAD KART') + ' — CORNER PSI'; wrap.appendChild(lbl); var cornerWrap = document.createElement('div'); cornerWrap.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:4px;'; var basePsi = isMiata ? 28 : 10; if (!_su.lf_psi || _su.lf_psi < 5) { _su.lf_psi = basePsi; _su.rf_psi = basePsi; _su.lr_psi = basePsi + (isMiata ? 0 : 0.5); _su.rr_psi = basePsi + (isMiata ? 0 : 1); } cornerWrap.appendChild(_planAdjCell('LF', 'lf_psi', 'PSI', 0.5, 1, 'var(--white)')); cornerWrap.appendChild(_planAdjCell('RF', 'rf_psi', 'PSI', 0.5, 1, 'var(--white)')); cornerWrap.appendChild(_planAdjCell('LR', 'lr_psi', 'PSI', 0.5, 1, 'var(--gold)')); cornerWrap.appendChild(_planAdjCell('RR', 'rr_psi', 'PSI', 0.5, 1, 'var(--gold)')); wrap.appendChild(cornerWrap); var row = document.createElement('div'); row.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:6px;'; if (isMiata) { row.appendChild(_planAdjCell('BRAKE BIAS', 'brake_bias', '%F', 1, 0, '#6b8cff')); row.appendChild(_planAdjCell('CAMBER L', 'camber_l', 'deg', 0.25, 2, 'var(--muted)')); } else { row.appendChild(_planAdjCell('SEAT', 'seat_pos', 'mm', 5, 0, '#6b8cff')); row.appendChild(_planAdjCell('REAR WIDTH', 'rear_track_width_mm', 'mm', 1, 0, 'var(--amber)')); } wrap.appendChild(row); var note = document.createElement('div'); note.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);line-height:1.5;margin-top:6px;'; note.textContent = isMiata ? 'Scale sheet for cross % — stagger tool disabled on road course.' : 'Widen rear 2–5 mm to free the kart; narrow to tighten. Tools tab has NTK gear baselines.'; wrap.appendChild(note); return wrap; } function _buildCarPlanCardV2(c, hasBars, hasWing, stgRange) { var wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;flex-direction:column;gap:10px;'; var carType = _getCarType(c); var track = S.curTrack || {}; var roadMode = _getGarageMode(c) === 'road'; // Track context chip if (track.name) { var tChip = document.createElement('div'); tChip.style.cssText = 'background:rgba(200,150,10,.06);border:1px solid rgba(200,150,10,.15);padding:5px 10px;font-family:Share Tech Mono;font-size:10px;color:var(--amber);letter-spacing:.5px;'; tChip.textContent = (track.short || track.name).toUpperCase() + (track.size ? ' ' + track.size.toUpperCase() : '') + (track.surface ? ' ' + track.surface.toUpperCase() : ''); wrap.appendChild(tChip); } if (roadMode) return _buildRoadPlanCard(c, wrap, carType); // Kart uses its own layout (already works in V1) if (carType === 'kart') return _buildCarPlanCard(c, hasBars, hasWing, stgRange); // Car SVG — full width, division-specific wrap.appendChild(_carSVGv2(carType, hasWing)); // Corner PSI — 2x2 grid matching car var cornerWrap = document.createElement('div'); cornerWrap.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:4px;'; cornerWrap.appendChild(_planAdjCell('LF', 'lf_psi', 'PSI', 0.5, 1, 'var(--white)')); cornerWrap.appendChild(_planAdjCell('RF', 'rf_psi', 'PSI', 0.5, 1, 'var(--white)')); cornerWrap.appendChild(_planAdjCell('LR', 'lr_psi', 'PSI', 0.5, 1, 'var(--gold)')); cornerWrap.appendChild(_planAdjCell('RR', 'rr_psi', 'PSI', 0.5, 1, 'var(--gold)')); wrap.appendChild(cornerWrap); // Center row — stagger + bite/wedge var cols = carType==='sprint'||carType==='latemodel' ? 3 : 2; var centerRow = document.createElement('div'); centerRow.style.cssText = 'display:grid;grid-template-columns:repeat('+cols+',1fr);gap:6px;'; centerRow.appendChild(_planAdjCell('STAGGER R', 'stagger_r', 'in', 0.125, 2, 'var(--amber)')); if (carType==='sprint'||carType==='latemodel'||carType==='modified') centerRow.appendChild(_planAdjCell('STAGGER F', 'stagger_f', 'in', 0.125, 2, 'var(--muted)')); if (carType==='sprint'||carType==='micro') centerRow.appendChild(_planAdjCell('BITE', 'bite', 'turns', 0.25, 1, 'var(--amber)')); else if (carType==='latemodel'||carType==='modified') centerRow.appendChild(_planAdjCell('WEDGE', 'wedge', 'lbs', 5, 0, 'var(--amber)')); wrap.appendChild(centerRow); // Division quirks (panhard, j-bar, torsion bars, wing, ride heights, etc) var quirks = _getDivisionQuirks(carType, hasWing); var divQuirks = quirks.filter(function(q){ return ['lf_psi','rf_psi','lr_psi','rr_psi','stagger_r','stagger_f'].indexOf(q.key)===-1; }); if (divQuirks.length > 0) { var dNames = {sprint:'SPRINT',latemodel:'LATE MODEL',modified:'MODIFIED',micro:'MICRO',stock:'STOCK',generic:'SETUP'}; var qLabel = document.createElement('div'); qLabel.style.cssText = 'font-family:Barlow Condensed;font-size:12px;font-weight:700;color:var(--muted);letter-spacing:.12em;margin-top:4px;'; qLabel.textContent = (dNames[carType]||'') + ' SETTINGS'; wrap.appendChild(qLabel); var qGrid = document.createElement('div'); qGrid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:6px;'; divQuirks.forEach(function(q) { if (q.type === 'opts') { var qc = document.createElement('div'); qc.style.cssText = 'background:var(--dark2);border:1px solid rgba(255,255,255,.06);padding:8px 9px;'; var ql = document.createElement('div'); ql.style.cssText = 'font-family:Share Tech Mono;font-size:10px;color:var(--muted);margin-bottom:4px;'; ql.textContent = q.label; qc.appendChild(ql); var sel = document.createElement('select'); sel.style.cssText = 'background:var(--dark);border:1px solid var(--dark4);color:var(--white);font-family:Share Tech Mono;font-size:16px;padding:4px;width:100%;font-weight:700;min-height:44px;'; (q.opts || []).forEach(function(opt) { var o = document.createElement('option'); o.value = opt; o.textContent = opt; if ((_su[q.key]||'')===opt) o.selected = true; sel.appendChild(o); }); sel.onchange = function() { _su[q.key] = this.value; }; qc.appendChild(sel); if (q.tip) { var qt = document.createElement('div'); qt.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--muted);margin-top:4px;opacity:.7;line-height:1.4;'; qt.textContent = q.tip; qc.appendChild(qt); } qGrid.appendChild(qc); } else { var dec2 = q.step < 0.1 ? 3 : q.step < 1 ? 1 : 0; var adjC = _planAdjCell(q.label, q.key, q.unit||'', q.step||0.5, dec2, 'var(--white)'); if (q.tip) { var t2 = document.createElement('div'); t2.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--muted);padding:0 4px 4px;opacity:.7;line-height:1.4;text-align:center;'; t2.textContent = q.tip; adjC.appendChild(t2); } qGrid.appendChild(adjC); } }); wrap.appendChild(qGrid); } // Torsion bars if (hasBars) { var bLbl = document.createElement('div'); bLbl.style.cssText = 'font-family:Barlow Condensed;font-size:12px;font-weight:700;color:var(--muted);letter-spacing:.1em;margin-top:2px;'; bLbl.textContent = 'TORSION BARS'; wrap.appendChild(bLbl); var bGrid = document.createElement('div'); bGrid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:6px;'; [{l:'LF',k:'lf_bar'},{l:'RF',k:'rf_bar'},{l:'LR',k:'lr_bar'},{l:'RR',k:'rr_bar'}].forEach(function(b){ bGrid.appendChild(_planAdjCell(b.l+' BAR', b.k, 'in', 0.001, 3, 'var(--white)')); }); wrap.appendChild(bGrid); } // Wing if (hasWing) { var wLbl = document.createElement('div'); wLbl.style.cssText = 'font-family:Barlow Condensed;font-size:12px;font-weight:700;color:var(--muted);letter-spacing:.1em;margin-top:2px;'; wLbl.textContent = 'WING SETTINGS'; wrap.appendChild(wLbl); var wGrid = document.createElement('div'); wGrid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:6px;'; [{l:'FRONT WING',k:'front_wing',u:'deg',s:0.5},{l:'REAR WING',k:'rear_wing',u:'deg',s:0.5}].forEach(function(wf){ wGrid.appendChild(_planAdjCell(wf.l, wf.k, wf.u, wf.s, 1, 'var(--white)')); }); wrap.appendChild(wGrid); } // Hunter rec banner if (S._hunterRec && Object.keys(S._hunterRec).length > 0) { var banner = document.createElement('div'); banner.style.cssText = 'background:rgba(208,25,14,.08);border:1px solid rgba(208,25,14,.3);padding:7px 10px;font-family:Share Tech Mono;font-size:10px;color:var(--amber);letter-spacing:.5px;'; banner.innerHTML = '\u26A1 HUNTER REC ACTIVE'; wrap.appendChild(banner); } return wrap; } // ═══ BATCH B — Fan Features + Card Builder + Rights ═══ // ── B1: Fan Photo Collection ──────────────────────────────────────────────── function _buildFanPhotoCollection(el){ if(!el)return; var wrap=document.createElement('div'); wrap.style.cssText='background:var(--dark2);border:1px solid rgba(208,25,14,.1);padding:14px;margin-bottom:12px;'; wrap.innerHTML='
RACE NIGHT PHOTOS
' +'
Snap photos at the track. Saved to your device.
' +'
'; el.appendChild(wrap); setTimeout(function(){ var pb=document.getElementById('fan-photo-btn'); if(pb)pb.onclick=function(){ var inp=document.createElement('input');inp.type='file';inp.accept='image/*';inp.capture='environment';inp.style.display='none'; inp.onchange=function(){if(!inp.files||!inp.files[0])return;var reader=new FileReader();reader.onload=function(){var photos=JSON.parse(localStorage.getItem('bb_fan_photos')||'[]');photos.unshift({data:reader.result,ts:Date.now(),track:S.curTrack?S.curTrack.name:'Unknown'});if(photos.length>20)photos=photos.slice(0,20);try{localStorage.setItem('bb_fan_photos',JSON.stringify(photos));}catch(e){toast('Storage full');return;}toast('Photo saved');};reader.readAsDataURL(inp.files[0]);};document.body.appendChild(inp);inp.click(); }; var gb=document.getElementById('fan-gallery-btn'); if(gb)gb.onclick=function(){_showFanGallery();}; },50); } function _showFanGallery(){ var photos=JSON.parse(localStorage.getItem('bb_fan_photos')||'[]'); if(!photos.length){toast('No photos yet');return;} var m=document.createElement('div');m.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:9999;'; var scroll=document.createElement('div');scroll.style.cssText='height:100%;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:16px;'; var h='
PHOTO GALLERY ('+photos.length+')
'; h+='
'; photos.forEach(function(p){h+='
'+(p.track||'')+'
'+new Date(p.ts).toLocaleDateString()+'
';}); h+='
';scroll.innerHTML=h;m.appendChild(scroll);m.onclick=function(e){if(e.target===m)m.remove();};document.body.appendChild(m); setTimeout(function(){var cb=document.getElementById('close-fan-gallery');if(cb)cb.onclick=function(){m.remove();};},50); } // ── B2+B3: Card Builder — shared fan/driver with live preview ─────────────── var _STICKERS=['\u{1F3C1}','\u{1F3CE}','\u{1F525}','\u{2B50}','\u{1F3C6}','\u{1F4AA}','\u{26A1}','\u{1F6DE}','\u{2699}','\u{1F52E}','\u{1F3AF}','\u{1F4A5}','\u{1F60E}','\u{1F929}','\u{1F44F}','\u{270C}','\u{1F91F}','\u{1F4F8}','\u{1F389}','\u{1F38A}','\u{1F451}','\u{1F48E}','\u{1F397}','\u{1F39F}','\u{1F3AA}','\u{26FD}','\u{1F6E3}','\u{1F6A8}','\u{1F4E2}','\u{1F399}','\u{1F3B5}','\u{1F37B}','\u{1F32D}','\u{1F354}','\u{1F9E4}','\u{1FA96}','\u{1F9F0}','\u{2764}','\u{1F30D}']; var _BORDERS=[{name:'RACING RED',color:'#D0190E'},{name:'CHAMPION GOLD',color:'#C8960A'},{name:'CLEAN WHITE',color:'#F2EDE6'},{name:'MIDNIGHT BLUE',color:'#60a5fa'},{name:'VICTORY GREEN',color:'#4CAF50'},{name:'SUNSET ORANGE',color:'#FF6B35'}]; var _cardOpts={photo:null,story:'',showStory:true,showTrack:true,showDate:true,showWeather:true,showCar:true,showBadge:true,showStickers:true,stickers:[],border:0}; function _openCardBuilder(mode){ _cardOpts={photo:null,story:'',showStory:true,showTrack:true,showDate:true,showWeather:true,showCar:mode==='driver',showBadge:mode==='fan',showStickers:true,stickers:[],border:0}; var storyKeys=[];for(var i=0;i'+(isFan?'I WAS THERE':'RACE NIGHT CARD')+''; h+='
'; h+='
YOUR STORY
'; h+='
SHOW ON CARD
'; h+='
'; [{id:'showStory',l:'STORY'},{id:'showTrack',l:'TRACK'},{id:'showDate',l:'DATE'},{id:'showWeather',l:'WEATHER'},(isFan?{id:'showBadge',l:'I WAS THERE'}:{id:'showCar',l:'CAR #'}),{id:'showStickers',l:'STICKERS'}].forEach(function(t){ h+=''; }); h+='
'; h+='
'; h+='
'; h+='
STICKERS (MAX 5)
'; h+='
'; h+=''; scroll.innerHTML=h;document.body.appendChild(m); setTimeout(function(){ document.getElementById('cb-close').onclick=function(){m.remove();}; _cbBuildPhotos(mode);_cbBuildBorders(mode);_cbBuildStickers(mode); document.querySelectorAll('[id^="cb-show"]').forEach(function(el){el.onchange=function(){_cardOpts[el.id.replace('cb-','')]=el.checked;_cbRender(mode);};}); var storyEl=document.getElementById('cb-story');if(storyEl){var _st;storyEl.oninput=function(){_cardOpts.story=storyEl.value;clearTimeout(_st);_st=setTimeout(function(){_cbRender(mode);},300);};} document.getElementById('cb-download').onclick=function(){var c=document.getElementById('cb-canvas');_saveCanvas(c,(mode==='fan'?'i-was-there-':'race-night-')+new Date().toISOString().slice(0,10)+'.png','Card saved');}; _cbRender(mode); },30); } function _cbBuildPhotos(mode){ var area=document.getElementById('cb-photos');if(!area)return; var photos=[];try{photos=JSON.parse(localStorage.getItem('bb_fan_photos')||'[]');}catch(e){} var h='
\u{1F4F7}
'; photos.forEach(function(p,i){h+='
';}); area.innerHTML=h; setTimeout(function(){ document.getElementById('cb-snap').onclick=function(){var inp=document.createElement('input');inp.type='file';inp.accept='image/*';inp.capture='environment';inp.style.display='none';inp.onchange=function(){if(!inp.files||!inp.files[0])return;var r=new FileReader();r.onload=function(){_cardOpts.photo=r.result;_cbRender(mode);};r.readAsDataURL(inp.files[0]);};document.body.appendChild(inp);inp.click();}; document.querySelectorAll('.cb-photo-opt').forEach(function(el){el.onclick=function(){_cardOpts.photo=photos[parseInt(el.dataset.idx)].data;document.querySelectorAll('.cb-photo-opt').forEach(function(o){o.style.borderColor='transparent';});el.style.borderColor='var(--gold)';_cbRender(mode);};}); },30); } function _cbBuildBorders(mode){ var el=document.getElementById('cb-borders');if(!el)return; _BORDERS.forEach(function(b,i){var sw=document.createElement('div');sw.style.cssText='aspect-ratio:1;background:'+b.color+';cursor:pointer;border:3px solid '+(i===0?'var(--white)':'transparent')+';min-height:44px;';sw.onclick=function(){_cardOpts.border=i;el.querySelectorAll('div').forEach(function(d){d.style.borderColor='transparent';});sw.style.borderColor='var(--white)';_cbRender(mode);};el.appendChild(sw);}); } function _cbBuildStickers(mode){ var el=document.getElementById('cb-stickers');if(!el)return; _STICKERS.forEach(function(s,i){var cell=document.createElement('div');cell.style.cssText='aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;background:var(--dark);border:2px solid var(--dark4);min-height:48px;';cell.textContent=s;cell.onclick=function(){var idx=_cardOpts.stickers.indexOf(i);if(idx>-1){_cardOpts.stickers.splice(idx,1);cell.style.borderColor='var(--dark4)';}else{if(_cardOpts.stickers.length>=5){toast('Max 5');return;}_cardOpts.stickers.push(i);cell.style.borderColor='var(--gold)';}_cbRender(mode);};el.appendChild(cell);}); } function _cbRender(mode){ var c=document.getElementById('cb-canvas');if(!c)return;var o=_cardOpts;var isFan=mode==='fan';var W=400,H=isFan?560:400;c.width=W;c.height=H;var ctx=c.getContext('2d');var bc=_BORDERS[o.border].color; ctx.fillStyle=bc;ctx.fillRect(0,0,W,H);ctx.fillStyle='#0D0C0B';ctx.fillRect(6,6,W-12,H-12); if(o.photo){var img=new Image();img.onload=function(){ctx.drawImage(img,16,16,W-32,isFan?240:180);_cbRenderText(ctx,mode,o,bc,W,H,isFan?266:206);};img.src=o.photo;} else{ctx.fillStyle='#1E1C19';ctx.fillRect(16,16,W-32,isFan?240:180);ctx.font='400 12px Share Tech Mono';ctx.fillStyle='#9A9080';ctx.textAlign='center';ctx.fillText('Tap a photo above',W/2,isFan?140:110);ctx.textAlign='left';_cbRenderText(ctx,mode,o,bc,W,H,isFan?266:206);} } function _cbRenderText(ctx,mode,o,bc,W,H,startY){ var y=startY;var isFan=mode==='fan'; if(isFan&&o.showBadge){ctx.fillStyle=bc;ctx.beginPath();ctx.moveTo(16,y);ctx.lineTo(200,y);ctx.lineTo(192,y+28);ctx.lineTo(16,y+28);ctx.closePath();ctx.fill();ctx.font='900 16px Barlow Condensed';ctx.fillStyle='#0D0C0B';ctx.fillText('I WAS THERE',24,y+20);y+=36;} if(!isFan&&o.showCar){ctx.font='900 52px Barlow Condensed';ctx.fillStyle='#D0190E';ctx.fillText('#'+(S.cur?S.cur.car_number:'??'),20,y+46);y+=56;} if(o.showTrack){ctx.font='700 20px Barlow Condensed';ctx.fillStyle='#F2EDE6';ctx.fillText(S.curTrack?S.curTrack.name:'Race Track',20,y+18);y+=26;} if(o.showDate){ctx.font='400 13px Share Tech Mono';ctx.fillStyle='#C8960A';ctx.fillText(new Date().toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric',year:'numeric'}),20,y+14);y+=22;} if(o.showWeather&&S.wx){ctx.font='400 11px Share Tech Mono';ctx.fillStyle='#9A9080';ctx.fillText(Math.round(S.wx.temp||0)+'\u00B0F DA:'+Math.round(S.wx.density_altitude||0)+'ft',20,y+12);y+=20;} if(o.showStory&&o.story){ctx.font='italic 12px Barlow';ctx.fillStyle='#9A9080';var words=o.story.split(' ');var line='';var lines=[];var maxW=W-40;words.forEach(function(w){var test=line+(line?' ':'')+w;if(ctx.measureText(test).width>maxW&&line){lines.push(line);line=w;}else{line=test;}});if(line)lines.push(line);lines.slice(0,3).forEach(function(l){y+=15;ctx.fillText(l,20,y);});y+=8;} if(o.showStickers&&o.stickers.length){ctx.font='24px serif';y+=6;o.stickers.forEach(function(si,idx){ctx.fillText(_STICKERS[si],20+idx*42,y+24);});y+=34;} ctx.fillStyle=bc;ctx.fillRect(20,H-36,W-40,1);ctx.font='700 10px Barlow Condensed';ctx.fillStyle='#9A9080';ctx.fillText('racer.wiki',20,H-18); } // ── B2: Fan Hero Card entry point in Extras ───────────────────────────────── function _buildFanHeroCard(el){ if(!el)return; var wrap=document.createElement('div'); wrap.style.cssText='background:linear-gradient(135deg,rgba(200,150,10,.06),rgba(208,25,14,.04));border:1px solid rgba(200,150,10,.15);padding:14px;margin-bottom:12px;'; wrap.innerHTML='
I WAS THERE CARD
' +'
Pick your photo, border, and stickers. Add your story.
' +''; el.appendChild(wrap); setTimeout(function(){var lb=document.getElementById('hero-launch-btn');if(lb)lb.onclick=function(){_openCardBuilder('fan');};},50); } // ── B5: Geometry save collector helper ─────────────────────────────────────── function _collectGeometry(){ return { toe_f:parseFloat(($("lb-geo-toe-f")&&$("lb-geo-toe-f").value)||0)||null, toe_r:parseFloat(($("lb-geo-toe-r")&&$("lb-geo-toe-r").value)||0)||null, bump_lf:($("lb-geo-bump-lf")&&$("lb-geo-bump-lf").value)||"", bump_rf:($("lb-geo-bump-rf")&&$("lb-geo-bump-rf").value)||"", ackerman:parseFloat(($("lb-geo-ackerman")&&$("lb-geo-ackerman").value)||0)||null, trail_l:parseFloat(($("lb-geo-trail-l")&&$("lb-geo-trail-l").value)||0)||null, trail_r:parseFloat(($("lb-geo-trail-r")&&$("lb-geo-trail-r").value)||0)||null, pinion:parseFloat(($("lb-geo-pinion")&&$("lb-geo-pinion").value)||0)||null, nose_height:parseFloat(($("lb-geo-nose")&&$("lb-geo-nose").value)||0)||null, spoiler:parseFloat(($("lb-geo-spoiler")&&$("lb-geo-spoiler").value)||0)||null, rc_f:parseFloat(($("lb-geo-rc-f")&&$("lb-geo-rc-f").value)||0)||null, rc_r:parseFloat(($("lb-geo-rc-r")&&$("lb-geo-rc-r").value)||0)||null, }; } /** ─── AiM / MyChron DRK upload (parser + garage save path) ─── * Parser: drk-parser-min.js (inlined below). UX/save layer only — scaling fixes are Opus-owned. * Flow: loggerHandleFile → _logParseDRKBuf → _logRenderDrkSummary → _logSaveDrkToBbLogger → bb-logger-save */ window.parseDRK=function(buf){var u8=new Uint8Array(buf),dv=new DataView(buf),len=u8.length;function fa(sig){var h=[];for(var i=0;i<=len-4;i++){if(u8[i]===sig[0]&&u8[i+1]===sig[1]&&u8[i+2]===sig[2]&&u8[i+3]===sig[3])h.push(i);}return h;}function rs(off,ml){var s="";for(var i=0;i=32&&b<127)s+=String.fromCharCode(b);}return s.trim();}var ppx=fa([80,80,88,2]),meta={class_name:"",track_name:"",racer_name:"",session_name:""};if(ppx.length){var p=ppx[0];meta.class_name=rs(p+32,40);meta.track_name=rs(p+72,40);meta.racer_name=rs(p+112,40);meta.session_name=rs(p+152,40);}var mmx=fa([77,77,88,2]),chs=[];for(var mi=0;mi0&&sc<1e7)chs.push({name:nm,sampleCount:sc,scaleA:sA,scaleB:sB,data:null});}var rrx=fa([82,82,88,2]);if(!rrx.length)throw new Error("No RRX block");var cursor=rrx[0]+4128;for(var ci=0;cilen){vals[si]=NaN;continue;}vals[si]=dv.getUint16(cursor,true);cursor+=2;}ch.data=vals;if(isFinite(ch.scaleA)&&isFinite(ch.scaleB)&&ch.scaleA!==ch.scaleB&&Math.abs(ch.scaleA)<1e6&&Math.abs(ch.scaleB)<1e6){var pn=Math.min(ch.scaleA,ch.scaleB),px=Math.max(ch.scaleA,ch.scaleB),sd=new Float64Array(ch.sampleCount);for(var j=0;jmx)mx=d[k];}ch.stats={min:cnt?mn:0,max:cnt?mx:0,avg:cnt?sum/cnt:0,count:cnt};}return {metadata:meta,channels:chs,totalBytes:len};}; function _logResolveDrkCarId(){ var c=S.cur; if(!c||c._demo||c._local||!c.id)return null; if(/^preview/i.test(String(c.id)))return null; return c.id; } function _logResolveDrkTrackId(){ return S.curTrack&&S.curTrack.id?S.curTrack.id:null; } async function _logEnsureTrackIdForDrk(){ if(!S.curTrack||S.curTrack.id)return S.curTrack&&S.curTrack.id||null; var slug=S.curTrack.page_slug||_trackSlug(S.curTrack.name); if(!slug)return null; try{ var sb=(typeof _SB!=="undefined"?_SB:"https://zmrouoqututfndplboyc.supabase.co"); var ak=(typeof _AK!=="undefined"?_AK:null); if(!ak)return null; var r=await fetch(sb+"/rest/v1/bb_tracks?page_slug=eq."+encodeURIComponent(slug)+"&select=id,page_slug,name&limit=1",{headers:{apikey:ak,Authorization:"Bearer "+ak,Accept:"application/json"}}); var rows=await r.json(); if(rows&&rows[0]&&rows[0].id){S.curTrack.id=rows[0].id;S.curTrack.page_slug=rows[0].page_slug||slug;return rows[0].id;} }catch(e){console.log("[BB] DRK track id lookup",e);} return null; } async function _logEnsureWxForDrkSave(){ var wx=S.wx||{}; if(wx.temp!=null||wx.temp_f!=null)return wx; if(typeof loadWx==="function"){try{await loadWx();}catch(e){}} return S.wx||{}; } function _logDrkWxFields(){ var snap={}; if(typeof _weatherPayloadForApi==="function"){ var p=_weatherPayloadForApi(); if(p&&p.temp_f!=null){ snap.temp_f=p.temp_f;snap.humidity=p.humidity;snap.density_altitude=p.density_altitude; snap.wind_speed=p.wind_speed;snap.dewpoint=p.dewpoint;snap.track_condition=p.track_condition; snap.surface_temp_f=p.surface_temp_f;snap.wind_dir=S.wx&&S.wx.wind_dir; } } if(snap.temp_f==null&&S.wx){ snap.temp_f=S.wx.temp!=null?S.wx.temp:S.wx.temp_f;snap.humidity=S.wx.humidity; snap.density_altitude=S.wx.density_altitude!=null?S.wx.density_altitude:S.wx.da; snap.wind_speed=S.wx.wind_speed;snap.dewpoint=S.wx.dewpoint;snap.wind_dir=S.wx.wind_dir; if(typeof _trackConditionFromWx==="function")snap.track_condition=_trackConditionFromWx(); } return snap; } function _logHeadingDelta(to,from){var d=to-from;while(d>180)d-=360;while(d<-180)d+=360;return d;} function _logWindDirToDeg(windDir){ if(windDir==null)return null; if(typeof windDir==="number"&&isFinite(windDir))return((windDir%360)+360)%360; var s=String(windDir).trim().toUpperCase(),map={N:0,NNE:22.5,NE:45,ENE:67.5,E:90,ESE:112.5,SE:135,SSE:157.5,S:180,SSW:202.5,SW:225,WSW:247.5,W:270,WNW:292.5,NW:315,NNW:337.5}; if(map[s]!=null)return map[s]; var m=s.match(/^(\d+(?:\.\d+)?)/);return m?((parseFloat(m[1])%360)+360)%360:null; } function _logEstimateStraightHeading(h,spd,minSpd){ minSpd=minSpd||20;var pts=[],i;for(i=0;i0.4)dir="clockwise";else if(avg<-0.4)dir="counterclockwise"; var conf=Math.min(1,(Math.abs(avg)/4)*Math.min(1,cnt/80)); return{track_direction:dir,confidence:Math.round(conf*100)/100,heading_samples:cnt,mean_heading_rate_deg:Math.round(avg*100)/100,straight_heading_deg:straight?Math.round(straight*10)/10:null,gps_channels:{heading:hdgCh.name,speed:spdCh?spdCh.name:null}}; } function _logDrkWindContext(dirCtx,wx){ if(!wx||wx.wind_speed==null)return null; var straight=dirCtx&&dirCtx.straight_heading_deg,windDeg=_logWindDirToDeg(wx.wind_dir); if(straight==null||windDeg==null)return{wind_speed_mph:Math.round(wx.wind_speed),wind_from:wx.wind_dir||null,summary:"Wind "+Math.round(wx.wind_speed)+"mph (need GPS heading + wind dir)",drying_notes:[]}; var blowTo=(windDeg+180)%360,rel=_logHeadingDelta(blowTo,straight),summary; if(Math.abs(rel)<40)summary="Tailwind on fastest straight"; else if(Math.abs(rel)>140)summary="Headwind into fastest straight"; else if(rel>0)summary="Crosswind from infield"; else summary="Crosswind from outside"; var notes=[],td=dirCtx.track_direction; if(td==="counterclockwise"){if(summary.indexOf("Headwind")>=0)notes.push("Inside line (turns 1–2) may stay damp longer");if(summary.indexOf("Tailwind")>=0)notes.push("Back straight may dry faster");if(summary.indexOf("Crosswind from infield")>=0)notes.push("Outside of turns 3–4 may see more airflow");} else if(td==="clockwise"){if(summary.indexOf("Headwind")>=0)notes.push("High-side entry may hold moisture longer");if(summary.indexOf("Tailwind")>=0)notes.push("Primary straight may dry first");} return{wind_speed_mph:Math.round(wx.wind_speed),wind_from_deg:windDeg,wind_from:wx.wind_dir||null,straight_heading_deg:straight,relative_wind_deg:Math.round(rel*10)/10,summary:summary,drying_notes:notes}; } function _logCollectTrackPhotoRefs(){ var refs=[],track=S.curTrack,trackName=track&&track.name,trackKey=track?("bb_surface_"+String(track.id||track.name||"track").replace(/\W/g,"_")):null; if(trackKey){try{var snap=JSON.parse(localStorage.getItem(trackKey)||"{}");if(snap.last_cam_scan){var sm=snap.last_cam_scan.scan_mode||"moisture";refs.push({source:"bb_surface",ts:snap.last_cam_scan.ts||null,turn:sm==="composition"?"off_line":"race_line",scan_type:sm,section:sm==="composition"?"t2":"front_straight"});}}catch(e){}} var carId=S.cur?(S.cur.id||"local"):"local"; try{var log=JSON.parse(localStorage.getItem("bb_camGuide_"+carId)||"[]");for(var ci=0;cibestCount){best=today;bestCount=tc;bestKey=_walkKey();}} try{ var prefix="bb_walk_"+carId+"_"+trackSlug+"_"; for(var i=0;ibestCount){best=d;bestCount=c;bestKey=k;} } }catch(e){} if(!best||bestCount<1)return null; best._walk_key=bestKey;return best; } function _logGuidedWalkSectionIntel(){ var tw=S._trackWalk; if(!tw||!tw.spots||!tw.conds)return null; var spotToSec={T1:"t1",T2:"t2",T3:"t3",T4:"t4",Front:"front_straight",Back:"back_straight"}; var condMap={greasy:88,tacky:74,prime:58,drying:54,slick:30,dry:22}; var sections={},i; for(i=0;ibest.sampleCount)best=chs[i];}} if(!best||!best.sampleCount)return null; var hz=10,sec=best.sampleCount/hz,mins=sec/60; return mins>0.2?Math.round(mins*10)/10:null; } function _logDeriveInstrumentation(raw){ raw=raw||{}; var out={heat_up_rate:null,bottoming_rate:null}; var hu=_logExpNum(raw.heat_up_rate); if(hu==null){ var pre=_logExpNum(raw.tire_temp_pre_f),post=_logExpNum(raw.tire_temp_post_f),mins=_logExpNum(raw.run_minutes); if(pre!=null&&post!=null&&mins!=null&&mins>0)hu=Math.round(((post-pre)/mins)*1000)/1000; } out.heat_up_rate=hu; var br=_logExpNum(raw.bottoming_rate); if(br==null){ var ev=_logExpNum(raw.shock_bottom_events),laps=_logExpNum(raw.laps); var used=_logExpNum(raw.shock_travel_used_in),avail=_logExpNum(raw.shock_travel_avail_in); if(ev!=null&&laps!=null&&laps>0)br=Math.round((ev/laps)*1000)/1000; else if(used!=null&&avail!=null&&avail>0)br=Math.round(Math.min(1.5,used/avail)*1000)/1000; } out.bottoming_rate=br; return out; } function _logSyncPyrometerPrimary(draft){ draft=draft||{}; var corner=draft.tire_pyrometer_corner||"rr"; if(draft.tire_pre&&draft.tire_pre[corner])draft.tire_temp_pre_f=draft.tire_pre[corner]; if(draft.tire_post&&draft.tire_post[corner])draft.tire_temp_post_f=draft.tire_post[corner]; if(!draft.tire_temp_pre_f&&draft.tire_pre&&draft.tire_pre.rr)draft.tire_temp_pre_f=draft.tire_pre.rr; if(!draft.tire_temp_post_f&&draft.tire_post&&draft.tire_post.rr)draft.tire_temp_post_f=draft.tire_post.rr; return draft; } function _logBuildSetupSnapshot(draft){ draft=draft||{}; var su=typeof _su!=="undefined"?_su:{},c=S.cur||{}; var snap={ captured_from:"active_setup_sheet", captured_at:new Date().toISOString(), car_id:c.id||null, lf_psi:su.lf_psi,rf_psi:su.rf_psi,lr_psi:su.lr_psi,rr_psi:su.rr_psi, stagger_r:su.stagger_r,stagger_f:su.stagger_f,stagger:su.stagger, panhard:su.panhard,j_bar_height:su.j_bar_height,bite:su.bite, left_pct:su.left_pct,rear_pct:su.rear_pct,cross_pct:su.cross_pct, lf_spring:su.lf_spring,rf_spring:su.rf_spring,lr_spring:su.lr_spring,rr_spring:su.rr_spring, wing_angle:su.wing_angle,rr_duro:su.rr_duro, comp_low:draft.comp_low!==""&&draft.comp_low!=null?draft.comp_low:(c.comp_low!=null?c.comp_low:null), comp_high:draft.comp_high!==""&&draft.comp_high!=null?draft.comp_high:(c.comp_high!=null?c.comp_high:null), reb_low:draft.reb_low!==""&&draft.reb_low!=null?draft.reb_low:(c.reb_low!=null?c.reb_low:null), reb_high:draft.reb_high!==""&&draft.reb_high!=null?draft.reb_high:(c.reb_high!=null?c.reb_high:null), bump_engaged:draft.bump_engaged!==""&&draft.bump_engaged!=null?draft.bump_engaged:(c.bump_engaged!=null?c.bump_engaged:null), helper_soft:draft.helper_soft!==""&&draft.helper_soft!=null?draft.helper_soft:(c.helper_soft!=null?c.helper_soft:null), sipe_count:draft.sipe_count!==""&&draft.sipe_count!=null?draft.sipe_count:(c.sipe_count!=null?c.sipe_count:null), groove_density:draft.groove_density!==""&&draft.groove_density!=null?draft.groove_density:(c.groove_density!=null?c.groove_density:null), panhard_height:draft.panhard_height!==""&&draft.panhard_height!=null?draft.panhard_height:(su.panhard!=null?su.panhard:null) }; var h={}; if(snap.stagger_r!=null&&snap.stagger_r!=="")h.stagger=_logExpNum(snap.stagger_r); else if(snap.stagger!=null&&snap.stagger!=="")h.stagger=_logExpNum(snap.stagger); if(snap.panhard_height!=null&&snap.panhard_height!=="")h.panhard_height=_logExpNum(snap.panhard_height); else if(snap.panhard!=null&&snap.panhard!=="")h.panhard_height=_logExpNum(snap.panhard); ["comp_low","comp_high","reb_low","reb_high","bump_engaged","helper_soft","sipe_count","groove_density"].forEach(function(k){ if(snap[k]!=null&&snap[k]!==""){var n=_logExpNum(snap[k]);if(n!=null)h[k]=n;} }); snap.harness=h; return snap; } function _logHarnessSetupFromCapture(capture){ if(!capture||!capture.setup_snapshot)return {}; var snap=capture.setup_snapshot,h=snap.harness||{},out={},k; for(k in h)if(h.hasOwnProperty(k)&&h[k]!=null)out[k]=h[k]; if(out.stagger==null&&snap.stagger_r!=null)out.stagger=_logExpNum(snap.stagger_r); if(out.panhard_height==null&&snap.panhard!=null)out.panhard_height=_logExpNum(snap.panhard); return out; } function _logBlankDrkExpDraft(){ return{experiment_id:"",arm:"",track_state:"",tire_pyrometer_corner:"rr", tire_temp_pre_f:"",tire_temp_post_f:"",run_minutes:"",laps:"", shock_travel_used_in:"",shock_travel_avail_in:"",shock_travel_corner:"lr", shock_bottom_events:"",surface_temp_f:"",air_temp_f:"", tire_pre:{lf:"",rf:"",lr:"",rr:""},tire_post:{lf:"",rf:"",lr:"",rr:""}, shock_travel_notes:"",bottoming_notes:"",instrumentation:"",wheel_hop_rating:"",lateral_loss_rating:"", comp_low:"",comp_high:"",reb_low:"",reb_high:"",panhard_height:"",bump_engaged:"",helper_soft:"",sipe_count:"",groove_density:"", section_moisture:{t1:"",t2:"",t3:"",t4:"",front:"",back:""},drk_filename:"",notes:"",enabled:false}; } var _logDrkTrackStates=["greasy","tacky","drying","slick","dry_slick","rubbered"]; var _logOvalSections=[ {id:"front_straight",label:"Front straight",offset:0}, {id:"t1",label:"Turn 1",offset:35}, {id:"t2",label:"Turn 2",offset:75}, {id:"back_straight",label:"Back straight",offset:180}, {id:"t3",label:"Turn 3",offset:215}, {id:"t4",label:"Turn 4",offset:325} ]; function _logLoadDrkExpDraft(){ try{var raw=JSON.parse(localStorage.getItem(_DRK_EXP_DRAFT_KEY)||"null");if(raw&&typeof raw==="object")return Object.assign(_logBlankDrkExpDraft(),raw);}catch(e){} return _logBlankDrkExpDraft(); } function _logSaveDrkExpDraft(d){ _logDrkExpDraft=d;try{localStorage.setItem(_DRK_EXP_DRAFT_KEY,JSON.stringify(d));}catch(e){} } function _logInitDrkExpDraft(fileName,parsed){ var d=_logLoadDrkExpDraft(); d.drk_filename=fileName||d.drk_filename||""; if(parsed){ var estMins=_logEstimateDrkRunMinutes(parsed); if(estMins!=null&&!d.run_minutes)d.run_minutes=String(estMins); } if(!d.track_state&&typeof _trackConditionFromWx==="function"){var tc=_trackConditionFromWx();if(tc)d.track_state=String(tc).toLowerCase();} if(S.wx&&S.wx.temp!=null&&!d.air_temp_f)d.air_temp_f=String(S.wx.temp); if(S.wx&&S.wx.surface_temp_f!=null&&!d.surface_temp_f)d.surface_temp_f=String(S.wx.surface_temp_f); var walkData=_logFindTrackWalkData(); if(walkData)_logMergeWalkMoistureIntoDraft(d,_logBuildWalkSectionIntel(walkData)); _logSyncPyrometerPrimary(d); _logSaveDrkExpDraft(d); return d; } function _logDrkExpById(id){for(var i=0;i<_logDrkExpProgram.length;i++)if(_logDrkExpProgram[i].id===id)return _logDrkExpProgram[i];return null;} function _logReadDrkExperimentCapture(parsed){ var d=_logDrkExpDraft||_logLoadDrkExpDraft(); _logSyncPyrometerPrimary(d); var hasRaw=!!(d.experiment_id||d.tire_temp_pre_f||d.tire_temp_post_f||d.run_minutes||d.shock_travel_used_in||d.shock_travel_avail_in||d.shock_bottom_events); if(!d.enabled&&!hasRaw&&!d.arm&&!d.shock_travel_notes&&!d.bottoming_notes&&!d.instrumentation)return null; var exp=_logDrkExpById(d.experiment_id); var instrRaw={ tire_temp_pre_f:_logExpNum(d.tire_temp_pre_f),tire_temp_post_f:_logExpNum(d.tire_temp_post_f),run_minutes:_logExpNum(d.run_minutes), shock_travel_used_in:_logExpNum(d.shock_travel_used_in),shock_travel_avail_in:_logExpNum(d.shock_travel_avail_in), shock_bottom_events:_logExpNum(d.shock_bottom_events),laps:_logExpNum(d.laps) }; var derived=_logDeriveInstrumentation(Object.assign({},instrRaw,{heat_up_rate:null,bottoming_rate:null})); var setupSnap=_logBuildSetupSnapshot(d); return{ experiment_id:d.experiment_id||null,experiment_name:exp?exp.name:null,study_targets:exp?exp.studies:null, arm:d.arm||null,track_state:d.track_state||null, setup_snapshot:setupSnap, surface_temp_f:_logExpNum(d.surface_temp_f),air_temp_f:_logExpNum(d.air_temp_f),laps:instrRaw.laps, tire_temps_f:{pre:d.tire_pre||{},post:d.tire_post||{}}, tire_pyrometer_corner:d.tire_pyrometer_corner||"rr", tire_temp_pre_f:instrRaw.tire_temp_pre_f,tire_temp_post_f:instrRaw.tire_temp_post_f,run_minutes:instrRaw.run_minutes, shock_travel_used_in:instrRaw.shock_travel_used_in,shock_travel_avail_in:instrRaw.shock_travel_avail_in, shock_travel_corner:d.shock_travel_corner||"lr",shock_bottom_events:instrRaw.shock_bottom_events, responses:{heat_up_rate:derived.heat_up_rate,bottoming_rate:derived.bottoming_rate}, shock_travel_notes:d.shock_travel_notes||null,bottoming_notes:d.bottoming_notes||null, instrumentation:d.instrumentation||null,wheel_hop_rating:d.wheel_hop_rating||null,lateral_loss_rating:d.lateral_loss_rating||null, section_moisture:d.section_moisture||{},drk_filename:d.drk_filename||null,notes:d.notes||null, linked_instrumentation:typeof _instrCollectRefs==="function"?_instrCollectRefs().filter(function(r){return!d.experiment_id||r.experiment_id===d.experiment_id;}):[], _provenance:{study_targets:exp?exp.studies:null,instrumentation:instrRaw,derived:derived,source:"garage_drk_capture"}, captured_at:new Date().toISOString() }; } function _logAppendDrkExperimentRun(capture,saveId){ if(!capture||!capture.experiment_id)return; var runs=[];try{runs=JSON.parse(localStorage.getItem(_DRK_EXP_RUNS_KEY)||"[]");}catch(e){} runs.push(Object.assign({},capture,{logger_save_id:saveId||null})); if(runs.length>500)runs=runs.slice(-500); try{localStorage.setItem(_DRK_EXP_RUNS_KEY,JSON.stringify(runs));}catch(e2){} } function _logMoistureObsScore(state){ var m={greasy:15,tacky:35,drying:55,slick:72,dry_slick:88,rubbered:92,heavy:25,prime:45}; var k=String(state||"").toLowerCase().replace(/\s+/g,"_"); return m[k]!=null?m[k]:null; } function _logSectionHeadingDeg(straight,offset,cw){var h=(straight+(cw?-offset:offset)+360)%360;return h;} function _logWindDryingFactor(sectionH,windDeg,windSpd){ if(sectionH==null||windDeg==null)return{boost:0,label:"no wind data"}; var blowTo=(windDeg+180)%360,rel=_logHeadingDelta(blowTo,sectionH),boost=0,label="crosswind"; if(Math.abs(rel)<40){boost=0.22+(windSpd||0)/80;label="tailwind exposure";} else if(Math.abs(rel)>140){boost=-0.12;label="headwind / sheltered";} else{boost=0.06;label="crosswind scrub";} return{boost:boost,label:label,relative_wind_deg:Math.round(rel*10)/10}; } function _logEstimateSectionDrying(dirCtx,windCtx,wx,photos,sectionObs,walkIntel){ var straight=dirCtx&&dirCtx.straight_heading_deg,cw=dirCtx&&dirCtx.track_direction==="clockwise"; var temp=wx&&(wx.temp_f!=null?wx.temp_f:wx.temp),hum=wx&&wx.humidity,wSpd=wx&&wx.wind_speed,windDeg=windCtx&&windCtx.wind_from_deg; var base=0.35; if(temp!=null)base+=Math.max(0,Math.min(0.35,(temp-55)/120)); if(hum!=null)base+=Math.max(0,Math.min(0.25,(100-hum)/140)); if(wSpd!=null)base+=Math.max(0,Math.min(0.2,wSpd/35)); base=Math.round(Math.min(1,base)*100); var photoBySec={};(photos||[]).forEach(function(p){var s=p.section;if(s&&s!=="unspecified"&&s!=="multi")photoBySec[s]=(photoBySec[s]||0)+1;}); var obs=sectionObs||{},walkSecs=walkIntel&&walkIntel.sections||{},out=[]; _logOvalSections.forEach(function(sec){ var secH=straight!=null?_logSectionHeadingDeg(straight,sec.offset,cw):null; var wf=_logWindDryingFactor(secH,windDeg,wSpd); var score=base+Math.round(wf.boost*100); if(photoBySec[sec.id])score+=Math.min(12,photoBySec[sec.id]*5); var wi=walkSecs[sec.id]; var obsKey=sec.id.indexOf("straight")>=0?(sec.id.indexOf("front")>=0?"front":"back"):sec.id; var obsVal=obs[obsKey]||obs[sec.id],obsScore=_logMoistureObsScore(obsVal); var moisture=wi&&wi.moisture_score!=null?wi.moisture_score:obsScore; if(moisture!=null){ if(moisture>=60)score+=12; else if(moisture>=45)score+=6; else if(moisture<=28)score-=10; } if(wi&&wi.avg_grip!=null){ if(wi.avg_grip<=35)score-=6; else if(wi.avg_grip>=70)score+=4; } if(wi&&wi.photo_count)score+=Math.min(10,wi.photo_count*6); var rate=score>=58?"faster":score<=42?"slower":"neutral"; var factors=[wf.label]; if(wSpd!=null)factors.push(Math.round(wSpd)+"mph wind"); if(hum!=null)factors.push(hum+"% RH"); if(photoBySec[sec.id])factors.push(photoBySec[sec.id]+" photo(s)"); if(wi&&wi.dominant_tag)factors.push("walk: "+wi.dominant_tag.toLowerCase()); else if(wi&&wi.moisture_score!=null)factors.push("walk moisture ~"+wi.moisture_score); else if(obsVal)factors.push("observed "+obsVal); if(wi&&wi.photo_count)factors.push(wi.photo_count+" walk soil photo"+(wi.photo_count===1?"":"s")); out.push({section:sec.id,label:sec.label,relative_rate:rate,drying_score:Math.max(0,Math.min(100,score)), heading_deg:secH!=null?Math.round(secH*10)/10:null,observed_moisture:obsVal||(wi&&wi.moisture_label)||null,observed_score:moisture, photo_refs:(photoBySec[sec.id]||0)+(wi&&wi.photo_count||0),walk_photo_refs:wi&&wi.photo_count||0,factors:factors}); }); out.sort(function(a,b){return b.drying_score-a.drying_score;}); var fastest=out[0],slowest=out[out.length-1]; var summary=fastest&&slowest&&fastest.section!==slowest.section ?fastest.label+" drying fastest · "+slowest.label+" lagging" :"Section drying model (wind + wx foundation)"; if(walkIntel&&walkIntel.walk_points)summary+=" · "+walkIntel.walk_points+" walk pts"; return{ sections:out, summary:summary, model:walkIntel&&walkIntel.walk_points?"v1_wind_wx_walk_photos":"v0_wind_wx_photos", base_index:base, track_walk_points:walkIntel&&walkIntel.walk_points||0, track_walk_photos:walkIntel&&walkIntel.photo_count||0 }; } var _TRACK_ORIENT_STORAGE="bb_track_orientation_v1"; var _TRACK_LINE_PROG_STORAGE="bb_track_line_progression_v1"; var _TRACK_SESSION_LOG_STORAGE="bb_track_session_log_v1"; var _TRACK_SNAPS_STORAGE="bb_track_snaps_v1"; var _TS_PRESETS=[ {id:"slicking",label:"Slicking up",short:"SLICK+"}, {id:"rubbering",label:"Rubbering up",short:"RUBBER+"}, {id:"drying_lanes",label:"Drying lanes",short:"DRY LANE"}, {id:"cooling",label:"Cooling off",short:"COOL"}, {id:"stable",label:"Stable",short:"STABLE"} ]; function _tsSyncQuickLogToSessionLog(feel){ if(typeof _logAppendSessionLog!=="function")return; try{ var su=typeof _su!=="undefined"?_su:{}; var finishEl=document.getElementById("bb-ql-finish"),startEl=document.getElementById("bb-ql-start"); var barEl=document.getElementById("bb-ql-bar-lr"),wingEl=document.getElementById("bb-ql-wing"); var lrBar=barEl&&barEl.value?parseFloat(barEl.value):null; var wing=wingEl&&wingEl.value?parseFloat(wingEl.value):null; if(lrBar!=null&&!isNaN(lrBar))su.lr_bar_turns=lrBar; if(wing!=null&&!isNaN(wing))su.wing_angle=wing; var CCR=window.CrewChiefReport; var pf=typeof _qlPhaseFeel!=="undefined"?_qlPhaseFeel:{}; var traitEntry=CCR&&CCR.feelChipToTrait?CCR.feelChipToTrait(pf.entry):null; var traitMid=CCR&&CCR.feelChipToTrait?CCR.feelChipToTrait(pf.mid):null; var traitExit=CCR&&CCR.feelChipToTrait?CCR.feelChipToTrait(pf.exit):null; var primaryFeel=feel||pf.mid||pf.entry||pf.exit||null; _logAppendSessionLog({ session:typeof _qlSessionLabel!=="undefined"?_qlSessionLabel:_tsGuessSessionLabel(), track_state:typeof _qlTrackState!=="undefined"&&_qlTrackState?_qlTrackState:(typeof _logInferTrackState==="function"?_logInferTrackState():null), driver_feel:primaryFeel, feel_entry:pf.entry||null, feel_mid:pf.mid||null, feel_exit:pf.exit||null, trait_entry:traitEntry, trait_mid:traitMid, trait_exit:traitExit, rf_psi:su.rf_psi!=null?su.rf_psi:null, lf_psi:su.lf_psi!=null?su.lf_psi:null, rr_psi:su.rr_psi!=null?su.rr_psi:null, lr_psi:su.lr_psi!=null?su.lr_psi:null, stagger:su.stagger!=null?su.stagger:null, left_pct:su.left_pct!=null?su.left_pct:null, rear_pct:su.rear_pct!=null?su.rear_pct:null, wing_angle:wing!=null&&!isNaN(wing)?wing:(su.wing_angle!=null?su.wing_angle:(S.cur&&S.cur.wing_angle!=null?S.cur.wing_angle:null)), lr_bar_turns:lrBar!=null&&!isNaN(lrBar)?lrBar:(su.bite!=null?su.bite:(su.lr_bar_turns!=null?su.lr_bar_turns:null)), finishing_position:finishEl&&finishEl.value?parseInt(finishEl.value,10):null, starting_position:startEl&&startEl.value?parseInt(startEl.value,10):null, note:primaryFeel?"Quick log · feel "+primaryFeel:null, source:"quick_log" }); if(typeof _rtUpdateLauncherMeter==="function")_rtUpdateLauncherMeter(); }catch(e){} } function _tsTrackSlug(){return typeof _logTrackIntelSlug==="function"?_logTrackIntelSlug():"unknown";} function _tsLoadSnaps(){ try{var all=JSON.parse(localStorage.getItem(_TRACK_SNAPS_STORAGE)||"{}");return all[_tsTrackSlug()]||[];}catch(e){return[];} } function _tsSaveSnaps(rows){ try{var slug=_tsTrackSlug(),all=JSON.parse(localStorage.getItem(_TRACK_SNAPS_STORAGE)||"{}");all[slug]=rows;localStorage.setItem(_TRACK_SNAPS_STORAGE,JSON.stringify(all));}catch(e){} } function _tsGuessSessionLabel(){ try{ var slug=S.curTrack&&typeof _trackSlug==="function"?_trackSlug(S.curTrack.name):""; var date=new Date().toISOString().slice(0,10).replace(/-/g,""); var carId=S.cur?String(S.cur.id||S.cur.car_number||"car").replace(/\W+/g,""):""; var rKey=(slug&&carId)?"bb_race_"+slug+"_"+carId+"_"+date:null; if(rKey){var rd=JSON.parse(localStorage.getItem(rKey)||"null");if(rd&&rd.phase)return rd.phase;} }catch(e){} var logs=typeof _logLoadSessionLog==="function"?_logLoadSessionLog():[]; if(logs[0]&&logs[0].session)return logs[0].session; return "Tonight"; } function _tsAppendSnap(entry){ try{ var rows=_tsLoadSnaps(); rows.unshift(Object.assign({ts:new Date().toISOString(),session:_tsGuessSessionLabel()},entry)); if(rows.length>20)rows=rows.slice(0,20); _tsSaveSnaps(rows); if(typeof _logRefreshTrackConditionIntel==="function")_logRefreshTrackConditionIntel(); if(typeof _ccRefreshFromGarageIfMounted==="function")_ccRefreshFromGarageIfMounted(); return rows; }catch(e){return[];} } function _tsSubmitSnap(trend,note){ var preset=null;for(var i=0;i<_TS_PRESETS.length;i++){if(_TS_PRESETS[i].id===trend){preset=_TS_PRESETS[i];break;}} var beforeVc=typeof _logVehicleContextForCrewChief==="function"?_logVehicleContextForCrewChief():{}; var row=_tsAppendSnap({trend:trend,label:preset?preset.label:trend,note:note||null,session:_tsGuessSessionLabel()}); if(typeof toast==="function"){ if(_ccIsAdvancedGarageClass())toast("Track snap · "+(preset?preset.label:trend)+" · evolution priors on Generate"); else toast("Track snap · "+(preset?preset.label:trend)); } if(_ccIsAdvancedGarageClass()){ var afterVc=typeof _logVehicleContextForCrewChief==="function"?_logVehicleContextForCrewChief():{}; _ccCaptureFeedbackToast(beforeVc,afterVc,["track_snap"]); } return row; } function _tsLatestSnapLine(){ var snaps=_tsLoadSnaps();if(!snaps.length)return ""; var s=snaps[0],when=s.ts?new Date(s.ts).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"}):""; return (s.label||s.trend||"snap")+(when?" @ "+when:"")+(s.note?" · "+String(s.note).slice(0,60):""); } function _tsRenderTrackSnapBar(mount,opts){ if(!mount)return; opts=opts||{}; mount.innerHTML=""; var box=document.createElement("div"); box.style.cssText="padding:10px;margin-bottom:10px;background:rgba(56,189,248,.05);border:1px solid rgba(56,189,248,.22);border-left:3px solid #38bdf8;border-radius:6px"; var hdr=document.createElement("div"); hdr.style.cssText="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap"; var title=document.createElement("div"); title.style.cssText="font-family:var(--mono);font-size:7px;color:#38bdf8;letter-spacing:1px"; title.textContent="TRACK SNAP · optional · 1 tap"; if(typeof _ccIsAdvancedGarageClass==="function"&&_ccIsAdvancedGarageClass()){ var valHint=document.createElement("div"); valHint.style.cssText="font-size:8px;color:#7dd3fc;line-height:1.45;margin-bottom:6px"; valHint.textContent="1 tap fills evolution when you skip full notes — shows on Crew Chief Generate, not coach clutter"; box.appendChild(valHint); } var sess=document.createElement("div"); sess.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted)"; sess.textContent=_tsGuessSessionLabel(); hdr.appendChild(title);hdr.appendChild(sess);box.appendChild(hdr); var btnRow=document.createElement("div"); btnRow.style.cssText="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px"; _TS_PRESETS.forEach(function(p){ var b=document.createElement("button"); b.type="button"; b.textContent=p.short; b.title=p.label; b.style.cssText="padding:8px 10px;font-family:var(--mono);font-size:8px;font-weight:700;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.35);color:#7dd3fc;cursor:pointer;border-radius:4px"; b.onclick=function(){_tsSubmitSnap(p.id,noteInp&¬eInp.value?noteInp.value.trim():null);if(noteInp&&!opts.keepNote)noteInp.value="";if(latestEl)latestEl.textContent=_tsLatestSnapLine()||"No snaps yet";}; btnRow.appendChild(b); }); box.appendChild(btnRow); var noteInp=document.createElement("input"); noteInp.placeholder="Optional note (middle rubbering…)"; noteInp.style.cssText=_logDrkExpInputStyle()+";font-size:10px;margin-bottom:6px"; box.appendChild(noteInp); var latestEl=document.createElement("div"); latestEl.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);line-height:1.5"; latestEl.textContent=_tsLatestSnapLine()||"No snaps yet — tap a preset when the track changes"; box.appendChild(latestEl); mount.appendChild(box); } var _LINE_PROG_TEMPLATES={ dirt_oval_default:{label:"Dirt oval · slick transition",early:"Bottom / low — tacky grip; establish lane first laps",mid:"Middle-up — split surface; move before field pins you",late:"Rubber or cushion — one lane; RR temp management"}, dirt_oval_heavy:{label:"Dirt oval · heavy / tacky",early:"Low groove — high bite; baseline stagger",mid:"Mid lane for passes — T3–T4 crossover if split",late:"Bottom reserve — don't chase slick with bite"}, bullring_quarter:{label:"1/4-mile bullring",early:"Bottom + bank — RR drives off tacky",mid:"Stagger down window — free center before LR bite",late:"Upper T1–T2 if slick — lane before setup saves anyone"}, bc39_ims:{label:"BC39 / IMS · multi-lane bullring",early:"Bottom / low — tacky on ~12.3s laps; log lane_commit; protect baseline stagger on 8° bank",mid:"Middle-up before field pins you — T3–T4 crossover when split shows; qual passing points > hero lane",late:"Rubber or slider top — one clean lane; RR temp; Night 2 qual pipeline rewards position advanced"}, bc39_ims_slick:{label:"BC39 / IMS · slick window",early:"Free center first — stagger ↓ small steps (1/5-mile ≠ IMW ¼-miles)",mid:"LR ↓ before chasing bite; multi-lane still live if traffic allows",late:"RR protect + gear ↑; never add bite into S2 slick — one DOF per run"} }; var _TRACK_INTEL_EVENT_PRESETS={ "the-dirt-track-at-ims":{ label:"BC39 / The Dirt Track at IMS",event:"BC39",mode:"bc39",line_template:"bc39_ims", slugs:["the-dirt-track-at-ims","ims-dirt-track-in","dirt-track-at-ims"], orientation:{front_straight_deg:96,back_straight_deg:276,direction:"counterclockwise",source:"bc39_preset",banking_deg:8}, track_profile:{label:"1/5-mile bullring inside IMS T3",length_mi:0.2,banking_deg:8,lap_target_s:12.3,multi_lane:true,width_ft:"50-60"}, drying_model:{ note:"CCW bullring · back straight (~276°) often loses sheen first in W–SW wind (~250–280°); east/front apron holds longer (IMS wall shadow)", default_wind_from_deg:270,validate_field:"sheen_loss_west_back_first", reserve_end:"east_front_low",fastest_section_hint:"back_straight" }, surface_evolution:"S0 tacky → S1 split (one heat) → S2 slick by Night 2 feature on warm late-June evening", recommendation_priorities:[ {lever:"qual_pipeline",text:"Night 2: advance positions in 10-lap qual races — top 18 lock feature grid",basis:"event_prior"}, {lever:"lane",text:"Multi-lane from lap 1 — log lane_commit each session; don't lock one lane early",basis:"event_prior"}, {lever:"stagger",text:"Smaller stagger steps than IMW ¼-miles; gear ~1 notch above Kokomo baseline",basis:"event_prior"}, {lever:"slick_sequence",text:"Tacky→slick: stagger ↓ → LR ↓ → RR protect → soften → gear ↑ (one DOF per run)",basis:"event_prior"} ] } }; function _logIsBc39Mode(){return!!_logMatchTrackIntelPreset();} function _logInferTrackState(){ var logs=_logLoadSessionLog(); if(logs[0]&&logs[0].track_state)return logs[0].track_state; var draft=typeof _logLoadDrkExpDraft==="function"?(_logDrkExpDraft||_logLoadDrkExpDraft()):null; if(draft&&draft.track_state)return draft.track_state; if(typeof _trackConditionFromWx==="function"){ var tc=String(_trackConditionFromWx()||"").toLowerCase().replace(/\s+/g,"_"); if(tc)return tc; } return null; } function _logMatchTrackIntelPreset(){ var slug=_logTrackIntelSlug(); var keys=Object.keys(_TRACK_INTEL_EVENT_PRESETS); for(var i=0;i=0))return ep; } var nm=(S.curTrack&&((S.curTrack.name||"")+" "+(S.curTrack.short||"")))||""; if(/bc39|dirt track at ims|ims turn 3|the dirt track at ims/i.test(nm)||/bc39|dirt-track-at-ims|dirt_track_at_ims|ims-dirt/.test(slug)) return _TRACK_INTEL_EVENT_PRESETS["the-dirt-track-at-ims"]; return null; } function _logApplyTrackIntelEventPreset(silent){ var ep=_logMatchTrackIntelPreset();if(!ep)return false; var existO=_logLoadTrackOrientation(); if(!existO||existO.source!=="gps") _logSaveTrackOrientation(Object.assign({},ep.orientation,{source:"bc39_preset",event:ep.event||null,label:ep.label})); var lp=_logLoadLineProgression(); if((!lp||lp.template_id!==ep.line_template)&&_LINE_PROG_TEMPLATES[ep.line_template]){ var t=_LINE_PROG_TEMPLATES[ep.line_template]; if(!lp||lp.template_id!==ep.line_template) _logSaveLineProgression({template_id:ep.line_template,early:t.early,mid:t.mid,late:t.late,source:"bc39_preset"}); } if(!silent&&typeof toast==="function")toast(ep.label+" intel preset applied"); return true; } function _logTrackIntelSlug(){ if(S.curTrack&&typeof _trackSlug==="function")return _trackSlug(S.curTrack.name); if(S.curTrack&&S.curTrack.name)return String(S.curTrack.name).toLowerCase().replace(/[^a-z0-9]/g,"_"); return "unknown"; } function _logLoadTrackOrientation(){ try{var all=JSON.parse(localStorage.getItem(_TRACK_ORIENT_STORAGE)||"{}");return all[_logTrackIntelSlug()]||null;}catch(e){return null;} } function _logSaveTrackOrientation(obj){ try{var all=JSON.parse(localStorage.getItem(_TRACK_ORIENT_STORAGE)||"{}");all[_logTrackIntelSlug()]=Object.assign({updated_at:new Date().toISOString()},obj);localStorage.setItem(_TRACK_ORIENT_STORAGE,JSON.stringify(all));}catch(e){} } function _logApplyOrientationPreset(dirCtx){ var preset=_logLoadTrackOrientation(),d=Object.assign({},dirCtx||{}); if(preset){ if(d.straight_heading_deg==null&&preset.front_straight_deg!=null)d.straight_heading_deg=preset.front_straight_deg; if((!d.track_direction||d.track_direction==="unknown")&&preset.direction)d.track_direction=preset.direction; d.orientation_preset=preset; } return d; } function _logLoadLineProgression(){ try{var all=JSON.parse(localStorage.getItem(_TRACK_LINE_PROG_STORAGE)||"{}");return all[_logTrackIntelSlug()]||null;}catch(e){return null;} } function _logSaveLineProgression(obj){ try{var all=JSON.parse(localStorage.getItem(_TRACK_LINE_PROG_STORAGE)||"{}");all[_logTrackIntelSlug()]=Object.assign({updated_at:new Date().toISOString()},obj);localStorage.setItem(_TRACK_LINE_PROG_STORAGE,JSON.stringify(all));}catch(e){toast("Could not save line notes");} } function _logDefaultLineProgression(){ var ep=_logMatchTrackIntelPreset(); if(ep&&_LINE_PROG_TEMPLATES[ep.line_template]){var t=_LINE_PROG_TEMPLATES[ep.line_template];return{template_id:ep.line_template,early:t.early,mid:t.mid,late:t.late,source:"bc39_preset"};} var st=_logInferTrackState(); if(ep&&st&&(st.indexOf("slick")>=0||st.indexOf("dry")>=0)&&_LINE_PROG_TEMPLATES.bc39_ims_slick){var ts=_LINE_PROG_TEMPLATES.bc39_ims_slick;return{template_id:"bc39_ims_slick",early:ts.early,mid:ts.mid,late:ts.late,source:"bc39_slick"};} var tc=typeof _trackConditionFromWx==="function"?String(_trackConditionFromWx()||"").toLowerCase():""; var tpl=tc.indexOf("heavy")>=0||tc.indexOf("tacky")>=0||tc.indexOf("greasy")>=0?_LINE_PROG_TEMPLATES.dirt_oval_heavy:_LINE_PROG_TEMPLATES.dirt_oval_default; return{template_id:tc.indexOf("heavy")>=0?"dirt_oval_heavy":"dirt_oval_default",early:tpl.early,mid:tpl.mid,late:tpl.late}; } function _logLoadSessionLog(){ try{var all=JSON.parse(localStorage.getItem(_TRACK_SESSION_LOG_STORAGE)||"{}");return all[_logTrackIntelSlug()]||[];}catch(e){return[];} } function _logAppendSessionLog(entry){ try{ var slug=_logTrackIntelSlug(),all=JSON.parse(localStorage.getItem(_TRACK_SESSION_LOG_STORAGE)||"{}"),rows=all[slug]||[]; rows.unshift(Object.assign({ts:new Date().toISOString()},entry)); if(rows.length>24)rows=rows.slice(0,24); all[slug]=rows;localStorage.setItem(_TRACK_SESSION_LOG_STORAGE,JSON.stringify(all)); if(typeof _logRefreshTrackConditionIntel==="function")_logRefreshTrackConditionIntel(); return rows; }catch(e){return[];} } function _logWindCompassLabel(deg){ if(deg==null||!isFinite(deg))return "?"; var dirs=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"]; return dirs[Math.round(((deg%360)+360)%360/22.5)%16]; } function _logSectionLabelShort(id){ var m={front_straight:"Front",back_straight:"Back",t1:"T1",t2:"T2",t3:"T3",t4:"T4"}; return m[id]||id; } function _logBuildDryingPrediction(dirCtx,windCtx,wx,dryingSections){ var straight=dirCtx&&dirCtx.straight_heading_deg,windDeg=windCtx&&windCtx.wind_from_deg,wSpd=windCtx&&windCtx.wind_speed_mph; var hum=wx&&wx.humidity,temp=wx&&(wx.temp_f!=null?wx.temp_f:wx.temp); var note="",confidence="low",fastest=null,slowest=null; if(dryingSections&&dryingSections.sections&&dryingSections.sections.length){ fastest=dryingSections.sections[0];slowest=dryingSections.sections[dryingSections.sections.length-1]; } if(straight!=null&&windDeg!=null){ var blowTo=(windDeg+180)%360,backH=(straight+180)%360; var relBack=_logHeadingDelta(blowTo,backH),relFront=_logHeadingDelta(blowTo,straight); var windLbl=_logWindCompassLabel(windDeg); if(Math.abs(relBack)<50)note="Wind from "+windLbl+(wSpd?" ~"+wSpd+"mph":"")+" — back straight exposed → likely dries first"; else if(Math.abs(relFront)<50)note="Wind from "+windLbl+(wSpd?" ~"+wSpd+"mph":"")+" — front straight tailwind → may hold moisture longer"; else if(windCtx&&windCtx.summary)note=windCtx.summary+(wSpd?" · "+wSpd+"mph "+windLbl:""); else note="Crosswind from "+windLbl+" — ends may split; validate sheen on hot laps"; confidence=dirCtx&&dirCtx.confidence>=0.5?"gps":"preset"; if(fastest&&slowest&&fastest.section!==slowest.section)note+=" · model: "+_logSectionLabelShort(fastest.section)+" fastest / "+_logSectionLabelShort(slowest.section)+" reserve"; }else if(windCtx&&windCtx.summary){ note=windCtx.summary+(wSpd?" ("+wSpd+"mph)":""); if(fastest)note+=" · "+_logSectionLabelShort(fastest.section)+" drying fastest (wx model)"; confidence="wx_only"; }else if(fastest){ note="Section model only — "+_logSectionLabelShort(fastest.section)+" drying fastest; add GPS heading or track orientation for wind call"; confidence="sections_only"; }else{ note="Log track walk or section moisture for drying call"; confidence="none"; } if(hum!=null&&hum<55&&temp!=null&&temp>=72)note+=(note?". ":"")+"Low RH + warm air → accelerated slick window"; var ep=typeof _logMatchTrackIntelPreset==="function"?_logMatchTrackIntelPreset():null; if(ep&&ep.drying_model){ if(!note||note.indexOf("sheen")<0)note+=(note?". ":"")+ep.drying_model.note; if(ep.drying_model.validate_field)note+=" · validate: "+ep.drying_model.validate_field.replace(/_/g," "); confidence=confidence==="none"?"event_preset":confidence; } var preset=dirCtx&&dirCtx.orientation_preset; if(preset&&preset.front_straight_deg!=null&&!dirCtx.confidence)note+=" (orientation preset ~"+Math.round(preset.front_straight_deg)+"° front)"; return{note:note,confidence:confidence,wind_from_deg:windDeg,wind_compass:windDeg!=null?_logWindCompassLabel(windDeg):null,fastest_section:fastest?fastest.section:null,slowest_section:slowest?slowest.section:null,drying_index:dryingSections&&dryingSections.base_index!=null?dryingSections.base_index:null}; } function _logBuildTrackIntelPrimaryLine(tcx){ if(!tcx)return ""; var tcTxt="TRACK INTELLIGENCE — "; if(tcx.track_direction&&tcx.track_direction!=="unknown")tcTxt+=String(tcx.track_direction).toUpperCase()+(tcx.track_direction_confidence!=null?" ("+Math.round(tcx.track_direction_confidence*100)+"%)":""); else if(tcx.direction_detail&&tcx.direction_detail.straight_heading_deg!=null)tcTxt+="heading ~"+tcx.direction_detail.straight_heading_deg+"° (lap direction inconclusive)"; else if(tcx.orientation_preset&&tcx.orientation_preset.front_straight_deg!=null)tcTxt+="preset ~"+Math.round(tcx.orientation_preset.front_straight_deg)+"° front"; else tcTxt+="direction unknown"; if(tcx.wind_context&&tcx.wind_context.summary)tcTxt+=" · "+tcx.wind_context.summary; if(tcx.photo_count)tcTxt+=" · "+tcx.photo_count+" photo"+(tcx.photo_count===1?"":"s"); if(tcx.track_walk_context&&tcx.track_walk_context.walk_points)tcTxt+=" · "+tcx.track_walk_context.walk_points+" walk pt"+(tcx.track_walk_context.walk_points===1?"":"s"); if(tcx.instrumentation_context){var ic=tcx.instrumentation_context;if(ic.lidar_count)tcTxt+=" · "+ic.lidar_count+" LiDAR";if(ic.thermal_count)tcTxt+=" · "+ic.thermal_count+" thermal";} if(tcx.manual_instrumentation&&tcx.manual_instrumentation.responses){var mi=tcx.manual_instrumentation.responses;if(mi.heat_up_rate!=null)tcTxt+=" · heat "+mi.heat_up_rate+"°F/min";if(mi.bottoming_rate!=null)tcTxt+=" · bottom "+mi.bottoming_rate;} return tcTxt; } function _logRefreshTrackConditionIntel(){ var m=$("track-condition-intel-mount"); if(m&&typeof _buildTrackConditionIntelPanel==="function")_buildTrackConditionIntelPanel(m); var tcEl=document.getElementById("drk-track-context-block"); if(tcEl&&typeof _logSess!=="undefined"&&_logSess&&_logSess.parsed){ var tcx=_logBuildTrackConditionContext(_logSess.parsed); _logRenderDrkTrackContextBlock(tcEl,tcx); } if(typeof _buildTrackAnalysisTools==="function"){var tm=$("track-analysis-tools-mount");if(tm)_buildTrackAnalysisTools(tm);} } function _buildTrackConditionIntelPanel(mount){ if(!mount)return; mount.innerHTML=""; if(!S.curTrack){ mount.innerHTML='
Select a track for drying prediction, line progression notes, and between-run session log.
'; return; } var parsed=typeof _logSess!=="undefined"&&_logSess&&_logSess.parsed?_logSess.parsed:null; var tcx=_logBuildTrackConditionContext(parsed); var wrap=document.createElement("div"); wrap.style.cssText="background:var(--dark2);border:1px solid rgba(180,120,40,.22);border-left:3px solid var(--amber);padding:12px;margin-bottom:10px"; var hdr=document.createElement("div"); hdr.style.cssText="font-family:var(--mono);font-size:8px;color:var(--amber);letter-spacing:1px;margin-bottom:8px"; hdr.textContent="TRACK CONDITION INTELLIGENCE · "+(S.curTrack.short||S.curTrack.name).toUpperCase().substring(0,32)+( _logIsBc39Mode()?" · BC39 MODE":""); wrap.appendChild(hdr); if(_logIsBc39Mode()){ var bcBadge=document.createElement("div"); bcBadge.style.cssText="font-family:var(--mono);font-size:7px;color:#fbbf24;line-height:1.55;margin-bottom:8px;padding:8px;background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.25)"; var ep=_logMatchTrackIntelPreset(); bcBadge.innerHTML="BC39 MODE · 96°/276° CCW · ~12.3s laps · multi-lane bullring
"+(ep&&ep.surface_evolution?ep.surface_evolution:""); wrap.appendChild(bcBadge); } var primary=document.createElement("div"); primary.style.cssText="font-family:var(--mono);font-size:8px;color:#d4a574;line-height:1.65;margin-bottom:10px;padding:8px;background:rgba(180,120,40,.06);border:1px solid rgba(180,120,40,.12)"; primary.textContent=_logBuildTrackIntelPrimaryLine(tcx); wrap.appendChild(primary); var snapMount=document.createElement("div");snapMount.id="track-snap-bar-mount"; _tsRenderTrackSnapBar(snapMount);wrap.appendChild(snapMount); if(tcx.drying_prediction&&tcx.drying_prediction.note){ var dp=document.createElement("div"); dp.style.cssText="font-family:var(--mono);font-size:8px;color:#7dd3fc;line-height:1.55;margin-bottom:10px;padding:8px;background:rgba(56,189,248,.06);border:1px solid rgba(56,189,248,.15)"; dp.innerHTML='DRYING PREDICTION
'+tcx.drying_prediction.note; wrap.appendChild(dp); } if(tcx.drying_sections&&tcx.drying_sections.sections&&tcx.drying_sections.sections.length){ var ds=document.createElement("div"); ds.style.cssText="font-family:var(--mono);font-size:7px;color:#94a3b8;line-height:1.55;margin-bottom:10px"; ds.textContent=tcx.drying_sections.summary+" · "+tcx.drying_sections.sections.slice(0,4).map(function(s){return _logSectionLabelShort(s.section.replace("_straight",""))+" "+s.relative_rate;}).join(" · "); wrap.appendChild(ds); } var orientRow=document.createElement("div"); orientRow.style.cssText="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-bottom:10px"; var preset=_logLoadTrackOrientation()||{}; function orientFld(lbl,key,ph){var d=document.createElement("div");var L=document.createElement("div");L.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);margin-bottom:3px";L.textContent=lbl; var inp=document.createElement("input");inp.value=preset[key]!=null?String(preset[key]):"";inp.placeholder=ph||"";inp.style.cssText=_logDrkExpInputStyle();inp.inputMode="decimal"; inp.onchange=function(){var o=_logLoadTrackOrientation()||{};o[key]=inp.value?parseFloat(inp.value):null;o.direction=o.direction||"counterclockwise";o.source="manual";_logSaveTrackOrientation(o);_logRefreshTrackConditionIntel();}; d.appendChild(L);d.appendChild(inp);return d;} orientRow.appendChild(orientFld("FRONT STR °","front_straight_deg","96")); orientRow.appendChild(orientFld("BACK STR °","back_straight_deg","276")); var dirWrap=document.createElement("div");var dL=document.createElement("div");dL.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);margin-bottom:3px";dL.textContent="DIRECTION"; var dirSel=document.createElement("select");dirSel.style.cssText=_logDrkExpInputStyle(); dirSel.innerHTML=''; dirSel.value=preset.direction||"counterclockwise"; dirSel.onchange=function(){var o=_logLoadTrackOrientation()||{};o.direction=dirSel.value;o.source="manual";_logSaveTrackOrientation(o);_logRefreshTrackConditionIntel();}; dirWrap.appendChild(dL);dirWrap.appendChild(dirSel);orientRow.appendChild(dirWrap); wrap.appendChild(orientRow); if(_logMatchTrackIntelPreset()){ var bcBtn=document.createElement("button");bcBtn.type="button";bcBtn.textContent="Apply BC39 / IMS preset (96° / 276° CCW)";bcBtn.style.cssText="width:100%;margin-bottom:10px;padding:8px;font-family:var(--mono);font-size:8px;background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.3);color:#fbbf24;cursor:pointer"; bcBtn.onclick=function(){_logApplyTrackIntelEventPreset(false);_logRefreshTrackConditionIntel();}; wrap.appendChild(bcBtn); } var lpHdr=document.createElement("div"); lpHdr.style.cssText="display:flex;justify-content:space-between;align-items:center;margin:8px 0 6px;flex-wrap:wrap;gap:6px"; var lpTitle=document.createElement("div");lpTitle.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);letter-spacing:1px";lpTitle.textContent="LINE PROGRESSION (early / mid / late run)"; var tplSel=document.createElement("select");tplSel.style.cssText="font-family:var(--mono);font-size:7px;padding:4px 6px;background:var(--dark);border:1px solid rgba(255,255,255,.12);color:var(--white)"; tplSel.innerHTML=''+Object.keys(_LINE_PROG_TEMPLATES).map(function(k){return'';}).join(""); lpHdr.appendChild(lpTitle);lpHdr.appendChild(tplSel);wrap.appendChild(lpHdr); var lp=_logLoadLineProgression()||_logDefaultLineProgression(); var lpGrid=document.createElement("div");lpGrid.style.cssText="display:grid;grid-template-columns:1fr;gap:6px;margin-bottom:10px"; ["early","mid","late"].forEach(function(phase){ var d=document.createElement("div");var L=document.createElement("div");L.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);margin-bottom:2px";L.textContent=phase.toUpperCase()+" RUN"; var ta=document.createElement("textarea");ta.rows=2;ta.value=lp[phase]||"";ta.style.cssText=_logDrkExpInputStyle()+";resize:vertical;min-height:36px;font-size:10px"; ta.oninput=function(){lp[phase]=ta.value;_logSaveLineProgression(lp);}; d.appendChild(L);d.appendChild(ta);lpGrid.appendChild(d); }); tplSel.onchange=function(){var t=_LINE_PROG_TEMPLATES[tplSel.value];if(!t)return;lp.template_id=tplSel.value;lp.early=t.early;lp.mid=t.mid;lp.late=t.late;_logSaveLineProgression(lp);_logRefreshTrackConditionIntel();}; wrap.appendChild(lpGrid); var snapHdr=document.createElement("div");snapHdr.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);letter-spacing:1px;margin:8px 0 6px";snapHdr.textContent="BETWEEN-RUN SNAPSHOT (trackside log)"; wrap.appendChild(snapHdr); var snapGrid=document.createElement("div");snapGrid.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:6px"; var sessInp=document.createElement("input");sessInp.placeholder="Session (Heat 2, Qual…)";sessInp.style.cssText=_logDrkExpInputStyle(); var stateSel=document.createElement("select");stateSel.style.cssText=_logDrkExpInputStyle(); stateSel.innerHTML=''+_logDrkTrackStates.map(function(s){return'';}).join(""); snapGrid.appendChild(sessInp);snapGrid.appendChild(stateSel);wrap.appendChild(snapGrid); if(typeof _ccIsAdvancedGarageClass==="function"&&_ccIsAdvancedGarageClass()&&window.CrewChiefReport&&window.CrewChiefReport.buildSmartCaptureDefaults){ var vcSnap=typeof _logVehicleContextForCrewChief==="function"?_logVehicleContextForCrewChief():{}; var stSnap=typeof _logInferTrackState==="function"?_logInferTrackState():null; var profSnap=window.CrewChiefReport.resolveCaptureProfile?window.CrewChiefReport.resolveCaptureProfile(vcSnap):null; var smartSnap=window.CrewChiefReport.buildSmartCaptureDefaults(vcSnap,stSnap,profSnap,{sessionLog:vcSnap.session_log||[],overrideStore:_loadCaptureOverrides(),carId:S.cur&&S.cur.id}); if(smartSnap&&smartSnap.active){ var logsSnap=typeof _logLoadSessionLog==="function"?_logLoadSessionLog():[]; var lastSess=logsSnap[0]; if(window.CrewChiefReport.suggestNextSessionLabel&&lastSess)sessInp.placeholder=window.CrewChiefReport.suggestNextSessionLabel(lastSess.session||"Heat"); else if(smartSnap.values.session)sessInp.placeholder=smartSnap.values.session; if(smartSnap.values.track_state){ var ts=String(smartSnap.values.track_state); if(_logDrkTrackStates.indexOf(ts)>=0)stateSel.value=ts; else if(/slick/.test(ts))stateSel.value="dry_slick"; else if(/tacky|heavy|rubber/.test(ts))stateSel.value="tacky"; } var snapHint=document.createElement("div"); snapHint.style.cssText="font-family:var(--mono);font-size:7px;color:#39d0a3;line-height:1.5;margin-bottom:6px;padding:6px 8px;background:rgba(57,208,163,.05);border:1px solid rgba(57,208,163,.15)"; snapHint.innerHTML="Smart session defaults · "+(smartSnap.headline||"from your history + track state"); wrap.insertBefore(snapHint,snapGrid); } } var laneInp=document.createElement("input");laneInp.placeholder="Lane commit (low / mid / high / slider)";laneInp.style.cssText=_logDrkExpInputStyle()+";margin-bottom:6px"; wrap.appendChild(laneInp); var noteTa=document.createElement("textarea");noteTa.rows=2;noteTa.placeholder="Quick note — sheen, handling, what changed";noteTa.style.cssText=_logDrkExpInputStyle()+";resize:vertical;min-height:40px;margin-bottom:6px"; wrap.appendChild(noteTa); var logBtn=document.createElement("button");logBtn.type="button";logBtn.textContent="Log snapshot";logBtn.style.cssText="width:100%;padding:9px;font-family:var(--mono);font-size:9px;background:rgba(74,222,128,.08);border:1px solid rgba(74,222,128,.25);color:#4ade80;cursor:pointer;margin-bottom:8px"; logBtn.onclick=function(){ if(!sessInp.value&&!stateSel.value&&!noteTa.value){toast("Add session label or note");return;} _logAppendSessionLog({session:sessInp.value||"run",track_state:stateSel.value||null,lane:laneInp.value||null,note:noteTa.value||null}); sessInp.value="";stateSel.value="";laneInp.value="";noteTa.value=""; toast("Session snapshot logged"); }; wrap.appendChild(logBtn); var logs=_logLoadSessionLog(); if(logs.length){ var logList=document.createElement("div");logList.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);line-height:1.55;max-height:72px;overflow-y:auto"; logList.innerHTML=logs.slice(0,5).map(function(r){ var when=r.ts?new Date(r.ts).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"}):""; return'
'+(r.session||"run")+''+(r.track_state?" · "+r.track_state:"")+(r.lane?" · "+r.lane:"")+' '+when+''+(r.note?"
"+String(r.note).slice(0,80):"")+'
'; }).join(""); wrap.appendChild(logList); } var gearRow=document.createElement("button");gearRow.type="button";gearRow.textContent="Recommended Track Analysis Tools (thermal + LiDAR) →";gearRow.style.cssText="width:100%;margin-top:8px;padding:10px;font-family:var(--head);font-size:11px;font-weight:900;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.35);color:#fb923c;cursor:pointer"; gearRow.onclick=function(){if(typeof _openThermalGearModal==="function")_openThermalGearModal();}; wrap.appendChild(gearRow); mount.appendChild(wrap); } /* ---- AI Crew Chief panel (Report + Recommendations + BC39 Brief) ---- */ var _ccLastReport=null; var _ccLastRecs=null; var _CC_EXP_TARGET_RUNS=6; function _ccRatingNum(v){var n=parseFloat(String(v||"").replace(/[^\d.\-]/g,""));return isFinite(n)?n:null;} function _ccRunsForBlock(blockId){ var runs=[];try{runs=JSON.parse(localStorage.getItem(_DRK_EXP_RUNS_KEY)||"[]");}catch(e){} if(!blockId||blockId==="__sample__")return []; if(blockId.indexOf("exp_")===0){ var parts=blockId.split("_"),expId=parts.slice(2).join("_"),day=parts[1]; return runs.filter(function(r){return(r.captured_at||"").split("T")[0]===day&&r.experiment_id===expId;}); } if(blockId.indexOf("night_")===0){ var d=blockId.slice(6); return runs.filter(function(r){return(r.captured_at||"").split("T")[0]===d;}); } return runs; } function _ccCaptureToRow(capture,i){ var row={ run_id:capture.logger_save_id||("L"+(i+1)), experiment:capture.experiment_id, study_targets:capture.study_targets, arm:capture.arm, track_state:capture.track_state, surface_temp_f:capture.surface_temp_f, laps:capture.laps, tire_temp_pre_f:capture.tire_temp_pre_f, tire_temp_post_f:capture.tire_temp_post_f, run_minutes:capture.run_minutes, shock_travel_used_in:capture.shock_travel_used_in, shock_travel_avail_in:capture.shock_travel_avail_in, shock_bottom_events:capture.shock_bottom_events, wheel_hop_energy:_ccRatingNum(capture.wheel_hop_rating), lateral_loss:_ccRatingNum(capture.lateral_loss_rating), bottoming_rate:capture.responses&&capture.responses.bottoming_rate, heat_up_rate:capture.responses&&capture.responses.heat_up_rate, noise_label:i%2?"x":"y" }; var hs=_logHarnessSetupFromCapture(capture); for(var hk in hs)if(hs.hasOwnProperty(hk))row[hk]=hs[hk]; return row; } function _ccRunsToRecords(runs){ if(!window.CrewChiefReport||!window.CrewChiefReport.rowToRecord)return []; return runs.map(function(r,i){return window.CrewChiefReport.rowToRecord(_ccCaptureToRow(r,i));}); } function _ccSampleReport(){ return {"header":{"name":"Demo A-B-A night","track":"I-44 Riverside","date":"2026-06-14","source":"real","n_runs":48}, "track_condition":{"track_state":"dry_slick","drying_prediction":{"note":"Wind from NW ~16mph — back straight exposed → likely dries first","confidence":"preset"}, "line_progression":{"early":"Bottom / low — tacky grip","mid":"Middle-up — split surface","late":"Rubber lane — RR temp management"}, "orientation_preset":{"front_straight_deg":96,"back_straight_deg":276,"direction":"counterclockwise"}, "wind_context":{"summary":"Tailwind on fastest straight","wind_from_deg":318,"wind_speed_mph":16}}, "key_findings":[{"study":"P1","stratum":"tacky","level":"HIGH","text":"Raising the panhard/J-bar -> more grip loss. That HURT (big effect, tacky)."}], "reversals":["Raising the panhard/J-bar: effect REVERSES by track state -- set it by the surface, not a fixed number."], "null_check":{"status":"OK","text":"Null control clean -- the engine isn't inventing signal. Good."}, "needs_more_data":["G1","G2"],"correlations":{"treatments":[],"responses":[]}, "studies":[],"stats":{"tests":6,"z_threshold":2.64,"n_signal":5,"n_signal_bonf":5},"generated_at":new Date().toISOString()}; } async function _ccListBlocks(){ var blocks=[{id:"__tonight__",name:"Tonight · all logged exp runs"}]; var localRuns=[];try{localRuns=JSON.parse(localStorage.getItem(_DRK_EXP_RUNS_KEY)||"[]");}catch(e){} try{ var j=await _ccFetchApi("list",_ccApiPayload(localRuns)); if(j.blocks&&j.blocks.length)blocks=j.blocks; }catch(e){ var byDate={}; localRuns.forEach(function(r){var d=(r.captured_at||"").split("T")[0]||"unknown";if(!byDate[d])byDate[d]=[];byDate[d].push(r);}); Object.keys(byDate).sort().reverse().forEach(function(d){ blocks.push({id:"night_"+d,name:d+" · "+byDate[d].length+" exp runs (local)"}); var byExp={}; byDate[d].forEach(function(r){var ex=r.experiment_id||"misc";if(!byExp[ex])byExp[ex]=[];byExp[ex].push(r);}); Object.keys(byExp).forEach(function(ex){blocks.push({id:"exp_"+d+"_"+ex,name:d+" · "+ex+" ("+byExp[ex].length+" local)"});}); }); } if(!blocks.some(function(b){return b.id==="__sample__";}))blocks.push({id:"__sample__",name:"Sample: Demo A-B-A night"}); return blocks; } function _ccApiPayload(localRuns){ var today=new Date().toISOString().split("T")[0]; var parsed=typeof _logSess!=="undefined"&&_logSess&&_logSess.parsed?_logSess.parsed:null; return{ user_id:S.user&&S.user.user_id||null, car_id:S.cur&&S.cur.id||null, track_id:S.curTrack&&S.curTrack.id||null, today:today, track_label:S.curTrack?(S.curTrack.short||S.curTrack.name):null, track_context:_logBuildTrackConditionContext(parsed), local_runs:localRuns||[] }; } function _ccApiErrorHint(msg,status,action){ var m=String(msg||"").toLowerCase(); if(m.indexOf("no experiment runs")>=0||m.indexOf("no captures")>=0) return "Link DRK uploads to EXP arms in the logger (A/B/C), log track state + lane, then Generate again."; if(m.indexOf("sample report")>=0||m.indexOf("client-side")>=0) return "Demo sample runs in-browser only — pick Tonight or a dated block for real data."; if(m.indexOf("engine not loaded")>=0||m.indexOf("crewchiefreport")>=0) return "Refresh the page — crew-chief-report.browser.js did not load."; if(status===401||status===403)return "Sign in again if blocks look empty."; if(status>=500)return "Server hiccup — local runs still work offline if saved on this device."; if(action==="list")return "Block list fell back to runs saved on this device."; return "Check connection; the panel can still use runs saved locally."; } function _ccFormatGenerateError(e){ if(!e)return "Generate failed — try again."; if(typeof e==="string")return e; var parts=[e.message||String(e)]; if(e.hint)parts.push(e.hint); else if(e.apiFallback)parts.push(e.apiFallback); return parts.filter(Boolean).join(" · "); } async function _ccFetchApi(action,payload){ var base=typeof _SB!=="undefined"?_SB:"https://zmrouoqututfndplboyc.supabase.co"; var url=base+"/functions/v1/crew-chief-report?action="+action; var headers={"Content-Type":"application/json"}; if(typeof _AK!=="undefined"&&_AK)headers.apikey=_AK; if(S.token)headers.Authorization="Bearer "+S.token; var r; try{r=await fetch(url,{method:"POST",headers:headers,body:JSON.stringify(payload||{})});} catch(netErr){ var ne=new Error("Crew Chief API unreachable"); ne.hint=_ccApiErrorHint("",0,action); ne.cause=netErr; throw ne; } var j=await r.json().catch(function(){return{};}); if(!r.ok){ var err=new Error(j.error||("Crew Chief API error ("+r.status+")")); err.status=r.status; err.hint=j.hint||_ccApiErrorHint(j.error,r.status,action); err.raw=j; throw err; } return j; } var _ccLastRecsFromApi=null; var _ccLastGenMeta=null; var _CC_DISCIPLINE="One lever at a time · clean A-B-A · log lane + track state every run"; async function _ccGenerateReport(blockId){ if(blockId==="__sample__"){ _ccLastGenMeta={source:"sample",apiFallback:null,runCount:48,thin:false}; return _ccSampleReport(); } var localRuns=[];try{localRuns=JSON.parse(localStorage.getItem(_DRK_EXP_RUNS_KEY)||"[]");}catch(e){} var apiFallbackNote=null; try{ var payload=_ccApiPayload(localRuns); payload.block_id=blockId; var j=await _ccFetchApi("generate",payload); if(j.report){ _ccLastRecsFromApi=j.recommendations||null; var nRuns=j.report.header&&j.report.header.n_runs!=null?j.report.header.n_runs:(j.meta&&j.meta.n_captures)||0; _ccLastGenMeta={source:"api",apiFallback:null,runCount:nRuns,thin:_ccReportIsThin(j.report),meta:j.meta||null}; return j.report; } }catch(apiErr){ apiFallbackNote=(apiErr.message||String(apiErr))+(apiErr.hint?(" · "+apiErr.hint):""); console.warn("[BB] crew-chief API fallback:",apiErr.message||apiErr); } if(!window.CrewChiefReport||!window.CrewChiefReport.crewChiefReport){ var engErr=new Error("Crew Chief engine not loaded"); engErr.hint=_ccApiErrorHint(engErr.message,0,"generate"); if(apiFallbackNote)engErr.apiFallback="API: "+apiFallbackNote; throw engErr; } var runs=blockId==="__tonight__"?localRuns:_ccRunsForBlock(blockId); var today=new Date().toISOString().split("T")[0]; if(blockId==="__tonight__")runs=runs.filter(function(r){return(r.captured_at||"").split("T")[0]===today;}); if(!runs.length){ var noRuns=new Error("No experiment runs for this block"); noRuns.hint=_ccApiErrorHint(noRuns.message,0,"generate"); if(apiFallbackNote)noRuns.apiFallback="API: "+apiFallbackNote; throw noRuns; } var records=_ccRunsToRecords(runs); var parsed=typeof _logSess!=="undefined"&&_logSess&&_logSess.parsed?_logSess.parsed:null; var tcx=_logBuildTrackConditionContext(parsed); var ep=_logMatchTrackIntelPreset(); if(ep&&ep.drying_model&&(!tcx.drying_prediction||!tcx.drying_prediction.note)) tcx.drying_prediction=Object.assign({},tcx.drying_prediction||{},{note:ep.drying_model.note,confidence:"event_preset"}); if(!tcx.track_state&&runs.length){ var stCounts={}; runs.forEach(function(r){if(r.track_state)stCounts[r.track_state]=(stCounts[r.track_state]||0)+1;}); var top=Object.keys(stCounts).sort(function(a,b){return stCounts[b]-stCounts[a];})[0]; if(top)tcx.track_state=top; } var session={ name:blockId==="__tonight__"?"Tonight":blockId.replace(/^night_|^exp_/,""), track:S.curTrack?(S.curTrack.short||S.curTrack.name):null, date:(runs[0]&&runs[0].captured_at?runs[0].captured_at.split("T")[0]:today), source:"real",n_runs:records.length }; var bundle=window.CrewChiefReport.crewChiefReport(records,{session:session,trackContext:tcx}); _ccLastRecsFromApi=null; _ccLastGenMeta={source:"client",apiFallback:apiFallbackNote,runCount:records.length,thin:_ccReportIsThin(bundle.report)}; return bundle.report; } function _ccReportIsThin(r){ if(!r)return false; var n=r.header&&r.header.n_runs!=null?r.header.n_runs:0; var findings=(r.key_findings||[]).length; if(!n)return true; return n<6||findings===0; } function _ccEsc(s){return String(s==null?"":s).replace(/[&<>]/g,function(c){return({"&":"&","<":"<",">":">"}[c]);});} function _ccPanelBox(variant,title,lines,foot){ var colors={empty:"rgba(255,255,255,.04)",loading:"rgba(57,208,163,.06)",error:"rgba(239,68,68,.08)",info:"rgba(56,189,248,.06)",warn:"rgba(251,191,36,.08)",ok:"rgba(57,208,163,.08)",discipline:"rgba(57,208,163,.05)"}; var borders={empty:"rgba(255,255,255,.1)",loading:"rgba(57,208,163,.25)",error:"rgba(239,68,68,.35)",info:"rgba(56,189,248,.25)",warn:"rgba(251,191,36,.3)",ok:"rgba(57,208,163,.35)",discipline:"rgba(57,208,163,.2)"}; var titleColors={empty:"var(--muted)",loading:"#39d0a3",error:"#f87171",info:"#38bdf8",warn:"#fbbf24",ok:"#39d0a3",discipline:"#39d0a3"}; var lineList=Array.isArray(lines)?lines:(lines!=null&&lines!==""?[String(lines)]:[]); var s='
'; if(title)s+='
'+_ccEsc(title)+'
'; (lineList).forEach(function(ln){ s+='
'+_ccEsc(ln)+'
'; }); if(foot)s+='
'+_ccEsc(foot)+'
'; s+='
'; return s; } function _ccRenderPanelLoading(mount,msg){ if(!mount)return; mount.innerHTML=_ccPanelBox("loading",msg||"Generating report…",["Pulling run block · merging track intel · ranking recommendations"],"This usually takes a few seconds"); } function _ccRenderPanelError(mount,errMsg){ if(!mount)return; mount.innerHTML=_ccPanelBox("error","Could not generate",String(errMsg||"Unknown error").split(/\s*·\s*/),"Still stuck? Try Sample block to verify the panel, then link real DRK runs."); } function _ccRenderPanelEmptyInitial(outReport,outRecs,outBrief){ if(outReport)outReport.innerHTML=_ccPanelBox("empty","No report yet",[ "1. Select track + car, log session track state in Track Condition Intel", "2. Link each DRK upload to an EXP arm (A/B/C) in the logger", "3. Pick Tonight or a dated run block, then Generate" ],_CC_DISCIPLINE); if(outRecs)outRecs.innerHTML=_ccPanelBox("empty","No recommendations yet",[ "Recommendations rank setup moves after a report runs.", "With thin data you may see PRIOR tags — event/setup priors, not tonight's A-B-A proof.", "Never stack two levers in one run — confirm each change with a clean A-B-A." ],_CC_DISCIPLINE); if(outBrief){ if(_logIsBc39Mode()){ var ep=_logMatchTrackIntelPreset(); var pri=(ep&&ep.recommendation_priorities)||[]; outBrief.innerHTML=_ccPanelBox("info","BC39 Brief · event priors ready",[ "Generate to fuse tonight's runs with IMS Dirt preset (orientation, drying, line templates).", pri.length?("Preset priorities loaded ("+pri.length+") — full brief after Generate."):"BC39 preset active — log runs for data-backed findings." ].concat(pri.slice(0,3).map(function(p,i){return (i+1)+". "+p.text;})),_CC_DISCIPLINE+" · Night 2 qual: positions advanced lock top 18"); }else outBrief.innerHTML=_ccPanelBox("empty","BC39 Brief unavailable","Select The Dirt Track at IMS (BC39) to unlock this tab."); } } function _ccDisciplineBar(extra){ return '
'+_ccEsc(_CC_DISCIPLINE+(extra?(" · "+extra):""))+'
'; } function _ccDataStatus(report,meta,recOut){ var findings=(report&&report.key_findings||[]).length; var runs=meta&&meta.runCount!=null?meta.runCount:(report&&report.header&&report.header.n_runs)||0; if(recOut&&recOut.dataNote)return {level:recOut.mode==="findings"||recOut.mode==="mixed"?"ok":"note",msg:recOut.dataNote}; if(!runs)return {level:"empty",msg:"No linked experiment runs yet."}; if(findings===0&&runs<=2)return {level:"thin",msg:"Not enough experimental data yet — using track priors only."}; if(findings===0)return {level:"priors",msg:"No confident findings yet — track priors + intel only."}; return {level:"ok",msg:findings+" data-backed finding(s) — confirm each with A-B-A."}; } function _ccBuildNightSummaryLines(report,meta,recOut){ var h=report&&report.header||{},tc=report&&report.track_condition||{},st=tc.track_state||"unknown"; var runs=meta&&meta.runCount!=null?meta.runCount:h.n_runs||0; var ds=_ccDataStatus(report,meta,recOut); var lines=[(h.track||"Track")+" · "+(h.date||"tonight")+" · "+runs+" run(s) · surface "+st]; if(ds.msg)lines.push(ds.msg); if(report&&report.key_findings&&report.key_findings[0])lines.push("Top finding: "+report.key_findings[0].text); else if(recOut&&recOut.recommendations&&recOut.recommendations[0])lines.push("Top move: #"+recOut.recommendations[0].rank+" "+recOut.recommendations[0].action); if(report&&report.null_check)lines.push("Null check: "+report.null_check.text); if(tc.session_log&&tc.session_log[0]){ var s=tc.session_log[0]; lines.push("Last session: "+(s.session||"run")+(s.track_state?" ("+s.track_state+")":"")+(s.lane?" · "+s.lane:"")); } return lines.slice(0,6); } function _ccIsGrassrootsGarageClass(){ var c=S.cur||{},cls=String(c.class||c.car_class||"").toLowerCase(); var ct=typeof _getCarType==="function"?_getCarType(c):""; if(ct==="quartermidget"||ct==="outlawkart"||ct==="lightningsprint")return true; if(ct==="micro"&&/600|restricted micro|micro sprint/i.test(cls))return true; return /quarter.midget|qma|\bqm\b|outlaw|600 micro|restricted micro|lightning|glls/i.test(cls); } function _ccIsAdvancedGarageClass(){ if(_ccIsGrassrootsGarageClass())return false; var c=S.cur||{},cls=String(c.class||c.car_class||"").toLowerCase(); var ct=typeof _getCarType==="function"?_getCarType(c):""; if(ct==="sprint"||/410|360|305|winged sprint|maxim|midget|usac/i.test(cls))return true; if(ct==="latemodel"||ct==="modified"||/late model|super late|modified|imca modified|crate late/i.test(cls))return true; return false; } var _CAPTURE_OVERRIDE_KEY='bb_capture_overrides_v1'; function _loadCaptureOverrides(){ try{return JSON.parse(localStorage.getItem(_CAPTURE_OVERRIDE_KEY)||'{"version":1,"entries":[]}');}catch(e){return{version:1,entries:[]};} } function _saveCaptureOverrides(store){ try{localStorage.setItem(_CAPTURE_OVERRIDE_KEY,JSON.stringify(store));}catch(e){} } try{window._loadCaptureOverrides=_loadCaptureOverrides;window._saveCaptureOverrides=_saveCaptureOverrides}catch(e){} function _ccAdvancedDataCaptureCoachHtml(){ if(!_ccIsAdvancedGarageClass())return ""; var CCR=window.CrewChiefReport; if(!CCR||!CCR.buildAdvancedDataCaptureCoachPayload)return ""; var vc=typeof _logVehicleContextForCrewChief==="function"?_logVehicleContextForCrewChief():{}; var st=(typeof _logInferTrackState==="function"?_logInferTrackState():null)||"unknown"; var profile=CCR.resolveCaptureProfile?CCR.resolveCaptureProfile(vc):null; if(!profile||!CCR.isSeriousCaptureProfile||!CCR.isSeriousCaptureProfile(profile))return ""; var payload=CCR.buildAdvancedDataCaptureCoachPayload(vc,st,profile); if(!payload||!payload.active)return ""; var s='
'; s+='
'; s+='
DATA CAPTURE · '+_ccEsc(payload.tierLabel||"Baseline")+'
'; s+='
'+Math.round(payload.completeness||0)+'% on file
'; s+='
'; s+='
'+_ccEsc(payload.headline)+'
'; if(payload.topPriority){ s+='
'+_ccEsc(payload.topPriority.cta)+' · unlocks '+_ccEsc((payload.topPriority.unlocks||[]).slice(0,2).join(", ").replace(/_/g," "))+'
'; } if(payload.pivotLine){ var pcol=payload.pivotStale?'#fca5a5':(payload.pivotComplete?'#39d0a3':'#f5a623'); s+='
'+_ccEsc(payload.pivotLine)+'
'; } if(payload.priorities&&payload.priorities.length>1){ s+='
Also: '+payload.priorities.slice(1,3).map(function(p){return _ccEsc(p.label);}).join(" · ")+'
'; } s+='
'; s+=''; if(payload.showPivotButton)s+=''; if(payload.showFocusedButton)s+=''; if(payload.showPlatformButton)s+=''; s+='
'; return s; } function _ccCaptureFeedbackToast(beforeVc,afterVc,extraFields){ var CCR=window.CrewChiefReport; if(!CCR||!CCR.buildPostCaptureFeedback||!CCR.assessDataCaptureState||!CCR.resolveCaptureProfile)return; var st=(typeof _logInferTrackState==="function"?_logInferTrackState():null)||"unknown"; var profile=CCR.resolveCaptureProfile(afterVc||beforeVc||{}); if(!profile||!CCR.isSeriousCaptureProfile(profile))return; var before=CCR.assessDataCaptureState(beforeVc||{},st,(beforeVc&&beforeVc.session_log)||[],profile); var after=CCR.assessDataCaptureState(afterVc||{},st,(afterVc&&afterVc.session_log)||[],profile); var fb=CCR.buildPostCaptureFeedback(before,after,extraFields||[]); if(typeof toast==="function")toast(fb.headline+(fb.nextGap?" · Next: "+fb.nextGap.label:"")); var coach=document.getElementById("cc-night-coach-mount"); if(coach&&typeof _ccRenderNightCoach==="function")_ccRenderNightCoach(coach); if(typeof _rtUpdateLauncherMeter==="function")_rtUpdateLauncherMeter(); } function _ccGrassrootsContextCoachHtml(){ if(!_ccIsGrassrootsGarageClass())return ""; var vc=typeof _logVehicleContextForCrewChief==="function"?_logVehicleContextForCrewChief():{}; var st=(typeof _logInferTrackState==="function"?_logInferTrackState():null)||"unknown"; var chips=[],gaps=[]; if(vc.chassis_name&&String(vc.chassis_name).trim())chips.push("Chassis: "+vc.chassis_name); else gaps.push("chassis mfr + model"); var m=vc.setup_measurements||{}; if(m.lf_psi!=null||m.rf_psi!=null||m.stagger!=null){ var mp=[];if(m.lf_psi!=null||m.rf_psi!=null)mp.push("psi LF "+(m.lf_psi!=null?m.lf_psi:"?")+"/RF "+(m.rf_psi!=null?m.rf_psi:"?")); if(m.stagger!=null)mp.push("stagger "+m.stagger+'"');if(m.left_pct!=null&&m.rear_pct!=null)mp.push("cross ~"+(parseFloat(m.left_pct)+parseFloat(m.rear_pct)-100).toFixed(1)+"%"); if(mp.length)chips.push("Setup: "+mp.join(" · ")); }else gaps.push("tire psi or stagger"); if(st&&st!=="unknown")chips.push("Track: "+st);else gaps.push("track state"); if(vc.trait_entry||vc.trait_mid||vc.trait_exit)chips.push("Feel: "+[vc.trait_entry,vc.trait_mid,vc.trait_exit].filter(Boolean).join(" / ")); if(vc.geo_notes&&String(vc.geo_notes).trim())chips.push("Mods/notes on file"); if(vc.front_susp||vc.setup_style)chips.push("Front: "+(vc.front_susp||vc.setup_style)); var s='
'; s+='
YOUR CAR CONTEXT
'; if(chips.length)s+='
'+chips.map(function(x){return "• "+_ccEsc(x);}).join("
")+'
'; else s+='
Nothing on file yet — class baseline + surface guidance still work.
'; if(gaps.length)s+='
Add '+gaps.slice(0,2).join(" + ")+' in Garage → Setup to personalize faster.
'; var CCR=window.CrewChiefReport; if(CCR&&CCR.resolvePivotTier&&CCR.assessPivotCompleteness){ var profile=CCR.resolveCaptureProfile?CCR.resolveCaptureProfile(vc):''; var tier=CCR.resolvePivotTier(vc,profile); if(tier.tier===2){ var pivot=CCR.assessPivotCompleteness(vc,vc.car_type,profile); var pcol=pivot.stale?'#fca5a5':(pivot.complete?'#39d0a3':'#f5a623'); s+='
'+_ccEsc(pivot.coachLine)+'
'; s+='
'; s+=''; if(tier.features&&tier.features.platformIntegrity)s+=''; s+='
'; }else if(tier.tier===3&&tier.hint){ s+='
'+_ccEsc(tier.hint)+'
'; } } s+='
'; return s; } function _ccBuildDriverBriefLines(report,recOut,meta){ var tc=report&&report.track_condition||{},st=tc.track_state||"unknown"; var lp=tc.line_progression||{},phase=lp.early?"early: "+lp.early:(lp.mid?"mid: "+lp.mid:""); var ds=_ccDataStatus(report,meta,recOut); var lines=[]; lines.push("Track "+st+(phase?(" · "+phase.split(":")[0]):"")+(_logIsBc39Mode()?" · BC39 bullring":"")); if(_ccIsGrassrootsGarageClass()){ var vc=(tc&&tc.vehicle_context)||(typeof _logVehicleContextForCrewChief==="function"?_logVehicleContextForCrewChief():{}); var carParts=[]; if(vc.chassis_name)carParts.push(vc.chassis_name); var vm=vc.setup_measurements||{}; if(vm.lf_psi!=null||vm.rf_psi!=null)carParts.push("psi LF "+(vm.lf_psi!=null?vm.lf_psi:"?")+"/RF "+(vm.rf_psi!=null?vm.rf_psi:"?")); if(vm.stagger!=null)carParts.push("stagger "+vm.stagger+'"'); if(carParts.length)lines.push("Your car: "+carParts.join(" · ")); else if(recOut&&recOut.contextAssessment&&recOut.contextAssessment.completeness<35)lines.push("Your car: class baseline only — add chassis + psi in Setup to personalize."); if(window.CrewChiefReport&&window.CrewChiefReport.buildGrassrootsContextGapLine&&recOut&&recOut.contextAssessment){ var gapLn=window.CrewChiefReport.buildGrassrootsContextGapLine(recOut.contextAssessment); if(gapLn)lines.push(gapLn); } if(recOut&&recOut.recommendations){ var persRec=null; for(var yi=0;yiBC39'; } function _ccTrackSections(tc){ if(!tc)return []; if(tc.summary_sections&&tc.summary_sections.length)return tc.summary_sections; if(window.CrewChiefReport&&window.CrewChiefReport.formatTrackConditionSummary){ var fmt=window.CrewChiefReport.formatTrackConditionSummary(tc); return fmt.sections||[]; } return []; } function _ccRenderReport(r,mount,meta){ if(!mount)return; if(!r){mount.innerHTML=_ccPanelBox("empty","No report",["Generate a run block to see findings and track context."],_CC_DISCIPLINE);return;} meta=meta||_ccLastGenMeta||{}; var h=r.header||{},hh=[h.track,h.date,h.source,(h.n_runs!=null?h.n_runs+" runs":null)].filter(Boolean).join(" · "); var thin=_ccReportIsThin(r); var s=''; if(meta.apiFallback)s+=_ccPanelBox("warn","Offline fallback",["API: "+meta.apiFallback,"Report built from runs saved on this device."],null); else if(meta.source==="client"&&!meta.apiFallback&&meta.runCount)s+=''; else if(meta.source==="api"&&thin)s+=_ccPanelBox("warn","Thin data — read carefully",[ (h.n_runs!=null?h.n_runs+" run(s) in block":"Few runs captured")+" — findings may not survive A-B-A confirmation yet.", "Keep changing one lever per run until null control stays clean." ],null); s+='
'+_ccEsc(h.name||"Run Block")+_ccBc39LiveChip()+'
'+_ccEsc(hh)+'
'; var nsLines=_ccBuildNightSummaryLines(r,meta,_ccLastRecs); s+='
NIGHT SUMMARY
'; nsLines.forEach(function(ln){s+='
• '+_ccEsc(ln)+'
';}); s+='
'; s+='
KEY FINDINGS
'; if(!r.key_findings||!r.key_findings.length){ s+=_ccPanelBox("info",thin?"No confident findings yet":"No findings in this block",[ thin?"Need more clean A-B-A pairs on the same track state — one DOF per run.":"This block has no linked experiment arms or runs yet.", "Log track state + lane each session so findings stratify correctly.", (r.needs_more_data&&r.needs_more_data.length)?("Studies waiting on data: "+r.needs_more_data.join(", ")):"Target 6+ runs per lever before booking a setup change." ],null); } (r.key_findings||[]).slice(0,5).forEach(function(f){s+='
'+f.level+''+_ccEsc(f.text)+'
';}); (r.reversals||[]).forEach(function(x){s+='
↻ '+_ccEsc(x)+'
';}); if(r.null_check)s+='
Sanity: '+_ccEsc(r.null_check.text)+'
'; if(r.needs_more_data&&r.needs_more_data.length&&r.key_findings&&r.key_findings.length)s+='
Still needs more data: '+_ccEsc(r.needs_more_data.join(", "))+'
'; s+='
'; s+='
TRACK CONDITION SUMMARY
'; var secs=_ccTrackSections(r.track_condition); if(!secs.length)s+=_ccPanelBox("info","Track intel incomplete",[ "Set track in garage and refresh Track Condition Intel (wind, line progression, session log).", _logIsBc39Mode()?"BC39 preset still applies orientation + drying priors even without a full session log.":"Partial intel is OK — priors fill gaps until you log sessions." ],null); secs.forEach(function(sec){ s+='
'; if(sec.label)s+='
'+_ccEsc(sec.label.toUpperCase())+'
'; if(sec.phases)s+=['early','mid','late'].filter(function(p){return sec.phases[p];}).map(function(p){return '
'+p.toUpperCase()+' · '+_ccEsc(sec.phases[p])+'
';}).join(''); else if(sec.items&&sec.items.length)s+=sec.items.map(function(it){return '
• '+_ccEsc(it)+'
';}).join(''); else s+='
'+_ccEsc(sec.text||"")+'
'; s+='
'; }); s+='
'; s+='
CORRELATION / CONFOUND
'; var ct=(r.correlations&&r.correlations.treatments)||[],cr=(r.correlations&&r.correlations.responses)||[]; if(!ct.length&&!cr.length)s+='
Nothing flagged — keep logging lane, tire temps, and one lever per run so confounds stay visible.
'; ct.forEach(function(c){s+='
⚠ '+_ccEsc(c.note)+'
';}); cr.forEach(function(c){s+='
~ '+_ccEsc(c.note)+'
';}); s+='
'; s+=_ccDisciplineBar(thin?"confirm every move with A-B-A before the feature":null); mount.innerHTML=s; } function _ccExportText(r){ if(window.CrewChiefReport&&window.CrewChiefReport.renderCrewChief)return window.CrewChiefReport.renderCrewChief({report:r},r.header&&r.header.name); return JSON.stringify(r,null,2); } function _ccGenerateRecommendations(report){ if(_ccLastRecsFromApi)return _ccLastRecsFromApi; if(!window.CrewChiefReport||!window.CrewChiefReport.recommend)return null; return window.CrewChiefReport.recommend(report); } function _ccRecTagAndColor(r){ var CCR=window.CrewChiefReport; if(CCR&&CCR.recTag){ var raw=CCR.recTag(r)||'PRIOR'; var tag=String(raw).replace(/^\[|\]$/g,''); var tagColor='#94a3b8'; if(/YOUR DATA|TRY FIRST|BOOK|HIGH/i.test(tag))tagColor='#39d0a3'; else if(/BUILDER|ADV CHASSIS|PRINCIPLE|VALIDATE|MODERATE/i.test(tag))tagColor='#c4b5fd'; else if(/EVOLUTION|PLAN AHEAD|TRACK SNAP/i.test(tag))tagColor='#38bdf8'; else if(/A-B-A|CONFLICT/i.test(tag))tagColor='#34d399'; else if(/REF|SCAFFOLD|PUBLIC|PRIOR/i.test(tag))tagColor='#64748b'; else if(r.confidence==='HIGH')tagColor='#39d0a3'; else if(r.confidence==='MODERATE')tagColor='#60a5fa'; return {tag:tag,tagColor:tagColor}; } var isPrior=r.basis==='prior'; var isPub=r.flags&&r.flags.public_baseline; var isChassis=r.flags&&r.flags.chassis_specific; var isScaffold=r.flags&&r.flags.scaffold; var isBuilder=r.flags&&r.flags.builder_signal; var isAbaDisc=r.flags&&r.flags.aba_discipline; var isEvo=r.flags&&r.flags.advanced_track_evolution; var isLmEvo=r.flags&&r.flags.late_model_evolution; var isSnap=r.flags&&(r.flags.track_snap||r.flags.advanced_track_snap); var isConflict=r.flags&&r.flags.track_evolution_conflict; var tag=isPrior?(isScaffold?(isChassis?'SCAFFOLD · CHASSIS':'SCAFFOLD · BASELINE'):(isChassis?'CHASSIS BASELINE':(isPub?'PUBLIC BASELINE':'PRIOR'))):r.confidence; var tagColor=isPrior?(isScaffold?'#64748b':(isChassis?'#c4b5fd':(isPub?'#38bdf8':'#94a3b8'))):(r.confidence==='HIGH'?'#39d0a3':r.confidence==='MODERATE'?'#60a5fa':'#fbbf24'); if(isPrior&&isSnap){tag='TRACK SNAP';tagColor='#38bdf8';} else if(isPrior&&isConflict){tag='CONFLICT';tagColor='#f97316';} else if(isPrior&&isBuilder){tag='BUILDER · '+((r.flags.builder_mfr||'SIGNAL').toUpperCase());tagColor='#c4b5fd';} else if(isPrior&&isAbaDisc){tag=r.lever&&r.lever.indexOf('aba_interpret')===0?'A-B-A · READ':'A-B-A';tagColor='#34d399';} else if(isPrior&&isLmEvo){tag=r.flags.advanced_forward_plan?'PLAN AHEAD · LM':'EVOLUTION · LM';tagColor='#38bdf8';} else if(isPrior&&isEvo){tag=r.flags.advanced_forward_plan?'PLAN AHEAD':'EVOLUTION · ADV';tagColor='#38bdf8';} if(r.flags&&r.flags.try_first){tag='TRY FIRST · '+tag;tagColor='#39d0a3';} return {tag:tag,tagColor:tagColor}; } function _ccRecCard(r,tierLabel){ var tc=_ccRecTagAndColor(r); var tier=tierLabel?(''+_ccEsc(tierLabel)+''):''; var h='
'; h+='
'+tier+'#'+(r.rank||'?')+' '+_ccEsc(tc.tag)+''+_ccEsc(r.action)+'
'; h+='
'+_ccEsc(r.reason)+'
'; if(r.caveat)h+='
'+_ccEsc(r.caveat)+'
'; if(r.flags&&r.flags.conditional)h+='
Only if: '+_ccEsc(r.flags.conditional)+'
'; h+='
'; return h; } function _ccRenderRecommendations(out,mount,report){ if(!mount)return; if(!out||!out.recommendations||!out.recommendations.length){ var ep=_logMatchTrackIntelPreset(); var priLines=(ep&&ep.recommendation_priorities)?ep.recommendation_priorities.slice(0,4).map(function(p,i){return (i+1)+". [PRIOR] "+p.text;}):[]; mount.innerHTML=_ccPanelBox("empty","No ranked recommendations",[ "Generate a report first — or log track state so priors can rank moves.", "With zero runs you may still see event priors on the Driver tab." ].concat(priLines.length?["Event priors (not from tonight's data):"]:[]).concat(priLines),_CC_DISCIPLINE); return; } var modeColors={findings:"#39d0a3",mixed:"#fbbf24",priors:"#94a3b8"}; var allPrior=!out.recommendations.some(function(r){return r.basis==="finding";}); var hasData=out.recommendations.some(function(r){return r.basis==="finding";}); var isAdv=_ccIsAdvancedGarageClass()&&!_ccIsGrassrootsGarageClass(); var s=''; if(isAdv){ s+=_ccAdvancedDataCaptureCoachHtml(); } if(out.tryFirst&&out.tryFirst.action){ var tfReason=out.tryFirstReason||out.tryFirst.reason||''; var tierBadge=out.tryFirstTier&&out.tryFirstTier.label?(' · '+out.tryFirstTier.label):''; var tierHint=out.tryFirstTier&&out.tryFirstTier.hint?out.tryFirstTier.hint:'Race-night priority · one lever'; s+=_ccPanelBox("ok","Try first tonight"+tierBadge,[ out.tryFirst.action, tfReason?("Why: "+tfReason):null, out.tryFirst.caveat?("Note: "+out.tryFirst.caveat):null ].filter(Boolean),tierHint); }else if(out.prioritizationNote){ s+=_ccPanelBox("info","Tonight's priority",[out.prioritizationNote],null); } if(isAdv&&out.dataQualityNote&&out.dataQualityNote.headline){ var dqLines=[out.dataQualityNote.headline]; if(out.dataQualityNote.unlocks&&out.dataQualityNote.unlocks.length){ dqLines.push('Unlocks: '+out.dataQualityNote.unlocks.join(' · ')); } var dqKind=out.dataQualityNote.strength==='strong'?'ok':(out.dataQualityNote.strength==='thin'?'warn':'info'); s+=_ccPanelBox(dqKind,'Data quality',dqLines,null); } if(out.dataNote)s+=_ccPanelBox(hasData?"ok":"info",hasData?"Your logged data leads":"Insufficient experiment data",[out.dataNote],null); else if(allPrior)s+=_ccPanelBox("warn","Priors only — no data-backed moves yet",[ isAdv?"Reference layer only until scale rows exist at this track.":"Every item below is an event/setup prior until A-B-A runs confirm it.", "Pick one lever, run A-B-A on the same track state, then regenerate." ],null); if(out.multiSessionPlanning&&out.multiSessionPlanning.lines&&out.multiSessionPlanning.lines.length){ s+=_ccPanelBox("info","Track evolution · plan ahead",out.multiSessionPlanning.lines,_CC_DISCIPLINE+" · one lever per session"); } if(out.nightSummary&&out.nightSummary.hasContent&&out.nightSummary.lines&&out.nightSummary.lines.length){ var nsLines=out.nightSummary.lines.slice(); if(out.nightSummary.worked&&out.nightSummary.worked.length)nsLines.push("What worked: "+out.nightSummary.worked.join(" · ")); s+=_ccPanelBox("ok","Night summary · track evolution",nsLines,"End-of-night read · logged data + passive signals"); } s+='
Mode: '+_ccEsc(out.mode)+' · '+_ccEsc(out.summary||"")+_ccBc39LiveChip()+'
'; if(isAdv&&(out.actionRecommendations||[]).length){ s+='
RACE-NIGHT MOVES ('+Math.min(out.actionRecommendations.length,5)+')
'; (out.actionRecommendations||[]).slice(0,5).forEach(function(r,i){ if(out.tryFirst&&r.action===out.tryFirst.action&&r.lever===out.tryFirst.lever)return; s+=_ccRecCard(r,i===0?'NEXT':null); }); if(out.recommendationGroups&&out.recommendationGroups.canWait&&out.recommendationGroups.canWait.length){ s+='
CAN WAIT · REFERENCE
'; s+='
'; out.recommendationGroups.canWait.forEach(function(w){ s+='
'+_ccEsc(w.action)+'
'; }); s+='
'; } var shown=new Set((out.actionRecommendations||[]).slice(0,5).map(function(r){return r.lever+':'+r.action;})); var rest=out.recommendations.filter(function(r){return !shown.has(r.lever+':'+r.action);}); if(rest.length){ s+='
Full list ('+out.recommendations.length+' items)'; rest.forEach(function(r){s+=_ccRecCard(r);}); s+='
'; } }else{ out.recommendations.forEach(function(r){s+=_ccRecCard(r);}); } s+=_ccDisciplineBar(allPrior?"do not stack priors — prove one lever at a time":null); mount.innerHTML=s; } function _ccBuildBc39BriefMd(report,recOut){ var L=[],P=function(x){L.push(x);}; var ep=_logMatchTrackIntelPreset(); var tc=report&&report.track_condition||_logBuildTrackConditionContext(typeof _logSess!=="undefined"&&_logSess?_logSess.parsed:null); P("# BC39 Crew Chief Brief"); P("*The Dirt Track at IMS · USAC Midget · Jun 30–Jul 1, 2026*"); P(""); P("## Expected track behavior"); if(ep){ P("- **Geometry:** 1/5-mile CCW bullring (~12.3s laps), ~8° bank, inside IMS Turn 3"); P("- **Orientation:** front ~96° / back ~276°"); if(ep.surface_evolution)P("- **Evolution:** "+ep.surface_evolution); if(ep.drying_model&&ep.drying_model.note)P("- **Drying:** "+ep.drying_model.note); } if(tc&&tc.drying_prediction&&tc.drying_prediction.note)P("- **Live drying call:** "+tc.drying_prediction.note); if(tc&&tc.line_progression){ var lp=tc.line_progression; if(lp.early)P("- **Early run:** "+lp.early); if(lp.mid)P("- **Mid run:** "+lp.mid); if(lp.late)P("- **Late run:** "+lp.late); } if(tc&&tc.session_log&&tc.session_log.length){ P("- **Recent sessions:** "+tc.session_log.slice(0,3).map(function(r){return(r.session||"run")+(r.track_state?" ("+r.track_state+")":"")+(r.lane?" · "+r.lane:"");}).join(" · ")); } P(""); P("## Top recommendation priorities (tonight)"); var pri=(ep&&ep.recommendation_priorities)||[]; pri.forEach(function(p,i){P((i+1)+". **[EVENT PRIOR]** "+p.text);}); if(recOut&&recOut.recommendations&&recOut.recommendations.length){ P(""); P("## Ranked setup moves (from report)"); recOut.recommendations.slice(0,8).forEach(function(r){ var tag=r.basis==="prior"?"PRIOR":r.confidence; P(r.rank+". **["+tag+"]** "+r.action); P(" - "+r.reason); }); }else if(report&&report.key_findings&&report.key_findings.length){ P(""); P("## Key findings (data-backed)"); report.key_findings.slice(0,5).forEach(function(f,i){P((i+1)+". **["+f.level+"]** "+f.text);}); }else{ P(""); P("_No experiment findings yet — priorities above are event priors only. Link DRK runs to EXP arms and regenerate._"); } P(""); P("## Discipline"); P("- One DOF per run · log lane_commit · never add bite into S2 slick"); P("- Night 2 qual: **positions advanced** in 10-lap races lock top 18"); P(""); P("*Generated "+new Date().toISOString()+" · data over myth*"); return L.join("\n"); } function _ccRenderDriverBrief(report,recOut,mount,meta){ if(!mount)return; if(!_logIsBc39Mode()&&!_ccIsGrassrootsGarageClass()){ mount.innerHTML=_ccPanelBox("empty","Driver Brief unavailable","Select The Dirt Track at IMS (BC39) or a grassroots class (QM, Outlaw, Micro, Lightning) for the Driver tab."); return; } meta=meta||_ccLastGenMeta||{}; var lines=_ccBuildDriverBriefLines(report,recOut,meta); var ds=_ccDataStatus(report,meta,recOut); var banner=""; if(!report)banner=_ccPanelBox("info","Preset driver read — Generate to refresh",[_logIsBc39Mode()?"Event priors below until DRK runs land.":"Class baseline + surface guidance below until you Generate with logged runs."],null); else if(ds.level==="thin"||ds.level==="priors"||ds.level==="note")banner=_ccPanelBox("info","Calm read", [ds.msg],null); var body='
'; body+='
DRIVER BRIEF · '+lines.length+' lines'+(_logIsBc39Mode()?_ccBc39LiveChip():"")+'
'; lines.forEach(function(ln,i){body+='
'+_ccEsc(ln)+'
';}); body+='
'; var fullMd=report?_ccBuildBc39BriefMd(report,recOut):null; body+='
Full BC39 brief (export)'; if(fullMd)body+='
'+_ccEsc(fullMd)+'
'; body+='
'; body+=_ccDisciplineBar(_logIsBc39Mode()?"show driver lines 1–2 only — one lever tonight":"one lever per run — your setup sheet personalizes advice"); mount.innerHTML=banner+body; } function _ccRenderBc39Brief(report,recOut,mount){ _ccRenderDriverBrief(report,recOut,mount,_ccLastGenMeta); } function _ccExportMd(r){ if(window.CrewChiefReport&&window.CrewChiefReport.renderMarkdown)return window.CrewChiefReport.renderMarkdown({report:r}); return "```json\n"+JSON.stringify(r,null,2)+"\n```"; } function _ccExportRecMd(recOut,report){ var L=[]; if(window.CrewChiefReport&&window.CrewChiefReport.renderRecommendations){ L.push(window.CrewChiefReport.renderRecommendations(recOut,report&&report.header?report.header.name:"Setup")); } if(_logIsBc39Mode())L.push("\n\n---\n\n"+_ccBuildBc39BriefMd(report,recOut)); return L.join("\n"); } function _ccDownload(text,name,type){ var b=new Blob([text],{type:type||"text/plain"}),u=URL.createObjectURL(b),a=document.createElement("a"); a.href=u;a.download=name;a.click();setTimeout(function(){URL.revokeObjectURL(u);},1500); } function _ccLoadTonightExpRuns(){ var today=new Date().toISOString().split("T")[0],runs=[]; try{runs=JSON.parse(localStorage.getItem(_DRK_EXP_RUNS_KEY)||"[]");}catch(e){} return runs.filter(function(r){return(r.captured_at||"").split("T")[0]===today;}); } function _ccSuggestNextArm(expRuns,exp){ if(!exp||!exp.arms||!exp.arms.length)return "A"; var order=exp.arms.map(function(a){return a.k;}); if(!expRuns.length)return order[0]; var last=expRuns[expRuns.length-1],lastArm=last.arm||order[0],idx=order.indexOf(lastArm); if(idx<0)return order[0]; if(order.length===2)return order[(idx+1)%2]; return order[(idx+1)%order.length]; } function _ccExpLeverHint(expId){ var m={"EXP-2":"HS comp (comp_low / comp_high)","EXP-3":"HS reb (reb_high)","EXP-1":"panhard_height","EXP-4":"bump_engaged (0/1)","EXP-5":"helper_soft (0/1)","EXP-6":"sipe_count / groove_density","EXP-0":"stagger"}; return m[expId]||"one DOF lever on setup sheet"; } function _ccCoachState(){ var draft=typeof _logLoadDrkExpDraft==="function"?_logLoadDrkExpDraft():{}; var expId=draft.experiment_id||"EXP-2"; var exp=typeof _logDrkExpById==="function"?_logDrkExpById(expId):null; if(!exp&&typeof _logDrkExpProgram!=="undefined"&&_logDrkExpProgram.length){exp=_logDrkExpProgram[0];expId=exp.id;} var tonight=_ccLoadTonightExpRuns(); var expRuns=exp?tonight.filter(function(r){return r.experiment_id===expId;}):[]; var surface=draft.track_state||(typeof _logInferTrackState==="function"?_logInferTrackState():null)||"unknown"; var nextArm=draft.arm||_ccSuggestNextArm(expRuns,exp); var armCounts={}; if(exp)exp.arms.forEach(function(a){armCounts[a.k]=0;}); expRuns.forEach(function(r){if(r.arm)armCounts[r.arm]=(armCounts[r.arm]||0)+1;}); return{expId:expId,expName:exp?exp.name:"Experiment",exp:exp,leverHint:_ccExpLeverHint(expId),runs:expRuns.length,target:_CC_EXP_TARGET_RUNS,nextArm:nextArm,surface:surface,armCounts:armCounts,tonightTotal:tonight.length}; } function _ccScrollToDrkUpload(){ if(typeof switchTab==="function")switchTab("garage"); setTimeout(function(){ var acc=document.getElementById("acc-camera"); if(acc){ if(typeof toggleAcc==="function"){var body=document.getElementById("body-acc-camera");if(body&&body.style.display==="none")toggleAcc("acc-camera");} acc.scrollIntoView({behavior:"smooth",block:"start"}); } },200); } function _ccRenderNightCoach(mount){ if(!mount)return; var st=_ccCoachState(); var armDetail=st.exp&&st.exp.arms?st.exp.arms.map(function(a){return a.k+": "+(st.armCounts[a.k]||0);}).join(" · "):""; var s='
'; s+='
EXPERIMENT NIGHT COACH
'; s+='
Tonight: '+_ccEsc(st.expId)+" · "+_ccEsc(st.expName)+' — need arm '+_ccEsc(st.nextArm)+' ('+st.runs+'/'+st.target+' runs · '+_ccEsc(st.surface)+')
'; if(armDetail)s+='
Arms logged: '+_ccEsc(armDetail)+'
'; if(typeof _getCarType==="function"&&S.cur&&_getCarType(S.cur)==="outlawkart"&&(!S.cur.chassis_name||!String(S.cur.chassis_name).trim())){ s+='
Add chassis mfr + model in Garage → Setup → Chassis so outlaw priors route to QRC/Phantom/Toigo/Slack/Ultramax.
'; } var qmCls=(S.cur&&(S.cur.class||S.cur.car_class))||""; if(/quarter.midget|qma|\bqm\b/i.test(qmCls)&&(!S.cur||!S.cur.chassis_name||!String(S.cur.chassis_name).trim())){ s+='
Add chassis mfr (Bullrider, Stanley/RSR, NC, etc.) in Garage → Setup → Chassis so QM priors use your manufacturer baseline sheet.
'; } if(typeof _getCarType==="function"&&S.cur&&_getCarType(S.cur)==="micro"&&(!S.cur.chassis_name||!String(S.cur.chassis_name).trim())){ s+='
Add chassis mfr + model in Garage → Setup → Chassis so 600 micro priors route to Hyper vs other builder guidance.
'; } if(/lightning|glls/i.test(qmCls)&&(!S.cur||!S.cur.chassis_name||!String(S.cur.chassis_name).trim())){ s+='
Add chassis mfr (Hyper, Saldana, etc.) in Garage → Setup → Chassis so Lightning priors use winged vs wingless baseline guidance.
'; } if(S.cur&&S.cur.chassis_name&&String(S.cur.chassis_name).trim()){ s+='
Chassis on file: '+_ccEsc(S.cur.chassis_name)+' — thin-data nights route manufacturer baseline first when available.
'; } s+='
Lever tonight: '+_ccEsc(st.leverHint)+' — sync sheet, then link DRK with EXP + arm. Harness reads setup snapshot, not invented arm defaults.
'; s+=_ccGrassrootsContextCoachHtml(); s+=_ccAdvancedDataCaptureCoachHtml(); s+=''; s+='
'; mount.innerHTML=s; var btn=mount.querySelector("#cc-coach-drk-btn"); if(btn)btn.onclick=function(){_ccScrollToDrkUpload();toast("Link EXP + arm when DRK saves");}; } function _buildCrewChiefReportPanel(mount){ if(!mount)return; mount.innerHTML=""; var wrap=document.createElement("div"); wrap.style.cssText="background:var(--dark2);border:1px solid rgba(57,208,163,.22);border-left:3px solid #39d0a3;padding:12px"; var ccBanner=null; if(_logIsBc39Mode()){ ccBanner=document.createElement("div"); ccBanner.style.cssText="font-family:var(--mono);font-size:8px;color:#fbbf24;padding:8px 10px;margin-bottom:10px;background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.25);line-height:1.55"; ccBanner.innerHTML="BC39 MODE ACTIVE · IMS Dirt preset · event priors work even before tonight's A-B-A data lands"; wrap.appendChild(ccBanner); } var coachMount=document.createElement("div"); coachMount.id="cc-night-coach-mount"; wrap.appendChild(coachMount); _ccRenderNightCoach(coachMount); var statusEl=document.createElement("div"); statusEl.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);min-height:14px;margin-bottom:8px;line-height:1.5"; statusEl.textContent="Pick a run block and Generate — Sample block demos a full night without DRK data."; wrap.appendChild(statusEl); var tabBtns={},panels={},activeTab="report",outBrief=null; var bar=document.createElement("div"); bar.style.cssText="display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:8px"; var sel=document.createElement("select");sel.id="cc-block-sel";sel.style.cssText="flex:1;min-width:160px;padding:8px;background:var(--dark);border:1px solid rgba(255,255,255,.12);color:var(--white);font-family:var(--mono);font-size:10px"; var genBtn=document.createElement("button");genBtn.type="button";genBtn.textContent="Generate";genBtn.style.cssText="padding:9px 14px;background:#39d0a3;color:#06120d;border:none;border-radius:6px;font-weight:800;font-size:11px;cursor:pointer"; var exJ=document.createElement("button");exJ.type="button";exJ.textContent="JSON"; var exM=document.createElement("button");exM.type="button";exM.textContent="Markdown"; var exT=document.createElement("button");exT.type="button";exT.textContent="Text"; var exB=document.createElement("button");exB.type="button";exB.textContent="BC39 Brief"; [exJ,exM,exT,exB].forEach(function(b){b.style.cssText="padding:7px 10px;background:transparent;border:1px solid rgba(255,255,255,.15);color:var(--white);border-radius:6px;font-size:10px;cursor:pointer";}); if(!_logIsBc39Mode())exB.style.display="none"; bar.appendChild(sel);bar.appendChild(genBtn);bar.appendChild(exJ);bar.appendChild(exM);bar.appendChild(exT);bar.appendChild(exB); wrap.appendChild(bar); var hint=document.createElement("div"); hint.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);line-height:1.55;margin-bottom:10px"; hint.textContent="Runs → harness → Report + ranked Recommendations. PRIOR = event/setup prior, not from tonight's data. "+_CC_DISCIPLINE+"."; wrap.appendChild(hint); var tabs=document.createElement("div"); tabs.style.cssText="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap"; wrap.appendChild(tabs); var tabNames=[{id:"report",label:"Report"},{id:"recs",label:"Recommendations"},{id:"brief",label:"Driver"}]; tabNames.forEach(function(t){ if(t.id==="brief"&&!_logIsBc39Mode()&&!_ccIsGrassrootsGarageClass())return; var b=document.createElement("button");b.type="button";b.textContent=t.label; b.style.cssText="padding:7px 12px;border:1px solid rgba(255,255,255,.12);background:var(--dark);color:var(--muted);font-family:var(--mono);font-size:9px;font-weight:700;cursor:pointer;border-radius:6px"; b.onclick=function(){activeTab=t.id;Object.keys(tabBtns).forEach(function(k){tabBtns[k].style.background=k===activeTab?"rgba(57,208,163,.15)":"var(--dark)";tabBtns[k].style.color=k===activeTab?"#39d0a3":"var(--muted)";tabBtns[k].style.borderColor=k===activeTab?"rgba(57,208,163,.35)":"rgba(255,255,255,.12)";});Object.keys(panels).forEach(function(k){panels[k].style.display=k===activeTab?"block":"none";});}; tabBtns[t.id]=b;tabs.appendChild(b); var p=document.createElement("div");p.style.display=t.id===activeTab?"block":"none";panels[t.id]=p;wrap.appendChild(p); }); var outReport=document.createElement("div");panels.report.appendChild(outReport); var outRecs=document.createElement("div");panels.recs.appendChild(outRecs); if(panels.brief){outBrief=document.createElement("div");panels.brief.appendChild(outBrief);} mount.appendChild(wrap); _ccRenderPanelEmptyInitial(outReport,outRecs,outBrief); Object.keys(tabBtns).forEach(function(k){tabBtns[k].click();}); async function populate(){ statusEl.textContent="Loading run blocks…"; try{ var bl=await _ccListBlocks(); sel.innerHTML=bl.map(function(b){return '';}).join(""); statusEl.textContent=bl.length>1?"Ready — "+(bl.length-1)+" block(s) besides Tonight/Sample.":"Ready — link DRK runs or try Sample."; }catch(e){ sel.innerHTML=''; statusEl.textContent="Block list offline — "+_ccFormatGenerateError(e); } } async function runGenerate(){ genBtn.disabled=true; genBtn.textContent="Generating…"; genBtn.style.opacity="0.65"; statusEl.textContent="Generating from "+sel.options[sel.selectedIndex].text+"…"; _ccRenderPanelLoading(outReport,"Building report"); _ccRenderPanelLoading(outRecs,"Ranking recommendations"); if(outBrief)_ccRenderPanelLoading(outBrief,"Building driver brief"); try{ _ccLastReport=await _ccGenerateReport(sel.value); if(window.CrewChiefReport&&window.CrewChiefReport.normalizeTrackContext&&_ccLastReport.track_condition) _ccLastReport.track_condition=window.CrewChiefReport.normalizeTrackContext(_ccLastReport.track_condition); _ccLastRecs=_ccGenerateRecommendations(_ccLastReport); _ccRenderReport(_ccLastReport,outReport,_ccLastGenMeta); _ccRenderRecommendations(_ccLastRecs,outRecs,_ccLastReport); if(panels.brief&&outBrief)_ccRenderDriverBrief(_ccLastReport,_ccLastRecs,outBrief,_ccLastGenMeta); _ccRenderNightCoach(coachMount); var meta=_ccLastGenMeta||{}; var parts=["Done"]; if(meta.source==="sample")parts.push("demo sample"); else if(meta.source==="api")parts.push("live API"); else parts.push("local engine"); if(meta.runCount!=null)parts.push(meta.runCount+" runs"); if(meta.thin)parts.push("thin data — confirm with A-B-A"); if(meta.apiFallback)parts.push("API fallback used"); statusEl.textContent=parts.join(" · "); if(ccBanner&&meta.thin&&sel.value!=="__sample__") ccBanner.innerHTML="BC39 MODE ACTIVE · thin run block — priors + track intel OK · book nothing until A-B-A confirms"; }catch(e){ var msg=_ccFormatGenerateError(e); _ccRenderPanelError(outReport,msg); _ccRenderPanelError(outRecs,msg); if(outBrief)_ccRenderPanelError(outBrief,msg); statusEl.textContent=msg; _ccLastReport=null; _ccLastRecs=null; throw e; }finally{ genBtn.disabled=false; genBtn.textContent="Generate"; genBtn.style.opacity="1"; } } genBtn.onclick=async function(){try{await runGenerate();toast("AI Crew Chief ready");}catch(e){toast(_ccFormatGenerateError(e).split(" · ")[0]);}}; window._ccRunGenerate=runGenerate; exJ.onclick=function(){if(!_ccLastReport){toast("Generate first");return;}_ccDownload(JSON.stringify({report:_ccLastReport,recommendations:_ccLastRecs},null,2),"crew-chief-full.json","application/json");}; exM.onclick=function(){if(!_ccLastReport){toast("Generate first");return;}_ccDownload(_ccExportMd(_ccLastReport)+"\n\n---\n\n"+(_ccLastRecs&&window.CrewChiefReport?window.CrewChiefReport.renderRecommendations(_ccLastRecs):""),"crew-chief-report.md","text/markdown");}; exT.onclick=function(){if(!_ccLastReport){toast("Generate first");return;}_ccDownload(_ccExportText(_ccLastReport)+"\n\n"+(window.CrewChiefReport&&_ccLastRecs?window.CrewChiefReport.renderRecommendations(_ccLastRecs):""),"crew-chief-report.txt","text/plain");}; exB.onclick=function(){if(!_logIsBc39Mode())return;var md=_ccBuildBc39BriefMd(_ccLastReport,_ccLastRecs);_ccDownload(md,"bc39-crew-chief-brief.md","text/markdown");}; window.crewChief={listBlocks:_ccListBlocks,generate:_ccGenerateReport,recommend:function(id){return _ccGenerateReport(id).then(function(r){return _ccGenerateRecommendations(r);});},fetchApi:_ccFetchApi}; populate(); } function _logVehicleContextForCrewChief(){ var c=S.cur||{},cls=c.class||c.car_class||""; var ct=typeof _getCarType==="function"?_getCarType(c):"generic"; if(/quarter.midget|qma|\bqm\b/i.test(cls))ct="quartermidget"; if(/lightning|glls/i.test(cls))ct="lightningsprint"; var winged=typeof isWingedCarClass==="function"?isWingedCarClass(cls):(/winged|\bwing\b|305|360|410/i.test(cls)&&!/non.?wing/i.test(cls)); var phil=typeof _chassisPhilosophyForCar==="function"?_chassisPhilosophyForCar(c):null; var chName=c.chassis_name||""; var su=typeof _su!=="undefined"?_su:{}; var measurements={}; if(su.lf_psi!=null&&su.lf_psi!=="")measurements.lf_psi=su.lf_psi; if(su.rf_psi!=null&&su.rf_psi!=="")measurements.rf_psi=su.rf_psi; if(su.lr_psi!=null&&su.lr_psi!=="")measurements.lr_psi=su.lr_psi; if(su.rr_psi!=null&&su.rr_psi!=="")measurements.rr_psi=su.rr_psi; if(su.left_pct!=null)measurements.left_pct=su.left_pct; if(su.rear_pct!=null)measurements.rear_pct=su.rear_pct; if(su.cross_pct!=null)measurements.cross_pct=su.cross_pct; else if(su.left_pct!=null&&su.rear_pct!=null)measurements.cross_pct=Math.round((parseFloat(su.left_pct)+parseFloat(su.rear_pct)-100)*10)/10; if(su.stagger!=null&&!isNaN(su.stagger))measurements.stagger=su.stagger; if(su.ride_ht_f)measurements.ride_ht_f=su.ride_ht_f; if(su.ride_ht_r)measurements.ride_ht_r=su.ride_ht_r; if(su.ride_ht_lf!=null)measurements.ride_ht_lf=su.ride_ht_lf; if(su.ride_ht_rf!=null)measurements.ride_ht_rf=su.ride_ht_rf; if(su.ride_ht_lr!=null)measurements.ride_ht_lr=su.ride_ht_lr; if(su.ride_ht_rr!=null)measurements.ride_ht_rr=su.ride_ht_rr; if(measurements.ride_ht_lf!=null&&measurements.ride_ht_rf!=null&&measurements.ride_ht_lr!=null&&measurements.ride_ht_rr!=null){ measurements.rake_in=Math.round((((measurements.ride_ht_lr+measurements.ride_ht_rr)/2)-((measurements.ride_ht_lf+measurements.ride_ht_rf)/2))*1000)/1000; } if(c.wheelbase)measurements.wheelbase=c.wheelbase; if(c.rear_susp)measurements.rear_susp=c.rear_susp; if(c.rear_end_type)measurements.rear_end_type=c.rear_end_type; if(c.drivetrain)measurements.drivetrain=c.drivetrain; if(c.comp_lf!=null||c.comp_low!=null)measurements.shock_comp_lf=c.comp_lf!=null?c.comp_lf:c.comp_low; if(c.reb_lr!=null||c.reb_low!=null)measurements.shock_reb_lr=c.reb_lr!=null?c.reb_lr:c.reb_low; if(c.pivot_afrh_complete==='yes')measurements.pivot_afrh_logged=true; if(c.pivot_pickup_notes)measurements.pivot_pickup_notes=c.pivot_pickup_notes; if(su.panhard!=null)measurements.panhard=su.panhard; if(c.j_ladder||su.j_ladder)measurements.j_ladder=c.j_ladder||su.j_ladder; if(su.seat_pos!=null)measurements.seat_pos=su.seat_pos; if(su.rear_track_width_mm!=null)measurements.rear_track_width_mm=su.rear_track_width_mm; if(su.front_track_width_mm!=null)measurements.front_track_width_mm=su.front_track_width_mm; if(su.wing_angle!=null)measurements.wing_angle=su.wing_angle; if((ct==="sprint"||/midget|410|360|305|sprint/i.test(cls))&&su.bite!=null&&su.bite!==""&&measurements.lr_bar_turns==null){ measurements.lr_bar_turns=su.bite; } if(su.j_bar_height!=null&&measurements.j_bar_height==null)measurements.j_bar_height=su.j_bar_height; if(su.bar_lr!=null&&su.bar_lr!==""&&measurements.bar_lr_height==null)measurements.bar_lr_height=su.bar_lr; if(measurements.left_pct==null||measurements.rear_pct==null||measurements.cross_pct==null){ try{ var _scId2=c.id||"local"; var _scD2=JSON.parse(localStorage.getItem("bb_scale_"+_scId2)||"null"); if(_scD2&&_scD2.current&&_scD2.current.lf&&typeof _calcScale==="function"){ var _sm2=_calcScale(_scD2.current.lf,_scD2.current.rf,_scD2.current.lr,_scD2.current.rr); if(_sm2){ if(measurements.left_pct==null)measurements.left_pct=Math.round(_sm2.left*10)/10; if(measurements.rear_pct==null)measurements.rear_pct=Math.round(_sm2.rear*10)/10; if(measurements.cross_pct==null)measurements.cross_pct=Math.round(_sm2.cross*10)/10; } } }catch(_scE){} } if(su.stagger_r!=null&&!isNaN(su.stagger_r)&&measurements.stagger==null)measurements.stagger=su.stagger_r; var measKeys=Object.keys(measurements); return{ car_type:ct,car_class:cls,chassis_name:chName||null,chassis_mfr:phil?phil.mfr:null, chassis_philosophy:phil?phil.philosophy:null,chassis_model_missing:!String(chName||"").trim(), winged:!!winged,engine:c.engine||null,front_susp:c.front_susp||null,setup_style:c.front_susp||null, geo_notes:c.geo_notes||null,trait_entry:c.trait_entry||null,trait_mid:c.trait_mid||null,trait_exit:c.trait_exit||null, trait_notes:c.trait_notes||null, rear_susp:c.rear_susp||null,rear_end_type:c.rear_end_type||null,drivetrain:c.drivetrain||null, pivot_complete:!!c.pivot_complete,pivot_afrh_complete:c.pivot_afrh_complete||null, user_mods:[c.geo_notes,c.pivot_pickup_notes,c.trait_notes].filter(Boolean).join(" | ")||null, setup_measurements:measKeys.length?measurements:null, session_log:typeof _logLoadSessionLog==="function"?_logLoadSessionLog().slice(0,8):[], track_snaps:typeof _tsLoadSnaps==="function"?_tsLoadSnaps().slice(0,12):[] }; } function _logBuildTrackConditionContext(parsed,wx){ wx=wx||_logDrkWxFields();var dirRaw=_logDetectTrackDirection(parsed),dir=_logApplyOrientationPreset(dirRaw),wind=_logDrkWindContext(dir,wx),photos=_logCollectTrackPhotoRefs(); var walkData=_logFindTrackWalkData(),walkIntel=walkData||S._trackWalk?_logBuildWalkSectionIntel(walkData||{points:{}}):null; var expDraft=_logDrkExpDraft||_logLoadDrkExpDraft(); _logMergeWalkMoistureIntoDraft(expDraft,walkIntel); var drying=_logEstimateSectionDrying(dir,wind,wx,photos.photos,expDraft.section_moisture,walkIntel); var instr=_logBuildInstrumentationContext(); var expCap=typeof _logReadDrkExperimentCapture==="function"?_logReadDrkExperimentCapture(parsed):null; var dryingPred=_logBuildDryingPrediction(dir,wind,wx,drying); var lineProg=_logLoadLineProgression()||_logDefaultLineProgression(); var sessionLog=_logLoadSessionLog(); if(dirRaw&&dirRaw.straight_heading_deg!=null&&dirRaw.confidence>=0.35){ var existO=_logLoadTrackOrientation(); if(!existO||existO.source==="gps"||!existO.source){ _logSaveTrackOrientation({front_straight_deg:dirRaw.straight_heading_deg,back_straight_deg:(dirRaw.straight_heading_deg+180)%360,direction:dirRaw.track_direction!=="unknown"?dirRaw.track_direction:"counterclockwise",source:"gps",confidence:dirRaw.confidence}); } } var ep=_logMatchTrackIntelPreset(),inferredState=_logInferTrackState(); var ctx={ track_direction:dir.track_direction,track_direction_confidence:dir.confidence,direction_detail:dir, orientation_preset:dir.orientation_preset||_logLoadTrackOrientation(),wind_context:wind, track_photos:photos.photos,photo_count:photos.photo_count,track_walk_photo_count:photos.track_walk_photo_count||0, track_walk_context:walkIntel,drying_sections:drying,drying_prediction:dryingPred,line_progression:lineProg, session_log:sessionLog.slice(0,8), track_snaps:typeof _tsLoadSnaps==="function"?_tsLoadSnaps().slice(0,12):[], instrumentation_context:instr, track_state:inferredState, manual_instrumentation:expCap?{experiment_id:expCap.experiment_id,arm:expCap.arm,study_targets:expCap.study_targets,responses:expCap.responses,raw:expCap._provenance&&expCap._provenance.instrumentation}:null, captured_at:new Date().toISOString(), vehicle_context:_logVehicleContextForCrewChief() }; if(ep){ ctx.event_context={event:ep.event,mode:ep.mode||"bc39",label:ep.label,profile:ep.track_profile, surface_evolution:ep.surface_evolution,drying:ep.drying_model,recommendation_priorities:ep.recommendation_priorities}; } return ctx; } function _logDrkSummaryMeta(parsed,extra){ extra=extra||{};var wx=_logDrkWxFields(); return Object.assign({wx:wx,trackContext:_logBuildTrackConditionContext(parsed,wx)},extra); } function _logDrkExpInputStyle(){return"width:100%;padding:6px 8px;background:var(--dark);border:1px solid rgba(255,255,255,.12);color:var(--white);font-family:var(--mono);font-size:11px;box-sizing:border-box";} function _logDrkExpLabelStyle(){return"font-family:var(--mono);font-size:7px;color:var(--muted);letter-spacing:.5px;margin:0 0 3px";} function _logRenderDrkExpCaptureForm(card,parsed,fileName,meta){ var draft=_logDrkExpDraft||_logInitDrkExpDraft(fileName,parsed); var wrap=document.createElement("div");wrap.id="drk-exp-capture";wrap.style.cssText="margin:10px 0;padding:10px;background:rgba(120,80,200,.06);border:1px solid rgba(120,80,200,.22)"; var head=document.createElement("div");head.style.cssText="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap"; var ht=document.createElement("div");ht.style.cssText="font-family:var(--mono);font-size:8px;color:#c4b5fd;letter-spacing:1px";ht.textContent="EXPERIMENTAL CAPTURE"; var togg=document.createElement("label");togg.style.cssText="font-family:var(--mono);font-size:8px;color:var(--muted);display:flex;align-items:center;gap:6px;cursor:pointer"; var cb=document.createElement("input");cb.type="checkbox";cb.checked=!!draft.enabled;cb.onchange=function(){draft.enabled=cb.checked;_logSaveDrkExpDraft(draft);}; togg.appendChild(cb);togg.appendChild(document.createTextNode("Link this run to experiment")); head.appendChild(ht);head.appendChild(togg);wrap.appendChild(head); var expHint=document.createElement("div");expHint.id="drkexp-hint";expHint.style.cssText="font-family:var(--mono);font-size:8px;color:#94a3b8;line-height:1.55;margin-bottom:8px;padding:8px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);display:none"; wrap.appendChild(expHint); var derivedPreview=document.createElement("div");derivedPreview.id="drkexp-derived";derivedPreview.style.cssText="font-family:var(--mono);font-size:8px;color:#4ade80;line-height:1.5;margin-bottom:8px;display:none"; wrap.appendChild(derivedPreview); function refreshExpUi(){ var exp=_logDrkExpById(expSel.value),needs=exp?_logDrkExpNeedsMeas(exp.studies):{heat_up:false,bottoming:false}; if(exp){ expHint.style.display="block"; var parts=["Studies: "+exp.studies]; if(needs.heat_up)parts.push("needs pyrometer → heat_up_rate"); if(needs.bottoming)parts.push("needs shock travel → bottoming_rate"); expHint.textContent=exp.id+" · "+exp.name+" — "+parts.join(" · "); }else expHint.style.display="none"; pyroBlock.style.display=(needs.heat_up||!expSel.value)?"block":"none"; shockBlock.style.display=(needs.bottoming||!expSel.value)?"block":"none"; if(needs.heat_up||needs.bottoming){ pyroBlock.style.borderColor=needs.heat_up?"rgba(74,222,128,.35)":"rgba(255,255,255,.08)"; shockBlock.style.borderColor=needs.bottoming?"rgba(251,146,60,.35)":"rgba(255,255,255,.08)"; } _logSyncPyrometerPrimary(draft); var d=_logDeriveInstrumentation({tire_temp_pre_f:draft.tire_temp_pre_f,tire_temp_post_f:draft.tire_temp_post_f,run_minutes:draft.run_minutes,shock_travel_used_in:draft.shock_travel_used_in,shock_travel_avail_in:draft.shock_travel_avail_in,shock_bottom_events:draft.shock_bottom_events,laps:draft.laps}); var bits=[]; if(d.heat_up_rate!=null)bits.push("heat_up_rate ≈ "+d.heat_up_rate+" °F/min"); if(d.bottoming_rate!=null)bits.push("bottoming_rate ≈ "+d.bottoming_rate); if(bits.length){derivedPreview.style.display="block";derivedPreview.textContent="Derived (harness-ready): "+bits.join(" · ");} else derivedPreview.style.display="none"; } var grid=document.createElement("div");grid.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:8px"; function fld(lbl,id,val,ph,key,onIn){var d=document.createElement("div");var L=document.createElement("div");L.style.cssText=_logDrkExpLabelStyle();L.textContent=lbl; var inp=document.createElement("input");inp.id=id;inp.value=val!=null&&val!==""?String(val):"";inp.placeholder=ph||"";inp.style.cssText=_logDrkExpInputStyle();inp.inputMode="decimal"; var k=key||id.replace(/^drkexp_/,"");inp.oninput=function(){draft[k]=inp.value;_logSavePyrometerIfNeeded(k);_logSaveDrkExpDraft(draft);refreshExpUi();if(onIn)onIn(inp.value);};d.appendChild(L);d.appendChild(inp);return d;} function _logSavePyrometerIfNeeded(k){ if(k==="tire_temp_pre_f"||k==="tire_temp_post_f"){ var c=draft.tire_pyrometer_corner||"rr"; draft.tire_pre=draft.tire_pre||{};draft.tire_post=draft.tire_post||{}; if(k==="tire_temp_pre_f")draft.tire_pre[c]=draft.tire_temp_pre_f; else draft.tire_post[c]=draft.tire_temp_post_f; } } var expSel=document.createElement("select");expSel.id="drkexp_experiment_id";expSel.style.cssText=_logDrkExpInputStyle(); expSel.innerHTML=''+_logDrkExpProgram.map(function(e){return'";}).join(""); var expWrap=document.createElement("div");expWrap.appendChild((function(){var L=document.createElement("div");L.style.cssText=_logDrkExpLabelStyle();L.textContent="EXPERIMENT ID";return L;})());expWrap.appendChild(expSel); var armSel=document.createElement("select");armSel.id="drkexp_arm";armSel.style.cssText=_logDrkExpInputStyle(); function fillArms(){var exp=_logDrkExpById(expSel.value);armSel.innerHTML=''+(exp?exp.arms.map(function(a){return'";}).join(""):"");} fillArms(); expSel.onchange=function(){draft.experiment_id=expSel.value;draft.arm="";if(draft.experiment_id){draft.enabled=true;cb.checked=true;}fillArms();_logSaveDrkExpDraft(draft);refreshExpUi();}; armSel.onchange=function(){draft.arm=armSel.value;_logSaveDrkExpDraft(draft);}; var armWrap=document.createElement("div");armWrap.appendChild((function(){var L=document.createElement("div");L.style.cssText=_logDrkExpLabelStyle();L.textContent="TREATMENT ARM";return L;})());armWrap.appendChild(armSel); var stSel=document.createElement("select");stSel.id="drkexp_track_state";stSel.style.cssText=_logDrkExpInputStyle(); stSel.innerHTML=''+_logDrkTrackStates.map(function(s){return'"+s+"";}).join(""); stSel.onchange=function(){draft.track_state=stSel.value;_logSaveDrkExpDraft(draft);}; var stWrap=document.createElement("div");stWrap.appendChild((function(){var L=document.createElement("div");L.style.cssText=_logDrkExpLabelStyle();L.textContent="TRACK STATE";return L;})());stWrap.appendChild(stSel); grid.appendChild(expWrap);grid.appendChild(armWrap); grid.appendChild(stWrap); grid.appendChild(fld("DRK FILENAME","drkexp_drk_filename",draft.drk_filename||fileName,"MyChron file name")); var leverBlock=document.createElement("details");leverBlock.style.cssText="margin-top:8px;grid-column:1/-1"; leverBlock.innerHTML='Lever on car (harness fields — overrides sheet if set)'; var leverGrid=document.createElement("div");leverGrid.style.cssText="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:6px"; leverGrid.appendChild(fld("HS comp low","drkexp_comp_low",draft.comp_low,"clicks","comp_low")); leverGrid.appendChild(fld("HS comp high","drkexp_comp_high",draft.comp_high,"clicks","comp_high")); leverGrid.appendChild(fld("HS reb high","drkexp_reb_high",draft.reb_high,"clicks","reb_high")); leverGrid.appendChild(fld("Panhard ht","drkexp_panhard_height",draft.panhard_height||(typeof _su!=="undefined"&&_su.panhard!=null?_su.panhard:""),"in","panhard_height")); leverGrid.appendChild(fld("Bump in (0/1)","drkexp_bump_engaged",draft.bump_engaged,"0 or 1","bump_engaged")); leverGrid.appendChild(fld("Helper soft (0/1)","drkexp_helper_soft",draft.helper_soft,"0 or 1","helper_soft")); leverBlock.appendChild(leverGrid); var snapPrev=document.createElement("div");snapPrev.id="drkexp-setup-snap-preview";snapPrev.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);margin-top:6px;line-height:1.5"; leverBlock.appendChild(snapPrev); function refreshSnapPreview(){ var sp=_logBuildSetupSnapshot(draft),bits=[]; if(sp.harness){Object.keys(sp.harness).forEach(function(k){bits.push(k+"="+sp.harness[k]);});} snapPrev.textContent=bits.length?"Snapshot harness: "+bits.join(" · "):"Snapshot: sheet PSI/stagger/panhard — fill lever fields if shocks not on sheet"; } refreshSnapPreview(); var _origRefresh=refreshExpUi; refreshExpUi=function(){_origRefresh();refreshSnapPreview();}; grid.appendChild(leverBlock); grid.appendChild(fld("RUN MINUTES","drkexp_run_minutes",draft.run_minutes,"e.g. 4.5","run_minutes")); grid.appendChild(fld("LAPS","drkexp_laps",draft.laps,"optional","laps")); grid.appendChild(fld("SURFACE TEMP °F","drkexp_surface_temp_f",draft.surface_temp_f,"IR gun","surface_temp_f")); wrap.appendChild(grid); var pyroBlock=document.createElement("div");pyroBlock.id="drkexp-pyro-block";pyroBlock.style.cssText="margin-top:10px;padding:8px;border:1px solid rgba(255,255,255,.08);background:rgba(74,222,128,.04)"; var pyroLbl=document.createElement("div");pyroLbl.style.cssText=_logDrkExpLabelStyle();pyroLbl.textContent="PYROMETER → heat_up_rate (RR center convention)"; pyroBlock.appendChild(pyroLbl); var pyroGrid=document.createElement("div");pyroGrid.style.cssText="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px"; pyroGrid.appendChild(fld("PRE °F","drkexp_tire_temp_pre_f",draft.tire_temp_pre_f,"before","tire_temp_pre_f")); pyroGrid.appendChild(fld("POST °F","drkexp_tire_temp_post_f",draft.tire_temp_post_f,"within 15s","tire_temp_post_f")); pyroGrid.appendChild(fld("RUN MIN","drkexp_run_minutes2",draft.run_minutes,"minutes","run_minutes")); pyroBlock.appendChild(pyroGrid); var cornerToggle=document.createElement("details");cornerToggle.style.cssText="margin-top:6px"; cornerToggle.innerHTML='All corners (optional)'; var tireGrid=document.createElement("div");tireGrid.style.cssText="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-top:6px"; ["lf","rf","lr","rr"].forEach(function(corner){ var d=document.createElement("div");var L=document.createElement("div");L.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);text-align:center";L.textContent=corner.toUpperCase(); var row=document.createElement("div");row.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:4px"; var pre=document.createElement("input");pre.placeholder="pre";pre.value=(draft.tire_pre&&draft.tire_pre[corner])||"";pre.style.cssText=_logDrkExpInputStyle();pre.inputMode="decimal"; var post=document.createElement("input");post.placeholder="post";post.value=(draft.tire_post&&draft.tire_post[corner])||"";post.style.cssText=_logDrkExpInputStyle();post.inputMode="decimal"; pre.oninput=function(){draft.tire_pre=draft.tire_pre||{};draft.tire_pre[corner]=pre.value;if(corner===(draft.tire_pyrometer_corner||"rr")){draft.tire_temp_pre_f=pre.value;_logSaveDrkExpDraft(draft);refreshExpUi();}else _logSaveDrkExpDraft(draft);}; post.oninput=function(){draft.tire_post=draft.tire_post||{};draft.tire_post[corner]=post.value;if(corner===(draft.tire_pyrometer_corner||"rr")){draft.tire_temp_post_f=post.value;_logSaveDrkExpDraft(draft);refreshExpUi();}else _logSaveDrkExpDraft(draft);}; row.appendChild(pre);row.appendChild(post);d.appendChild(L);d.appendChild(row);tireGrid.appendChild(d); }); cornerToggle.appendChild(tireGrid);pyroBlock.appendChild(cornerToggle);wrap.appendChild(pyroBlock); var shockBlock=document.createElement("div");shockBlock.id="drkexp-shock-block";shockBlock.style.cssText="margin-top:10px;padding:8px;border:1px solid rgba(255,255,255,.08);background:rgba(251,146,60,.04)"; var shockLbl=document.createElement("div");shockLbl.style.cssText=_logDrkExpLabelStyle();shockLbl.textContent="SHOCK TRAVEL → bottoming_rate (zip-tie or events/lap)"; shockBlock.appendChild(shockLbl); var shockGrid=document.createElement("div");shockGrid.style.cssText="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px"; shockGrid.appendChild(fld("USED (in)","drkexp_shock_travel_used_in",draft.shock_travel_used_in,"zip-tie","shock_travel_used_in")); shockGrid.appendChild(fld("AVAIL (in)","drkexp_shock_travel_avail_in",draft.shock_travel_avail_in,"total shaft","shock_travel_avail_in")); shockGrid.appendChild(fld("BOTTOM EVENTS","drkexp_shock_bottom_events",draft.shock_bottom_events,"if logged","shock_bottom_events")); shockBlock.appendChild(shockGrid); var shockNotes=document.createElement("div");shockNotes.style.cssText="margin-top:6px"; function ta(lbl,key,ph){var d=document.createElement("div");var L=document.createElement("div");L.style.cssText=_logDrkExpLabelStyle();L.textContent=lbl; var t=document.createElement("textarea");t.rows=2;t.placeholder=ph||"";t.value=draft[key]||"";t.style.cssText=_logDrkExpInputStyle()+";resize:vertical;min-height:36px"; t.oninput=function(){draft[key]=t.value;_logSaveDrkExpDraft(draft);};d.appendChild(L);d.appendChild(t);return d;} shockNotes.appendChild(ta("NOTES (corner, which shock)","shock_travel_notes","LR shock, turn 3 entry...")); shockBlock.appendChild(shockNotes);wrap.appendChild(shockBlock); var notesGrid=document.createElement("div");notesGrid.style.cssText="display:grid;grid-template-columns:1fr;gap:8px;margin-top:8px"; notesGrid.appendChild(ta("OTHER INSTRUMENTATION","instrumentation","hop rating 1-10, pyrometer brand...")); wrap.appendChild(notesGrid); refreshExpUi(); var secLbl=document.createElement("div");secLbl.style.cssText=_logDrkExpLabelStyle()+"margin-top:8px";secLbl.textContent="SECTION MOISTURE (optional — feeds drying model)"; wrap.appendChild(secLbl); var secGrid=document.createElement("div");secGrid.style.cssText="display:grid;grid-template-columns:repeat(3,1fr);gap:6px"; [{k:"front",l:"Front"},{k:"t1",l:"T1"},{k:"t2",l:"T2"},{k:"back",l:"Back"},{k:"t3",l:"T3"},{k:"t4",l:"T4"}].forEach(function(s){ var d=document.createElement("div");var L=document.createElement("div");L.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted)";L.textContent=s.l; var sel=document.createElement("select");sel.style.cssText=_logDrkExpInputStyle();sel.innerHTML=''+_logDrkTrackStates.map(function(st){return'"+st+"";}).join(""); sel.onchange=function(){draft.section_moisture=draft.section_moisture||{};draft.section_moisture[s.k]=sel.value;_logSaveDrkExpDraft(draft);if(typeof _logRefreshDrkTrackContext==="function")_logRefreshDrkTrackContext(parsed,fileName,meta);}; d.appendChild(L);d.appendChild(sel);secGrid.appendChild(d); }); wrap.appendChild(secGrid); var instrCard=document.createElement("div"); instrCard.style.cssText="margin-top:10px;padding:10px;background:rgba(251,146,60,.08);border:2px solid rgba(251,146,60,.35)"; var instrCardHdr=document.createElement("div"); instrCardHdr.style.cssText="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap"; var instrCardTitle=document.createElement("div"); instrCardTitle.style.cssText="font-family:var(--head);font-size:12px;font-weight:900;color:#fb923c;letter-spacing:.5px"; instrCardTitle.textContent="RECOMMENDED TRACK ANALYSIS TOOLS"; var instrCounts=document.createElement("div"); instrCounts.id="drkexp-instr-counts"; instrCounts.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted)"; var ic=typeof _logBuildInstrumentationContext==="function"?_logBuildInstrumentationContext():null; instrCounts.textContent=ic?(ic.lidar_count||0)+" LiDAR · "+(ic.thermal_count||0)+" thermal linked":"Import LiDAR or thermal for this session"; instrCardHdr.appendChild(instrCardTitle);instrCardHdr.appendChild(instrCounts); instrCard.appendChild(instrCardHdr); var gearTop=document.createElement("button");gearTop.type="button";gearTop.textContent="Thermal dongles + LiDAR gear guide →";gearTop.style.cssText="width:100%;padding:10px;font-family:var(--mono);font-size:9px;font-weight:700;background:rgba(251,146,60,.15);border:1px solid rgba(251,146,60,.4);color:#fb923c;cursor:pointer;margin-bottom:8px"; gearTop.onclick=function(){if(typeof _openThermalGearModal==="function")_openThermalGearModal();}; instrCard.appendChild(gearTop); var instrLbl=document.createElement("div");instrLbl.style.cssText=_logDrkExpLabelStyle();instrLbl.textContent="LINK SCAN TO THIS SESSION (section + import)"; instrCard.appendChild(instrLbl); var instrRow=document.createElement("div");instrRow.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:6px"; var secPick=document.createElement("select");secPick.style.cssText=_logDrkExpInputStyle(); secPick.innerHTML=''; instrRow.appendChild(secPick); var gearLnk=document.createElement("button");gearLnk.type="button";gearLnk.textContent="Thermal gear guide →";gearLnk.style.cssText="padding:6px 8px;font-family:var(--mono);font-size:8px;background:rgba(251,146,60,.08);border:1px solid rgba(251,146,60,.25);color:#fb923c;cursor:pointer"; gearLnk.onclick=function(){if(typeof _openThermalGearModal==="function")_openThermalGearModal();}; instrRow.appendChild(gearLnk);instrCard.appendChild(instrRow); var instrBtns=document.createElement("div");instrBtns.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:4px"; [{lbl:"Import LiDAR",kind:"lidar"},{lbl:"Import thermal",kind:"thermal"}].forEach(function(spec){ var b=document.createElement("button");b.type="button";b.textContent=spec.lbl;b.style.cssText="padding:8px;font-family:var(--mono);font-size:8px;background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.22);color:#7dd3fc;cursor:pointer"; b.onclick=function(){var ctx=typeof _instrCurrentContext==="function"?_instrCurrentContext({experiment_id:draft.experiment_id,arm:draft.arm,section:secPick.value||null}):{};if(typeof _instrImportScanFile==="function")_instrImportScanFile(spec.kind,ctx);setTimeout(function(){var el=document.getElementById("drkexp-instr-counts");if(el&&typeof _logBuildInstrumentationContext==="function"){var ic2=_logBuildInstrumentationContext();el.textContent=(ic2.lidar_count||0)+" LiDAR · "+(ic2.thermal_count||0)+" thermal linked";}if(typeof _logRefreshDrkTrackContext==="function")_logRefreshDrkTrackContext(parsed,fileName,meta);},400);}; instrBtns.appendChild(b); }); instrCard.appendChild(instrBtns); var dev=typeof _instrDetectDevice==="function"?_instrDetectDevice():null; if(dev&&dev.lidar_likely){var lidarHint=document.createElement("div");lidarHint.style.cssText="font-family:var(--mono);font-size:7px;color:#38bdf8;line-height:1.45;margin-bottom:4px";lidarHint.textContent="LiDAR: capture in Scan/Polycam on iPhone Pro, then import USDZ/JSON here.";instrCard.appendChild(lidarHint);} wrap.appendChild(instrCard); var btnRow=document.createElement("div");btnRow.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px"; var bPrint=document.createElement("button");bPrint.type="button";bPrint.style.cssText="padding:9px;font-family:var(--mono);font-size:9px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.15);color:var(--white);cursor:pointer";bPrint.textContent="\uD83D\uDCC4 TRACKSIDE SHEET"; bPrint.onclick=function(){_logOpenDrkTracksideSheet();}; var bSave=document.createElement("button");bSave.type="button";bSave.style.cssText="padding:9px;font-family:var(--mono);font-size:9px;background:rgba(120,80,200,.12);border:1px solid rgba(120,80,200,.35);color:#c4b5fd;cursor:pointer"; bSave.textContent=meta.saveId?"\u21BB UPDATE CLOUD + LOG RUN":"\u2714 SAVE EXPERIMENT LOCALLY"; bSave.onclick=function(){ draft.drk_filename=draft.drk_filename||fileName;_logSaveDrkExpDraft(draft); var cap=_logReadDrkExperimentCapture(parsed);if(cap)_logAppendDrkExperimentRun(cap,meta.saveId||null); if(meta.saveId&&_logSess&&_logSess.parsed&&typeof _logSaveDrkToBbLogger==="function"){ bSave.textContent="Updating...";_logSaveDrkToBbLogger(_logSess.parsed,fileName).then(function(r){ if(r&&r.ok){toast("Experiment data synced to logger DB \u2714");bSave.textContent="\u21BB UPDATE CLOUD + LOG RUN";} else{bSave.textContent="Update failed";toast("Cloud update failed");} }); }else{toast("Experiment run logged locally \u2014 sign in + upload DRK to cloud-sync");} }; btnRow.appendChild(bPrint);btnRow.appendChild(bSave);wrap.appendChild(btnRow); card.appendChild(wrap); } function _logRefreshDrkTrackContext(parsed,fileName,meta){ var tcEl=document.getElementById("drk-track-context-block");if(!tcEl)return; var wxSnap=meta.wx||_logDrkWxFields(),tcx=_logBuildTrackConditionContext(parsed,wxSnap); meta.trackContext=tcx;_logRenderDrkTrackContextBlock(tcEl,tcx); } function _logRenderDrkTrackContextBlock(el,tcx){ if(!el||!tcx)return; el.innerHTML=""; el.style.cssText="font-family:var(--mono);font-size:8px;color:#d4a574;margin:0 0 6px;padding:8px;background:rgba(180,120,40,.06);border:1px solid rgba(180,120,40,.18);line-height:1.65"; var primary=document.createElement("div"); primary.textContent=_logBuildTrackIntelPrimaryLine(tcx); el.appendChild(primary); if(tcx.drying_prediction&&tcx.drying_prediction.note){ var dp=document.createElement("div"); dp.style.cssText="margin-top:6px;padding:6px 8px;background:rgba(56,189,248,.06);border:1px solid rgba(56,189,248,.12);color:#7dd3fc;font-size:7px;line-height:1.55"; dp.innerHTML='DRYING '+tcx.drying_prediction.note; el.appendChild(dp); }else if(tcx.wind_context&&tcx.wind_context.drying_notes&&tcx.wind_context.drying_notes.length){ var dn=document.createElement("div"); dn.style.cssText="margin-top:6px;font-size:7px;color:var(--muted);line-height:1.55"; dn.textContent=tcx.wind_context.drying_notes.join(" · "); el.appendChild(dn); } if(tcx.line_progression&&(tcx.line_progression.early||tcx.line_progression.mid||tcx.line_progression.late)){ var lp=document.createElement("div"); lp.style.cssText="margin-top:6px;padding:6px 8px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);color:#94a3b8;font-size:7px;line-height:1.5"; lp.innerHTML='LINE early: '+(tcx.line_progression.early||"—")+' · mid: '+(tcx.line_progression.mid||"—")+' · late: '+(tcx.line_progression.late||"—"); el.appendChild(lp); } if(tcx.drying_sections&&tcx.drying_sections.sections&&tcx.drying_sections.sections.length){ var dryLine=document.createElement("div"); dryLine.id="drk-drying-block"; dryLine.style.cssText="margin-top:6px;font-size:7px;color:#94a3b8;line-height:1.55"; var ds=tcx.drying_sections; dryLine.textContent=ds.summary+" · "+ds.sections.slice(0,3).map(function(s){var w=s.walk_photo_refs?" walk":"";return s.label.replace(" straight","")+" "+s.relative_rate+" ("+s.drying_score+")"+w;}).join(" · "); el.appendChild(dryLine); } if(tcx.session_log&&tcx.session_log.length){ var sl=document.createElement("div"); sl.style.cssText="margin-top:6px;font-size:7px;color:var(--muted);line-height:1.5"; sl.textContent="SESSION · "+tcx.session_log.slice(0,3).map(function(r){return(r.session||"run")+(r.track_state?" "+r.track_state:"")+(r.lane?" @"+r.lane:"");}).join(" · "); el.appendChild(sl); } } function _logOpenDrkTracksideSheet(){ var car=S.cur,track=S.curTrack,draft=_logDrkExpDraft||_logLoadDrkExpDraft(); var carLbl=car?"#"+(car.car_number||"?")+" "+(car.name||""):"—",trackLbl=track?(track.short||track.name):"—"; var dateStr=new Date().toLocaleDateString(); var expRows=_logDrkExpProgram.map(function(e){ return""+e.id+""+e.name+""+e.arms.map(function(a){return' '+a.lbl;}).join("
")+""; }).join(""); var html='Trackside Log — '+trackLbl+''+ '

Trackside Experiment Log

'+dateStr+' · '+trackLbl+' · '+carLbl+'
'+ '

Print this sheet for the trailer. After the session, upload the .drk in Garage and fill the Experimental Capture panel — or type values here and transfer.

'+ '

Experiment checklist

'+expRows+'
IDStudyArms (check one)Run notes
'+ '

Pyrometer (heat_up_rate) — RR center

Pre °FPost °FRun minLaps
'+ '

Shock travel (bottoming_rate)

Used (in)Avail (in)Bottom eventsCorner
'+ '

Tire temps °F — all corners (optional)

'+ ''+ '
CornerPrePostCornerPrePost
LFLR
RFRR
'+ '

Shock travel / bottoming notes

Zip-tie marks · max travel · bottoming corners · events per lap
'+ '

Section moisture

'+ '
FrontT1T2BackT3T4
Circle: greasy · tacky · drying · slick · dry_slick · rubbered
'+ '

Between-run log

SessionStateLaneNote
'+ '

Line progression (early / mid / late)

PhaseWhere to search
Early run
Mid run
Late run
'+ '

Drying prediction (wind + orientation)

Which end dries first? Sheen loss on hot laps? Wind direction:
'+ '

Instrumentation

Wheel hop 1-10
Lateral loss 1-10
Best lap
Laps
'+ '
Garage DRK upload links experiment ID + arm + temps into bb_logger_data full_stats.
'+ ''; var w=window.open("","_blank","width=480,height=720");if(!w){toast("Allow pop-ups for trackside sheet");return;} w.document.open();w.document.write(html);w.document.close(); } function _logDrkSparklineSvg(samples,opts){ opts=opts||{}; if(!samples||!samples.length)return ""; var maxPts=opts.maxPts||120,step=Math.max(1,Math.floor(samples.length/maxPts)),pts=[],mn=Infinity,mx=-Infinity,i,j; for(i=0;imx)mx=v;} if(pts.length<2)return ""; var W=opts.width||280,H=opts.height||44,pad=2,rng=mx-mn||1,path=[]; for(j=0;j"; } function _logRenderDrkSummary(session,fileName,meta){ meta=meta||{}; var parsed=session.parsed,res=document.getElementById("logger-result"); if(!res||!parsed)return; var an=_logAnalyze(session);_logSess.an=an;res.innerHTML=""; var card=document.createElement("div");card.style.cssText="background:var(--dark2);border:1px solid rgba(245,166,35,.25);padding:14px"; var meta2=parsed.metadata||{}; var hn=document.createElement("div");hn.style.cssText="font-family:var(--head);font-size:16px;font-weight:900;color:var(--amber);letter-spacing:.5px"; hn.textContent=(meta2.track_name||meta2.session_name||"MYCHRON SESSION").toUpperCase();card.appendChild(hn); var sub=document.createElement("div");sub.style.cssText="font-family:var(--mono);font-size:8px;color:var(--muted);margin:4px 0 10px;line-height:1.6"; var links=[];if(S.cur)links.push("#"+(S.cur.car_number||"?")+" "+(S.cur.name||S.cur.class||""));if(S.curTrack)links.push(S.curTrack.short||S.curTrack.name); sub.textContent=fileName+(meta2.class_name?" \u00b7 "+meta2.class_name:"")+(links.length?" \u00b7 "+links.join(" \u00b7 "):"");card.appendChild(sub); if(meta.saveStatus){var badge=document.createElement("div");badge.style.cssText="font-family:var(--mono);font-size:8px;letter-spacing:1px;margin-bottom:10px;padding:6px 8px;border:1px solid "+(meta.saved?"rgba(44,200,50,.35)":"rgba(245,166,35,.25)")+";color:"+(meta.saved?"#4CAF50":"var(--amber)")+";background:"+(meta.saved?"rgba(44,200,50,.08)":"rgba(245,166,35,.06)");badge.textContent=meta.saveStatus;card.appendChild(badge);} var eng=_logFindDrkChannel(parsed,/^engine$/i),spd=_logFindDrkChannel(parsed,/gps_speed/i),lat=_logFindDrkChannel(parsed,/lateral_acc|gps_latacc/i); var stats=[["RPM MAX",eng?Math.round(eng.stats.max):"--"],["SPEED MAX",spd?Math.round(spd.stats.max*0.621371)+" mph":"--"],["LAT G",lat?lat.stats.max.toFixed(2)+"g":"--"],["CHANNELS",String((parsed.channels||[]).length)],["SAMPLES",((parsed.channels||[]).reduce(function(s,c){return s+(c.sampleCount||0);},0)).toLocaleString()],["CLASS",meta2.class_name||"--"]]; var grid=document.createElement("div");grid.style.cssText="display:grid;grid-template-columns:repeat(3,1fr);gap:5px;margin-bottom:10px"; stats.forEach(function(s){var d=document.createElement("div");d.style.cssText="background:var(--dark);padding:8px 4px;text-align:center";var lbl=document.createElement("div");lbl.style.cssText="font-family:var(--mono);font-size:7px;color:var(--muted);letter-spacing:1px";lbl.textContent=s[0];var v=document.createElement("div");v.style.cssText="font-family:var(--mono);font-size:13px;color:var(--white);font-weight:700;margin-top:2px";v.textContent=s[1];d.appendChild(lbl);d.appendChild(v);grid.appendChild(d);}); card.appendChild(grid); var wxSnap=meta.wx||_logDrkWxFields(); if(wxSnap.temp_f!=null){var wxLine=document.createElement("div");wxLine.style.cssText="font-family:var(--mono);font-size:8px;color:#94a3b8;margin:0 0 10px;padding:8px;background:rgba(56,189,248,.06);border:1px solid rgba(56,189,248,.15)";var wxTxt=Math.round(wxSnap.temp_f)+"\u00b0F";if(wxSnap.humidity!=null)wxTxt+=" \u00b7 "+wxSnap.humidity+"% humid";if(wxSnap.density_altitude!=null)wxTxt+=" \u00b7 "+Math.round(wxSnap.density_altitude)+"ft DA";if(wxSnap.wind_speed!=null)wxTxt+=" \u00b7 "+Math.round(wxSnap.wind_speed)+"mph wind";if(wxSnap.track_condition)wxTxt+=" \u00b7 "+String(wxSnap.track_condition).toUpperCase();wxLine.textContent="WX @ SAVE \u2014 "+wxTxt;card.appendChild(wxLine);} var tcx=meta.trackContext||_logBuildTrackConditionContext(parsed,wxSnap); if(tcx&&(tcx.track_direction!=="unknown"||tcx.wind_context||tcx.photo_count||tcx.drying_sections||tcx.drying_prediction)){ var tcLine=document.createElement("div");tcLine.id="drk-track-context-block";tcLine.style.cssText="margin:0 0 10px"; card.appendChild(tcLine);_logRenderDrkTrackContextBlock(tcLine,tcx); } _logRenderDrkExpCaptureForm(card,parsed,fileName,meta); var chartsWrap=document.createElement("div");chartsWrap.style.cssText="display:grid;grid-template-columns:1fr;gap:10px;margin:10px 0"; [{ch:eng,label:"RPM",color:"#E85D04",conv:null},{ch:spd,label:"GPS SPEED (mph)",color:"#38bdf8",conv:function(v){return v*0.621371;}},{ch:lat,label:"LATERAL G",color:"#a78bfa",conv:null}].forEach(function(spec){ if(!spec.ch)return;var d=spec.ch.physicalData||spec.ch.data;if(!d||!d.length)return;var samp=[],si; for(si=0;si=0)row.speed=d[si]*0.621371; if(n.indexOf("lateral")>=0)row.lat_g=d[si]; if(n.indexOf("longitudinal")>=0)row.lon_g=d[si]; if(n==="engine"||n.indexOf("rpm")>=0)row.rpm=d[si]; rows.push(row); } } return {brand:"AiM DRK",headers:[],cols:{},rows:rows,parsed:parsed}; }catch(e){return null;} } /** Pocket Data Logger — GPS lap loop, G-force, pit/caution/green classification */ var _pocketLogger = { active: false, watchId: null, motionOn: false, sf: null, pit: null, session: null, curLap: null, lastPos: null, awayFromSf: false, lastSfCross: 0, lowSpeedSince: 0, cautionSegStart: 0, peakG: { lat: 0, lon: 0, brake: 0, accel: 0 }, lapPeakG: { lat: 0, lon: 0, brake: 0, accel: 0 }, engineSamples: [], motionSamples: [], motionInterval: null, _motionPaused: false, lastHapticTime: 0, _lastMotion: null, uiHost: null, tickTimer: null }; function _plTrackSlug() { if (!S.curTrack || !S.curTrack.name) return ''; return S.curTrack.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'); } function _plMinLapMs() { var sz = (S.curTrack && S.curTrack.size) || '1/4'; if (/1\/10|1\/8|1\/6/.test(sz)) return 18000; if (/1\/5|1\/4/.test(sz)) return 22000; return 28000; } function _plSfRadiusM() { return 18; } function _plHaversineM(lat1, lon1, lat2, lon2) { var R = 6371000; var dLat = (lat2 - lat1) * Math.PI / 180; var dLon = (lon2 - lon1) * Math.PI / 180; var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } function _plMpsToMph(mps) { return mps * 2.23694; } function _plLoadSf() { var slug = _plTrackSlug(); if (!slug) return null; try { var d = JSON.parse(localStorage.getItem('bb_sf_' + slug) || 'null'); if (d && d.lat != null && d.lon != null) return d; } catch (e) {} return null; } function _plSaveSf(lat, lon) { var slug = _plTrackSlug(); if (!slug) return; var o = { lat: lat, lon: lon, ts: Date.now() }; localStorage.setItem('bb_sf_' + slug, JSON.stringify(o)); _pocketLogger.sf = o; } function _plLoadPit() { var slug = _plTrackSlug(); if (!slug) return null; try { return JSON.parse(localStorage.getItem('bb_pit_' + slug) || 'null'); } catch (e) { return null; } } function _plSavePit(lat, lon) { var slug = _plTrackSlug(); if (!slug) return; var o = { lat: lat, lon: lon, radius: 40, ts: Date.now() }; localStorage.setItem('bb_pit_' + slug, JSON.stringify(o)); _pocketLogger.pit = o; } function _plNewSession() { return { id: 'pl_' + Date.now(), started: Date.now(), ended: null, track: S.curTrack ? S.curTrack.name : '', car: S.cur ? ('#' + (S.cur.car_number || '?')) : '', phase: 'practice', laps: [], cautions: 0, pitLaps: 0, greenLaps: 0, bestLap: null, maxSpeed: 0, maxLatG: 0, maxRpm: 0, sfAuto: false }; } function _plNewLap(n) { return { n: n, start: Date.now(), end: null, time: null, type: 'green', maxSpeed: 0, avgSpeed: 0, speedSum: 0, speedCnt: 0, lowSpeedSec: 0, latG: 0, lonG: 0, brakeG: 0, rpmMax: 0, rpmAvg: 0, rpmSamples: [], lugEvents: 0 }; } function _plClassifyLap(lap, best) { if (lap.lowSpeedSec > 12 || lap.maxSpeed < 12) return 'pit'; if (best && lap.time && lap.time > best * 1.35) return 'caution'; if (lap.lowSpeedSec > 5 && lap.maxSpeed < 28) return 'caution'; return 'green'; } function _plFinishLap() { var pl = _pocketLogger; var lap = pl.curLap; var sess = pl.session; if (!lap || !sess) return; lap.end = Date.now(); lap.time = (lap.end - lap.start) / 1000; if (lap.speedCnt) lap.avgSpeed = lap.speedSum / lap.speedCnt; lap.latG = pl.lapPeakG.lat; lap.lonG = pl.lapPeakG.lon; lap.brakeG = pl.lapPeakG.brake; var best = sess.bestLap; lap.type = _plClassifyLap(lap, best); sess.laps.push(lap); if (lap.type === 'green') sess.greenLaps++; else if (lap.type === 'pit') sess.pitLaps++; else sess.cautions++; if (!best || (lap.type === 'green' && lap.time < best)) sess.bestLap = lap.time; if (lap.maxSpeed > sess.maxSpeed) sess.maxSpeed = lap.maxSpeed; if (lap.latG > sess.maxLatG) sess.maxLatG = lap.latG; if (lap.rpmMax > sess.maxRpm) sess.maxRpm = lap.rpmMax; pl.curLap = _plNewLap(sess.laps.length + 1); pl.lapPeakG = { lat: 0, lon: 0, brake: 0, accel: 0 }; pl.cautionSegStart = 0; window._lastGForce = window._lastGForce || {}; window._lastGForce.lat_max = sess.maxLatG; window._lastGForce.lon_max = Math.max(window._lastGForce.lon_max || 0, lap.lonG); window._lastGForce.brake = Math.max(window._lastGForce.brake || 0, lap.brakeG); _plPersist(); _plRenderUI(); } function _plOnPosition(pos) { var pl = _pocketLogger; if (!pl.active || !pl.session) return; var lat = pos.coords.latitude; var lon = pos.coords.longitude; var spd = pos.coords.speed != null ? _plMpsToMph(pos.coords.speed) : 0; var now = Date.now(); pl.lastPos = { lat: lat, lon: lon, spd: spd, ts: now }; if (!pl.sf && spd > 18) { _plSaveSf(lat, lon); pl.session.sfAuto = true; toast('S/F line set from GPS'); } if (!pl.sf) return; var dist = _plHaversineM(lat, lon, pl.sf.lat, pl.sf.lon); var near = dist < _plSfRadiusM(); if (!near && dist > _plSfRadiusM() * 2.5) pl.awayFromSf = true; if (near && pl.awayFromSf && (now - pl.lastSfCross) > _plMinLapMs()) { if (pl.curLap && pl.curLap.speedCnt > 3) _plFinishLap(); else if (pl.curLap) { pl.curLap.start = now; pl.lapPeakG = { lat: 0, lon: 0, brake: 0, accel: 0 }; } pl.lastSfCross = now; pl.awayFromSf = false; } var lap = pl.curLap; if (!lap) return; if (spd > lap.maxSpeed) lap.maxSpeed = spd; if (spd > 0) { lap.speedSum += spd; lap.speedCnt++; } if (spd < 15) { if (!pl.lowSpeedSince) pl.lowSpeedSince = now; lap.lowSpeedSec += (pos.coords.speed != null ? 1 : 0.5); } else { pl.lowSpeedSince = 0; } var ee = window._engineEarLive; if (ee && ee.rpm && (now - ee.ts) < 3000) { lap.rpmSamples.push(ee.rpm); if (ee.rpm > lap.rpmMax) lap.rpmMax = ee.rpm; var earProf = (typeof window._earClassProfile === 'function') ? window._earClassProfile() : (ee.profile || null); var lugPct = earProf && earProf.lugPct ? earProf.lugPct : 0.82; var lugFloor = earProf ? Math.max(earProf.rpmMin, lap.rpmMax * lugPct) : lap.rpmMax * lugPct; if (spd > 25 && ee.rpm < lugFloor && pl.lapPeakG.lat > 0.8) lap.lugEvents++; if (earProf && ee.rpm > earProf.rpmMax + 100) lap.limiterHits = (lap.limiterHits || 0) + 1; } if (pl.pit && _plHaversineM(lat, lon, pl.pit.lat, pl.pit.lon) < (pl.pit.radius || 40) && spd < 20) { lap._inPit = true; } } function _plOnMotion(e) { var pl = _pocketLogger; if (!pl.active) return; pl._lastMotion = e; var acc = e.acceleration; if (!acc) return; var lx = acc.x || 0; var ly = acc.y || 0; var latG = Math.abs(lx / 9.81); var lonG = Math.abs(ly / 9.81); if (latG > pl.peakG.lat) pl.peakG.lat = latG; if (lonG > pl.peakG.lon) pl.peakG.lon = lonG; if (ly < -1) pl.peakG.brake = Math.max(pl.peakG.brake, Math.abs(ly / 9.81)); if (ly > 1) pl.peakG.accel = Math.max(pl.peakG.accel, ly / 9.81); if (latG > pl.lapPeakG.lat) pl.lapPeakG.lat = latG; if (lonG > pl.lapPeakG.lon) pl.lapPeakG.lon = lonG; if (pl.peakG.brake > pl.lapPeakG.brake) pl.lapPeakG.brake = pl.peakG.brake; } function _plPersist() { var pl = _pocketLogger; if (!pl.session) return; try { localStorage.setItem('bb_pocket_session', JSON.stringify(pl.session)); var hist = JSON.parse(localStorage.getItem('bb_pocket_history') || '[]'); var idx = hist.findIndex(function(h) { return h.id === pl.session.id; }); var snap = { id: pl.session.id, track: pl.session.track, started: pl.session.started, laps: pl.session.laps.length, best: pl.session.bestLap, green: pl.session.greenLaps, caution: pl.session.cautions, pit: pl.session.pitLaps }; if (idx >= 0) hist[idx] = snap; else hist.unshift(snap); localStorage.setItem('bb_pocket_history', JSON.stringify(hist.slice(0, 20))); } catch (e) {} } function _plClearMotionInterval() { var pl = _pocketLogger; if (pl.motionInterval) { clearInterval(pl.motionInterval); pl.motionInterval = null; } } function _phoneLoggerIsActive() { var pl = _pocketLogger; return pl && (pl.active || pl._motionPaused); } function _updateFloatingLoggerBtn(label) { var btn = document.getElementById('floating-stop-logger'); if (btn) btn.textContent = label || 'Stop Logging'; } function _startPhoneMotionInterval() { var pl = _pocketLogger; _plClearMotionInterval(); pl.motionInterval = setInterval(function() { if (!pl.active || pl._motionPaused || !pl._lastMotion) return; var ev = pl._lastMotion; var acc = ev.accelerationIncludingGravity || ev.acceleration; pl.motionSamples.push({ ts: Date.now(), accel: acc ? { x: acc.x, y: acc.y, z: acc.z } : null, gyro: ev.rotationRate ? { x: ev.rotationRate.alpha, y: ev.rotationRate.beta, z: ev.rotationRate.gamma } : null, slow: !!(pl.lastPos && pl.lastPos.spd < 15) }); if (pl.motionSamples.length > 400) pl.motionSamples.shift(); if (pl.motionSamples.length % 20 === 0) { var lapNote = (pl.session && pl.session.laps) ? (' • L' + pl.session.laps.length + ' laps') : ''; _plSetPhoneLoggerStatus('Logging... ' + pl.motionSamples.length + ' samples collected' + lapNote); } analyzeAndHaptic(pl.motionSamples); }, 100); } function pausePhoneDataLogger() { var pl = _pocketLogger; if (!pl.active || pl._motionPaused) return; _plClearMotionInterval(); pl._motionPaused = true; _updateFloatingLoggerBtn('Resume Logging'); var n = (pl.motionSamples && pl.motionSamples.length) || 0; _plSetPhoneLoggerStatus('⏸️ Paused • ' + n + ' samples'); showFloatingStopButton(); showLoggingBadge(); } function _plResumeHardware() { var pl = _pocketLogger; if (pl.active || !pl.session) return; pl.active = true; if (pl.session.ended) pl.session.ended = null; var nextN = (pl.session.laps && pl.session.laps.length) ? pl.session.laps.length + 1 : 1; pl.curLap = pl.curLap || _plNewLap(nextN); pl.watchId = navigator.geolocation.watchPosition( function(pos) { _plOnPosition(pos); }, function(err) { toast('GPS: ' + (err.message || 'error')); }, { enableHighAccuracy: true, maximumAge: 400, timeout: 12000 } ); if (window.DeviceMotionEvent && !pl.motionOn) { var startMotion = function() { pl.motionOn = true; window.addEventListener('devicemotion', _plOnMotion, true); }; if (typeof DeviceMotionEvent.requestPermission === 'function') { DeviceMotionEvent.requestPermission().then(function(p) { if (p === 'granted') startMotion(); }).catch(function() {}); } else { startMotion(); } } if (!pl.tickTimer) pl.tickTimer = setInterval(function() { _plRenderUI(); }, 1000); } function resumePhoneDataLogger() { var pl = _pocketLogger; if (pl.active && !pl._motionPaused) return; if (!pl.active && pl.session) _plResumeHardware(); pl._motionPaused = false; _updateFloatingLoggerBtn('Stop Logging'); _plSetPhoneLoggerStatus('📍 Logging resumed...'); _startPhoneMotionInterval(); _phoneLoggerUIOn(); _plRenderUI(); } function _floatingLoggerClick() { var pl = _pocketLogger; if (pl.active && !pl._motionPaused) stopPhoneDataLogger(); else resumePhoneDataLogger(); } function showFloatingStopButton() { var btn = document.getElementById('floating-stop-logger'); if (btn) btn.style.display = 'block'; } function hideFloatingStopButton() { var btn = document.getElementById('floating-stop-logger'); if (btn) btn.style.display = 'none'; } function showLoggingBadge() { var badge = document.getElementById('logging-active-badge'); if (badge) badge.style.display = 'inline-block'; } function hideLoggingBadge() { var badge = document.getElementById('logging-active-badge'); if (badge) badge.style.display = 'none'; } function _phoneLoggerUIOn() { _updateFloatingLoggerBtn('Stop Logging'); showFloatingStopButton(); showLoggingBadge(); } function _phoneLoggerUIOff() { hideFloatingStopButton(); hideLoggingBadge(); } function beforeTabChange(nextTab) { var pl = _pocketLogger; if (!pl.active && !pl._motionPaused) return true; var curEl = document.querySelector('.tp.on'); var curTab = curEl && curEl.id ? curEl.id.replace('t-', '') : ''; if (nextTab === curTab) return true; var stop = confirm('Phone logging is still running. Stop and save before leaving this tab?'); if (!stop) return false; stopPhoneDataLogger(true); return true; } function startPhoneDataLogger(phase) { if (!navigator.geolocation) { alert('GPS not available on this device.'); return; } if (!('DeviceMotionEvent' in window)) { toast('Motion sensors unavailable — GPS lap logging only'); } var pl = _pocketLogger; if (pl.active) return; pl.motionSamples = []; pl.lastHapticTime = 0; pl._motionPaused = false; toast('Phone data logging started'); _plSetPhoneLoggerStatus('📍 Logging phone sensors...'); _plStart(phase || 'practice'); _startPhoneMotionInterval(); _phoneLoggerUIOn(); } function _plStart(phase) { if (!navigator.geolocation) { toast('GPS not available'); return; } var pl = _pocketLogger; if (pl.active) return; phase = phase || pl._nextPhase || 'Outing'; pl.sf = _plLoadSf(); pl.pit = _plLoadPit(); pl.session = _plNewSession(); pl.session.phase = phase; pl._nextPhase = phase; pl.curLap = _plNewLap(1); pl.active = true; pl.awayFromSf = false; pl.lastSfCross = Date.now(); pl.peakG = { lat: 0, lon: 0, brake: 0, accel: 0 }; pl.lapPeakG = { lat: 0, lon: 0, brake: 0, accel: 0 }; _markTrackArrival && _markTrackArrival(); pl.watchId = navigator.geolocation.watchPosition( function(pos) { _plOnPosition(pos); }, function(err) { toast('GPS: ' + (err.message || 'error')); }, { enableHighAccuracy: true, maximumAge: 400, timeout: 12000 } ); if (window.DeviceMotionEvent) { var startMotion = function() { pl.motionOn = true; window.addEventListener('devicemotion', _plOnMotion, true); }; if (typeof DeviceMotionEvent.requestPermission === 'function') { DeviceMotionEvent.requestPermission().then(function(p) { if (p === 'granted') startMotion(); }).catch(function() {}); } else { startMotion(); } } pl.tickTimer = setInterval(function() { _plRenderUI(); }, 1000); toast('Pocket logger ON — drive through S/F to start laps'); _plRenderUI(); _phoneLoggerUIOn(); } function _plStop() { var pl = _pocketLogger; if (!pl.active) return; if (typeof _plStopHardware === 'function') _plStopHardware(); else { _plClearMotionInterval(); if (pl.curLap && pl.curLap.speedCnt > 5) _plFinishLap(); pl.active = false; if (pl.watchId != null) { navigator.geolocation.clearWatch(pl.watchId); pl.watchId = null; } if (pl.motionOn) { window.removeEventListener('devicemotion', _plOnMotion, true); pl.motionOn = false; } if (pl.tickTimer) { clearInterval(pl.tickTimer); pl.tickTimer = null; } if (pl.session) pl.session.ended = Date.now(); } pl._motionPaused = true; _plPersist(); window._lastPocketSession = pl.session; _updateFloatingLoggerBtn('Resume Logging'); var n = (pl.motionSamples && pl.motionSamples.length) || 0; _plSetPhoneLoggerStatus('⏸️ Paused • ' + n + ' samples'); showFloatingStopButton(); showLoggingBadge(); _plRenderUI(); toast('Logging paused — tap Resume or END STINT to save'); } function _plEndStint() { stopPhoneDataLogger(); } function _plSetSfHere() { if (!navigator.geolocation) { toast('GPS unavailable'); return; } navigator.geolocation.getCurrentPosition(function(pos) { _plSaveSf(pos.coords.latitude, pos.coords.longitude); toast('Start/finish line saved'); _plRenderUI(); }, function() { toast('Could not get GPS fix'); }, { enableHighAccuracy: true, timeout: 8000 }); } function _plSetPitHere() { if (!navigator.geolocation) { toast('GPS unavailable'); return; } navigator.geolocation.getCurrentPosition(function(pos) { _plSavePit(pos.coords.latitude, pos.coords.longitude); _pocketLogger.pit = _plLoadPit(); toast('Pit lane marker saved'); _plRenderUI(); }, function() { toast('Could not get GPS fix'); }, { enableHighAccuracy: true, timeout: 8000 }); } function _plLapColor(type) { if (type === 'green') return '#2DB87F'; if (type === 'caution') return '#F5A623'; if (type === 'pit') return '#60a5fa'; return 'var(--muted)'; } function _plRenderLapList(sess) { if (!sess || !sess.laps.length) return '
No laps yet — cross the S/F line at speed.
'; var h = '
'; sess.laps.slice().reverse().slice(0, 12).forEach(function(l) { var col = _plLapColor(l.type); var tag = l.type.toUpperCase().substring(0, 5); h += '
' + 'L' + l.n + '' + '' + _fmtLap(l.time) + '' + '' + tag + '' + (l.latG ? '' + l.latG.toFixed(2) + 'g' : '') + (l.rpmMax ? '' + l.rpmMax + 'rpm' : '') + '
'; }); return h + '
'; } function _plRenderUI() { var host = _pocketLogger.uiHost; if (!host) return; var pl = _pocketLogger; var sess = pl.session; var on = pl.active; var sf = pl.sf || _plLoadSf(); var spd = pl.lastPos ? Math.round(pl.lastPos.spd) : '--'; host.innerHTML = ''; var card = document.createElement('div'); card.style.cssText = 'background:var(--dark2);border:1px solid rgba(245,166,35,.25);padding:14px;margin-bottom:10px'; var hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px'; hdr.innerHTML = '
POCKET LOGGER
' + '
GPS laps · G-force · pit/caution/green
' + '
LIVE
' + '
' + spd + ' mph
'; card.appendChild(hdr); var status = document.createElement('div'); status.id = 'pl-pocket-status'; status.style.cssText = 'font-family:var(--mono);font-size:8px;color:var(--muted);margin-bottom:8px'; if (pl._loggerStatusText) status.innerHTML = pl._loggerStatusText; card.appendChild(status); var stats = document.createElement('div'); stats.style.cssText = 'display:grid;grid-template-columns:repeat(4,1fr);gap:4px;margin-bottom:10px'; var items = [ ['BEST', sess && sess.bestLap ? _fmtLap(sess.bestLap) : '--'], ['LAPS', sess ? sess.laps.length : '0'], ['LAT G', pl.peakG.lat ? pl.peakG.lat.toFixed(2) : '--'], ['GREEN', sess ? sess.greenLaps : '0'] ]; items.forEach(function(it) { var d = document.createElement('div'); d.style.cssText = 'background:var(--dark);padding:6px 4px;text-align:center'; d.innerHTML = '
' + it[0] + '
' + '
' + it[1] + '
'; stats.appendChild(d); }); card.appendChild(stats); var badges = document.createElement('div'); badges.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px'; [['CAUTION', sess ? sess.cautions : 0, '#F5A623'], ['PIT', sess ? sess.pitLaps : 0, '#60a5fa'], ['S/F', sf ? 'SET' : 'AUTO', sf ? '#2DB87F' : 'var(--muted)']].forEach(function(b) { var s = document.createElement('span'); s.style.cssText = 'font-family:var(--mono);font-size:7px;letter-spacing:1px;padding:3px 8px;border:1px solid rgba(255,255,255,.08);color:' + b[2]; s.textContent = b[0] + ' ' + b[1]; badges.appendChild(s); }); card.appendChild(badges); if (sess) { var list = document.createElement('div'); list.innerHTML = _plRenderLapList(sess); card.appendChild(list); } var btns = document.createElement('div'); btns.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:10px'; var phaseLabel = (sess && sess.phase) ? sess.phase : ''; if (!on) { var start = document.createElement('button'); start.style.cssText = 'grid-column:1/-1;padding:12px;background:rgba(208,25,14,.15);border:1px solid rgba(208,25,14,.4);color:var(--white);font-family:var(--head);font-size:14px;font-weight:900;letter-spacing:1px;cursor:pointer'; start.textContent = phaseLabel ? ('▶ LOG ' + phaseLabel.toUpperCase()) : '▶ START LOGGING'; start.onclick = function() { startPhoneDataLogger(phaseLabel || 'practice'); }; btns.appendChild(start); } else { var endSt = document.createElement('button'); endSt.style.cssText = 'grid-column:1/-1;padding:12px;background:rgba(245,166,35,.15);border:1px solid var(--amber);color:var(--amber);font-family:var(--head);font-size:14px;font-weight:900;cursor:pointer'; endSt.textContent = '■ END STINT — SAVE DATA'; endSt.onclick = stopPhoneDataLogger; btns.appendChild(endSt); var pause = document.createElement('button'); pause.style.cssText = 'padding:8px;background:var(--dark);border:1px solid rgba(255,255,255,.08);color:var(--muted);font-family:var(--mono);font-size:8px;cursor:pointer'; pause.textContent = 'PAUSE GPS'; pause.onclick = _plStop; btns.appendChild(pause); } [['SET S/F HERE', _plSetSfHere], ['MARK PIT', _plSetPitHere]].forEach(function(pair) { var b = document.createElement('button'); b.style.cssText = 'padding:8px;background:var(--dark);border:1px solid rgba(255,255,255,.08);color:var(--muted);font-family:var(--mono);font-size:8px;letter-spacing:1px;cursor:pointer'; b.textContent = pair[0]; b.onclick = pair[1]; btns.appendChild(b); }); var ear = document.createElement('button'); ear.style.cssText = 'padding:8px;background:rgba(208,25,14,.08);border:1px solid rgba(208,25,14,.25);color:var(--red);font-family:var(--mono);font-size:8px;letter-spacing:1px;cursor:pointer'; ear.textContent = 'ENGINE EAR'; ear.onclick = function() { if (typeof _openAudioAnalyzer === 'function') _openAudioAnalyzer(); else toast('Engine Ear loading…'); }; btns.appendChild(ear); if (sess && sess.laps.length && !on) { var save = document.createElement('button'); save.style.cssText = 'grid-column:1/-1;padding:10px;background:rgba(44,200,50,.08);border:1px solid rgba(44,200,50,.25);color:#4CAF50;font-family:var(--head);font-size:12px;font-weight:900;cursor:pointer'; save.textContent = '💾 SAVE TO LOGBOOK'; save.onclick = _plSaveSession; btns.appendChild(save); } card.appendChild(btns); host.appendChild(card); } function _plSaveSession() { var sess = _pocketLogger.session || window._lastPocketSession; if (!sess || !sess.laps.length) { toast('No lap data'); return; } var an = { laps: sess.laps.filter(function(l) { return l.type === 'green'; }).map(function(l) { return l.time; }), maxSpeed: sess.maxSpeed, maxLatG: sess.maxLatG, maxRPM: sess.maxRpm, channels: ['GPS', 'G-force', 'Engine Ear'] }; if (!an.laps.length) an.laps = sess.laps.map(function(l) { return l.time; }); _logSess = { brand: 'Pocket Logger', rows: [], an: an, pocket: sess }; window._lastGForce = { lat_max: sess.maxLatG, lon_max: 0, brake: 0, accel: 0 }; window._lastPocketSession = sess; _plPersist(); if (S.token && S.cur) { if (typeof processPhoneLoggerSession === 'function') { var toSave = Object.assign({}, sess); if (!toSave.ended) toSave.ended = Date.now(); processPhoneLoggerSession(toSave); } else { toast('Session saved locally'); } } else { toast('Session saved locally'); } } function _buildPocketLoggerPanel(host) { _pocketLogger.uiHost = host; _pocketLogger.sf = _plLoadSf(); _pocketLogger.pit = _plLoadPit(); try { var saved = JSON.parse(localStorage.getItem('bb_pocket_session') || 'null'); if (saved && !saved.ended) _pocketLogger.session = saved; } catch (e) {} _plRenderUI(); } function _buildRaceLoggerStrip(el, phase) { if (!el) return; var wrap = document.createElement('div'); wrap.id = 'race-pocket-logger'; wrap.style.cssText = 'margin-bottom:12px'; el.insertBefore(wrap, el.firstChild); if (phase && !_pocketLogger.active && (!_pocketLogger.session || !_pocketLogger.session.laps || !_pocketLogger.session.laps.length)) { _pocketLogger._nextPhase = phase; } _buildPocketLoggerPanel(wrap); if (phase) { var hint = document.createElement('div'); hint.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);margin-top:-6px;margin-bottom:8px;line-height:1.5'; hint.textContent = _pocketLogger.active ? ('Logging ' + phase.toUpperCase() + ' — END STINT when you leave the track.') : ('Before ' + phase.toUpperCase() + ': tap LOG ' + phase.toUpperCase() + '. END STINT in pits for instant debrief.'); wrap.appendChild(hint); } } /** Stint debrief — between each on-track event in race night flow */ function _plRaceNightKey() { if (!S.curTrack || !S.cur) return null; var slug = typeof _trackSlug === 'function' ? _trackSlug(S.curTrack.name) : ''; var carId = String(S.cur.id || S.cur.car_number || 'car').replace(/\W+/g, ''); var date = new Date().toISOString().slice(0, 10).replace(/-/g, ''); return (slug && carId) ? ('bb_race_' + slug + '_' + carId + '_' + date) : null; } function _plPhaseKey(name) { return (name || 'outing').toLowerCase().replace(/\s+/g, '_').replace(/s$/,''); } // ── FLAGSHIP COACH (drag-style mastery for dirt) ───────────────────────────── function _analyzeStintCoach(stint, prior, rData, phaseName) { var tips = []; var push = function(pri, text) { tips.push({ pri: pri, text: text }); }; if (!stint || !stint.laps || !stint.laps.length) return tips; var an = stint.analyzed || (typeof _plAnalyzeStint === 'function' ? _plAnalyzeStint(stint) : null); if (!an) return tips; var pk = _plPhaseKey(phaseName); var feel = rData && rData['feel_' + pk]; var where = rData && rData['where_' + pk]; var start = rData && rData['start_' + pk]; var fin = rData && rData['finish_' + pk]; if (an.consistency != null && an.consistency < 72) { push(1, 'Consistency ' + an.consistency + '% — lap spread is costing you. Same line and lift point before you change setup.'); } else if (an.consistency >= 88) { push(4, 'Consistency ' + an.consistency + '% — driving is repeatable. Good time to make one small setup move.'); } if (an.fade != null && an.fade > 0.18) { push(1, 'Second half ' + an.fade.toFixed(2) + 's slower — tires fading or track slicked. Check pyro/temps before next outing.'); } else if (an.fade != null && an.fade < -0.18) { push(2, 'Picked up ' + Math.abs(an.fade).toFixed(2) + 's in the second half — track came in. Log PSI now.'); } if (an.lugLaps > 0) { push(1, 'Engine lug lap' + (an.lugLapNums.length > 1 ? 's' : '') + ' ' + an.lugLapNums.join(', ') + ' — gear/jet/pass before next session.'); } if (feel === 'LOOSE' && an.fade > 0.12) { push(1, 'Felt LOOSE and pace dropped — rear tire/stagger/shock, not just line.'); } if (feel === 'TIGHT' && an.maxLatG > 0 && an.maxLatG < 0.85) { push(2, 'Felt TIGHT but G never built — binding or entry line before adding stagger.'); } if (feel && where) { push(3, 'Driver note: ' + feel + ' in ' + where + ' — Hunter will weight changes there.'); } if (prior && prior.analyzed && prior.analyzed.best && an.best) { var delta = an.best - prior.analyzed.best; if (delta < -0.07) push(2, 'Best lap ' + Math.abs(delta).toFixed(3) + 's quicker vs last stint — last change is working.'); else if (delta > 0.09) push(1, 'Best lap ' + delta.toFixed(3) + 's off vs last stint — undo or revisit last change before stacking more.'); } if (start && fin && fin > start + 1) { push(2, 'Lost ' + (fin - start) + ' spots — if feel was OK, racecraft/rest starts; if not, fix handling first.'); } if (fin === 1) push(4, 'Won the segment — photograph PSI/stagger/shocks before anyone touches the car.'); if (an.cautions > 2) push(3, an.cautions + ' cautions — note whether pace loss was traffic or tire temp.'); tips.sort(function(a, b) { return a.pri - b.pri; }); return tips.slice(0, 5); } function _renderFlagshipCoachCard(parent, tips, opts) { if (!parent || !tips || !tips.length) return; opts = opts || {}; var priLabel = { 1: 'Fix first', 2: 'Watch', 3: 'Note', 4: 'Good' }; var box = document.createElement('div'); box.style.cssText = 'background:linear-gradient(135deg,rgba(208,25,14,.1),rgba(13,12,11,.95));border:1px solid rgba(208,25,14,.35);border-left:4px solid var(--red);padding:14px;margin-bottom:12px'; var title = document.createElement('div'); title.style.cssText = 'font-family:Share Tech Mono;font-size:7px;letter-spacing:2px;color:var(--red);margin-bottom:8px'; title.textContent = 'HUNTER — ' + (opts.title || (opts.phase ? opts.phase.toUpperCase() + ' READ' : 'SESSION READ')); box.appendChild(title); tips.forEach(function(t) { var row = document.createElement('div'); row.style.cssText = 'padding:8px 0;border-bottom:1px solid rgba(255,255,255,.06);font-size:12px;line-height:1.55;color:var(--white)'; row.innerHTML = '
' + (priLabel[t.pri] || 'Note') + '
' + t.text; box.appendChild(row); }); var actions = document.createElement('div'); actions.style.cssText = 'display:flex;gap:8px;margin-top:10px;flex-wrap:wrap'; if (opts.showApply && typeof _syncActiveSetupEverywhere === 'function') { var ap = document.createElement('button'); ap.style.cssText = 'flex:1;min-width:120px;padding:8px 10px;background:rgba(208,25,14,.2);border:1px solid rgba(208,25,14,.4);color:var(--white);font-family:Barlow Condensed;font-size:12px;font-weight:900;cursor:pointer'; ap.textContent = 'SYNC SETUP → LOGBOOK'; ap.onclick = function() { _syncActiveSetupEverywhere(); }; actions.appendChild(ap); } var hb = document.createElement('button'); hb.style.cssText = 'flex:1;min-width:120px;padding:8px 10px;background:var(--dark2);border:1px solid rgba(255,255,255,.12);color:var(--white);font-family:Barlow Condensed;font-size:12px;font-weight:900;cursor:pointer'; hb.textContent = 'ASK HUNTER ▶'; hb.onclick = function() { if (typeof switchTab === 'function') switchTab('hunter'); var msg = 'Review my ' + (opts.phase || 'last') + ' stint. Top issues: ' + tips.slice(0, 2).map(function(t) { return t.text; }).join(' '); setTimeout(function() { if (typeof sendToHunter === 'function') sendToHunter(msg); }, 300); }; actions.appendChild(hb); box.appendChild(actions); parent.insertBefore(box, parent.firstChild); S._lastCoachTips = tips; return box; } function _buildBetweenHeatsChecklist(el, curPhase, rData, rKey, phases) { var box = document.createElement('div'); box.style.cssText = 'background:linear-gradient(135deg,rgba(245,166,35,.08),transparent);border:1px solid rgba(245,166,35,.28);padding:12px;margin-bottom:12px'; var h = document.createElement('div'); h.style.cssText = 'font-family:Barlow Condensed;font-size:14px;font-weight:900;color:var(--amber);margin-bottom:8px;letter-spacing:1px'; h.textContent = 'BEFORE ' + (curPhase || 'NEXT').toUpperCase(); box.appendChild(h); var ul = document.createElement('ul'); ul.style.cssText = 'margin:0;padding:0 0 0 16px;font-family:Share Tech Mono;font-size:10px;color:var(--white);line-height:1.65'; var items = [ 'Same burnout length and entry line — repeatability before setup changes.', 'Confirm tire PSI matches Garage setup (sync pushes to Logbook).', 'Log what you change in the box below — Hunter reads it next phase.' ]; if (typeof _su !== 'undefined' && _su) { if (_su.stagger_r != null) items.unshift('Active stagger: ' + _su.stagger_r + '" — verify before rolling out.'); var psi = [_su.lf_psi && 'LF ' + _su.lf_psi, _su.lr_psi && 'LR ' + _su.lr_psi, _su.rr_psi && 'RR ' + _su.rr_psi].filter(Boolean).join(' · '); if (psi) items.splice(1, 0, 'Notebook PSI: ' + psi); } if (S.wx && S.wx.density_altitude) { items.push('Tonight DA ~' + Math.round(S.wx.density_altitude).toLocaleString() + ' ft — jetting/ timing if engine feels flat.'); } var nextIdx = phases.indexOf(curPhase) + 1; if (nextIdx < phases.length) items.push('Next phase: ' + phases[nextIdx].toUpperCase() + ' — start pocket logger before you hit the track.'); items.forEach(function(it) { var li = document.createElement('li'); li.style.marginBottom = '4px'; li.innerHTML = it; ul.appendChild(li); }); box.appendChild(ul); el.appendChild(box); } function _syncActiveSetupEverywhere(silent) { if (typeof _su === 'undefined' || !_su) { if (!silent) toast('No setup loaded'); return; } S.active_setup = JSON.parse(JSON.stringify(_su)); var map = [['lb-psi-lf', 'lf_psi'], ['lb-psi-rf', 'rf_psi'], ['lb-psi-lr', 'lr_psi'], ['lb-psi-rr', 'rr_psi'], ['lb-stagger', 'stagger_r'], ['lb-gear', 'gear']]; map.forEach(function(p) { var el = $(p[0]); if (el && _su[p[1]] != null && _su[p[1]] !== '') el.value = _su[p[1]]; }); try { if (S.cur && S.cur.id) localStorage.setItem('bb_active_setup_' + S.cur.id, JSON.stringify(S.active_setup)); } catch (e) {} if (!silent) toast('Setup synced to Logbook fields'); } function _showLogbookCoach(entry) { var tips = []; if (entry.finish === 1) tips.push({ pri: 4, text: 'Win logged — save exact PSI/stagger/shocks as your baseline for this track.' }); else if (entry.finish && entry.finish <= 3) tips.push({ pri: 3, text: 'Podium — note one thing that felt best; duplicate it next week before chasing speed.' }); else if (entry.finish && entry.finish > 5) tips.push({ pri: 2, text: 'Finish P' + entry.finish + ' — compare best lap to winners; handling vs track position?' }); if (entry.best_lap) tips.push({ pri: 3, text: 'Best lap ' + entry.best_lap + 's logged — Hunter uses this for trend reads.' }); if (!tips.length) return; var banner = $('lb-coach-banner'); if (!banner) { banner = document.createElement('div'); banner.id = 'lb-coach-banner'; var form = $('logbook-form') || $('t-logbook'); if (form) form.insertBefore(banner, form.firstChild); } if (!banner) return; banner.style.cssText = 'background:rgba(208,25,14,.08);border:1px solid rgba(208,25,14,.25);border-left:3px solid var(--red);padding:12px;margin-bottom:12px;font-family:Share Tech Mono;font-size:10px;color:var(--white);line-height:1.6'; banner.innerHTML = '
HUNTER — LOGBOOK READ
' + tips.map(function(t) { return t.text; }).join('
'); toast(tips[0].text.substring(0, 72) + (tips[0].text.length > 72 ? '…' : '')); } function _resolveTrackId() { if (S.curTrack && S.curTrack.id) return String(S.curTrack.id); if (S.curTrack && S.curTrack.page_slug) return String(S.curTrack.page_slug); var sel = $('lb-track-sel'); return sel && sel.value ? String(sel.value) : ''; } function getMostRecentScaleChange() { if (!S.cur) return null; var carId = S.cur.id || 'local'; var sd = JSON.parse(localStorage.getItem('bb_scale_' + carId) || '{"adjustments":[]}'); var adj = (sd.adjustments || []).slice(-1)[0]; if (!adj) return null; return { id: adj.id || ('sc_' + adj.ts), ts: adj.ts, desc: adj.desc, type: adj.type, deltas: adj.deltas, before: adj.before, after: adj.after }; } function _showScaleFeedbackPanel(prefix, hintText) { var panel = $(prefix + '-scale-feedback'); if (!panel) return; var recent = getMostRecentScaleChange(); panel.style.display = 'block'; var hint = $(prefix + '-scale-feedback-hint'); if (hint) { hint.textContent = recent ? ('Linked change: ' + recent.desc + ' → Cross ' + (recent.deltas && recent.deltas.cross > 0 ? '+' : '') + (recent.deltas ? recent.deltas.cross.toFixed(1) : '?') + '%') : (hintText || 'No scale change logged tonight — feedback still helps Hunter learn your car.'); } } async function _postScaleLearning(action, payload) { if (!S.cur || !S.cur.id) return { success: false }; var trackId = _resolveTrackId(); if (!trackId) return { success: false, error: 'no_track' }; try { var r = await fetch(HNTR, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ action: action, car_id: S.cur.id, track_id: trackId }, payload)) }); return await r.json(); } catch (e) { return { success: false, error: String(e) }; } } function getActiveScaleRecommendation() { if (S._lastWeightRec && (S._lastWeightRec.message || S._lastWeightRec.suggestion)) { return { source: 'weight_hint', message: S._lastWeightRec.message || '', suggestion: S._lastWeightRec.suggestion || null, captured_at: S._lastWeightRec.captured_at || Date.now() }; } if (S._hunterRec && Object.keys(S._hunterRec).length) { return { source: 'setup_rec', fields: Object.assign({}, S._hunterRec), captured_at: Date.now() }; } return null; } async function _fetchScaleLearningUpdate(actualChange, hunterRec) { await _postScaleLearning('update_from_scale', { actual_change: actualChange, hunter_recommendation: hunterRec || getActiveScaleRecommendation() || null }); } async function submitDebriefScaleFeedback(feel, notes) { if (!S.cur || !S.cur.id) { toast('Select a car first'); return; } var linked = getMostRecentScaleChange(); var feelVal = feel || (window._dbfScaleFeel || ''); var notesVal = notes != null ? notes : (($('dbf-scale-feedback-notes') && $('dbf-scale-feedback-notes').value.trim()) || ''); if (!feelVal) { toast('Pick how the car felt after your scale work'); return; } var d = await _postScaleLearning('update_from_scale_feedback', { feel: feelVal, notes: notesVal, linked_scale_change_id: linked ? linked.id : null }); var msg = $('dbf-scale-feedback-msg'); if (d.success) { if (msg) { msg.style.display = 'block'; msg.style.color = 'var(--green)'; msg.textContent = 'Scale feedback saved — Hunter linked to ' + (linked ? linked.desc : 'tonight'); } toast('Scale feedback saved — check What Hunter Learned'); fetchPostLogRecommendation(window._pendingDebrief || null, 'debrief'); setTimeout(function() { if (typeof _garageFlowGoTo === 'function') _garageFlowGoTo('learn'); }, 600); } else if (msg) { msg.style.display = 'block'; msg.style.color = 'var(--red)'; msg.textContent = 'Could not save scale feedback'; } } async function submitLogbookScaleFeedback(feel, notes) { if (!S.cur || !S.cur.id) { toast('Select a car first'); return; } var linked = getMostRecentScaleChange(); var feelVal = feel || (window._lbScaleFeel || ''); var notesVal = notes != null ? notes : (($('lb-scale-feedback-notes') && $('lb-scale-feedback-notes').value.trim()) || ''); if (!feelVal) { toast('Pick how the car felt after your scale work'); return; } var d = await _postScaleLearning('update_from_scale_feedback', { feel: feelVal, notes: notesVal, linked_scale_change_id: linked ? linked.id : null }); var msg = $('lb-scale-feedback-msg'); if (d.success) { if (msg) { msg.style.display = 'block'; msg.style.color = 'var(--green)'; msg.textContent = 'Scale feedback saved — linked to ' + (linked ? linked.desc : 'latest log'); } toast('Scale feedback saved'); } else if (msg) { msg.style.display = 'block'; msg.style.color = 'var(--red)'; msg.textContent = 'Could not save scale feedback'; } } function _pickScaleFeel(prefix, feel, btn) { window[prefix === 'dbf' ? '_dbfScaleFeel' : '_lbScaleFeel'] = feel; var panel = $(prefix + '-scale-feedback'); if (panel) panel.querySelectorAll('.dbf-btn').forEach(function(b) { b.classList.remove('sel'); }); if (btn) btn.classList.add('sel'); } function _escapeRecHtml(s) { return String(s || '').replace(/&/g, '&').replace(/ 120 ? '…' : '') : ''; } function _recConfidenceContextLine(p) { if (!p) return ''; if (p.source === 'combined_phone_scale') return 'Phone + scale history align'; if (p.source === 'contextual_bandit') return 'Based on phone logger data'; if (p.source === 'weight_hint') return 'Based on scale history at this track'; var reason = String(p.reason || ''); if (/often follow LR|often follow left-rear/i.test(reason)) return 'You often follow similar LR moves here'; if (/often follow RF|often follow.*wedge/i.test(reason)) return 'You often follow similar RF wedge moves here'; if (/confidence slightly boosted/i.test(reason)) return 'Matches your past follow-through here'; if (p.confidence != null && !isNaN(Number(p.confidence))) { var pct = Number(p.confidence) <= 1 ? Math.round(Number(p.confidence) * 100) : Math.round(Number(p.confidence)); if (pct >= 70) return pct + '% confidence'; } if (typeof _hasTrackWalkBoost === 'function' && S.cur && S.cur.id && _hasTrackWalkBoost(S.cur.id, typeof _resolveTrackId === 'function' ? _resolveTrackId() : null)) { return 'Surface read active at this track'; } return ''; } function _garageFlowGoTo(stepId) { var map = { scale: { tab: 'tools', anchor: 'scale-sheet-mount', toast: 'Scale Sheet — lock previous, make change, log' }, rec: { tab: 'garage', anchor: 'garage-active-rec', toast: 'Active recommendation' }, log: { tab: 'tools', anchor: 'scale-sheet-mount', toast: 'Log your scale change when ready' }, debrief: { tab: 'race', anchor: 'rn-debrief', toast: 'Post-race debrief' }, recap: { tab: 'logbook', anchor: 'night-breakdown-panel', toast: 'Post-race recap draft' }, learn: { tab: 'garage', anchor: 'hunter-learned-card', toast: 'What Hunter has learned' } }; var step = map[stepId]; if (!step) return; if (typeof switchTab === 'function') switchTab(step.tab); setTimeout(function() { var el = $(step.anchor) || document.getElementById(step.anchor); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); if (step.toast) toast(step.toast); }, step.tab === 'garage' ? 280 : 420); } function _renderGarageFlowStrip(activeStep) { var steps = [ { id: 'scale', label: '1 · Scale' }, { id: 'rec', label: '2 · Rec' }, { id: 'log', label: '3 · Log' }, { id: 'debrief', label: '4 · Debrief' }, { id: 'recap', label: '5 · Recap' }, { id: 'learn', label: '6 · Learned' } ]; var chips = steps.map(function(s) { var on = s.id === activeStep; return ''; }).join(''); var nextHint = { scale: 'Next: review Hunter\'s recommendation', rec: 'Next: make the change on the scale sheet and LOG it', log: 'Next: rate how the car felt after the change', debrief: 'Next: review and edit your recap draft', recap: 'Next: create your recap shirt or save the draft for later', learn: 'You\'re caught up — scale again when the track changes' }; return '
' + '
TONIGHT\'S FLOW
' + '
' + chips + '
' + (activeStep && nextHint[activeStep] ? '
→ ' + _escapeRecHtml(nextHint[activeStep]) + '
' : '') + '
'; } function _parseRecMessageText(text) { var raw = String(text || '').trim(); var headline = raw.split(REC_NOTE_SPLIT)[0].trim(); var reason = _extractSingleSupportNote(raw); if (!reason) { var followIdx = raw.indexOf(' You often follow'); if (followIdx >= 0) reason = raw.slice(followIdx + 1).trim().slice(0, 100); } return { headline: headline, reason: reason }; } function _formatSuggestionAction(suggestion) { if (!suggestion || typeof suggestion !== 'object') return ''; var s = suggestion; if (s.type === 'balance') { if (s.left_rear_weight) { var lr = String(s.left_rear_weight).trim(); return 'Try ' + lr + (/\blbs\b/i.test(lr) ? '' : ' lbs') + ' left-rear'; } if (s.right_front_wedge) { var rf = String(s.right_front_wedge).trim(); return 'Try ' + rf + (/"|inch/i.test(rf) ? '' : '"') + ' RF wedge'; } } if (s.type === 'refinement' && s.rear_tire_psi) return 'Try ' + s.rear_tire_psi + ' rear PSI'; if (s.rear_tire_psi) return 'Try ' + s.rear_tire_psi + ' rear PSI'; var parts = []; if (s.lr) parts.push((Number(s.lr) > 0 ? '+' : '') + s.lr + ' lbs LR'); if (s.rf) parts.push((Number(s.rf) > 0 ? '+' : '') + s.rf + ' lbs RF'); if (s.lf) parts.push((Number(s.lf) > 0 ? '+' : '') + s.lf + ' lbs LF'); if (s.rr) parts.push((Number(s.rr) > 0 ? '+' : '') + s.rr + ' lbs RR'); if (parts.length) return 'Try ' + parts.join(' · '); return ''; } function _plainReasonFromSource(source) { if (source === 'combined_phone_scale') return 'Based on recent phone data and your scale history at this track.'; if (source === 'contextual_bandit') return 'Based on recent phone logger data.'; return ''; } function _normalizeHunterRecPayload(input, label) { var payload = { label: label || 'HUNTER REC', headline: '', reason: '', action: '', confidence: null, source: '', suggestion: null, contextLine: '' }; if (!input) return payload; if (typeof input === 'string') { var parsed = _parseRecMessageText(input); payload.headline = parsed.headline; payload.reason = parsed.reason; payload.contextLine = _recConfidenceContextLine(payload); return payload; } var parsedMsg = _parseRecMessageText(input.message || input.headline || ''); payload.headline = parsedMsg.headline; payload.suggestion = input.suggestion && typeof input.suggestion === 'object' ? input.suggestion : null; payload.action = _formatSuggestionAction(payload.suggestion); payload.confidence = input.confidence != null ? input.confidence : null; payload.source = input.source || input.layer_used || ''; payload.reason = _extractSingleSupportNote(input.message || '') || parsedMsg.reason || ''; if (!payload.reason && input.reason && !REC_NOTE_SPLIT.test(String(input.reason))) { payload.reason = String(input.reason).slice(0, 100); } payload.contextLine = _recConfidenceContextLine(payload); return payload; } function _recDismissedKey() { var carId = S.cur && S.cur.id ? String(S.cur.id) : 'local'; return 'bb_rec_dismissed_' + carId; } function _recFingerprint(payload) { var p = typeof payload === 'string' ? _normalizeHunterRecPayload(payload) : _normalizeHunterRecPayload(payload || {}); return [p.headline, p.action, p.source].join('|').slice(0, 160); } function _isRecDismissed(payload) { try { var fp = _recFingerprint(payload); var list = JSON.parse(localStorage.getItem(_recDismissedKey()) || '[]'); return list.some(function(x) { return x.fp === fp && Date.now() - x.ts < 7 * 86400000; }); } catch (e) { return false; } } function _markRecDismissed(payload) { try { var fp = _recFingerprint(payload); var list = JSON.parse(localStorage.getItem(_recDismissedKey()) || '[]'); list.unshift({ fp: fp, ts: Date.now(), headline: _normalizeHunterRecPayload(payload).headline }); localStorage.setItem(_recDismissedKey(), JSON.stringify(list.slice(0, 24))); } catch (e) {} } function _parseSignedRecAmount(str) { if (str == null) return null; var m = String(str).match(/([+-]?\d+\.?\d*)/); return m ? parseFloat(m[1]) : null; } function _parseRecSuggestionActions(suggestion) { var out = { canApplySheet: false, canLogChange: false, scaleAdjType: '', setupDeltas: [], scaleCornerHints: {}, logLabel: '' }; if (!suggestion || typeof suggestion !== 'object') return out; var s = suggestion; if (s.type === 'refinement' && s.rear_tire_psi) { var psiDelta = _parseSignedRecAmount(s.rear_tire_psi); if (psiDelta != null) { out.setupDeltas.push({ field: 'lr_psi', delta: psiDelta }, { field: 'rr_psi', delta: psiDelta }); out.canApplySheet = true; out.logLabel = 'Rear PSI ' + (psiDelta > 0 ? '+' : '') + psiDelta; } } if (s.type === 'balance' && s.left_rear_weight) { var lrAmt = _parseSignedRecAmount(s.left_rear_weight); if (lrAmt != null) { out.scaleCornerHints.lr = lrAmt; out.canLogChange = true; out.canApplySheet = true; out.scaleAdjType = lrAmt < 0 ? 'lr_stop_dn' : 'ballast_left'; out.logLabel = 'LR weight ' + (lrAmt > 0 ? '+' : '') + lrAmt + ' lbs'; } } if (s.type === 'balance' && s.right_front_wedge) { var wedgeAmt = _parseSignedRecAmount(s.right_front_wedge); if (wedgeAmt != null) { out.canLogChange = true; out.scaleAdjType = wedgeAmt > 0 ? 'rf_jack_up' : 'rf_jack_dn'; out.logLabel = 'RF wedge ' + (wedgeAmt > 0 ? '+' : '') + wedgeAmt + '"'; } } if (s.lr || s.rf || s.lf || s.rr) { if (s.lr) { out.scaleCornerHints.lr = Number(s.lr); out.canLogChange = true; out.scaleAdjType = Number(s.lr) > 0 ? 'ballast_left' : 'ballast_right'; } if (s.rf) { out.scaleCornerHints.rf = Number(s.rf); out.canLogChange = true; if (!out.scaleAdjType) out.scaleAdjType = Number(s.rf) > 0 ? 'rf_jack_up' : 'rf_jack_dn'; } if (s.lf) out.scaleCornerHints.lf = Number(s.lf); if (s.rr) out.scaleCornerHints.rr = Number(s.rr); out.canApplySheet = Object.keys(out.scaleCornerHints).length > 0; out.logLabel = _formatSuggestionAction(s); } return out; } function _buildRecActionCaps(payload) { var p = _normalizeHunterRecPayload(payload); var caps = _parseRecSuggestionActions(p.suggestion); caps.payload = p; caps.logLabel = caps.logLabel || p.action || p.headline; return caps; } function _storeRecActionEntry(payload) { S._recActionMap = S._recActionMap || {}; var recId = 'rec_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 7); S._recActionMap[recId] = _buildRecActionCaps(payload); return recId; } function _clearActiveRecSurfaces() { var el = $('garage-active-rec'); if (el) el.innerHTML = ''; var banner = $('lb-coach-banner'); if (banner) banner.innerHTML = ''; var wr = $('sc-weight-rec'); if (wr) { wr.style.display = 'none'; wr.innerHTML = ''; } S._lastWeightRec = null; S._activeRecPayload = null; } function _primeScaleRecForLogging(caps) { if (!caps || !caps.payload) return; S._lastWeightRec = { message: caps.payload.headline, suggestion: caps.payload.suggestion, reason: caps.payload.reason, source: caps.payload.source, confidence: caps.payload.confidence, captured_at: Date.now() }; S._pendingRecLog = caps.logLabel || caps.payload.action || caps.payload.headline; } function _openScaleSheetForRec(caps) { if (typeof switchTab === 'function') switchTab('tools'); setTimeout(function() { var mount = $('scale-sheet-mount'); if (mount) mount.scrollIntoView({ behavior: 'smooth', block: 'start' }); var sel = $('sc-adj'); if (sel && caps.scaleAdjType) sel.value = caps.scaleAdjType; _primeScaleRecForLogging(caps); toast('Scale sheet ready — lock previous weights, make the change, then LOG'); }, 320); } function _applyRecToScaleSheet(caps) { if (!S.cur) { toast('Select a car first'); return false; } var hints = caps.scaleCornerHints || {}; if (!Object.keys(hints).length) return false; var carId = S.cur.id || 'local'; var sKey = 'bb_scale_' + carId; var sd = JSON.parse(localStorage.getItem(sKey) || '{"current":{},"previous":{},"adjustments":[]}'); var corners = ['lf', 'rf', 'lr', 'rr']; var hasCurrent = corners.some(function(k) { return parseFloat(sd.current[k]); }); if (!hasCurrent) { _openScaleSheetForRec(caps); toast('Enter corner weights first — Hunter prefilled the adjustment type'); return false; } corners.forEach(function(k) { if (hints[k]) { sd.current[k] = Math.round((parseFloat(sd.current[k]) || 0) + hints[k]); var input = $('sc-' + k); if (input) input.value = sd.current[k]; } }); localStorage.setItem(sKey, JSON.stringify(sd)); corners.forEach(function(k) { var input = $('sc-' + k); if (input) input.dispatchEvent(new Event('input')); }); if (typeof switchTab === 'function') switchTab('tools'); toast('Scale sheet updated — verify on the scales before locking'); return true; } function _applyRecToSetupSheet(caps) { if (!caps.setupDeltas || !caps.setupDeltas.length) return false; if (typeof switchTab === 'function') switchTab('garage'); caps.setupDeltas.forEach(function(d) { if (typeof adjSu === 'function') adjSu(d.field, d.delta); }); if (typeof _syncActiveSetupEverywhere === 'function') _syncActiveSetupEverywhere(true); toast('Setup sheet updated — review and save when ready'); return true; } function hunterRecAction(recId, action) { var entry = S._recActionMap && S._recActionMap[recId]; if (!entry) return; if (action === 'mark_done') { _markRecDismissed(entry.payload); _touchRecHistoryOutcome(entry.payload, 'done', 'Marked done'); _clearActiveRecSurfaces(); delete S._recActionMap[recId]; if (typeof _buildRecommendationHistory === 'function') _buildRecommendationHistory(); toast('Recommendation marked done'); return; } if (action === 'log_change') { _openScaleSheetForRec(entry); return; } if (action === 'apply_sheet') { var applied = false; if (entry.setupDeltas && entry.setupDeltas.length) applied = _applyRecToSetupSheet(entry); else if (entry.scaleCornerHints && Object.keys(entry.scaleCornerHints).length) applied = _applyRecToScaleSheet(entry); if (applied) _primeScaleRecForLogging(entry); else if (!entry.canApplySheet) toast('No sheet fields to prefill for this recommendation'); return; } } function _recTypeClass(payload) { var src = (payload && payload.source) || ''; var action = String((payload && payload.action) || (payload && payload.message) || '').toLowerCase(); if (/weight|lbs|ballast|lr|rf|cross|scale/.test(action) || src === 'weight_hint') return 'bb-rec-type-weight'; if (/balance|left %|rear %|wedge|stagger/.test(action)) return 'bb-rec-type-balance'; return 'bb-rec-type-refine'; } function _recTypeLabel(payload) { var cls = _recTypeClass(payload); if (cls === 'bb-rec-type-weight') return 'WEIGHT'; if (cls === 'bb-rec-type-balance') return 'BALANCE'; return 'REFINEMENT'; } function _renderRecActionButtons(recId, caps) { var p = caps.payload || {}; var html = '
'; if (caps.canLogChange && p.action) { html += ''; } else if (caps.canLogChange) { html += ''; } if (caps.canApplySheet) { html += ''; } html += ''; html += '
'; return html; } function _hunterRecTypeLabel(source) { if (source === 'combined_phone_scale') return 'Phone + scale'; if (source === 'contextual_bandit') return 'Phone data'; if (source === 'weight_hint') return 'Scale history'; return ''; } function _renderHunterRecCard(payload, opts) { opts = opts || {}; var theme = opts.theme || 'red'; var accent = theme === 'amber' ? 'var(--amber)' : 'var(--red)'; var bg = theme === 'amber' ? 'rgba(200,150,10,.06)' : 'rgba(208,25,14,.08)'; var border = theme === 'amber' ? 'rgba(200,150,10,.22)' : 'rgba(208,25,14,.28)'; var p = _normalizeHunterRecPayload(payload, opts.label); var typeLabel = _hunterRecTypeLabel(p.source); var typeBadge = _recTypeLabel(p); var typeClass = _recTypeClass(p); var contextParts = [p.contextLine, typeLabel].filter(Boolean); var html = '
' + '
' + '
HUNTER — ' + _escapeRecHtml(p.label) + '
' + '' + typeBadge + '' + '
'; if (p.action) { html += '
' + _escapeRecHtml(p.action) + '
'; if (p.headline && p.headline !== p.action) { html += '
' + _escapeRecHtml(p.headline) + '
'; } } else { html += '
' + _escapeRecHtml(p.headline) + '
'; } if (contextParts.length) { html += '
' + _escapeRecHtml(contextParts.join(' · ')) + '
'; } if (p.reason) { html += '
Why Hunter suggests this' + '
' + _escapeRecHtml(p.reason) + '
'; } if (opts.showActions !== false) { var recId = _storeRecActionEntry(payload); var caps = S._recActionMap[recId]; html += _renderRecActionButtons(recId, caps); html += ''; } html += '
'; return html; } function _presentHunterRecommendation(input, label, source) { var payload = input; if (typeof input === 'string') payload = { message: input }; else if (input && input.message) payload = Object.assign({}, input); else if (input && input.recommendation) payload = input.recommendation; if (source && payload && !payload.source) payload.source = source === 'phone_logger' ? 'contextual_bandit' : source; return _renderHunterRecCard(payload, { label: label || 'ACTIVE REC' }); } async function fetchPostLogRecommendation(sessionLog, source) { if (!sessionLog || typeof buildContext !== 'function') return; try { var r = await fetch(HNTR, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Post-race recommendation', guided_flow: 'post_log_recommendation', session_log: sessionLog, context: buildContext(), user_id: S.user ? S.user.user_id : null, user_email: S.user ? S.user.email : null, car_id: S.cur ? S.cur.id : null, track_id: _resolveTrackId() || null, ai_calls_this_session: _aiCallsThisSession || 0 }) }); var d = await r.json(); if (!d.response && !(d.recommendation && d.recommendation.message)) return; var label = source === 'debrief' ? 'POST-DEBRIEF REC' : (source === 'phone_logger' ? 'PHONE LOGGER REC' : 'POST-LOG REC'); var recPayload = d.recommendation || { message: d.response }; if (_isRecDismissed(recPayload)) return; showActiveRecommendationInGarage(recPayload, label, source, source === 'debrief' ? 'debrief' : 'rec'); saveRecommendationToHistory(recPayload.message || d.response, { source: source || 'logbook', routed: !!d.routed, rec: recPayload }); if (typeof _buildRecommendationHistory === 'function') _buildRecommendationHistory(); if (source === 'debrief') toast('Post-debrief rec ready — review your recap draft below'); var banner = $('lb-coach-banner'); if (!banner) { banner = document.createElement('div'); banner.id = 'lb-coach-banner'; var form = $('logbook-form') || $('t-logbook'); if (form) form.insertBefore(banner, form.firstChild); } if (banner) { banner.innerHTML = _renderHunterRecCard(recPayload, { label: label, theme: 'amber', showActions: true }); } } catch (e) {} } function _recHistoryKey() { var carId = S.cur && S.cur.id ? String(S.cur.id) : 'local'; return 'bb_rec_history_' + carId; } function saveRecommendationToHistory(response, meta) { if (!response) return; try { var hist = JSON.parse(localStorage.getItem(_recHistoryKey()) || '[]'); var recObj = meta && meta.rec && typeof meta.rec === 'object' ? meta.rec : null; var text = typeof response === 'string' ? String(response) : String(response.message || response); var entry = { id: 'rec_' + Date.now(), ts: Date.now(), text: text, source: (meta && meta.source) || 'garage', routed: !!(meta && meta.routed), track: (S.curTrack && S.curTrack.name) || '', track_id: (S.curTrack && S.curTrack.id) || (typeof _resolveTrackId === 'function' ? _resolveTrackId() : null) }; if (recObj) entry.rec = recObj; entry.recType = _classifyRecType(entry); entry.action = recObj ? _formatSuggestionAction(recObj.suggestion) : _inferRecActionFromText(text); hist.unshift(entry); localStorage.setItem(_recHistoryKey(), JSON.stringify(hist.slice(0, 16))); } catch (e) {} } function _classifyRecType(entry) { var s = entry.rec && entry.rec.suggestion; if (s && typeof s === 'object') { if (s.type === 'refinement' || s.rear_tire_psi) return 'refinement'; if (s.type === 'balance' || s.left_rear_weight || s.right_front_wedge) return 'balance'; if (s.lr || s.rf || s.lf || s.rr) return 'weight'; } var src = entry.source || (entry.rec && (entry.rec.source || entry.rec.layer_used)) || ''; if (src === 'contextual_bandit' || src === 'combined_phone_scale' || src === 'phone_logger' || src === 'car_tendency') return 'phone'; var text = String(entry.text || entry.action || '').toLowerCase(); if (/psi|pressure|refinement|repeatability/.test(text)) return 'refinement'; if (/wedge|cross|balance|left-rear|lr ballast|lateral/.test(text)) return 'balance'; if (/weight|lbs|ballast|lr |rf /.test(text)) return 'weight'; return 'general'; } function _inferRecActionFromText(text) { var t = String(text || ''); if (/rear tire pressure|rear psi|\+0\.[56] psi/i.test(t)) return 'Try small rear PSI tweak'; if (/left-rear|lr ballast|lr weight/i.test(t)) return 'Left-rear balance adjustment'; if (/right front|rf wedge/i.test(t)) return 'Right-front wedge adjustment'; if (/weight/i.test(t)) return 'Weight adjustment suggested'; return ''; } function _recTypeLabel(type) { if (type === 'weight') return 'WEIGHT'; if (type === 'balance') return 'BALANCE'; if (type === 'refinement') return 'REFINE'; if (type === 'phone') return 'PHONE'; return 'GENERAL'; } function _recTypeColor(type) { if (type === 'weight') return 'var(--amber)'; if (type === 'balance') return 'var(--red-hot)'; if (type === 'refinement') return '#6b8cff'; if (type === 'phone') return '#2DB87F'; return 'var(--muted)'; } function _formatRecHistoryDate(ts) { if (!ts) return ''; try { var d = new Date(ts); return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' · ' + d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); } catch (e) { return ''; } } function _touchRecHistoryOutcome(payload, outcome, detail) { try { var hist = JSON.parse(localStorage.getItem(_recHistoryKey()) || '[]'); var fp = _recFingerprint(payload); var idx = -1; for (var i = 0; i < hist.length; i++) { var itemFp = _recFingerprint(hist[i].rec || hist[i].text); if (itemFp === fp) { idx = i; break; } } if (idx < 0 && hist[0] && Date.now() - hist[0].ts < 86400000) idx = 0; if (idx < 0) return; hist[idx].outcome = outcome; hist[idx].outcomeDetail = detail || ''; hist[idx].outcomeTs = Date.now(); localStorage.setItem(_recHistoryKey(), JSON.stringify(hist)); } catch (e) {} } function _findScaleFollowUpAfterRec(recTs, carId) { if (!recTs || !carId) return null; try { var sd = JSON.parse(localStorage.getItem('bb_scale_' + carId) || '{"adjustments":[]}'); var windowMs = 72 * 3600000; var match = (sd.adjustments || []).find(function(a) { return a.ts >= recTs && a.ts <= recTs + windowMs; }); if (!match) return null; var detail = match.desc || match.type || 'Scale change logged'; if (match.deltas && typeof match.deltas.cross === 'number') { detail += ' · Cross ' + (match.deltas.cross > 0 ? '+' : '') + match.deltas.cross.toFixed(1) + '%'; } return { logged: true, detail: detail, ts: match.ts }; } catch (e) { return null; } } function _resolveRecHistoryOutcome(entry, carId) { if (entry.outcome === 'done') { return { status: 'done', label: 'Marked done', detail: entry.outcomeDetail || '' }; } if (entry.outcome === 'logged') { return { status: 'logged', label: 'Change logged', detail: entry.outcomeDetail || '' }; } var payload = entry.rec || entry.text; if (_isRecDismissed(payload)) { return { status: 'done', label: 'Acknowledged', detail: '' }; } var scale = _findScaleFollowUpAfterRec(entry.ts, carId); if (scale) { return { status: 'logged', label: 'Change logged', detail: scale.detail }; } try { var signals = typeof _loadLocalLearningSignals === 'function' ? _loadLocalLearningSignals(carId, entry.track_id) : null; if (signals && signals.scale_follow.length) { var after = signals.scale_follow.find(function(s) { return s.ts >= entry.ts && s.ts <= entry.ts + 72 * 3600000; }); if (after && after.followed) { return { status: 'logged', label: 'Scale follow-through', detail: after.direction === 'lr' ? 'LR adjustment tracked' : (after.direction === 'rf' ? 'RF wedge tracked' : 'Adjustment tracked') }; } } } catch (e2) {} return { status: 'open', label: 'No follow-up logged', detail: '' }; } function _collectRecommendationHistory(carId) { if (!carId) return []; try { var hist = JSON.parse(localStorage.getItem('bb_rec_history_' + carId) || '[]'); return hist.map(function(entry) { var norm = _normalizeHunterRecPayload(entry.rec || entry.text); var recType = entry.recType || _classifyRecType(entry); var outcome = _resolveRecHistoryOutcome(entry, carId); return { id: entry.id || ('rec_' + entry.ts), ts: entry.ts, track: entry.track || '', source: entry.source || norm.source || '', recType: recType, headline: norm.headline || entry.text || '', action: entry.action || norm.action || _inferRecActionFromText(entry.text), confidence: norm.confidence, outcome: outcome }; }); } catch (e) { return []; } } function _buildRecommendationHistory() { var mount = $('hunter-rec-history-mount'); if (!mount) return; if (!S.cur || !S.cur.id) { mount.innerHTML = ''; return; } var filter = S._recHistoryFilter || 'all'; var allItems = _collectRecommendationHistory(S.cur.id); if (!allItems.length) { mount.innerHTML = ''; return; } var items = filter === 'all' ? allItems : allItems.filter(function(it) { return it.recType === filter; }); var filters = [ { k: 'all', l: 'ALL' }, { k: 'weight', l: 'WEIGHT' }, { k: 'balance', l: 'BALANCE' }, { k: 'refinement', l: 'REFINE' }, { k: 'phone', l: 'PHONE' } ]; var filterHtml = filters.map(function(f) { var on = filter === f.k; return ''; }).join(''); var rows = items.length ? items.map(function(it) { var typeColor = _recTypeColor(it.recType); var outcomeColor = it.outcome.status === 'logged' ? '#2DB87F' : (it.outcome.status === 'done' ? 'var(--muted)' : 'var(--amber)'); var conf = it.confidence != null && !isNaN(Number(it.confidence)) ? '' + (Number(it.confidence) <= 1 ? Math.round(Number(it.confidence) * 100) : Math.round(Number(it.confidence))) + '% conf' : ''; return '
' + '
' + '
' + '' + _recTypeLabel(it.recType) + '' + '' + _escapeRecHtml(_formatRecHistoryDate(it.ts)) + '' + conf + '
' + (it.track ? '' + _escapeRecHtml(it.track) + '' : '') + '
' + '
' + _escapeRecHtml(it.headline.length > 140 ? it.headline.slice(0, 137) + '…' : it.headline) + '
' + (it.action ? '
' + _escapeRecHtml(it.action) + '
' : '') + '
' + _escapeRecHtml(it.outcome.label) + (it.outcome.detail ? ' — ' + _escapeRecHtml(it.outcome.detail) : '') + '
' + '
'; }).join('') : '
No ' + (filter === 'all' ? '' : _recTypeLabel(filter).toLowerCase() + ' ') + 'recommendations in history yet.
'; mount.innerHTML = '
' + '
' + '
RECOMMENDATION HISTORY
' + '
READ-ONLY · LAST ' + allItems.length + '
' + '
' + '
' + filterHtml + '
' + rows + '
'; } function showActiveRecommendationInGarage(response, label, source, flowStep) { if (!response) return; if (_isRecDismissed(response)) return; var recSource = source || (typeof response === 'object' && response && response.source) || null; if (recSource === 'phone_logger') label = label || 'PHONE LOGGER REC'; S._activeRecPayload = typeof response === 'string' ? { message: response, source: recSource } : Object.assign({}, response); var el = $('garage-active-rec'); if (!el && $('setup-sheet')) { el = document.createElement('div'); el.id = 'garage-active-rec'; el.style.marginBottom = '10px'; var sheet = $('setup-sheet'); var anchor = $('bb-rollin-lab'); if (anchor && anchor.parentNode === sheet) sheet.insertBefore(el, anchor.nextSibling); else sheet.insertBefore(el, sheet.firstChild); } if (!el) return; var step = flowStep || (source === 'debrief' ? 'debrief' : 'rec'); el.innerHTML = (typeof _renderGarageFlowStrip === 'function' ? _renderGarageFlowStrip(step) : '') + _presentHunterRecommendation(response, label || 'ACTIVE REC', recSource); if ($('t-garage') && $('t-garage').classList.contains('on')) { el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } if (typeof _bbSyncMobileThumbBars === 'function') _bbSyncMobileThumbBars(); } async function loadHunterLearnedInsights(carId, trackId) { if (!carId) return; if (typeof _isDemoMode === 'function' && _isDemoMode() && typeof _renderDemoHunterLearnedInsights === 'function') { _renderDemoHunterLearnedInsights(carId, trackId); return; } var garageCard = document.getElementById('hunter-learned-card'); var dashCard = document.getElementById('hunter-learned-dashboard'); var summaryEl = document.getElementById('learned-summary'); var resolvedTrack = trackId || (typeof _resolveTrackId === 'function' ? _resolveTrackId() : null); var patternsLine = typeof _patternsThisMonthLine === 'function' ? _patternsThisMonthLine(carId, resolvedTrack) : null; function hideAll() { if (garageCard) garageCard.style.display = 'none'; if (dashCard) dashCard.style.display = 'none'; } if (typeof _prefetchTrackMemoryBundleForLearning === 'function') { await new Promise(function(resolve) { _prefetchTrackMemoryBundleForLearning(function() { resolve(); }); }); } try { var res = await fetch(HNTR, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ action: 'get_learning_summary', user_id: S.user ? S.user.user_id : null, car_id: carId, track_id: resolvedTrack || null }, typeof _weatherPayloadForApi === 'function' ? _weatherPayloadForApi() : {}, typeof _feedbackPayloadForApi === 'function' ? _feedbackPayloadForApi() : {})) }); var data = await res.json(); var apiTrends = [].concat(data && data.trends || []).concat(data && data.tendencies || []); var insights = _collectLearningProfileInsights(data && data.summary, carId, resolvedTrack, apiTrends); var historyHtml = typeof _renderLearningHistoryPanel === 'function' ? _renderLearningHistoryPanel(carId, resolvedTrack) : ''; var hasHistory = historyHtml.indexOf('learning-history-panel') >= 0; if (!insights.length && !patternsLine && !hasHistory) { if (typeof _isReturningGarageUser === 'function' && !_isReturningGarageUser() && dashCard) { dashCard.innerHTML = '
Finish your first night, log a scale change, or complete a track walk — Hunter will start showing patterns and trends here.
'; dashCard.style.display = 'block'; } hideAll(); return; } var html = _renderWhatHunterLearnedSection(insights, patternsLine) + historyHtml; var walkDash = typeof _renderTrackWalkDashboardSummary === 'function' ? _renderTrackWalkDashboardSummary(carId) : ''; if (garageCard && summaryEl) { summaryEl.innerHTML = (typeof _renderGarageFlowStrip === 'function' ? _renderGarageFlowStrip('learn') : '') + html.replace(/^
' + '
PATTERNS THIS MONTH
' + '
' + _escapeRecHtml(patternsLine.text) + '
' + '
'; } function _computeLocalCarTendencies(carId) { var out = []; try { var all = JSON.parse(localStorage.getItem(_learningSignalsKey(carId)) || '{}'); var tracks = all.tracks || {}; var phone = []; var scaleFollow = []; Object.keys(tracks).forEach(function(tid) { var b = tracks[tid] || {}; if (Array.isArray(b.phone)) phone = phone.concat(b.phone); if (Array.isArray(b.scale_follow)) scaleFollow = scaleFollow.concat(b.scale_follow); }); var lr = scaleFollow.filter(function(s) { return s.direction === 'lr'; }); if (lr.length >= 4) { var lrHit = lr.filter(function(s) { return s.followed; }).length; if (lrHit / lr.length >= 0.6) out.push({ text: 'Car tendency: left-rear balance adjustments have worked repeatedly on this car.', score: 94, key: 'car_lr_habit', tendency: true }); } var rf = scaleFollow.filter(function(s) { return s.direction === 'rf'; }); if (rf.length >= 4) { var rfHit = rf.filter(function(s) { return s.followed; }).length; if (rfHit / rf.length >= 0.6) out.push({ text: 'Car tendency: right-front wedge moves are a familiar fix on this car.', score: 92, key: 'car_rf_habit', tendency: true }); } var lateralHigh = phone.filter(function(p) { return p.lateral_high; }); if (lateralHigh.length >= 3 && lr.length >= 2) { out.push({ text: 'Car tendency: this chassis often needs left-rear work when lateral loading runs high.', score: 93, key: 'car_lr_loaded', tendency: true }); } var scored = phone.filter(function(p) { return p.consistency_score != null; }); if (scored.length >= 5) { var recent = scored.slice(0, 5); var older = scored.slice(5, 12); if (older.length >= 2) { var rAvg = recent.reduce(function(a, b) { return a + Number(b.consistency_score); }, 0) / recent.length; var oAvg = older.reduce(function(a, b) { return a + Number(b.consistency_score); }, 0) / older.length; if (older.some(function(p) { return Number(p.consistency_score) < 58; }) && rAvg - oAvg >= 6) { out.push({ text: 'Car tendency: consistency on this car often improves after small rear pressure refinements.', score: 91, key: 'car_psi_consist', tendency: true }); } } } } catch (e) {} return out; } function _learningTrendCandidates(apiTrends, carId, trackId) { var out = []; (apiTrends || []).forEach(function(line, i) { var safe = _filterLearningInsightLine(line); if (!safe) return; var isCar = /Car tendency:/i.test(safe); out.push({ text: safe, score: (isCar ? 95 : 92) - i, key: (isCar ? 'car_tend_' : 'api_trend_') + i, trend: true, tendency: isCar }); }); _computeLocalCarTendencies(carId).forEach(function(item) { if (!out.some(function(o) { return o.key === item.key; })) out.push(item); }); _computeLocalScaleFollowTrend(carId, trackId).forEach(function(item) { if (!out.some(function(o) { return o.key === item.key; })) out.push(Object.assign({ trend: true }, item)); }); try { var phoneHistory = JSON.parse(localStorage.getItem('phone_logger_history') || '[]'); var relevant = phoneHistory; if (trackId) { var trackSessions = phoneHistory.filter(function(s) { return s.track_id === trackId || (s.track && S.curTrack && s.track === S.curTrack.name); }); if (trackSessions.length) relevant = trackSessions; } var phoneTrend = _computePhoneConsistencyTrendFromSessions(relevant); if (phoneTrend && !out.some(function(o) { return o.key === phoneTrend.key; })) out.push(Object.assign({ trend: true }, phoneTrend)); } catch (e) {} var recapTrend = _computeLocalRecapTrend(carId, trackId); if (recapTrend && !out.some(function(o) { return o.key === recapTrend.key; })) out.push(Object.assign({ trend: true }, recapTrend)); return out; } function _localLearningInsightCandidates(carId, trackId) { var out = []; try { var recHist = JSON.parse(localStorage.getItem(typeof _recHistoryKey === 'function' ? _recHistoryKey() : ('bb_rec_history_' + carId)) || '[]'); if (recHist[0] && recHist[0].text) { var recSafe = sanitizeRecapText(recHist[0].text, 96); if (recSafe && recSafe.length >= 18) out.push({ text: recSafe, score: 64, key: 'rec_hist' }); } } catch (e) {} try { var ld = JSON.parse(localStorage.getItem('bb_scale_learn_' + (carId || 'local')) || '{}'); Object.keys(ld).forEach(function(t) { if (!ld[t] || ld[t].length < 3) return; var label = t.replace(/_/g, ' '); out.push({ text: 'You often log ' + label + ' adjustments — Hunter is learning your scale habits.', score: 76, key: 'scale_local_' + t }); }); } catch (e2) {} try { var phoneHistory = JSON.parse(localStorage.getItem('phone_logger_history') || '[]'); var relevant = phoneHistory; if (trackId) { var trackSessions = phoneHistory.filter(function(s) { return s.track_id === trackId || (s.track && S.curTrack && s.track === S.curTrack.name); }); if (trackSessions.length) relevant = trackSessions; } if (relevant.length >= 2) { var consistVals = relevant.map(function(s) { return Number(s.consistency_score); }).filter(function(n) { return !isNaN(n) && n > 0; }); if (consistVals.length >= 2 && !out.some(function(o) { return o.key && o.key.indexOf('phone_trend') === 0; })) { var avgConsist = consistVals.reduce(function(a, b) { return a + b; }, 0) / consistVals.length; var pct = avgConsist <= 1 ? Math.round(avgConsist * 100) : Math.round(avgConsist); if (pct >= 60) out.push({ text: 'Phone logger trend: consistency averaging around ' + pct + '% across recent stints.', score: 82, key: 'phone_consist' }); else if (pct > 0) out.push({ text: 'Phone logger trend: consistency still building — more clean stints will sharpen this read.', score: 70, key: 'phone_consist_low' }); } var latVals = relevant.map(function(s) { return Number(s.avg_lateral_g); }).filter(function(n) { return !isNaN(n) && n > 0; }); if (latVals.length >= 2) { var avgLat = latVals.reduce(function(a, b) { return a + b; }, 0) / latVals.length; if (avgLat > 1.25) out.push({ text: 'Phone data trend: lateral loading running high (' + avgLat.toFixed(2) + 'g average).', score: 78, key: 'phone_lat' }); } } } catch (e3) {} return out; } function _collectLearningProfileInsights(summaryHtml, carId, trackId, apiTrends) { var seen = {}; var ranked = []; function add(text, score, key, trend, tendency, category) { var t = _filterLearningInsightLine(text); if (!t) return; var k = key || t.slice(0, 32); if (seen[k]) return; seen[k] = true; var item = { text: t, score: score != null ? score : _scoreLearningInsight(t), key: k, trend: !!trend, tendency: !!tendency }; item.category = category || _inferInsightCategory(item); ranked.push(item); } _learningTrendCandidates(apiTrends, carId, trackId).forEach(function(item) { add(item.text, item.score, item.key, true, item.tendency); }); _extractInsightsFromSummary(summaryHtml).forEach(function(line) { add(line, _scoreLearningInsight(line)); }); _localLearningInsightCandidates(carId, trackId).forEach(function(item) { add(item.text, item.score, item.key); }); _loadCachedTrackMemoryBestLaps(trackId || null).slice(0, 2).forEach(function(best, i) { if (!best || !best.time_sec) return; add('Track Memory personal best: ' + Number(best.time_sec).toFixed(3) + 's at ' + (best.track_name || best.track_id || 'this track') + '.', 88 - i, 'tm_pb_' + (best.track_id || i), false, false, 'memory'); }); ranked.sort(function(a, b) { return b.score - a.score; }); return ranked.slice(0, 6); } function _renderWhatHunterLearnedSection(insights, patternsLine) { var patternsHtml = typeof _renderPatternsThisMonthBlock === 'function' ? _renderPatternsThisMonthBlock(patternsLine) : ''; if (!insights || !insights.length) { return patternsHtml + (patternsHtml ? '' : '
Complete a few race nights, log scale changes, or finish a track walk — Hunter will start surfacing patterns here.
'); } var groups = {}; insights.forEach(function(item) { var cat = _inferInsightCategory(item); if (!groups[cat]) groups[cat] = []; groups[cat].push(item); }); var order = ['scale', 'phone', 'walk', 'memory', 'race', 'car', 'general']; var blocks = order.filter(function(cat) { return groups[cat] && groups[cat].length; }).map(function(cat) { var items = groups[cat].map(function(item) { var t = typeof item === 'string' ? item : item.text; var trend = item && item.trend; var tendency = item && item.tendency; var badge = tendency ? 'CAR' : (trend ? 'TREND' : ''); return '
  • ' + badge + _escapeRecHtml(t) + '
  • '; }).join(''); return '
    ' + _learningCategoryTitle(cat).toUpperCase() + '
      ' + items + '
    '; }).join(''); return patternsHtml + '
    WHAT HUNTER HAS LEARNED
    ' + '
    ' + blocks + '
    '; } // ============================================= // Soft Geofence Helper (Track Presence) // ============================================= function haversineDistance(lat1, lon1, lat2, lon2) { var R = 6371000; var toRad = function(deg) { return deg * Math.PI / 180; }; var phi1 = toRad(lat1); var phi2 = toRad(lat2); var dPhi = toRad(lat2 - lat1); var dLambda = toRad(lon2 - lon1); var a = Math.sin(dPhi / 2) * Math.sin(dPhi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(dLambda / 2) * Math.sin(dLambda / 2); var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } function isNearTrack(userLat, userLon, track) { if (!track || track.lat == null || track.lon == null) { return { within: false, distanceM: -1 }; } var radius = track.radiusM || 800; var distance = haversineDistance(userLat, userLon, track.lat, track.lon); return { within: distance <= radius, distanceM: Math.round(distance) }; } function getCurrentPosition(timeoutMs) { timeoutMs = timeoutMs || 8000; return new Promise(function(resolve, reject) { if (!navigator.geolocation) return reject(new Error('Geolocation not supported')); navigator.geolocation.getCurrentPosition(resolve, reject, { enableHighAccuracy: true, timeout: timeoutMs, maximumAge: 0 }); }); } function _loadTmManifestCached(cb) { if (S._tmManifest && S._tmManifest.tracks) return cb(S._tmManifest); if (S._tmManifestLoading) return S._tmManifestLoading.push(cb); S._tmManifestLoading = [cb]; fetch('https://racer.wiki/play/data/aerial-manifest.json').then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; }).then(function(d) { if (d) S._tmManifest = d; var cbs = S._tmManifestLoading || []; S._tmManifestLoading = null; cbs.forEach(function(fn) { fn(S._tmManifest || null); }); }); } function getTrackFromManifest(trackId, trackName, manifest) { var lat = null, lon = null, radiusM = 800; if (S.curTrack && S.curTrack.lat != null && S.curTrack.lon != null) { lat = S.curTrack.lat; lon = S.curTrack.lon; } var slug = _trackSlug(trackName || trackId || ''); var entry = manifest && manifest.tracks ? (manifest.tracks[trackId] || manifest.tracks[slug] || null) : null; if (entry) { if (entry.lat != null) lat = entry.lat; if (entry.lon != null) lon = entry.lon; if (entry.radiusM != null) radiusM = Math.max(Number(entry.radiusM) || 800, 140); } return { lat: lat, lon: lon, radiusM: radiusM, name: trackName || (entry && entry.name) || trackId || 'Tonight\'s Track' }; } function getTrackWalkStatusBadge(walkData) { if (!walkData || (walkData.presenceVerified === undefined && walkData.distanceM == null)) return ''; if (walkData.presenceVerified) { return '✓ Verified at track'; } if (walkData.distanceM != null && walkData.distanceM > 0) { return '⚠ Off-site (still saved)'; } return '⚠ Location unavailable'; } function _walkPresenceBadgeHtml(w) { return getTrackWalkStatusBadge(w); } function _completeTrackWalkWithSoftGeofence(data, key) { var trackName = (S.curTrack && S.curTrack.name) || ''; var trackId = typeof _resolveTrackId === 'function' ? _resolveTrackId() : _trackSlug(trackName); _loadTmManifestCached(function(manifest) { var track = getTrackFromManifest(trackId, trackName, manifest); getCurrentPosition().then(function(position) { var near = isNearTrack(position.coords.latitude, position.coords.longitude, track); _finishTrackWalkWithPresence(data, key, trackId, trackName, track, { presenceVerified: near.within, distanceM: near.distanceM, gpsAccuracyM: position.coords.accuracy != null ? Math.round(position.coords.accuracy) : null, skipped: false }); }).catch(function(err) { toast('GPS unavailable — walk saved without on-site verification. Enable location in browser settings to verify presence.'); _finishTrackWalkWithPresence(data, key, trackId, trackName, track, { presenceVerified: false, distanceM: -1, gpsAccuracyM: null, skipped: true }); }); }); } function _finishTrackWalkWithPresence(data, key, trackId, trackName, track, presence) { var entry = _recordTrackWalkComplete(S.cur.id, trackId, trackName, data, { walkKey: key, presence: presence }); _showTrackWalkPresenceToasts(track, presence, entry); if (typeof _refreshWalkEntryIfPresent === 'function') _refreshWalkEntryIfPresent(); } function _showTrackWalkPresenceToasts(track, presence, entry) { var tname = track.name || 'this track'; if (!presence.skipped && presence.presenceVerified === false && presence.distanceM > 0) { toast('Not at ' + tname + ' right now — walk saved, but on-site reads work best.'); } else if (!presence.skipped && presence.presenceVerified) { toast('✓ Track walk verified at ' + tname); } else if (presence.skipped) { toast('Walk saved for ' + tname + ' (GPS off — enable location to verify on-site).'); } else { toast('Track walk saved for ' + tname + '.'); } if (entry) { S._fiShare = typeof _fiBuildShare === 'function' ? _fiBuildShare('walk', { trackName: tname, insight: entry.insight }) : null; setTimeout(function() { if (typeof _applyFirstImpressionChrome === 'function') _applyFirstImpressionChrome(); toast('Walk complete — insight saved. Share from Extras or open Track Memory.'); }, 2200); } } function _trackWalkRewardsKey(carId) { return 'bb_track_walk_rewards_' + (carId || 'local'); } var TM_WALK_UNLOCK_MS = 14 * 24 * 60 * 60 * 1000; function _upsertTrackWalkUnlock(carId, trackId, trackName, pointCount, opts) { opts = opts || {}; if (!carId) return null; var tid = _normalizeLearningTrackKey(trackId, trackName); if (!tid) return null; var all = _loadTrackWalkRewards(carId); var existing = all.tracks[tid] || {}; var now = Date.now(); var unlockUntil = now + TM_WALK_UNLOCK_MS; var isFull = pointCount >= 22; var entry = Object.assign({}, existing, { ts: now, track_name: trackName || existing.track_name || '', walk_key: opts.walkKey || existing.walk_key || '', points: pointCount, unlock_until: unlockUntil, unlock_revoked: false, unlock_all: isFull || (!!existing.unlock_all && existing.unlock_until > now && pointCount >= 22) }); if (isFull) entry.unlock_all = true; all.tracks[tid] = entry; all.recent = (all.recent || []).filter(function(r) { return r.track_id !== tid; }); all.recent.unshift({ track_id: tid, track_name: entry.track_name, ts: entry.ts }); all.recent = all.recent.slice(0, 16); _saveTrackWalkRewards(carId, all); if (typeof _tmPushWalkReward === 'function') _tmPushWalkReward(carId, tid, entry); try { localStorage.setItem('tm_pending_unlock', JSON.stringify({ trackId: tid, trackName: trackName || tid, tier: entry.unlock_all ? 'all' : 'partial', via: 'walk', expiresAt: unlockUntil, ts: now })); } catch (_tm) {} return entry; } function _maybeRefreshTrackWalkUnlock(data, key) { if (!S.cur || !S.cur.id || !data || !S.curTrack) return; var count = typeof _walkCount === 'function' ? _walkCount(data) : 0; if (count < 1) return; var trackId = typeof _resolveTrackId === 'function' ? _resolveTrackId() : (S.curTrack.id || _trackSlug(S.curTrack.name)); var trackName = S.curTrack.name || ''; _upsertTrackWalkUnlock(S.cur.id, trackId, trackName, count, { walkKey: key }); } function _revokeTrackMemoryWalkUnlock(carId, trackId) { if (!carId) return; var tid = _normalizeLearningTrackKey(trackId, null); var all = _loadTrackWalkRewards(carId); var changed = false; Object.keys(all.tracks || {}).forEach(function(k) { var entry = all.tracks[k]; if (!entry || entry.unlock_revoked) return; var matchesTrack = tid && (k === tid || _trackSlug(k) === _trackSlug(tid)); var revoke = matchesTrack || !!entry.unlock_all; if (!revoke) return; entry.unlock_revoked = true; entry.unlock_revoked_at = Date.now(); changed = true; }); if (changed) _saveTrackWalkRewards(carId, all); } function _loadTrackWalkRewards(carId) { try { var raw = JSON.parse(localStorage.getItem(_trackWalkRewardsKey(carId)) || '{}'); if (!raw.tracks) raw.tracks = {}; if (!Array.isArray(raw.recent)) raw.recent = []; return raw; } catch (e) { return { tracks: {}, recent: [] }; } } function _saveTrackWalkRewards(carId, data) { try { localStorage.setItem(_trackWalkRewardsKey(carId), JSON.stringify(data)); } catch (e) {} } function _tmSyncToken() { try { if (S.token) return S.token; var auth = JSON.parse(localStorage.getItem('bb_auth') || 'null'); return auth && auth.token ? auth.token : null; } catch (e) { return null; } } function _tmSyncUserId() { try { if (S.user && S.user.user_id) return S.user.user_id; var auth = JSON.parse(localStorage.getItem('bb_auth') || 'null'); return auth && auth.user && auth.user.user_id ? auth.user.user_id : null; } catch (e) { return null; } } function _tmCanSync() { return !!(_tmSyncToken() && _tmSyncUserId()); } function _tmPushWalkReward(carId, trackId, entry) { if (!_tmCanSync() || !carId || !trackId || !entry) return; var tid = _normalizeLearningTrackKey(trackId, entry.track_name); if (!tid) return; var payload = { token: _tmSyncToken(), walk_rewards: [{ car_id: String(carId), track_id: tid, track_name: entry.track_name || null, walk_key: entry.walk_key || null, insight: entry.insight || null, points: entry.points || 0, walk_count: entry.walk_count || 1, presence_verified: entry.presenceVerified, distance_m: entry.distanceM != null ? entry.distanceM : null, gps_accuracy_m: entry.gpsAccuracyM != null ? entry.gpsAccuracyM : null, boost_until: entry.boost_until ? new Date(entry.boost_until).toISOString() : null, unlock_until: entry.unlock_until ? new Date(entry.unlock_until).toISOString() : null, unlock_all: !!entry.unlock_all, unlock_revoked: !!entry.unlock_revoked, badge: entry.badge || null, shared: !!entry.shared, walk_history: entry.walk_history || [], updated_at: new Date().toISOString() }], unlocks: [{ track_id: tid, tier: 'full', via: 'walk', track_name: entry.track_name || null, expires_at: entry.unlock_until ? new Date(entry.unlock_until).toISOString() : null, updated_at: new Date().toISOString() }] }; fetch('/track-memory-sync?action=sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).catch(function() {}); } var _tmFanActivityCache = {}; function _tmPrefetchFanActivity(trackId) { var tid = _trackSlug(trackId || ''); if (!tid) return; fetch('/track-memory-sync?action=fan-activity&track=' + encodeURIComponent(tid) + '&days=7') .then(function(r) { return r.json(); }) .then(function(d) { if (d && d.ok) { _tmFanActivityCache[tid] = d; if (typeof _applyFirstImpressionChrome === 'function') _applyFirstImpressionChrome(); } }) .catch(function() {}); } function _tmFetchFanActivityForTrack(trackId, trackName) { var tid = _trackSlug(trackId || trackName || ''); if (_tmFanActivityCache[tid]) return _tmFanActivityCache[tid]; return null; } function _normalizeLearningTrackKey(trackId, trackName) { if (trackId != null && String(trackId).trim()) return String(trackId).trim(); if (trackName) return _trackSlug(trackName); return ''; } function _trackWalkInsightFromZones(zones, trackName) { if (!zones) return 'Full walk logged at ' + (trackName || 'this track') + ' — Hunter will weight surface reads here.'; var parts = []; if (zones.e1 && zones.e2 && zones.e1.grip != null && zones.e2.grip != null) { var diff = zones.e1.grip - zones.e2.grip; if (Math.abs(diff) > 6) parts.push((diff > 0 ? 'End 1' : 'End 2') + ' reads ' + Math.abs(diff) + ' grip pts stronger on your walk'); } var zLabels = { e1: 'End 1', e2: 'End 2', fs: 'Front straight', bs: 'Back straight' }; ['e1', 'e2', 'fs', 'bs'].forEach(function(z) { var zd = zones[z]; if (zd && zd.dominant_tag) parts.push(zLabels[z] + ' surface: ' + String(zd.dominant_tag).toLowerCase()); }); if (zones.soil_fingerprint && zones.soil_fingerprint.night_arc) { parts.push(String(zones.soil_fingerprint.night_arc).slice(0, 110)); } if (!parts.length) return 'Full walk logged at ' + (trackName || 'this track') + ' — Hunter will weight surface reads here.'; return parts.slice(0, 2).join('. ') + '.'; } function _fmtWalkFreshDate(ts) { if (!ts) return ''; try { return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } catch (e) { return ''; } } function _getActiveTrackWalkEntry(carId, trackId, trackName) { if (!carId) return null; var all = _loadTrackWalkRewards(carId); var tid = _normalizeLearningTrackKey(trackId, trackName); if (!tid) return null; return all.tracks && (all.tracks[tid] || all.tracks[_trackSlug(tid)] || all.tracks[_trackSlug(trackName || '')]) || null; } function _trackWalkContextLine() { if (!S.cur || !S.cur.id) return ''; var trackName = S.curTrack && S.curTrack.name; var trackId = typeof _resolveTrackId === 'function' ? _resolveTrackId() : null; if (!trackId && trackName) trackId = _trackSlug(trackName); if (!trackId) return ''; if (typeof _hasTrackWalkBoost !== 'function' || !_hasTrackWalkBoost(S.cur.id, trackId)) return ''; var w = _getActiveTrackWalkEntry(S.cur.id, trackId, trackName); if (!w || !w.insight) return ''; return String(w.insight).replace(/\s+/g, ' ').trim().slice(0, 140); } function _syncTrackWalkInsightToOtherCars(sourceCarId, tid, trackName, insight, boostUntil) { (S.cars || []).forEach(function(car) { if (!car || !car.id || car.id === sourceCarId) return; var all = _loadTrackWalkRewards(car.id); var prev = (all.tracks && all.tracks[tid]) || {}; all.tracks[tid] = { ts: prev.ts || Date.now(), track_name: trackName || prev.track_name || '', walk_key: prev.walk_key || '', points: prev.points || 0, insight: insight, badge: prev.badge || '', boost_until: boostUntil, shared: prev.shared || false, walk_history: prev.walk_history || [], walk_count: prev.walk_count || 0, insight_shared: true }; if (!prev.badge) delete all.tracks[tid].badge; _saveTrackWalkRewards(car.id, all); }); } function _renderTrackWalkDashboardSummary(carId) { var walks = _loadTrackWalkRewards(carId); var recent = (walks.recent || []).slice(0, 3); if (!recent.length) return ''; var parts = recent.map(function(r) { var name = r.track_name || r.track_id || 'Track'; if (name.length > 22) name = name.slice(0, 20) + '…'; return name + ' · ' + _fmtWalkFreshDate(r.ts); }); return '
    ' + 'TRACK WALKS' + _escapeRecHtml(parts.join(' | ')) + '
    '; } function _renderWalkEntryRewardBlock(carId, trackId, trackName) { var w = _getActiveTrackWalkEntry(carId, trackId, trackName); if (!w || !w.insight) return ''; var active = w.boost_until && w.boost_until > Date.now(); var fresh = active ? ('Fresh through ' + _fmtWalkFreshDate(w.boost_until)) : 'Walk again to refresh surface read'; var count = w.walk_count || (Array.isArray(w.walk_history) ? w.walk_history.length + 1 : 1); var lastLine = 'Last walked ' + _fmtWalkFreshDate(w.ts) + ' · ' + count + ' walk' + (count === 1 ? '' : 's') + ' total'; var head = w.badge ? ('★ ' + _escapeRecHtml(w.badge.replace(/^Track Walk Complete – /, 'Walk complete — '))) : 'Surface read available'; var presenceHtml = _walkPresenceBadgeHtml(w); return '
    ' + '
    ' + (active ? 'TRACK INSIGHT ACTIVE' : 'TRACK INSIGHT SAVED') + (presenceHtml ? ' · ' + presenceHtml : '') + '
    ' + '
    ' + head + '
    ' + '
    ' + _escapeRecHtml(w.insight || '') + '
    ' + '
    ' + _escapeRecHtml(fresh) + ' · ' + _escapeRecHtml(lastLine) + '
    ' + 'Draw your line →' + (typeof _renderFiShareBar === 'function' ? _renderFiShareBar('walk', { trackName: trackName, insight: w.insight }) : '') + (w.insight_shared ? '
    Insight shared from another car at this track
    ' : '') + '
    '; } function _refreshWalkEntryIfPresent() { var entry = document.querySelector('.bb-walk-entry'); if (entry && entry.parentNode && typeof _buildWalkSection === 'function') _buildWalkSection(entry.parentNode); } function _recordTrackWalkComplete(carId, trackId, trackName, walkData, opts) { opts = opts || {}; if (!carId) return null; var tid = _normalizeLearningTrackKey(trackId, trackName); if (!tid) return null; var all = _loadTrackWalkRewards(carId); var walkKey = opts.walkKey || ''; var existing = all.tracks[tid]; if (existing && walkKey && existing.walk_key === walkKey) return existing; var zones = typeof _walkZoneSummary === 'function' ? _walkZoneSummary(walkData || { points: {} }) : null; var insight = _trackWalkInsightFromZones(zones, trackName); var history = existing && Array.isArray(existing.walk_history) ? existing.walk_history.slice() : []; if (existing && existing.insight && existing.walk_key && existing.walk_key !== walkKey) { history.unshift({ ts: existing.ts, walk_key: existing.walk_key, insight: existing.insight, points: existing.points || 0 }); } history = history.slice(0, 5); var walkCount = existing && existing.walk_key && existing.walk_key !== walkKey ? ((existing.walk_count || 1) + 1) : (existing && existing.walk_count ? existing.walk_count : 1); var entry = { ts: Date.now(), track_name: trackName || '', walk_key: walkKey, points: zones && zones.total_points || 0, insight: insight, badge: 'Track Walk Complete – ' + (trackName || 'Tonight\'s Track'), boost_until: Date.now() + (30 * 24 * 60 * 60 * 1000), unlock_until: Date.now() + TM_WALK_UNLOCK_MS, unlock_all: true, unlock_revoked: false, shared: !!opts.shared, walk_history: history, walk_count: walkCount }; if (opts.presence) { entry.presenceVerified = !!opts.presence.presenceVerified; if (opts.presence.distanceM != null) entry.distanceM = opts.presence.distanceM; if (opts.presence.gpsAccuracyM != null) entry.gpsAccuracyM = opts.presence.gpsAccuracyM; } all.tracks[tid] = entry; all.recent = (all.recent || []).filter(function(r) { return r.track_id !== tid; }); all.recent.unshift({ track_id: tid, track_name: entry.track_name, ts: entry.ts }); all.recent = all.recent.slice(0, 16); _saveTrackWalkRewards(carId, all); if (typeof _tmPushWalkReward === 'function') _tmPushWalkReward(carId, tid, entry); _syncTrackWalkInsightToOtherCars(carId, tid, trackName, insight, entry.boost_until); try { localStorage.setItem('tm_pending_unlock', JSON.stringify({ trackId: tid, trackName: trackName || tid, tier: 'full', via: 'walk', ts: Date.now() })); } catch (_tm) {} if (typeof loadHunterLearnedInsights === 'function') loadHunterLearnedInsights(carId, tid); if (typeof _refreshWalkEntryIfPresent === 'function') _refreshWalkEntryIfPresent(); return entry; } function _hasTrackWalkBoost(carId, trackId) { if (!carId || !trackId) return false; var all = _loadTrackWalkRewards(carId); var tid = String(trackId); var w = all.tracks && (all.tracks[tid] || all.tracks[_trackSlug(tid)]); return !!(w && w.boost_until && w.boost_until > Date.now()); } function _learningHistoryTrackOptions(carId) { var seen = {}; var out = []; function add(id, name) { var key = _normalizeLearningTrackKey(id, name); if (!key || seen[key]) return; seen[key] = true; out.push({ id: key, name: name || key }); } if (S.curTrack) add(S.curTrack.id || _trackSlug(S.curTrack.name), S.curTrack.name); try { var all = JSON.parse(localStorage.getItem(_learningSignalsKey(carId)) || '{}'); Object.keys(all.tracks || {}).forEach(function(tid) { add(tid, tid); }); } catch (e) {} var walks = _loadTrackWalkRewards(carId); Object.keys(walks.tracks || {}).forEach(function(tid) { add(tid, walks.tracks[tid].track_name); }); _loadCachedTrackMemoryBestLaps(null).forEach(function(best) { add(best.track_id, best.track_name); }); (S.tracks || []).slice(0, 40).forEach(function(t) { add(t.id || _trackSlug(t.name), t.name); }); return out.sort(function(a, b) { return (a.name || '').localeCompare(b.name || ''); }); } function _collectLearningHistoryItems(carId, trackId, opts) { opts = opts || {}; var period = opts.period === 'all' ? 'all' : (opts.period || '30d'); var cutoff = period === 'all' ? 0 : (opts.cutoff || _learningPeriodCutoff(period)); var filterTrack = opts.trackFilter != null && opts.trackFilter !== '' ? opts.trackFilter : (trackId || ''); var catFilter = opts.categoryFilter || ''; var items = []; function inWindow(ts) { return !cutoff || !ts || ts >= cutoff; } function trackMatch(tid, tname) { if (!filterTrack) return true; var key = _normalizeLearningTrackKey(tid, tname); return key === filterTrack || _trackSlug(tname || '') === filterTrack || String(tid) === filterTrack; } var signalTracks = {}; try { var allSig = JSON.parse(localStorage.getItem(_learningSignalsKey(carId)) || '{}'); signalTracks = allSig.tracks || {}; } catch (e) {} Object.keys(signalTracks).forEach(function(tid) { if (!trackMatch(tid, tid)) return; var bucket = signalTracks[tid] || {}; var scaleF = (bucket.scale_follow || []).filter(function(s) { return inWindow(s.ts); }); if (scaleF.length >= 2) { var followed = scaleF.filter(function(s) { return s.followed; }).length; var pct = Math.round((followed / scaleF.length) * 100); items.push({ type: 'scale', text: 'Scale follow-through: ' + followed + '/' + scaleF.length + ' moves matched Hunter (' + pct + '%)', score: 84 + Math.min(10, pct - 50), key: 'hist_scale_' + tid, metric: pct + '%', track_id: tid }); } var recaps = (bucket.recap || []).filter(function(s) { return inWindow(s.ts); }); if (recaps.length >= 2) { var strong = recaps.filter(function(s) { return s.category === 'strong_finish'; }).length; var clean = recaps.filter(function(s) { return s.category === 'clean_laps'; }).length; if (strong >= 1) items.push({ type: 'recap', text: strong + ' strong finish' + (strong > 1 ? 'es' : '') + ' in last ' + recaps.length + ' logged nights here', score: 82, key: 'hist_recap_strong_' + tid, track_id: tid }); else if (clean >= 1) items.push({ type: 'recap', text: 'Clean-lap consistency noted on ' + clean + ' of last ' + recaps.length + ' nights here', score: 76, key: 'hist_recap_clean_' + tid, track_id: tid }); else items.push({ type: 'recap', text: recaps.length + ' race nights logged here — pattern building', score: 68, key: 'hist_recap_' + tid, track_id: tid }); } }); try { var phoneHistory = JSON.parse(localStorage.getItem('phone_logger_history') || '[]'); var relevant = phoneHistory.filter(function(s) { if (!inWindow(s.ts)) return false; return trackMatch(s.track_id, s.track); }); if (relevant.length >= 2) { var consistVals = relevant.map(function(s) { return _normalizeLocalConsistencyPct(s.consistency_score); }).filter(function(n) { return n != null; }); if (consistVals.length >= 2) { var avg = Math.round(consistVals.reduce(function(a, b) { return a + b; }, 0) / consistVals.length); items.push({ type: 'phone', text: 'Phone consistency averaging ' + avg + '% across ' + consistVals.length + ' recent stints', score: 80 + (avg >= 65 ? 6 : 0), key: 'hist_phone_consist', metric: avg + '%' }); } var latVals = relevant.map(function(s) { return Number(s.avg_lateral_g); }).filter(function(n) { return !isNaN(n) && n > 0; }); if (latVals.length >= 2) { var avgLat = latVals.reduce(function(a, b) { return a + b; }, 0) / latVals.length; if (avgLat > 1.2) items.push({ type: 'phone', text: 'Lateral loading averaging ' + avgLat.toFixed(2) + 'g — balance may need attention', score: 78, key: 'hist_phone_lat' }); } var phoneTrend = _computePhoneConsistencyTrendFromSessions(relevant); if (phoneTrend) items.push({ type: 'phone', text: phoneTrend.text.replace(/^Phone trend:\s*/i, ''), score: phoneTrend.score, key: phoneTrend.key, trend: true }); } } catch (e2) {} var walks = _loadTrackWalkRewards(carId); Object.keys(walks.tracks || {}).forEach(function(tid) { if (!trackMatch(tid, walks.tracks[tid].track_name)) return; var w = walks.tracks[tid]; if (!inWindow(w.ts)) return; items.push({ type: 'walk', text: w.insight, score: 96, key: 'hist_walk_' + tid, badge: w.badge, track_id: tid, unlocked: true }); }); _computeLocalScaleFollowTrend(carId, filterTrack || trackId).forEach(function(item) { if (!items.some(function(o) { return o.key === item.key; })) items.push(Object.assign({ type: 'scale' }, item, { trend: true })); }); var recapTrend = _computeLocalRecapTrend(carId, filterTrack || trackId); if (recapTrend && !items.some(function(o) { return o.key === recapTrend.key; })) { items.push(Object.assign({ type: 'recap' }, recapTrend, { text: recapTrend.text.replace(/^Race trend:\s*/i, ''), trend: true })); } _computeLocalCarTendencies(carId).forEach(function(item) { if (!items.some(function(o) { return o.key === item.key; })) items.push(Object.assign({ type: 'car' }, item)); }); _loadCachedTrackMemoryBestLaps(filterTrack || null).forEach(function(best, i) { if (!best || !best.time_sec) return; var ts = best.updated_at ? Date.parse(best.updated_at) : 0; if (!inWindow(ts || Date.now())) return; items.push({ type: 'memory', text: 'Track Memory PB: ' + Number(best.time_sec).toFixed(3) + 's' + (best.class_id ? ' (' + best.class_id + ')' : '') + ' at ' + (best.track_name || best.track_id || 'this track'), score: 85 - i, key: 'hist_tm_' + (best.track_id || i) + '_' + (best.class_id || ''), track_id: best.track_id }); }); try { Object.keys(signalTracks).forEach(function(tid) { if (!trackMatch(tid, tid)) return; var recGen = (signalTracks[tid].rec_gen || []).filter(function(s) { return inWindow(s.ts ? Date.parse(s.ts) : 0); }); if (recGen.length >= 2) { var withWalk = recGen.filter(function(s) { return s.had_walk; }).length; var withPhone = recGen.filter(function(s) { return s.had_phone; }).length; items.push({ type: 'car', text: withPhone + ' of last ' + recGen.length + ' recommendations used phone/scale data' + (withWalk ? (' · ' + withWalk + ' included walk context') : ''), score: 72, key: 'hist_recgen_' + tid }); } }); } catch (_rg) {} if (catFilter) { items = items.filter(function(item) { return _learningItemCategory(item) === catFilter; }); } items.sort(function(a, b) { return b.score - a.score; }); return items; } function _learningHistoryUiState() { if (!window._learningHistoryUi) { window._learningHistoryUi = { tab: 'insights', trackFilter: '', period: '30d', categoryFilter: '', expanded: false }; } return window._learningHistoryUi; } function _setLearningHistoryFilter(field, value) { var ui = _learningHistoryUiState(); ui[field] = value; if (S.cur && S.cur.id && typeof loadHunterLearnedInsights === 'function') { loadHunterLearnedInsights(S.cur.id, S.curTrack && S.curTrack.id); } } function _setLearningHistoryTab(tab) { _setLearningHistoryFilter('tab', tab === 'trends' ? 'trends' : 'insights'); } function _setLearningHistoryCategory(cat) { var ui = _learningHistoryUiState(); ui.categoryFilter = ui.categoryFilter === cat ? '' : cat; if (S.cur && S.cur.id && typeof loadHunterLearnedInsights === 'function') { loadHunterLearnedInsights(S.cur.id, S.curTrack && S.curTrack.id); } } function _toggleLearningHistoryExpanded() { var ui = _learningHistoryUiState(); ui.expanded = !ui.expanded; if (S.cur && S.cur.id && typeof loadHunterLearnedInsights === 'function') { loadHunterLearnedInsights(S.cur.id, S.curTrack && S.curTrack.id); } } function _formatLapDisplay(sec) { if (sec == null || sec === '') return ''; var n = Number(sec); if (!isFinite(n) || n <= 0) return String(sec); return n.toFixed(2); } function _startTrackMemoryMerch(opts) { opts = opts || {}; var trackName = opts.track_name || (S.curTrack && S.curTrack.name) || ''; var trackId = opts.track_id || (typeof _resolveTrackId === 'function' ? _resolveTrackId() : null); var lap = opts.lap_sec != null ? opts.lap_sec : opts.time_sec; var driverName = (S.cur && (S.cur.driver_name || S.cur.name)) || ''; var payload = { type: 'track_memory_shirt', track_name: trackName, track_id: trackId, lap_sec: lap, lap_display: _formatLapDisplay(lap), driver_name: driverName, driver_number: S.cur && S.cur.number, qr_own: true, source: 'track_memory' }; try { sessionStorage.setItem('recap_merch_draft', JSON.stringify(payload)); localStorage.setItem('bb_recap_merch_payload', JSON.stringify(payload)); } catch (e) {} var q = 'mode=track_memory'; if (trackId) q += '&track=' + encodeURIComponent(typeof _trackSlug === 'function' ? _trackSlug(trackId) : trackId); if (lap != null && lap !== '') q += '&lap=' + encodeURIComponent(lap); window.location.href = '/merch?' + q; } function _refreshGarageMerchBar() { var el = document.getElementById('garage-merch-bar'); if (!el || !S.token) return; fetch('/driver-page-api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'get', token: S.token }) }).then(function(r) { return r.json(); }).then(function(j) { if (!j.ok || !j.acct || !j.acct.slug) return; var slug = j.acct.slug; var live = !!(j.acct.paid_account || j.acct.verified); el.innerHTML = '' + '' + '' + ''; }).catch(function() {}); } function _learningHistoryAction(action) { if (action === 'scale' && typeof _garageFlowGoTo === 'function') _garageFlowGoTo('scale'); else if (action === 'rec' && typeof _garageFlowGoTo === 'function') _garageFlowGoTo('rec'); else if (action === 'flow' && typeof _garageFlowGoTo === 'function') _garageFlowGoTo('log'); else if (action === 'walk') window.location.href = '/track-walk'; else if (action === 'learn' && typeof _garageFlowGoTo === 'function') _garageFlowGoTo('learn'); else if (action === 'tm') { var tid = (_learningHistoryUiState().trackFilter) || (typeof _resolveTrackId === 'function' ? _resolveTrackId() : null); if (tid) window.open(_trackMemoryPlayUrl(tid, ''), '_blank'); } else if (action === 'tm_merch') { var ui = _learningHistoryUiState(); var bests = _loadCachedTrackMemoryBestLaps(ui.trackFilter || (typeof _resolveTrackId === 'function' ? _resolveTrackId() : null)); var best = bests.slice().sort(function(a, b) { return Number(a.time_sec) - Number(b.time_sec); })[0]; if (best && best.time_sec) { _startTrackMemoryMerch({ track_name: best.track_name, track_id: best.track_id || ui.trackFilter, lap_sec: best.time_sec }); } else { _startTrackMemoryMerch({}); } } } function _renderLearningGroupedHistory(items, limit) { if (!items || !items.length) return ''; var groups = {}; items.slice(0, limit || 12).forEach(function(item) { var cat = _learningItemCategory(item); if (!groups[cat]) groups[cat] = []; groups[cat].push(item); }); var order = ['scale', 'phone', 'walk', 'memory', 'race', 'car', 'general']; return order.filter(function(cat) { return groups[cat] && groups[cat].length; }).map(function(cat) { var lis = groups[cat].map(function(item) { return '
  • ' + _escapeRecHtml(item.text) + '
  • '; }).join(''); return '
    ' + _learningCategoryTitle(cat).toUpperCase() + '
      ' + lis + '
    '; }).join(''); } function _renderLearningHistoryPanel(carId, trackId) { if (!carId) return ''; var ui = _learningHistoryUiState(); var period = ui.period === 'all' ? 'all' : (ui.period || '30d'); var opts = { trackFilter: ui.trackFilter, period: period, categoryFilter: ui.categoryFilter || '' }; var trendPack = _computeLearningTrendMetrics(carId, trackId, opts); var items = trendPack.items; var metrics = trendPack.metrics; var walks = _loadTrackWalkRewards(carId); var periodCutoff = period === 'all' ? 0 : _learningPeriodCutoff(period); var badges = Object.keys(walks.tracks || {}).map(function(tid) { return walks.tracks[tid]; }) .filter(function(w) { return !periodCutoff || !w.ts || w.ts >= periodCutoff; }) .slice(0, 4); var trackOpts = _learningHistoryTrackOptions(carId); var trackSelect = ''; var periodBtns = '
    ' + ['30d', '90d', 'all'].map(function(p) { var label = p === 'all' ? 'ALL TIME' : (p === '90d' ? '90 DAYS' : '30 DAYS'); var on = (period === p) || (p === '30d' && period !== '90d' && period !== 'all'); return ''; }).join('') + '
    '; var cats = ['', 'scale', 'phone', 'walk', 'memory', 'race']; var catBtns = '
    ' + cats.map(function(c) { var label = c ? _learningCategoryTitle(c) : 'All'; var on = (ui.categoryFilter || '') === c; return ''; }).join('') + '
    '; var tabInsights = ui.tab !== 'trends'; var tabs = '
    ' + '' + '' + '
    '; var metricsHtml = metrics.length ? '
    ' + metrics.map(function(m) { return '
    ' + '
    ' + _escapeRecHtml(m.label).toUpperCase() + '
    ' + '
    ' + _escapeRecHtml(m.val) + '
    ' + (m.sub ? '
    ' + _escapeRecHtml(m.sub) + '
    ' : '') + '
    '; }).join('') + '
    ' : ''; var primary = items[0] || null; var primaryHtml = primary ? '
    ' + '
    TOP SIGNAL
    ' + '
    ' + _escapeRecHtml(primary.text) + '
    ' + '
    ' : ''; var historyBlock = ''; if (tabInsights) { var preview = ui.expanded ? items.slice(1) : items.slice(1, 4); historyBlock = primaryHtml; if (preview.length) { historyBlock += _renderLearningGroupedHistory(preview, ui.expanded ? 20 : 3); } else if (!primary) { historyBlock = '
    Complete a few race nights, log scale changes, phone stints, or a track walk — patterns will show up here filtered by track and time.
    '; } if (items.length > 4) { historyBlock += ''; } } else { historyBlock = metricsHtml; var tmBest = _loadCachedTrackMemoryBestLaps(ui.trackFilter || trackId || null).slice().sort(function(a, b) { return Number(a.time_sec) - Number(b.time_sec); })[0]; if (tmBest && tmBest.time_sec) { var tn = tmBest.track_name || ui.trackFilter || trackId || 'this track'; historyBlock += ''; } if (!metrics.length && !items.length) { historyBlock = '
    Trends need a little data first — log scale moves, run the phone logger, or walk the track to unlock follow-through rates and consistency reads.
    '; } else if (items.length) { historyBlock += _renderLearningGroupedHistory(items, ui.expanded ? 24 : 8); if (items.length > 8) { historyBlock += ''; } } } var badgeHtml = badges.length && tabInsights ? '
    ' + badges.map(function(w) { var active = w.boost_until && w.boost_until > Date.now(); var fresh = active ? (' · Fresh through ' + _fmtWalkFreshDate(w.boost_until)) : ''; var count = w.walk_count || 1; var tip = (w.insight || '') + '\nLast walked ' + _fmtWalkFreshDate(w.ts) + ' · ' + count + ' walks total' + fresh; return '★ ' + _escapeRecHtml((w.badge || w.track_name || 'Track Walk').replace(/^Track Walk Complete – /, '')) + fresh + ''; }).join('') + '
    ' : ''; var walkMetaHtml = ''; if (ui.trackFilter) { var tw = walks.tracks && walks.tracks[ui.trackFilter]; if (tw) { var wc = tw.walk_count || 1; walkMetaHtml = '
    ' + getTrackWalkStatusBadge(tw) + 'Last walked ' + _fmtWalkFreshDate(tw.ts) + ' · ' + wc + ' walk' + (wc === 1 ? '' : 's') + ' total' + (tw.boost_until && tw.boost_until > Date.now() ? ' · Fresh through ' + _fmtWalkFreshDate(tw.boost_until) : '') + (tw.distanceM != null && tw.presenceVerified === false ? ' · ~' + tw.distanceM + 'm away' : '') + ' · Draw your line →
    '; if (ui.expanded && Array.isArray(tw.walk_history) && tw.walk_history.length) { walkMetaHtml += '
      ' + tw.walk_history.map(function(h) { return '
    • ' + _fmtWalkFreshDate(h.ts) + ': ' + _escapeRecHtml(String(h.insight || '').slice(0, 80)) + '
    • '; }).join('') + '
    '; } } } var actions = '
    ' + '' + '' + '' + '' + (ui.trackFilter ? '' : '') + '' + '
    '; return '
    ' + '
    CAR TRENDS & HISTORY
    ' + tabs + (typeof _renderFanActivityDriverNudge === 'function' ? _renderFanActivityDriverNudge() : '') + '
    ' + trackSelect + periodBtns + '
    ' + catBtns + badgeHtml + walkMetaHtml + historyBlock + actions + '
    '; } function _showTrackWalkRewardToast(entry) { if (!entry) return; var fresh = entry.boost_until ? (' — fresh through ' + _fmtWalkFreshDate(entry.boost_until)) : ''; var tname = entry.track_name || 'this track'; toast('Track walk complete — ' + tname + ' surface read saved' + fresh); S._fiShare = typeof _fiBuildShare === 'function' ? _fiBuildShare('walk', { trackName: tname, insight: entry.insight }) : null; setTimeout(function() { if (typeof _applyFirstImpressionChrome === 'function') _applyFirstImpressionChrome(); toast('Share your walk — copy or text from the reward block in Extras'); }, 2800); } function _maybeGrantTrackWalkReward(data, key) { if (!S.cur || !S.cur.id || !data) return; var count = typeof _walkCount === 'function' ? _walkCount(data) : 0; if (count < 22 || data._walk_reward_recorded) return; data._walk_reward_recorded = true; if (typeof _walkSave === 'function') _walkSave(key, data); _completeTrackWalkWithSoftGeofence(data, key); } function _recordRecapLearningSignal(debrief) { if (!S.cur || !S.cur.id || !debrief) return; var trackId = typeof _resolveTrackId === 'function' ? _resolveTrackId() : null; if (!trackId) return; var pos = debrief.finishing_position || debrief.position || debrief.finish; var category = 'night_logged'; if (pos && Number(pos) <= 3) category = 'strong_finish'; else { try { var ph = JSON.parse(localStorage.getItem('phone_logger_history') || '[]'); if (ph[0] && _normalizeLocalConsistencyPct(ph[0].consistency_score) >= 75) category = 'clean_laps'; } catch (e) {} } _appendLocalLearningSignal(S.cur.id, trackId, 'recap', { ts: Date.now(), category: category }); } function _recordPhoneLoggerHistory(stint, meta) { if (!stint) return; try { var an = stint.analyzed || (typeof _plAnalyzeStint === 'function' ? _plAnalyzeStint(stint) : null); var analysis = typeof _pocketSessionAnalysis === 'function' ? _pocketSessionAnalysis(stint) : null; meta = meta || {}; var entry = { ts: meta.timestamp || Date.now(), track: stint.track || (S.curTrack && S.curTrack.name) || '', track_id: meta.track_id || (S.curTrack && S.curTrack.id) || (typeof _resolveTrackId === 'function' ? _resolveTrackId() : null), car_id: S.cur && S.cur.id, laps: meta.laps || (analysis && analysis.detected_laps) || stint.greenLaps || (an && an.greenCount) || 0, cautions: stint.cautions || 0, samples: analysis && analysis.total_samples || 0, avg_lateral_g: meta.avg_lateral_g || (analysis && analysis.avg_lateral_g) || null, consistency_score: meta.consistency_score || (analysis && analysis.consistency_score) || (an && an.consistency) || null, best_lap: stint.bestLap, phase: stint.phase || 'Outing' }; var hist = JSON.parse(localStorage.getItem('phone_logger_history') || '[]'); hist.unshift(entry); localStorage.setItem('phone_logger_history', JSON.stringify(hist.slice(0, 30))); if (typeof _appendLocalLearningSignal === 'function' && entry.track_id && S.cur && S.cur.id) { _appendLocalLearningSignal(S.cur.id, entry.track_id, 'phone', { ts: entry.ts, consistency_score: _normalizeLocalConsistencyPct(entry.consistency_score), detected_laps: entry.laps || 0, lateral_high: !!(entry.avg_lateral_g && Number(entry.avg_lateral_g) > 1.3) }); } if (typeof updateGlobalPhoneCounter === 'function') updateGlobalPhoneCounter(); } catch (e) {} } function detectCautionPeriods(stintOrData) { if (Array.isArray(stintOrData)) { return stintOrData.filter(function(d) { if (d.slow) return true; var az = Math.abs((d.accel && d.accel.z) || 0); var ax = Math.abs((d.accel && d.accel.x) || 0); return (az + ax) < 0.45; }); } if (!stintOrData || !stintOrData.laps) return []; return stintOrData.laps.filter(function(l) { return l.type === 'caution' || l.type === 'pit'; }); } function filterOutCautions(stintOrData, cautions) { if (Array.isArray(stintOrData)) { var flagged = cautions || detectCautionPeriods(stintOrData); var bad = {}; flagged.forEach(function(c) { if (c && c.ts) bad[c.ts] = true; }); return stintOrData.filter(function(d) { return !bad[d.ts]; }); } if (!stintOrData || !stintOrData.laps) return []; return stintOrData.laps.filter(function(l) { return l.type === 'green'; }); } function shouldTriggerHaptic() { return localStorage.getItem('phone_haptics_enabled') !== 'false'; } function savePhoneHapticSetting() { var el = document.getElementById('phone-haptics-enabled'); if (!el) return; localStorage.setItem('phone_haptics_enabled', el.checked ? 'true' : 'false'); } function initPhoneHapticSetting() { var el = document.getElementById('phone-haptics-enabled'); if (!el) return; el.checked = localStorage.getItem('phone_haptics_enabled') !== 'false'; } function triggerHaptic(kind) { if (!shouldTriggerHaptic()) return; var patterns = { heavy_braking: [80, 40, 80], heavy_corner: [120], normal: [30] }; if (navigator.vibrate) { try { navigator.vibrate(patterns[kind] || [30]); } catch (e) {} } if (window.watchDevice && window.watchCharacteristic) { try { var buf = new Uint8Array([kind === 'heavy_braking' ? 2 : kind === 'heavy_corner' ? 1 : 0]); window.watchCharacteristic.writeValue(buf); } catch (e) {} } } function analyzeAndHaptic(data) { if (!data || data.length < 20) return; var pl = _pocketLogger; if (!pl.active) return; if (pl.curLap && (pl.curLap.type === 'caution' || pl.curLap.type === 'pit')) return; if (pl.lastPos && pl.lastPos.spd < 15) return; var now = Date.now(); if (now - (pl.lastHapticTime || 0) < 2500) return; var recent = data.slice(-25); var cautions = detectCautionPeriods(recent); var cleanRecent = filterOutCautions(recent, cautions); if (cleanRecent.length < 15) return; var avgG = cleanRecent.reduce(function(sum, d) { return sum + Math.abs((d.accel && d.accel.z) || 0); }, 0) / cleanRecent.length; var maxG = Math.max.apply(null, cleanRecent.map(function(d) { return Math.abs((d.accel && d.accel.z) || 0); })); if (maxG > 2.1) { triggerHaptic('heavy_braking'); pl.lastHapticTime = now; return; } if (avgG > 1.5) { triggerHaptic('heavy_corner'); pl.lastHapticTime = now; return; } if (data.length >= 80 && data.length % 80 === 0) { triggerHaptic('normal'); pl.lastHapticTime = now; } } var PHONE_LOGGER_MIN_CLEAN_SAMPLES = 40; var PHONE_LOGGER_MAX_CAUTIONS = 3; function _countLapSamples(laps) { var n = 0; (laps || []).forEach(function(l) { n += l.speedCnt || 0; if (l.rpmSamples && l.rpmSamples.length) n += l.rpmSamples.length; }); return n; } function _countCleanPhoneSamples(stint, cleanLaps) { var fromLaps = _countLapSamples(cleanLaps); var motion = (stint && stint.motionSamples) || (typeof _pocketLogger !== 'undefined' && _pocketLogger && _pocketLogger.motionSamples) || []; if (!motion.length) return fromLaps; var cleanMotion = filterOutCautions(motion); return Math.max(fromLaps, cleanMotion.length); } function phoneSessionMeetsQualityThresholds(analysis) { if (!analysis) return false; var clean = analysis.total_clean_samples || 0; var cautions = analysis.detected_cautions != null ? analysis.detected_cautions : 999; return clean >= PHONE_LOGGER_MIN_CLEAN_SAMPLES && cautions <= PHONE_LOGGER_MAX_CAUTIONS; } function analyzePhoneDataForRecommendations(cleanLaps, stint) { if (cleanLaps && cleanLaps.laps && !stint && !Array.isArray(cleanLaps)) { stint = cleanLaps; cleanLaps = filterOutCautions(stint); } cleanLaps = cleanLaps || []; stint = stint || { laps: cleanLaps }; var allLaps = stint.laps || cleanLaps; var original_samples = _countLapSamples(allLaps); var detected_cautions = (stint.laps || allLaps).filter(function(l) { return l.type === 'caution' || l.type === 'pit'; }).length; var total_clean_samples = _countCleanPhoneSamples(stint, cleanLaps); var baseFail = { detected_laps: (cleanLaps || []).length, detected_cautions: detected_cautions, avg_lateral_g: null, consistency_score: null, total_clean_samples: total_clean_samples, original_samples: original_samples, clean_samples: total_clean_samples, meets_quality_threshold: false }; if (!allLaps.length || original_samples < 10) { return Object.assign({}, baseFail, { detected_laps: 0, total_clean_samples: 0, clean_samples: 0, note: 'Not enough data for analysis' }); } if (total_clean_samples < PHONE_LOGGER_MIN_CLEAN_SAMPLES) { return Object.assign({}, baseFail, { note: 'Need ' + PHONE_LOGGER_MIN_CLEAN_SAMPLES + '+ clean samples (have ' + total_clean_samples + ')' }); } if (detected_cautions > PHONE_LOGGER_MAX_CAUTIONS) { return Object.assign({}, baseFail, { note: 'Too many cautions (' + detected_cautions + ' — max ' + PHONE_LOGGER_MAX_CAUTIONS + ')' }); } var latSum = 0, latCnt = 0, maxLat = 0, lonSum = 0, lonCnt = 0, maxLon = 0; (cleanLaps || []).forEach(function(l) { if (l.latG) { latSum += l.latG; latCnt++; if (l.latG > maxLat) maxLat = l.latG; } if (l.lonG) { lonSum += l.lonG; lonCnt++; if (l.lonG > maxLon) maxLon = l.lonG; } }); var cleanStint = Object.assign({}, stint, { laps: cleanLaps || [] }); var an = typeof _plAnalyzeStint === 'function' ? _plAnalyzeStint(cleanStint) : null; var consistency_score = an && an.consistency != null ? an.consistency / 100 : null; var avg_lateral_g = latCnt ? latSum / latCnt : (stint.maxLatG || null); return { detected_laps: (cleanLaps || []).length, detected_cautions: detected_cautions, avg_lateral_g: avg_lateral_g, consistency_score: consistency_score, total_clean_samples: total_clean_samples, max_lateral_g: maxLat || stint.maxLatG || 0, avg_vertical_g: lonCnt ? lonSum / lonCnt : 0, max_vertical_g: maxLon || (stint.peakG && stint.peakG.lon) || 0, total_samples: total_clean_samples, original_samples: original_samples, clean_samples: total_clean_samples, meets_quality_threshold: true, best_lap: an && an.best, consistency: an && an.consistency, fade: an && an.fade, green_count: an && an.greenCount, lap_count: (stint.laps || []).length, max_lat_g: maxLat || stint.maxLatG || 0, cautions: detected_cautions, pit_laps: stint.pitLaps || 0 }; } function _buildPhoneLoggerSavePayload(archived, analysis) { var ts = Date.now(); var trackId = (S.curTrack && S.curTrack.id) || (typeof _resolveTrackId === 'function' ? _resolveTrackId() : null); var carId = S.cur && S.cur.id; return { type: 'phone_logger', source: 'phone_logger', timestamp: ts, car_id: carId, track_id: trackId, detected_laps: analysis.detected_laps || 0, detected_cautions: analysis.detected_cautions || 0, avg_lateral_g: analysis.avg_lateral_g != null ? analysis.avg_lateral_g : null, consistency_score: analysis.consistency_score != null ? analysis.consistency_score : null, total_clean_samples: analysis.total_clean_samples || 0, analysis: analysis, phase: archived.phase, track: (S.curTrack && S.curTrack.name) || archived.track, lap_detail: archived.laps, best_lap: archived.bestLap, max_speed: archived.maxSpeed, max_lat_g: archived.maxLatG, green_laps: archived.greenLaps, cautions: archived.cautions, setup_snapshot: Object.assign({}, _su) }; } function _pocketSessionAnalysis(stint) { if (!stint) return null; var cleanLaps = filterOutCautions(stint); var analysis = analyzePhoneDataForRecommendations(cleanLaps, stint); analysis.max_speed = stint.maxSpeed; analysis.max_rpm = stint.maxRpm; analysis.avg_lap = stint.analyzed && stint.analyzed.avg; return analysis; } function _pocketSessionToLog(stint) { if (!stint || !stint.laps || !stint.laps.length) return null; var cautions = detectCautionPeriods(stint); var analysis = _pocketSessionAnalysis(stint); var an = typeof _plAnalyzeStint === 'function' ? _plAnalyzeStint({ laps: filterOutCautions(stint), bestLap: stint.bestLap, maxLatG: stint.maxLatG }) : null; return { source: 'phone_logger', type: 'phone_logger', ts: new Date(stint.ended || stint.archivedAt || Date.now()).toISOString(), timestamp: Date.now(), phase: stint.phase || 'Outing', track: (S.curTrack && S.curTrack.name) || stint.track || ($('lb-track-sel') && $('lb-track-sel').value) || '', track_id: S.curTrack && S.curTrack.id ? S.curTrack.id : null, car_id: S.cur && S.cur.id, best_lap: stint.bestLap, max_speed: stint.maxSpeed, max_lat_g: stint.maxLatG, max_rpm: stint.maxRpm, green_laps: stint.greenLaps, cautions: stint.cautions, lap_count: stint.laps.length, consistency: an && an.consistency, fade: an && an.fade, result: an && an.consistency >= 88 ? 'STRONG RUN' : (an && an.consistency < 72 ? 'ROUGH NIGHT' : 'AVERAGE'), corner: an && an.fade > 0.18 ? 'EXIT' : 'FELT GOOD', notes: an ? ('Best ' + (an.best ? _fmtLap(an.best) : '?') + ', ' + an.greenCount + ' green, consistency ' + an.consistency + '%') : '', analysis: analysis, original_samples: analysis.original_samples, clean_samples: analysis.clean_samples, cautions_removed: cautions.length }; } function _savePocketSessionCloud(stint) { if (!S.token || !S.cur || !stint) return Promise.resolve({ success: false }); var cleanLaps = filterOutCautions(stint); var analysis = analyzePhoneDataForRecommendations(cleanLaps, stint); if (!phoneSessionMeetsQualityThresholds(analysis)) { return Promise.resolve({ success: false, skipped: true, note: analysis.note }); } var session = _buildPhoneLoggerSavePayload(stint, analysis); return fetch(AU + '?action=save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: S.token, car_id: S.cur.id, data_type: 'logger_session', data: session }) }).then(function(r) { return r.json(); }).catch(function() { return { success: false }; }); } function checkForActiveRecommendation() { if (!S.cur || !S.cur.id) return; try { var hist = JSON.parse(localStorage.getItem('bb_rec_history_' + S.cur.id) || '[]'); if (hist[0] && (hist[0].text || hist[0].rec) && !_isRecDismissed(hist[0].rec || hist[0].text) && typeof showActiveRecommendationInGarage === 'function') { showActiveRecommendationInGarage(hist[0].rec || hist[0].text, 'LAST REC', hist[0].source); } } catch (e) {} } async function checkForRecentPhoneData() { if (!S.cur || !S.cur.id || !S.token) return; try { var body = { token: S.token, data_type: 'logger_session', car_id: S.cur.id, limit: 1 }; if (S.curTrack && S.curTrack.id) body.track_id = S.curTrack.id; var res = await fetch(AU + '?action=load', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); var d = await res.json(); if (!d.success || !d.data || !d.data.length) return; var row = d.data[0]; var lastSession = row.data || row; var ts = lastSession.timestamp || (row.created_at ? new Date(row.created_at).getTime() : 0); var hoursSince = ts ? (Date.now() - ts) / (1000 * 60 * 60) : 999; var isPhone = lastSession.type === 'phone_logger' || lastSession.source === 'phone_logger'; if (!isPhone || hoursSince >= 3) return; var dedupKey = 'bb_phone_rec_' + S.cur.id + '_' + ts; if (sessionStorage.getItem(dedupKey)) return; var sessionLog = lastSession.analysis ? lastSession : (typeof _pocketSessionToLog === 'function' ? _pocketSessionToLog(lastSession) : lastSession); if (!sessionLog || !sessionLog.analysis) return; sessionStorage.setItem(dedupKey, '1'); await sendPhoneDataToRecommendations(sessionLog); } catch (e) { console.warn('Failed to check for recent phone data'); } } async function sendPhoneDataToRecommendations(sessionLog) { if (!sessionLog || !sessionLog.analysis) return; var analysis = sessionLog.analysis; if (!phoneSessionMeetsQualityThresholds(analysis)) { console.log('Skipping recommendation - phone session below quality threshold'); return; } var payload = { message: 'Phone logger session completed', guided_flow: 'post_log_recommendation', session_log: sessionLog, context: typeof buildContext === 'function' ? buildContext() : '', user_id: S.user ? S.user.user_id : null, user_email: S.user ? S.user.email : null, car_id: S.cur ? S.cur.id : null, track_id: (S.curTrack && S.curTrack.id) || (typeof _resolveTrackId === 'function' ? _resolveTrackId() : null), ai_calls_this_session: _aiCallsThisSession || 0 }; try { var res = await fetch(HNTR, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); var data = await res.json(); if (data.routed && (data.response || (data.recommendation && data.recommendation.message))) { var recPayload = data.recommendation || { message: data.response, source: 'contextual_bandit' }; showActiveRecommendationInGarage(recPayload, 'PHONE LOGGER REC', 'phone_logger'); saveRecommendationToHistory(recPayload.message || data.response, { source: 'phone_logger', routed: true, rec: recPayload }); if (typeof _buildRecommendationHistory === 'function') _buildRecommendationHistory(); setTimeout(function() { if (S.cur && S.cur.id) loadHunterLearnedInsights(S.cur.id, S.curTrack && S.curTrack.id); }, 600); } } catch (e) { console.warn('Failed to send phone data to recommendations'); } } async function processPhoneLoggerData(sessionLog) { return sendPhoneDataToRecommendations(sessionLog); } async function updateLearningFromPhoneSession(session) { if (!session || !session.analysis || !session.car_id) return; var a = session.analysis; var phoneMetrics = { detected_laps: a.detected_laps || 0, detected_cautions: a.detected_cautions || 0, avg_vertical_g: a.avg_vertical_g || null, max_vertical_g: a.max_vertical_g || null, avg_lateral_g: a.avg_lateral_g || null, consistency_score: a.consistency_score || null, total_clean_samples: a.total_clean_samples || a.total_samples || 0, last_phone_session: session.timestamp }; try { await fetch(HNTR, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update_from_phone_logger', car_id: session.car_id, track_id: session.track_id, phone_metrics: phoneMetrics }) }); } catch (e) { console.warn('updateLearningFromPhoneSession failed'); } } function _phoneSessionDeletedList() { try { return JSON.parse(localStorage.getItem('phone_logger_deleted') || '[]'); } catch (e) { return []; } } function _markPhoneSessionDeleted(timestamp) { if (!timestamp) return; try { var del = _phoneSessionDeletedList(); if (del.indexOf(timestamp) === -1) del.push(timestamp); localStorage.setItem('phone_logger_deleted', JSON.stringify(del.slice(-100))); } catch (e) {} } function updateGlobalPhoneCounter() { var phoneHistory = []; try { phoneHistory = JSON.parse(localStorage.getItem('phone_logger_history') || '[]'); } catch (e) {} if (typeof _isDemoMode === 'function' && _isDemoMode() && S.cur && S.cur.id) { phoneHistory = phoneHistory.filter(function(s) { return s.car_id === S.cur.id; }); } var counter = document.getElementById('phone-data-counter'); var sessionsEl = document.getElementById('phone-total-sessions'); if (!counter || !sessionsEl) return; if (!phoneHistory.length) { counter.style.display = 'none'; return; } sessionsEl.textContent = String(phoneHistory.length); counter.style.display = 'block'; } function showGlobalPhoneDataSummary() { var phoneHistory = []; try { phoneHistory = JSON.parse(localStorage.getItem('phone_logger_history') || '[]'); } catch (e) {} if (typeof _isDemoMode === 'function' && _isDemoMode() && S.cur && S.cur.id) { phoneHistory = phoneHistory.filter(function(s) { return s.car_id === S.cur.id; }); } if (!phoneHistory.length) { if (typeof updateGlobalPhoneCounter === 'function') updateGlobalPhoneCounter(); return; } var totalSessions = phoneHistory.length; var totalCleanLaps = phoneHistory.reduce(function(sum, s) { return sum + (s.laps || 0); }, 0); var totalCautions = phoneHistory.reduce(function(sum, s) { return sum + (s.cautions || 0); }, 0); var statsEl = document.getElementById('global-phone-stats'); var container = document.getElementById('global-phone-summary'); if (!statsEl || !container) return; statsEl.innerHTML = totalSessions + ' sessions • ' + totalCleanLaps + ' clean laps • ' + totalCautions + ' cautions filtered'; container.style.display = 'block'; if (typeof updateGlobalPhoneCounter === 'function') updateGlobalPhoneCounter(); } function clearAllPhoneData() { if (!confirm('Delete ALL phone logger data and history? This cannot be undone.')) return; if (!confirm('Are you absolutely sure?')) return; try { localStorage.removeItem('phone_logger_history'); localStorage.removeItem('last_phone_session'); localStorage.removeItem('phone_logger_deleted'); } catch (e) {} var container = document.getElementById('phone-session-history'); var list = document.getElementById('phone-session-list'); if (container) container.style.display = 'none'; if (list) list.innerHTML = ''; var globalSummary = document.getElementById('global-phone-summary'); if (globalSummary) globalSummary.style.display = 'none'; var phoneCounter = document.getElementById('phone-data-counter'); if (phoneCounter) phoneCounter.style.display = 'none'; var lastPhone = document.getElementById('last-phone-session'); if (lastPhone) lastPhone.style.display = 'none'; if (S.cur && S.cur.id && typeof loadHunterLearnedInsights === 'function') { loadHunterLearnedInsights(S.cur.id, S.curTrack && S.curTrack.id); } toast('All phone data cleared.'); } async function deletePhoneSession(timestamp) { if (!timestamp) return; if (!confirm('Delete this phone logger session?')) return; try { var history = JSON.parse(localStorage.getItem('phone_logger_history') || '[]'); history = history.filter(function(s) { var ts = s.ts || s.timestamp; return ts !== timestamp; }); localStorage.setItem('phone_logger_history', JSON.stringify(history)); } catch (e) {} _markPhoneSessionDeleted(timestamp); toast('Session removed.'); var filterTrackId = window._phoneHistoryTrackFilter || (S.curTrack && S.curTrack.id) || null; if (typeof renderPhoneSessionHistory === 'function') renderPhoneSessionHistory(filterTrackId); if (typeof showGlobalPhoneDataSummary === 'function') showGlobalPhoneDataSummary(); if (typeof showLastPhoneSession === 'function') showLastPhoneSession(); if (S.cur && S.cur.id && typeof loadHunterLearnedInsights === 'function') { loadHunterLearnedInsights(S.cur.id, S.curTrack && S.curTrack.id); } } function _plSetPhoneLoggerStatus(text) { var pl = _pocketLogger; if (pl) pl._loggerStatusText = text || ''; ['phone-logger-status', 'pl-pocket-status'].forEach(function(id) { var el = document.getElementById(id); if (el) el.innerHTML = text || ''; }); } function _buildPhoneLoggerCard() { var mount = document.getElementById('phone-logger-card-mount'); if (!mount) return; var pl = _pocketLogger; var statusText = (pl && pl._loggerStatusText) || 'Ready to log'; mount.innerHTML = '
    ' + 'Phone Data Logger' + '(uses your phone\'s motion sensors)' + '
    ' + 'Logs your car\'s movement during sessions. Helps Hunter learn how your car actually behaves on track.' + '
    ' + '
    ' + statusText + '
    ' + '
    ' + '' + '' + '
    ' + '' + '
    '; } function getTimeAgo(timestamp) { if (!timestamp) return 'unknown'; var diff = Date.now() - timestamp; var minutes = Math.floor(diff / 60000); if (minutes < 1) return 'just now'; if (minutes < 60) return minutes + 'm ago'; var hours = Math.floor(minutes / 60); if (hours < 24) return hours + 'h ago'; return new Date(timestamp).toLocaleDateString(); } async function showLastPhoneSessionInsideCard() { var container = document.getElementById('last-phone-session'); if (!container || !S.cur || !S.cur.id) return; try { var sessions = await _loadPhoneLoggerSessions(1); var last = sessions[0]; if (!last) { container.style.display = 'none'; return; } var timeAgo = getTimeAgo(last.timestamp); var a = last.analysis || {}; var html = 'Last logged: ' + timeAgo + '
    ' + (a.detected_laps || 0) + ' clean laps'; if (a.avg_lateral_g) html += ' • Lateral G: ' + Number(a.avg_lateral_g).toFixed(2); if (a.consistency_score != null) { var cs = Number(a.consistency_score); html += ' • Consistency: ' + (cs <= 1 ? Math.round(cs * 100) + '%' : cs); } container.innerHTML = html; container.style.display = 'block'; } catch (e) {} } async function _loadPhoneLoggerSessions(limit, opts) { opts = opts || {}; if (typeof _isDemoMode === 'function' && _isDemoMode() && S.cur && S.cur.id) { return _demoPhoneSessionsForCar(S.cur.id, limit || 6, opts); } if (!S.cur || !S.cur.id || !S.token) return []; try { var body = { token: S.token, data_type: 'logger_session', car_id: S.cur.id, limit: limit || 6 }; if (!opts.allTracks && S.curTrack && S.curTrack.id) body.track_id = S.curTrack.id; var res = await fetch(AU + '?action=load', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); var d = await res.json(); if (!d.success || !d.data) return []; var sessions = d.data.map(function(row) { var sess = row.data || row; if (!sess.timestamp && row.created_at) sess.timestamp = new Date(row.created_at).getTime(); if (row.id) sess._rowId = row.id; if (!sess.track_id && sess.track && S.tracks) { var tr = S.tracks.find(function(t) { return t.name === sess.track; }); if (tr) sess.track_id = tr.id; } return sess; }).filter(function(s) { if (s.type !== 'phone_logger' && s.source !== 'phone_logger') return false; var deleted = _phoneSessionDeletedList(); return deleted.indexOf(s.timestamp) === -1; }); if (opts.filterTrackId) { sessions = sessions.filter(function(s) { return s.track_id === opts.filterTrackId; }); } return sessions.slice(0, limit || sessions.length); } catch (e) { return []; } } async function showLastPhoneSession() { return showLastPhoneSessionInsideCard(); } async function _loadPhoneSessionNotes(limit) { if (!S.cur || !S.cur.id || !S.token) return []; try { var res = await fetch(AU + '?action=load', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: S.token, car_id: S.cur.id, data_type: 'phone_session_notes', limit: limit || 20 }) }); var d = await res.json(); if (!d.success || !d.data) return []; return d.data.map(function(row) { return row.data || row; }); } catch (e) { return []; } } async function savePhoneSessionNotes(carId, trackId, timestamp) { var notesEl = document.getElementById('phone-session-notes'); var notes = notesEl ? notesEl.value.trim() : ''; if (!notes) return; if (!S.token || !carId) { toast('Sign in to save notes'); return; } try { await fetch(AU + '?action=save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: S.token, car_id: carId, data_type: 'phone_session_notes', data: { car_id: carId, track_id: trackId, session_timestamp: timestamp, notes: notes, timestamp: Date.now() } }) }); toast('Notes saved.'); if (typeof renderPhoneSessionHistory === 'function') { renderPhoneSessionHistory(S.curTrack && S.curTrack.id); } } catch (e) { toast('Could not save notes'); } } function _showPhoneSessionNoteBox(session) { var status = document.getElementById('phone-logger-status'); if (!status || !session) return; var old = document.getElementById('phone-session-note-box'); if (old) old.remove(); var noteBox = document.createElement('div'); noteBox.id = 'phone-session-note-box'; noteBox.style.cssText = 'margin-top:12px;padding:10px;background:#1e2937;border-radius:8px'; noteBox.innerHTML = '
    Quick notes about this phone session (optional):
    ' + ''; var btn = document.createElement('button'); btn.type = 'button'; btn.textContent = 'Save Notes'; btn.style.cssText = 'margin-top:8px;padding:6px 12px;background:#3b82f6;color:white;border:none;border-radius:6px;font-size:13px;cursor:pointer'; btn.onclick = function() { savePhoneSessionNotes(session.car_id, session.track_id, session.timestamp); }; noteBox.appendChild(btn); status.appendChild(noteBox); } async function renderPhoneSessionHistory(filterTrackId) { if (!S.cur || !S.cur.id) return; window._phoneHistoryTrackFilter = filterTrackId || null; try { var phoneSessions = await _loadPhoneLoggerSessions(8, { allTracks: true, filterTrackId: filterTrackId || null }); var allNotes = await _loadPhoneSessionNotes(20); var container = document.getElementById('phone-session-history'); var list = document.getElementById('phone-session-list'); if (!container || !list) return; if (!phoneSessions.length) { container.style.display = 'none'; list.innerHTML = ''; return; } var html = ''; phoneSessions.forEach(function(session) { var ts = session.timestamp || Date.now(); var date = new Date(ts).toLocaleDateString(); var a = session.analysis || {}; var consist = a.consistency_score != null ? (Number(a.consistency_score) <= 1 ? Math.round(Number(a.consistency_score) * 100) + '%' : a.consistency_score) : ''; var trackLabel = session.track ? ('' + session.track + ' · ') : ''; var note = allNotes.find(function(n) { return n.session_timestamp === ts; }); var noteText = note && note.notes ? '
    Note: ' + String(note.notes).replace(/' : ''; html += '
    ' + '
    ' + trackLabel + date + ' — ' + (a.detected_laps || 0) + ' clean laps
    ' + '
    ' + (a.avg_lateral_g ? 'Lateral G: ' + Number(a.avg_lateral_g).toFixed(2) + ' ' : '') + (consist ? '| Consistency: ' + consist : '') + '
    ' + noteText + '' + '
    '; }); list.innerHTML = html; container.style.display = 'block'; } catch (e) { console.warn('Failed to load phone session history'); } } async function processPhoneLoggerSession(archived) { if (!archived || !archived.laps || !archived.laps.length) return; var cautions = detectCautionPeriods(archived); var cleanLaps = filterOutCautions(archived); var analysis = analyzePhoneDataForRecommendations(cleanLaps, archived); var meetsQuality = phoneSessionMeetsQualityThresholds(analysis); var session = _buildPhoneLoggerSavePayload(archived, analysis); session.original_samples = analysis.original_samples; session.clean_samples = analysis.clean_samples; session.cautions_removed = cautions.length; if (typeof _recordPhoneLoggerHistory === 'function') { _recordPhoneLoggerHistory(archived, { track_id: session.track_id, timestamp: session.timestamp, avg_lateral_g: analysis.avg_lateral_g, laps: analysis.detected_laps, saved: meetsQuality }); } if (meetsQuality && S.token && S.cur) { try { await fetch(AU + '?action=save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: S.token, car_id: S.cur.id, data_type: 'logger_session', data: session }) }); } catch (e) { console.warn('Failed to save phone logger session'); } await updateLearningFromPhoneSession(session); var sessionLog = typeof _pocketSessionToLog === 'function' ? _pocketSessionToLog(archived) : session; if (analysis.total_clean_samples > 50) { setTimeout(function() { sendPhoneDataToRecommendations(sessionLog); }, 400); } else { await sendPhoneDataToRecommendations(sessionLog); } try { localStorage.setItem('last_phone_session', String(session.timestamp)); } catch (e) {} } var summary = meetsQuality ? 'Phone session saved: ' : 'Session not saved — '; if (analysis.detected_laps) summary += analysis.detected_laps + ' clean laps'; if (analysis.detected_cautions) summary += ' • ' + analysis.detected_cautions + ' cautions'; if (analysis.avg_lateral_g) summary += ' • Avg Lateral G: ' + Number(analysis.avg_lateral_g).toFixed(2); if (!meetsQuality && analysis.note) summary += ' (' + analysis.note + ')'; toast(summary); if (meetsQuality) { setTimeout(function() { if (S.cur && S.cur.id) loadHunterLearnedInsights(S.cur.id, S.curTrack && S.curTrack.id); }, 800); } var statusNote = analysis.note ? analysis.note : ((analysis.detected_laps || 0) + ' clean laps • ' + cautions.length + ' cautions filtered'); _plSetPhoneLoggerStatus((meetsQuality ? 'Saved' : 'Not saved') + ' • ' + statusNote); if (meetsQuality) _showPhoneSessionNoteBox(session); if (meetsQuality && document.getElementById('phone-logger-card') && typeof showLastPhoneSessionInsideCard === 'function') { showLastPhoneSessionInsideCard(); } else if (meetsQuality && typeof showLastPhoneSession === 'function') { showLastPhoneSession(); } if (typeof showGlobalPhoneDataSummary === 'function') showGlobalPhoneDataSummary(); if (typeof buildRaceTab === 'function') buildRaceTab(); if (_pocketLogger) _pocketLogger.motionSamples = []; return session; } async function stopPhoneDataLogger(skipConfirm) { var pl = _pocketLogger; if (!pl.active) { _phoneLoggerUIOff(); return; } if (!skipConfirm) { var confirmStop = confirm( 'Stop phone data logging and save this session?\n\n' + 'You will not be able to continue this session after stopping.' ); if (!confirmStop) return; } pl._motionPaused = false; hideFloatingStopButton(); hideLoggingBadge(); _plSetPhoneLoggerStatus('Processing phone data...'); _plClearMotionInterval(); pl._loggerStatusText = 'Processing phone data...'; var phase = (pl.session && pl.session.phase) || 'Outing'; var archived = typeof _plArchiveStint === 'function' ? _plArchiveStint(phase) : null; if (!archived || !archived.laps || !archived.laps.length) { _plStop(); toast('No lap data to save'); _plSetPhoneLoggerStatus('No laps recorded'); pl._loggerStatusText = ''; _phoneLoggerUIOff(); return; } if (typeof _analyzeStintCoach === 'function' && typeof S !== 'undefined') { var rk = typeof _plRaceNightKey === 'function' ? _plRaceNightKey() : null; var rd = rk && typeof _raceLoad === 'function' ? _raceLoad(rk) : {}; var tips = _analyzeStintCoach(archived, null, rd, phase); if (tips.length) toast('Hunter: ' + tips[0].text.substring(0, 80) + (tips[0].text.length > 80 ? '…' : '')); } await processPhoneLoggerSession(archived); pl._loggerStatusText = ''; _phoneLoggerUIOff(); } async function _fetchWeightRecommendation(targetId) { if (!S.cur || !S.cur.id) return; var trackId = _resolveTrackId(); if (!trackId) return; var el = $(targetId); if (!el) return; el.style.display = 'block'; el.textContent = 'Loading weight hint…'; try { var r = await fetch(HNTR, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ action: 'get_weight_recommendation', car_id: S.cur.id, track_id: trackId }, typeof _hunterRecPayloadForApi === 'function' ? _hunterRecPayloadForApi() : {})) }); var d = await r.json(); var rec = d.recommendation; if (!rec) { el.style.display = 'none'; return; } S._lastWeightRec = { message: rec.message, suggestion: rec.suggestion || {}, confidence: rec.confidence, reason: rec.reason, source: rec.source, captured_at: Date.now() }; el.innerHTML = (typeof _renderGarageFlowStrip === 'function' ? _renderGarageFlowStrip('rec') : '') + _renderHunterRecCard(rec, { label: 'SCALE HELPER', theme: 'amber', showActions: true }); if (typeof showActiveRecommendationInGarage === 'function') { showActiveRecommendationInGarage(rec, 'ACTIVE REC', rec.source || 'weight_hint'); } } catch (e) { el.style.display = 'none'; } } async function _fetchDirtWelcomeHunter() { if (typeof _aiCallsThisSession !== 'undefined' && _aiCallsThisSession >= 12) return; if (!S.cur) return; try { var r = await fetch(HNTR, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Welcome #' + (S.cur.car_number || '?') + ' (' + (S.cur.class || S.cur.car_class || 'dirt') + ') to the garage. Brief crew-chief welcome: race night phases, pocket GPS logger, setup sync, track walk. Under 70 words.', discipline: _hunterDiscipline(), garage: 'dirt', guided_flow: 'dirt_onboard', context: typeof buildContext === 'function' ? buildContext() : '', ai_calls_this_session: _aiCallsThisSession || 0 }) }); var d = await r.json(); var reply = d.response || d.reply; if (reply && String(reply).trim()) { if (typeof _aiCallsThisSession !== 'undefined') _aiCallsThisSession++; if (typeof switchTab === 'function') switchTab('hunter'); setTimeout(function() { if (typeof addMsg === 'function') addMsg('h', String(reply).replace(/\n/g, ' ')); }, 400); } } catch (e) {} } function _plAnalyzeStint(sess) { if (!sess || !sess.laps || !sess.laps.length) return null; var green = sess.laps.filter(function(l) { return l.type === 'green' && l.time; }); var times = green.map(function(l) { return l.time; }); var best = times.length ? Math.min.apply(null, times) : sess.bestLap; var avg = times.length ? times.reduce(function(a, b) { return a + b; }, 0) / times.length : null; var sumSq = 0; if (avg) times.forEach(function(t) { sumSq += (t - avg) * (t - avg); }); var std = times.length > 1 ? Math.sqrt(sumSq / times.length) : 0; var consistency = (best && std != null) ? Math.max(0, Math.min(100, Math.round(100 - (std / best) * 400))) : null; var mid = Math.floor(times.length / 2); var firstHalf = times.slice(0, mid); var secondHalf = times.slice(mid); var fhAvg = firstHalf.length ? firstHalf.reduce(function(a, b) { return a + b; }, 0) / firstHalf.length : null; var shAvg = secondHalf.length ? secondHalf.reduce(function(a, b) { return a + b; }, 0) / secondHalf.length : null; var fade = (fhAvg && shAvg) ? shAvg - fhAvg : null; var lugLaps = sess.laps.filter(function(l) { return l.lugEvents > 0; }); var cautionTime = sess.laps.filter(function(l) { return l.type === 'caution'; }).reduce(function(s, l) { return s + (l.time || 0); }, 0); return { lapCount: sess.laps.length, greenCount: green.length, best: best, avg: avg, std: std, consistency: consistency, fade: fade, lugLaps: lugLaps.length, lugLapNums: lugLaps.map(function(l) { return l.n; }), maxLatG: sess.maxLatG || 0, maxSpeed: sess.maxSpeed || 0, maxRpm: sess.maxRpm || 0, cautions: sess.cautions || 0, pitLaps: sess.pitLaps || 0, cautionTime: cautionTime }; } function _plStintInsights(an, rData, phaseKey) { if (!an) return []; var out = []; if (an.best) out.push('Best green lap ' + _fmtLap(an.best) + (an.avg ? ' · avg ' + _fmtLap(an.avg) : '') + '.'); if (an.consistency != null) { out.push(an.consistency >= 85 ? 'Consistency ' + an.consistency + '% — clean laps.' : 'Consistency ' + an.consistency + '% — lap time spread worth a look.'); } if (an.fade != null && Math.abs(an.fade) > 0.15) { out.push(an.fade > 0 ? 'Second half ' + an.fade.toFixed(2) + 's slower — track slicked or tires faded.' : 'You picked up ' + Math.abs(an.fade).toFixed(2) + 's in the second half — track came in.'); } if (an.cautions > 0) out.push(an.cautions + ' caution lap' + (an.cautions > 1 ? 's' : '') + (an.cautionTime ? ' (~' + Math.round(an.cautionTime) + 's)' : '') + '.'); if (an.pitLaps > 0) out.push(an.pitLaps + ' pit lap' + (an.pitLaps > 1 ? 's' : '') + ' tagged.'); if (an.lugLaps > 0) out.push('Engine lug on lap' + (an.lugLapNums.length > 1 ? 's' : '') + ' ' + an.lugLapNums.join(', ') + ' under load — gear/jet/pass?'); if (an.maxLatG > 0.5) out.push('Peak ' + an.maxLatG.toFixed(2) + 'G lateral' + (an.maxRpm ? ' · ' + an.maxRpm + ' rpm peak' : '') + '.'); var feel = rData && rData['feel_' + phaseKey]; var where = rData && rData['where_' + phaseKey]; if (feel && an.fade > 0.2 && feel === 'LOOSE') out.push('You felt LOOSE and pace dropped — could be rear tire giving up, not just handling.'); if (feel === 'TIGHT' && an.maxLatG < 0.9) out.push('Felt TIGHT but G never built — might be binding or entry line, not stagger.'); if (where && feel) out.push('Driver note: ' + feel + (where ? ' in ' + where : '') + '.'); return out.slice(0, 6); } function _plStintChartSVG(laps, best) { if (!laps || !laps.length) return ''; var W = 280, H = 56, pad = 4; var times = laps.map(function(l) { return l.time || 0; }).filter(function(t) { return t > 0; }); if (!times.length) return ''; var mn = Math.min.apply(null, times), mx = Math.max.apply(null, times), rng = mx - mn || 1; var bw = Math.max(3, Math.floor((W - pad * 2) / laps.length) - 2); var out = ''; laps.forEach(function(l, i) { if (!l.time) return; var bh = Math.max(4, Math.round(((mx - l.time) / rng) * (H - pad * 2 - 12))); var x = pad + i * (bw + 2), y = H - pad - bh; var col = l.type === 'green' ? (l.time === best ? '#C8960A' : 'rgba(45,184,127,.55)') : l.type === 'caution' ? 'rgba(245,166,35,.7)' : 'rgba(96,165,250,.6)'; out += ''; if (l.lugEvents > 0) out += ''; }); out += ''; return out; } function _plStopHardware() { var pl = _pocketLogger; if (!pl.active) return; _plClearMotionInterval(); if (pl.curLap && pl.curLap.speedCnt > 5) _plFinishLap(); pl.active = false; if (pl.watchId != null) { navigator.geolocation.clearWatch(pl.watchId); pl.watchId = null; } if (pl.motionOn) { window.removeEventListener('devicemotion', _plOnMotion, true); pl.motionOn = false; } if (pl.tickTimer) { clearInterval(pl.tickTimer); pl.tickTimer = null; } if (pl.session) pl.session.ended = Date.now(); } function _plArchiveStint(phaseName) { _plStopHardware(); var sess = _pocketLogger.session || window._lastPocketSession; if (!sess || !sess.laps || !sess.laps.length) return null; var phase = phaseName || sess.phase || 'Outing'; var pk = _plPhaseKey(phase); var rKey = _plRaceNightKey(); var rData = typeof _raceLoad === 'function' ? _raceLoad(rKey) : { telemetry_stints: [] }; rData.telemetry_stints = rData.telemetry_stints || []; rData.telemetry_stints = rData.telemetry_stints.filter(function(s) { return s.phaseKey !== pk; }); var archived = { phaseKey: pk, phase: phase, id: sess.id, archivedAt: Date.now(), started: sess.started, ended: sess.ended || Date.now(), laps: JSON.parse(JSON.stringify(sess.laps)), bestLap: sess.bestLap, greenLaps: sess.greenLaps, cautions: sess.cautions, pitLaps: sess.pitLaps, maxSpeed: sess.maxSpeed, maxLatG: sess.maxLatG, maxRpm: sess.maxRpm, analyzed: _plAnalyzeStint(sess) }; rData.telemetry_stints.push(archived); if (rKey && typeof _raceSave === 'function') _raceSave(rKey, rData); window._lastPocketSession = archived; _pocketLogger.session = null; try { localStorage.removeItem('bb_pocket_session'); } catch (e) {} return archived; } function _plGetStint(rData, phaseName) { if (!rData || !rData.telemetry_stints) return null; var pk = _plPhaseKey(phaseName); for (var i = rData.telemetry_stints.length - 1; i >= 0; i--) { if (rData.telemetry_stints[i].phaseKey === pk) return rData.telemetry_stints[i]; } return null; } function _plPriorStintTonight(rData, phaseName) { if (!rData || !rData.telemetry_stints || !rData.telemetry_stints.length) return null; var pk = _plPhaseKey(phaseName); var idx = -1; for (var i = 0; i < rData.telemetry_stints.length; i++) { if (rData.telemetry_stints[i].phaseKey === pk) idx = i; } if (idx <= 0) return null; return rData.telemetry_stints[idx - 1]; } function _plHunterStintPrompt(stint, rData, phaseName) { var an = stint.analyzed || _plAnalyzeStint(stint); var pk = _plPhaseKey(phaseName); var start = rData['start_' + pk], fin = rData['finish_' + pk]; var feel = rData['feel_' + pk], where = rData['where_' + pk]; var msg = 'STINT DEBRIEF — ' + phaseName.toUpperCase() + ' just ended. '; if (start && fin) msg += 'Started P' + start + ', finished P' + fin + '. '; if (feel) msg += 'Driver felt ' + feel + (where ? ' in ' + where : '') + '. '; if (an) { msg += an.lapCount + ' laps logged (' + an.greenCount + ' green). Best ' + (an.best ? _fmtLap(an.best) : '?'); if (an.avg) msg += ', avg ' + _fmtLap(an.avg); if (an.consistency != null) msg += ', consistency ' + an.consistency + '%'; msg += '. '; if (an.cautions) msg += an.cautions + ' cautions. '; if (an.lugLaps) msg += 'Engine lug on laps ' + an.lugLapNums.join(', ') + '. '; if (an.maxLatG) msg += 'Peak ' + an.maxLatG.toFixed(2) + 'G. '; if (an.fade > 0.15) msg += 'Pace faded ' + an.fade.toFixed(2) + 's second half. '; } var chg = rData['change_' + pk]; if (chg) msg += 'Pit change note: ' + chg + '. '; msg += 'What ONE change before the next session? Tie driving data to setup. 80 words, crew chief tone.'; return msg; } function _plSealStintBeforeAdvance(curPhase, rData, rKey) { if (_pocketLogger.active || (_pocketLogger.session && _pocketLogger.session.laps && _pocketLogger.session.laps.length)) { _plArchiveStint(curPhase); if (rKey) rData = _raceLoad(rKey); } return rData; } function _buildStintDebriefPanel(el, phaseName, rData, rKey) { if (!el) return; var stint = _plGetStint(rData, phaseName); var wrap = document.createElement('div'); wrap.className = 'pl-stint-debrief'; wrap.style.cssText = 'margin-bottom:14px'; if (!stint || !stint.laps || !stint.laps.length) { if (_pocketLogger.active) { wrap.innerHTML = '
    Logging ' + phaseName.toUpperCase() + ' — tap END STINT when you exit the track.
    '; } else { wrap.innerHTML = '
    No pocket data for ' + phaseName.toUpperCase() + ' yet. Start logger before rolling onto the track.
    '; } el.appendChild(wrap); return; } var an = stint.analyzed || _plAnalyzeStint(stint); var prior = _plPriorStintTonight(rData, phaseName); var pk = _plPhaseKey(phaseName); var coachTips = _analyzeStintCoach(stint, prior, rData, phaseName); if (coachTips.length) _renderFlagshipCoachCard(wrap, coachTips, { phase: phaseName, showApply: true }); var card = document.createElement('div'); card.style.cssText = 'background:linear-gradient(135deg,rgba(245,166,35,.06),rgba(13,12,11,.9));border:1px solid rgba(245,166,35,.35);border-left:4px solid var(--amber);padding:14px'; var hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px'; hdr.innerHTML = '
    STINT DATA — ' + phaseName.toUpperCase() + '
    ' + '
    ' + (an && an.best ? _fmtLap(an.best) : '--') + ' BEST GREEN
    ' + '
    ' + stint.laps.length + ' laps
    ' + '' + (stint.greenLaps || 0) + 'G · ' + '' + (stint.cautions || 0) + 'C · ' + '' + (stint.pitLaps || 0) + 'P
    '; card.appendChild(hdr); var grid = document.createElement('div'); grid.style.cssText = 'display:grid;grid-template-columns:repeat(4,1fr);gap:4px;margin-bottom:10px'; [ ['CONSIST', an && an.consistency != null ? an.consistency + '%' : '--'], ['AVG', an && an.avg ? _fmtLap(an.avg) : '--'], ['LAT G', an && an.maxLatG ? an.maxLatG.toFixed(2) : '--'], ['TOP MPH', an && an.maxSpeed ? Math.round(an.maxSpeed) : '--'] ].forEach(function(it) { var d = document.createElement('div'); d.style.cssText = 'background:var(--dark);padding:6px 4px;text-align:center'; d.innerHTML = '
    ' + it[0] + '
    ' + it[1] + '
    '; grid.appendChild(d); }); card.appendChild(grid); if (prior && prior.analyzed && prior.analyzed.best && an && an.best) { var delta = an.best - prior.analyzed.best; var dEl = document.createElement('div'); dEl.style.cssText = 'font-family:Share Tech Mono;font-size:8px;margin-bottom:8px;color:' + (delta < -0.05 ? '#2DB87F' : delta > 0.05 ? 'var(--red)' : 'var(--muted)'); dEl.textContent = 'vs ' + prior.phase + ': ' + (delta < 0 ? '' : '+') + delta.toFixed(3) + 's on best lap'; card.appendChild(dEl); } var chart = document.createElement('div'); chart.innerHTML = _plStintChartSVG(stint.laps, an ? an.best : stint.bestLap); chart.style.marginBottom = '8px'; card.appendChild(chart); var leg = document.createElement('div'); leg.style.cssText = 'display:flex;gap:10px;margin-bottom:10px;font-family:Share Tech Mono;font-size:6px;color:var(--muted)'; leg.innerHTML = ' GREEN CAUTION PIT LUG'; card.appendChild(leg); var insights = _plStintInsights(an, rData, pk); if (insights.length) { var ul = document.createElement('div'); ul.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--white);line-height:1.65;margin-bottom:10px'; insights.forEach(function(line) { var p = document.createElement('div'); p.style.cssText = 'padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04)'; p.textContent = '▸ ' + line; ul.appendChild(p); }); card.appendChild(ul); } var hBtn = document.createElement('button'); hBtn.style.cssText = 'width:100%;padding:10px;background:rgba(208,25,14,.12);border:1px solid rgba(208,25,14,.35);color:var(--white);font-family:Barlow Condensed;font-size:13px;font-weight:900;letter-spacing:1px;cursor:pointer'; hBtn.textContent = 'ASK HUNTER — THIS STINT ▶'; hBtn.onclick = function() { var msg = _plHunterStintPrompt(stint, rData, phaseName); if (typeof switchTab === 'function') switchTab('hunter'); setTimeout(function() { if (typeof addMsg === 'function') addMsg('u', phaseName + ' stint debrief'); if (typeof sendToHunter === 'function') sendToHunter(msg); }, 300); }; card.appendChild(hBtn); wrap.appendChild(card); el.appendChild(wrap); } /** Engine Ear — class-aware mic spectrum → RPM band / limiter / carb mix → Hunter + pocket logger */ var _audioCtx = null, _audioStream = null, _audioAnalyser = null, _audioRecording = false; var _audioSamples = [], _audioAnimFrame = null; var _earHoldMs = 0, _earSessionMax = 0, _earPeakHoldStart = 0; window._engineEarLive = { rpm: 0, ts: 0, mix: '---', stability: 0, inBand: false, onLimiter: false, holdOk: false, profile: null }; var ENGINE_EAR_MODES = { carb: { mixLabel: 'MIX', showMix: true, hunterKind: 'jetting' }, methanol: { mixLabel: 'MIX', showMix: true, hunterKind: 'jetting' }, sealed: { mixLabel: 'SEALED', showMix: false, hunterKind: 'limiter_gear' }, efi: { mixLabel: 'EFI', showMix: false, hunterKind: 'flat_spot' } }; /** Per-class ear profiles — rpm band, firing model, analysis mode */ var ENGINE_EAR_PROFILES = [ { id: 'spec-miata', match: /spec miata|road-spec-miata/i, label: 'Spec Miata (BP 1.8)', mode: 'efi', cycles: 4, rpm: [5500, 7200], lugPct: 0.85, note: 'No jetting. Listen for flat spot, ping, or shift points.' }, { id: 'lo206-jr', match: /lo.?206.*jun|junior.*206/i, label: 'LO206 Junior', mode: 'sealed', cycles: 4, rpm: [5200, 5800], lugPct: 0.88, note: 'Never bounce the limiter — gear taller, not richer.' }, { id: 'lo206-sr', match: /lo.?206|206|briggs/i, label: 'LO206 / Briggs 4-cycle', mode: 'sealed', cycles: 4, rpm: [5800, 6100], lugPct: 0.85, note: 'Sealed — RPM window is the tune. Limiter before flag stand = taller gear.' }, { id: 'clone', match: /clone/i, label: 'Honda Clone', mode: 'sealed', cycles: 4, rpm: [5800, 6200], lugPct: 0.85, note: 'Sealed clone — same ear as LO206.' }, { id: 'outlaw-125', match: /outlaw\s*125|\b125\s*outlaw|mx\s*125/i, label: 'Outlaw 125 (MX)', mode: 'carb', cycles: 2, rpm: [8500, 11000], lugPct: 0.80, note: 'Push-start disk clutch · Tillotson/VM · jet for DA.' }, { id: 'outlaw-250', match: /outlaw\s*250|\b250\s*outlaw/i, label: 'Outlaw 250', mode: 'carb', cycles: 2, rpm: [7500, 10000], lugPct: 0.80, note: 'Stock bore 2-stroke · single carb · KAM beadlock RR.' }, { id: 'outlaw-500', match: /outlaw\s*500|open outlaw|500\s*open/i, label: 'Outlaw 500 Open', mode: 'carb', cycles: 2, rpm: [6500, 9000], lugPct: 0.78, note: 'MX 2-stroke or 4-stroke ≤550cc · offset chassis.' }, { id: 'pro-clone-flat', match: /pro clone|animal\b/i, label: 'Pro Clone Flat (KAM)', mode: 'carb', cycles: 4, rpm: [5800, 6500], lugPct: 0.85, note: 'AKRA open clone · slicks + prep OK at KAM · 87 oct gas.' }, { id: 'young-guns', match: /young gun/i, label: 'Young Guns (KAM)', mode: 'sealed', cycles: 4, rpm: [4200, 5200], lugPct: 0.88, note: 'Red plate 196 clone · beginner — no jetting, gear tall.' }, { id: 'jr-clone', match: /jr\s*[123]\s*clone|junior\s*[123]\s*clone/i, label: 'Jr Clone (KAM/AKRA)', mode: 'sealed', cycles: 4, rpm: [4500, 6200], lugPct: 0.88, note: 'Plate restrictor clone — sealed, gear only.' }, { id: 'outlaw-kart', match: /outlaw.*kart|outlaw.*125|outlaw.*250|cage kart/i, label: 'Outlaw Kart', mode: 'carb', cycles: 2, rpm: [7000, 9500], lugPct: 0.82, note: '2-stroke cage kart — carb mix matters. Pick 125/250/500 class.' }, { id: 'rok-tag', match: /rok|rotax|tag|2.?cycle|kt100/i, label: 'TAG / ROK 2-stroke', mode: 'carb', cycles: 2, rpm: [11000, 14000], lugPct: 0.82, note: 'Shifter kart — needle/pop-off window is everything.' }, { id: 'micro-600', match: /600|micro sprint|now600/i, label: '600 Micro', mode: 'methanol', cycles: 2, rpm: [10000, 12500], lugPct: 0.82, note: 'Methanol — rich/lean shows in mid harmonics. High RPM.' }, { id: 'jr-sprint', match: /junior sprint|jr\.?\s*sprint/i, label: 'Junior Sprint', mode: 'sealed', cycles: 4, rpm: [7000, 9000], lugPct: 0.85, note: '204cc Briggs — sealed, gear for RPM window.' }, { id: 'sprint-305', match: /305/i, label: '305 Sprint', mode: 'methanol', cycles: 4, rpm: [7600, 8200], lugPct: 0.82, note: 'Restricted sprint — wind it up, jet for DA.' }, { id: 'sprint-360', match: /360/i, label: '360 Sprint', mode: 'methanol', cycles: 4, rpm: [7200, 8000], lugPct: 0.82, note: 'Methanol sprint — ear confirms jet vs gear.' }, { id: 'sprint-410', match: /410|sprint/i, label: '410 Sprint', mode: 'methanol', cycles: 4, rpm: [7000, 7800], lugPct: 0.82, note: 'Open sprint — main jet + pill from DA.' }, { id: 'midget', match: /midget/i, label: 'Midget', mode: 'methanol', cycles: 4, rpm: [9000, 10500], lugPct: 0.82, note: 'High RPM methanol — ping = too lean.' }, { id: 'lm-602', match: /602/i, label: '602 Crate LM', mode: 'sealed', cycles: 4, rpm: [5800, 6200], lugPct: 0.85, note: 'Sealed crate — gear only, no jetting.' }, { id: 'lm-604', match: /604/i, label: '604 Crate LM', mode: 'sealed', cycles: 4, rpm: [6200, 6800], lugPct: 0.85, note: '604 has more RPM headroom than 602.' }, { id: 'lm-open', match: /late model|super late|limited late/i, label: 'Late Model', mode: 'carb', cycles: 4, rpm: [6500, 8000], lugPct: 0.82, note: 'Open LM — jet for DA, ear catches lean ping.' }, { id: 'modified', match: /modified|b-mod|sport mod|mod lite/i, label: 'Modified', mode: 'carb', cycles: 4, rpm: [5800, 7000], lugPct: 0.82, note: 'Mod — torque class, jet for track size + DA.' }, { id: 'stock', match: /stock|hobby|pure|compact|legend|bandolero/i, label: 'Stock / Hobby', mode: 'carb', cycles: 4, rpm: [5000, 6500], lugPct: 0.85, note: 'Stock motor — jet changes are small steps.' }, { id: 'drag', match: /drag|bracket|index|super comp|pro stock/i, label: 'Drag', mode: 'carb', cycles: 4, rpm: [4000, 7500], lugPct: 0.80, note: 'Use Drag Garage dial-in; ear confirms idle quality pre-stage.' } ]; function _earResolveProfile() { var cls = (typeof S !== 'undefined' && S.cur) ? (S.cur.class || S.cur.car_class || '') : ''; var lc = cls.toLowerCase(); var i, p; for (i = 0; i < ENGINE_EAR_PROFILES.length; i++) { p = ENGINE_EAR_PROFILES[i]; if (p.match.test(lc)) return _earProfileFrom(p); } if (typeof _bestClassMatch === 'function' && typeof GEAR_DB !== 'undefined') { var gk = _bestClassMatch(); var gd = GEAR_DB[gk]; if (gd && gd.rpm) { for (i = 0; i < ENGINE_EAR_PROFILES.length; i++) { if (ENGINE_EAR_PROFILES[i].id.indexOf('sprint') >= 0 && /sprint/i.test(gk)) break; } return { id: 'gear-db-' + gk, label: gk, mode: /sealed|602|604|lo206|junior sprint/i.test(gk) ? 'sealed' : (/miata/i.test(gk) ? 'efi' : (/sprint|micro|midget/i.test(gk) ? 'methanol' : 'carb')), cycles: /micro|600|outlaw|rok|tag|kt100/i.test(gk) ? 2 : 4, rpmMin: gd.rpm[0], rpmMax: gd.rpm[1], lugPct: 0.82, note: gd.note || '' }; } } if (typeof _getCarDiscipline === 'function' && S && S.cur && _getCarDiscipline(S.cur) === 'road') { return _earProfileFrom(ENGINE_EAR_PROFILES[2]); } return { id: 'generic', label: 'Generic engine', mode: 'carb', cycles: 4, rpmMin: 4500, rpmMax: 7500, lugPct: 0.82, note: 'Pick your class in Garage for a tuned RPM window.' }; } function _earProfileFrom(p) { return { id: p.id, label: p.label, mode: p.mode, cycles: p.cycles, rpmMin: p.rpm[0], rpmMax: p.rpm[1], lugPct: p.lugPct || 0.82, note: p.note || '' }; } window._earClassProfile = _earResolveProfile; function _earRpmColor(rpm, prof) { if (!rpm || !prof) return 'var(--white)'; if (rpm > prof.rpmMax + 150) return 'var(--red-hot)'; if (rpm >= prof.rpmMin && rpm <= prof.rpmMax + 100) return '#2DB87F'; if (rpm >= prof.rpmMin - 400) return 'var(--amber)'; return 'var(--muted)'; } function _earGuideHtml(prof) { var mode = ENGINE_EAR_MODES[prof.mode] || ENGINE_EAR_MODES.carb; var band = prof.rpmMin + '–' + prof.rpmMax + ' RPM'; return '' + '
    ' + 'HOW TO USE ENGINE EAR
    ' + 'Phone 3–4 ft from the exhaust header (not inside the car). Wind away from mic.

    ' + '1. IDLE (5 sec) — motor at stable idle. Ear learns baseline stability.' + (mode.showMix ? ' Rich/lean mix reads here.' : ' Sealed/EFI — mix gauge off; we watch RPM only.') + '
    ' + '2. SLOW REV — roll throttle slowly through ' + band + '. Confirms the mic tracks your class.' + '
    3. HOLD TEST (1 sec) — at safe WOT (standstill or pit lane), hold max RPM for one full second. ' + 'Green HOLD OK = peak captured in your class window. Do not bounce the limiter repeatedly.

    ' + '' + prof.label + ' · ' + band + '
    ' + prof.note + '
    '; } function _buildAudioTool() { var host = document.getElementById('pl-tools-ear-slot'); if (!host) { host = document.getElementById('checklist'); if (!host || !host.parentNode) return; var slot = document.createElement('div'); slot.id = 'pl-tools-ear-slot'; slot.style.cssText = 'margin:10px 0'; host.parentNode.insertBefore(slot, host); host = slot; } if (document.getElementById('audio-tool-card')) { var existing = document.getElementById('audio-tool-card'); var profUp = _earResolveProfile(); existing.innerHTML = '
    ENGINE EAR
    ' + '
    ' + profUp.label + ' · ' + profUp.rpmMin + '–' + profUp.rpmMax + ' RPM
    ' + '
    Tap for idle → rev → 1s hold test
    '; return; } var prof = _earResolveProfile(); var card = document.createElement('div'); card.id = 'audio-tool-card'; card.style.cssText = 'background:var(--dark2);border:1px solid rgba(208,25,14,.25);border-left:3px solid var(--red);padding:12px;cursor:pointer;margin-bottom:8px'; card.innerHTML = '
    ENGINE EAR
    ' + '
    ' + prof.label + ' · ' + prof.rpmMin + '–' + prof.rpmMax + ' RPM
    ' + '
    Tap for idle → rev → 1s hold test
    '; card.onclick = function() { _openAudioAnalyzer(); }; host.appendChild(card); } function _openAudioAnalyzer() { if (document.getElementById('audio-overlay')) return; var prof = _earResolveProfile(); var mode = ENGINE_EAR_MODES[prof.mode] || ENGINE_EAR_MODES.carb; _earHoldMs = 0; _earSessionMax = 0; _earPeakHoldStart = 0; var ov = document.createElement('div'); ov.id = 'audio-overlay'; ov.style.cssText = 'position:fixed;inset:0;background:var(--dark);z-index:8000;display:flex;flex-direction:column;overflow-y:auto;'; var hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid rgba(208,25,14,.12);flex-shrink:0;'; hdr.innerHTML = '
    ENGINE EAR
    ' + '
    ' + prof.label + '
    '; var closeBtn = document.createElement('button'); closeBtn.textContent = '\u2715 CLOSE'; closeBtn.style.cssText = 'font-family:var(--head);font-size:12px;color:var(--muted);background:none;border:1px solid var(--dark4);padding:6px 12px;cursor:pointer'; closeBtn.onclick = function() { _stopAudio(); ov.remove(); }; hdr.appendChild(closeBtn); ov.appendChild(hdr); var guide = document.createElement('div'); guide.style.cssText = 'padding:12px 16px 0;'; guide.innerHTML = _earGuideHtml(prof); ov.appendChild(guide); var canvasWrap = document.createElement('div'); canvasWrap.style.cssText = 'padding:12px 16px;flex-shrink:0;'; var cv = document.createElement('canvas'); cv.id = 'audio-spectrum'; cv.width = 340; cv.height = 160; cv.style.cssText = 'width:100%;height:160px;background:var(--dark2);border:1px solid var(--dark4);'; canvasWrap.appendChild(cv); var gauges = document.createElement('div'); gauges.style.cssText = 'display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:8px;'; gauges.innerHTML = '' + '
    EST. RPM
    ---
    ' + prof.rpmMin + '–' + prof.rpmMax + '
    ' + '
    IDLE STABILITY
    ---
    ' + '
    ' + (mode.showMix ? 'MIX' : 'HOLD TEST') + '
    ---
    '; canvasWrap.appendChild(gauges); var holdRow = document.createElement('div'); holdRow.id = 'audio-hold-row'; holdRow.style.cssText = 'font-family:var(--mono);font-size:9px;color:var(--muted);text-align:center;margin-top:8px;'; holdRow.textContent = 'HOLD at WOT ~1 sec for peak capture'; canvasWrap.appendChild(holdRow); ov.appendChild(canvasWrap); var ctrls = document.createElement('div'); ctrls.style.cssText = 'padding:0 16px 16px;display:flex;flex-direction:column;gap:8px;'; var listenBtn = document.createElement('button'); listenBtn.id = 'btn-listen'; listenBtn.style.cssText = 'padding:16px;background:var(--red);color:var(--white);font-family:var(--head);font-weight:900;font-size:16px;letter-spacing:2px;border:none;cursor:pointer;clip-path:polygon(10px 0%,100% 0%,calc(100% - 10px) 100%,0% 100%)'; listenBtn.textContent = '\uD83C\uDFA4 START LISTENING'; listenBtn.onclick = function() { _toggleAudio(cv, prof); }; ctrls.appendChild(listenBtn); var hunterArea = document.createElement('div'); hunterArea.id = 'audio-hunter'; hunterArea.style.cssText = 'display:none;padding:12px;background:var(--dark2);border-left:3px solid var(--gold);'; hunterArea.innerHTML = '
    HUNTER · ENGINE DIAGNOSIS
    '; ctrls.appendChild(hunterArea); ov.appendChild(ctrls); document.body.appendChild(ov); window._engineEarLive.profile = prof; } function _toggleAudio(canvas, prof) { prof = prof || _earResolveProfile(); var btn = document.getElementById('btn-listen'); if (_audioRecording) { _stopAudio(); btn.textContent = '\uD83C\uDFA4 START LISTENING'; btn.style.background = 'var(--red)'; _analyzeAudio(prof); return; } _earHoldMs = 0; _earSessionMax = 0; _earPeakHoldStart = 0; btn.textContent = '\u23F9 STOP — ANALYZING...'; btn.style.background = 'var(--amber)'; _audioRecording = true; _audioSamples = []; navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false } }).then(function(stream) { _audioStream = stream; _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); var source = _audioCtx.createMediaStreamSource(stream); _audioAnalyser = _audioCtx.createAnalyser(); _audioAnalyser.fftSize = 4096; _audioAnalyser.smoothingTimeConstant = 0.75; source.connect(_audioAnalyser); _drawSpectrum(canvas, prof); }).catch(function() { toast('Microphone access denied'); _audioRecording = false; btn.textContent = '\uD83C\uDFA4 START LISTENING'; btn.style.background = 'var(--red)'; }); } function _drawSpectrum(canvas, prof) { if (!_audioRecording || !_audioAnalyser) return; prof = prof || _earResolveProfile(); var ctx = canvas.getContext('2d'); var w = canvas.width, h = canvas.height; var bufLen = _audioAnalyser.frequencyBinCount; var data = new Uint8Array(bufLen); _audioAnalyser.getByteFrequencyData(data); if (_audioSamples.length < 120) _audioSamples.push(Array.from(data.slice(0, 256))); ctx.fillStyle = '#141311'; ctx.fillRect(0, 0, w, h); var barW = w / 128, x = 0; for (var i = 0; i < 128; i++) { var v = data[i] / 255, bh = v * h; var hue = v > 0.7 ? 0 : v > 0.4 ? 30 : 120; ctx.fillStyle = 'hsla(' + hue + ',80%,' + (30 + v * 40) + '%,' + (0.5 + v * 0.5) + ')'; ctx.fillRect(x, h - bh, barW - 1, bh); x += barW; } var peak = 0, peakIdx = 0; for (var j = 1; j < 80; j++) { if (data[j] > peak) { peak = data[j]; peakIdx = j; } } var sampleRate = _audioCtx ? _audioCtx.sampleRate : 44100; var freqPerBin = sampleRate / (_audioAnalyser.fftSize || 4096); var dominantFreq = peakIdx * freqPerBin; var cyc = prof.cycles || 4; var rpm = 0; if (dominantFreq > 12) { rpm = Math.round(dominantFreq * 60 * 2 / cyc); if (rpm < 800 || rpm > 16000) { var alt = Math.round(dominantFreq * 60 / cyc); if (alt > 800 && alt < 16000) rpm = alt; } } var rpmEl = document.getElementById('audio-rpm'); var inBand = rpm >= prof.rpmMin && rpm <= prof.rpmMax + 120; var onLim = rpm > prof.rpmMax + 150; if (rpmEl && rpm) { rpmEl.textContent = rpm; rpmEl.style.color = _earRpmColor(rpm, prof); if (rpm > _earSessionMax) _earSessionMax = rpm; var nearPeak = rpm >= _earSessionMax * 0.93 && rpm >= prof.rpmMin; var now = Date.now(); if (nearPeak && _earSessionMax >= prof.rpmMin) { if (!_earPeakHoldStart) _earPeakHoldStart = now; _earHoldMs = now - _earPeakHoldStart; } else { _earPeakHoldStart = 0; _earHoldMs = 0; } var holdOk = _earHoldMs >= 900; var mixEl = document.getElementById('audio-mix'); if (mixEl && !(ENGINE_EAR_MODES[prof.mode] || {}).showMix) { mixEl.textContent = holdOk ? 'HOLD OK' : (_earHoldMs > 200 ? Math.round(_earHoldMs / 100) / 10 + 's' : '—'); mixEl.style.color = holdOk ? '#2DB87F' : 'var(--amber)'; } var holdRow = document.getElementById('audio-hold-row'); if (holdRow && holdOk) holdRow.textContent = 'Peak ' + _earSessionMax + ' RPM captured in class window'; window._engineEarLive = { rpm: rpm, ts: now, mix: window._engineEarLive.mix, stability: window._engineEarLive.stability, inBand: inBand, onLimiter: onLim, holdOk: holdOk, profile: prof }; } _audioAnimFrame = requestAnimationFrame(function() { _drawSpectrum(canvas, prof); }); } function _stopAudio() { _audioRecording = false; if (_audioAnimFrame) cancelAnimationFrame(_audioAnimFrame); if (_audioStream) _audioStream.getTracks().forEach(function(t) { t.stop(); }); if (_audioCtx) _audioCtx.close().catch(function() {}); _audioStream = null; _audioCtx = null; _audioAnalyser = null; } function _analyzeAudio(prof) { prof = prof || _earResolveProfile(); var modeCfg = ENGINE_EAR_MODES[prof.mode] || ENGINE_EAR_MODES.carb; if (!_audioSamples.length) { toast('No audio captured'); return; } var panel = document.getElementById('audio-hunter'); if (panel) panel.style.display = 'block'; var msg = document.getElementById('audio-hunter-msg'); if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Analyzing engine signature...'; } var avg = new Array(256).fill(0); _audioSamples.forEach(function(s) { s.forEach(function(v, i) { avg[i] += v; }); }); avg = avg.map(function(v) { return Math.round(v / _audioSamples.length); }); var lowE = 0, midE = 0, highE = 0, vHighE = 0; for (var i = 0; i < 32; i++) lowE += avg[i]; for (i = 32; i < 96; i++) midE += avg[i]; for (i = 96; i < 192; i++) highE += avg[i]; for (i = 192; i < 256; i++) vHighE += avg[i]; var total = lowE + midE + highE + vHighE || 1; var lowPct = Math.round(lowE / total * 100); var vHighPct = Math.round(vHighE / total * 100); var varSum = 0; _audioSamples.forEach(function(s) { var diff = 0; s.forEach(function(v, idx) { diff += Math.abs(v - avg[idx]); }); varSum += diff / 256; }); var stability = Math.max(0, 100 - Math.round(varSum / _audioSamples.length * 2)); var qEl = document.getElementById('audio-quality'); if (qEl) { qEl.textContent = stability + '%'; qEl.style.color = stability > 80 ? '#2DB87F' : stability > 60 ? 'var(--amber)' : 'var(--red)'; } var mix = 'OK'; if (modeCfg.showMix) { mix = lowPct > 45 ? 'RICH' : lowPct < 25 ? 'LEAN' : 'OK'; var mEl = document.getElementById('audio-mix'); if (mEl) { mEl.textContent = mix; mEl.style.color = mix === 'OK' ? '#2DB87F' : mix === 'RICH' ? 'var(--amber)' : 'var(--red)'; } } window._engineEarLive.mix = mix; window._engineEarLive.stability = stability; window._engineEarLive.ts = Date.now(); window._engineEarLive.profile = prof; var ctx = 'ENGINE EAR — ' + prof.label + '. Class RPM window ' + prof.rpmMin + '-' + prof.rpmMax + '. '; ctx += 'Session peak RPM ' + (_earSessionMax || '?') + '. Hold test ' + (_earHoldMs >= 900 ? 'PASSED (1s+ at peak)' : 'incomplete — hold WOT 1 full second') + '. '; ctx += 'Idle stability ' + stability + '%. '; if (modeCfg.showMix) ctx += 'Mix estimate ' + mix + ' (low freq ' + lowPct + '%). '; else if (prof.mode === 'sealed') ctx += 'Sealed motor — no jetting; advise gear/RPM only. '; else if (prof.mode === 'efi') ctx += 'EFI Spec Miata — no jetting; flat spot, ping, shift advice only. '; if (vHighPct > 15) ctx += 'High-frequency energy — possible ping/det. '; if (_earSessionMax > prof.rpmMax + 100) ctx += 'Over class RPM ceiling — limiter or too much gear. '; if (_earSessionMax && _earSessionMax < prof.rpmMin - 300) ctx += 'Never reached powerband — too tall gear or short straight. '; if (S.cur && S.cur.engine) ctx += 'Engine field: ' + S.cur.engine + '. '; if (S.wx) ctx += 'DA: ' + Math.round(S.wx.density_altitude || 0) + 'ft. '; if (modeCfg.hunterKind === 'jetting') ctx += 'Give jetting/steps in 80 words. Crew chief tone.'; else if (modeCfg.hunterKind === 'limiter_gear') ctx += 'Give gear/RPM advice only — sealed engine. 80 words.'; else ctx += 'Give shift point / flat spot / driving advice — no carb jetting. 80 words.'; var payload = { message: ctx, history: [] }; if (typeof _hunterPayload === 'function') payload = _hunterPayload({ message: ctx, history: [] }); else { payload.token = (typeof S !== 'undefined' && S.token) || ''; payload.car_class = S.cur && (S.cur.class || S.cur.car_class) || ''; payload.car_number = S.cur && S.cur.car_number || ''; } fetch(typeof HNTR !== 'undefined' ? HNTR : '/api/hunter', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(function(r) { return r.json(); }).then(function(d) { if (msg) { msg.textContent = d.reply || d.response || d.raw || 'No diagnosis.'; msg.style.color = 'var(--white)'; } }).catch(function() { if (msg) msg.textContent = 'Connection error.'; }); } var _wireToolsOrig = typeof wireToolsCalcs === 'function' ? wireToolsCalcs : null; wireToolsCalcs = function() { if (_wireToolsOrig) _wireToolsOrig(); setTimeout(_buildAudioTool, 150); }; wireToolsCalcs = function() { if (_wireToolsOrig) _wireToolsOrig(); setTimeout(_buildAudioTool, 150); }; wireToolsCalcs = function() { if (_wireToolsOrig) _wireToolsOrig(); setTimeout(_buildAudioTool, 150); }; (function(){ var _bc=buildContext; buildContext=function(){ var base=_bc(); if(window._lastGForce&&window._lastGForce.lat_max){ var gf=window._lastGForce; base+=(base?" | ":"")+"gforce:lat_peak="+gf.lat_max.toFixed(2)+"G"; if(gf.brake!=null)base+=" brake="+gf.brake.toFixed(2)+"G"; if(gf.accel!=null)base+=" accel="+gf.accel.toFixed(2)+"G"; } var ps=window._lastPocketSession||(_pocketLogger&&_pocketLogger.session); if(ps&&ps.laps&&ps.laps.length){ base+=(base?" | ":"")+"pocket_logger:laps="+ps.laps.length+" best="+(ps.bestLap?ps.bestLap.toFixed(2)+"s":"?")+" green="+ps.greenLaps+" caution="+ps.cautions+" pit="+ps.pitLaps; if(ps.maxLatG)base+=" max_lat_g="+ps.maxLatG.toFixed(2); } return base; }; })(); if("serviceWorker" in navigator){var _swBlob=new Blob([_SW_SCRIPT],{type:"text/javascript"});var _swUrl=URL.createObjectURL(_swBlob);navigator.serviceWorker.register(_swUrl,{scope:"/"}).then(function(reg){console.log("[BB] SW ok",reg.scope);}).catch(function(e){console.log("[BB] SW err",e);});} // ── #4 LOGBOOK PERSISTENCE ──────────────────────────────────────────────────── function populateLogbookTracks(){var sel=$("lb-track-sel");if(!sel)return;var cur=sel.value;sel.innerHTML='';if(S.curTrack){var o=document.createElement("option");o.value=S.curTrack.name;o.textContent=S.curTrack.name+" ★";sel.appendChild(o);}(S.tracks||[]).slice().sort(function(a,b){return a.name.localeCompare(b.name);}).forEach(function(t){if(S.curTrack&&t.name===S.curTrack.name)return;var o=document.createElement("option");o.value=t.name;o.textContent=t.name+(t.city?(" — "+t.city+", "+t.state):"");sel.appendChild(o);});if(cur)sel.value=cur;else if(S.curTrack)sel.value=S.curTrack.name;} function autoFillLogbook(){ populateLogbookTracks(); if(typeof renderPhoneSessionHistory==="function")renderPhoneSessionHistory(S.curTrack&&S.curTrack.id); if(typeof updateLogbookPositionPrompt==='function')updateLogbookPositionPrompt(); try{ var cached=localStorage.getItem('bb_last_night_breakdown'); if(cached&&typeof showNightBreakdownCard==='function'){ var bd=_normalizeNightBreakdown(JSON.parse(cached)); showNightBreakdownCard(bd); } }catch(e){} // Fill tire pressures from current setup if(_su){ var f=[['lb-psi-lf','lf_psi'],['lb-psi-rf','rf_psi'],['lb-psi-lr','lr_psi'],['lb-psi-rr','rr_psi'],['lb-stagger','stagger_r']]; f.forEach(function(p){var el=$(p[0]);if(el&&_su[p[1]]&&!el.value)el.value=_su[p[1]];}); } if(typeof _syncActiveSetupEverywhere==='function')_syncActiveSetupEverywhere(true); // Fill DA from weather var daEl=$("lb-da");if(daEl&&S.wx&&S.wx.density_altitude&&!daEl.value)daEl.value=Math.round(S.wx.density_altitude); var stEl=$("lb-surf-temp");if(stEl&&typeof _effectiveSurfaceTempF==='function'&&!stEl.value){var _st=_effectiveSurfaceTempF();if(_st!=null)stEl.value=_st;} var ambEl=$("lb-ambient-temp");if(ambEl&&S.wx&&S.wx.temp&&!ambEl.value)ambEl.value=Math.round(S.wx.temp); // Pre-fill from last visit at this track if(S.token&&S.cur&&S.curTrack){ fetch(AU+'?action=load',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:S.token,car_id:S.cur.id,data_type:'logbook',limit:10})}).then(function(r){return r.json();}).then(function(d){ if(!d.success||!d.data)return; var trackName=S.curTrack.name; var last=d.data.find(function(row){return row.data&&row.data.track===trackName;}); if(!last)return; var e=last.data; // Show hint var hint=document.createElement('div'); hint.style.cssText='background:rgba(200,150,10,.06);border:1px solid rgba(200,150,10,.15);border-left:3px solid var(--gold);padding:10px 12px;margin-bottom:12px;'; hint.innerHTML='
    LAST VISIT: '+trackName+'
    ' +'
    ' +(e.finish?'P'+e.finish+' ':'')+(e.type||'')+' · '+(e.da?'DA '+e.da+'ft · ':'') +new Date(e.ts||last.created_at).toLocaleDateString() +'
    '; var lbForm=$('logbook-form')||$('t-logbook'); if(lbForm)lbForm.insertBefore(hint,lbForm.firstChild); // Pre-fill gear if empty if(e.gear){var gEl=$('lb-gear');if(gEl&&!gEl.value)gEl.value=e.gear;} }).catch(function(){}); } } async function saveLogEntry(){ if(!_canDo('cadet')){_upgradeGate('Logbook','cadet','Cadet','$8');return;} if(!S.token||!S.cur||S.cur._demo){_upgradeGate('Logbook','cadet','Cadet','$8');return;} var btn=$("lb-save-btn"),msg=$("lb-save-msg"); var entry={ ts:new Date().toISOString(), type:($("lb-type")&&$("lb-type").value)||"feature", track:($("lb-track-sel")&&$("lb-track-sel").value)||"", finish:parseInt(($("lb-finish")&&$("lb-finish").value)||0)||null, start:parseInt(($("lb-start")&&$("lb-start").value)||0)||null, cars:parseInt(($("lb-cars")&&$("lb-cars").value)||0)||null, best_lap:parseFloat(($("lb-bestlap")&&$("lb-bestlap").value)||0)||null, cond_start:($("lb-cond-start")&&$("lb-cond-start").value)||"", cond_end:($("lb-cond-end")&&$("lb-cond-end").value)||"", psi:{lf:parseFloat($("lb-psi-lf")&&$("lb-psi-lf").value)||null,rf:parseFloat($("lb-psi-rf")&&$("lb-psi-rf").value)||null,lr:parseFloat($("lb-psi-lr")&&$("lb-psi-lr").value)||null,rr:parseFloat($("lb-psi-rr")&&$("lb-psi-rr").value)||null}, stagger:parseFloat($("lb-stagger")&&$("lb-stagger").value)||null, gear:($("lb-gear")&&$("lb-gear").value)||"", shocks:{lf:parseInt($("lb-sh-lf")&&$("lb-sh-lf").value)||null,rf:parseInt($("lb-sh-rf")&&$("lb-sh-rf").value)||null,lr:parseInt($("lb-sh-lr")&&$("lb-sh-lr").value)||null,rr:parseInt($("lb-sh-rr")&&$("lb-sh-rr").value)||null}, geometry:typeof _collectGeometry==='function'?_collectGeometry():null, notes:($("lb-notes")&&$("lb-notes").value.trim())||"", jetting:($("lb-jetting")&&$("lb-jetting").value.trim())||"", da:S.wx?Math.round(S.wx.density_altitude||0):null, surface_temp_f:parseFloat($("lb-surf-temp")&&$("lb-surf-temp").value)||(typeof _effectiveSurfaceTempF==='function'?_effectiveSurfaceTempF():null), ambient_temp_f:parseFloat($("lb-ambient-temp")&&$("lb-ambient-temp").value)||(S.wx?Math.round(S.wx.temp||0):null), car_number:S.cur.car_number, car_class:S.cur.class||S.cur.car_class }; if(!entry.track){toast("Select a track first");return;} if(!entry.finish){ var flagged=false; try{flagged=localStorage.getItem('bb_needs_position')==='1';}catch(e){} if(flagged){ toast("Add your finishing position — we ask again here if you skipped debrief"); if(typeof updateLogbookPositionPrompt==='function')updateLogbookPositionPrompt(); var fp=$('lb-finish-prompt'); if(fp&&fp.value)entry.finish=parseInt(fp.value,10)||null; if(!entry.finish&&$('lb-finish'))entry.finish=parseInt($('lb-finish').value,10)||null; if(!entry.finish)return; } } var timingMeta=_collectTimingUploadMeta('lb'); if(timingMeta)Object.assign(entry,timingMeta); if(btn){btn.textContent="SAVING...";btn.disabled=true;} try{ var r=await fetch(AU+"?action=save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:S.token,car_id:S.cur.id,data_type:"logbook",data:entry})}); var d=await r.json(); if(d.success){ toast("Logged \u2714"); try{localStorage.removeItem('bb_needs_position');}catch(e){} if(typeof updateLogbookPositionPrompt==='function')updateLogbookPositionPrompt(); if(typeof showNightBreakdownCard==='function')showNightBreakdownCard(buildNightBreakdown(entry)); if(typeof _showLogbookCoach==='function')_showLogbookCoach(entry); if(typeof _showScaleFeedbackPanel==='function')_showScaleFeedbackPanel('lb','Tell Hunter how tonight felt after any scale work.'); if(typeof fetchPostLogRecommendation==='function')fetchPostLogRecommendation(entry,'logbook'); if(typeof _syncActiveSetupEverywhere==='function')_syncActiveSetupEverywhere(true); if(msg){msg.textContent="Saved \u2714";msg.style.display="block";msg.style.color="var(--green)";setTimeout(function(){msg.style.display="none";},3000);} // Clear notes fields only if($("lb-notes"))$("lb-notes").value=""; if($("lb-jetting"))$("lb-jetting").value=""; if($("lb-finish"))$("lb-finish").value=""; if($("lb-start"))$("lb-start").value=""; } else { toast("Save failed: "+(d.error||"")); if(msg){msg.textContent="Save failed";msg.style.display="block";msg.style.color="var(--red)";} } }catch(e){toast("Connection error");} if(btn){btn.textContent="SAVE TO LOGBOOK";btn.disabled=false;} } async function loadLogbook(){ var el=$("logbook-list");if(!el)return; if(S._demo&&typeof _renderDemoLogbook==='function'){_renderDemoLogbook(el);return;} if(!S.token||!S.cur||S.cur._demo){ el.innerHTML=''; el.appendChild(_demoBanner('Sign up to log race nights, track your history, and see season stats')); el.innerHTML+='
    LOGBOOK SAVES TO YOUR ACCOUNT
    Every race night, every track, every setup snapshot
    '; return; } el.innerHTML='
    LOADING...
    '; try{ var r=await fetch(AU+"?action=load",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:S.token,car_id:S.cur.id,data_type:"logbook",limit:50})}); var d=await r.json(); if(!d.success||!d.data||!d.data.length){ el.innerHTML='
    NO ENTRIES YET
    Log your first race night on the Logbook tab
    '; return; } el.innerHTML=""; // Header var hdr=document.createElement("div"); hdr.style.cssText="display:flex;align-items:center;gap:8px;margin-bottom:12px"; hdr.innerHTML='Race History'+d.data.length+' ENTRIES'; el.appendChild(hdr); // Season stats summary var entries = d.data.map(function(r){return r.data;}); var features = entries.filter(function(e){return e.type==='feature';}); var wins = features.filter(function(e){return e.finish===1;}).length; var top3 = features.filter(function(e){return e.finish&&e.finish<=3;}).length; var starts = features.length; var daVals = entries.map(function(e){return e.da;}).filter(function(v){return v&&!isNaN(v);}); var avgDA = daVals.length ? Math.round(daVals.reduce(function(a,b){return a+b;},0)/daVals.length) : null; var statsRow = document.createElement('div'); statsRow.style.cssText='display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:12px;'; var statDefs=[ {label:'WINS',val:wins,color:'var(--gold)'}, {label:'TOP 3',val:top3,color:'var(--amber)'}, {label:'STARTS',val:starts,color:'var(--white)'}, {label:'AVG DA',val:avgDA!=null?avgDA.toLocaleString()+'ft':'--',color:'var(--muted)'} ]; statDefs.forEach(function(s){ statsRow.innerHTML+='
    ' +'
    '+s.val+'
    ' +'
    '+s.label+'
    ' +'
    '; }); el.appendChild(statsRow); // Track-by-track breakdown var byTrack={}; features.forEach(function(e){ var tn=e.track||'Unknown'; if(!byTrack[tn])byTrack[tn]={starts:0,wins:0,top3:0,best:99}; byTrack[tn].starts++; if(e.finish===1)byTrack[tn].wins++; if(e.finish&&e.finish<=3)byTrack[tn].top3++; if(e.finish&&e.finish1){ var tbDiv=document.createElement('div'); tbDiv.style.cssText='margin-bottom:12px;'; var tbHdr=document.createElement('div'); tbHdr.style.cssText='font-family:Share Tech Mono;font-size:10px;letter-spacing:2px;color:var(--muted);margin-bottom:6px;cursor:pointer;display:flex;align-items:center;gap:6px'; tbHdr.innerHTML='BY TRACK ('+trackKeys.length+') tap to expand'; var tbBody=document.createElement('div'); tbBody.style.cssText='display:none;'; trackKeys.sort(function(a,b){return byTrack[b].starts-byTrack[a].starts;}).forEach(function(tn){ var t=byTrack[tn]; tbBody.innerHTML+='
    '+tn+'
    '+t.starts+' starts'+(t.wins?' · '+t.wins+'W':'')+(t.top3>t.wins?' · '+t.top3+' top3':'')+(t.best<99?' · best P'+t.best:'')+'
    '; }); tbHdr.onclick=function(){tbBody.style.display=tbBody.style.display==='none'?'block':'none';}; tbDiv.appendChild(tbHdr);tbDiv.appendChild(tbBody);el.appendChild(tbDiv); } d.data.forEach(function(row){ var e=row.data;var dt=new Date(e.ts||row.created_at); var card=document.createElement("div"); card.style.cssText="background:var(--dark2);border:1px solid rgba(208,25,14,.08);border-left:3px solid "+(e.finish===1?"var(--gold)":e.finish<=3?"var(--amber)":"rgba(208,25,14,.3)")+";padding:10px 12px;margin-bottom:6px;cursor:pointer"; var typeLabel={feature:"FEATURE",heat:"HEAT",practice:"PRACTICE",qualifier:"QUALIFIER"}[e.type]||(e.type||"").toUpperCase(); var finStr=e.finish?("P"+e.finish+(e.cars?"/"+e.cars+" cars":"")):""; var h='
    ' +'
    '+(e.track||"Unknown Track")+'
    ' +'
    '+dt.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"2-digit"})+'
    ' +'
    ' +''+typeLabel+'' +(finStr?''+finStr+'':"") +(e.da?'DA '+e.da.toLocaleString()+'ft':"") +(e.confidence?'CONF '+e.confidence+'/10':"") +'
    '; if(e.notes)h+='
    '+e.notes+'
    '; // Expandable detail var detId='lb-det-'+row.id; h+=''; card.innerHTML=h; card.onclick=function(){var det=document.getElementById(detId);if(det)det.style.display=det.style.display==='none'?'block':'none';}; el.appendChild(card); }); }catch(err){el.innerHTML='
    Could not load logbook
    ';} } /* ── COACH STEPPED DEBRIEF ── */ var _DBF={ step:0, ans:{}, steps:[ { q:"How did tonight go?", sub:"overall result", key:"result", opts:["WIN","STRONG RUN","AVERAGE","ROUGH NIGHT","DNF / DNS"] }, { q:"Where was the car off?", sub:"corner position", key:"corner", opts:["ENTRY","MID-CORNER","EXIT","EVERYWHERE","FELT GOOD"] }, { q:"When did it start?", sub:"onset of handling issue", key:"onset", opts:["FIRST LAP","AFTER HEAT RACE","TRACK CHANGED","ALL NIGHT","NOT APPLICABLE"] }, { q:"What did you change?", sub:"mid-race adjustments", key:"changes", opts:["NOTHING CHANGED"], hasText:true, placeholder:"Describe any changes made — shocks, PSI, wedge, wing..." }, { q:"How were you tonight?", sub:"driver condition", key:"driver", opts:["LOCKED IN","SOLID","A BIT OFF","DISTRACTED","PHYSICAL ISSUE"] }, { q:"Compared to the field?", sub:"relative pace", key:"pace", opts:["FASTEST GROUP","INSIDE TOP 5","MID-PACK","BACK MARKERS","ONLY ISSUE WAS ME"] } ] }; function openDebrief(){ if(!S.token||S._demo){_upgradeGate('Coach Debrief','cadet','Cadet','$8');return;} if(!_canDo('cadet')){_upgradeGate('Coach Debrief','cadet','Cadet','$8');return;} _DBF.step=0;_DBF.ans={}; document.getElementById("dbf-overlay").style.display="flex"; _dbfRender(); var sheet=document.querySelector('.dbf-sheet'); if(sheet&&typeof _bbMobileKeyboardScroll==='function')_bbMobileKeyboardScroll(sheet); } function closeDebrief(){ if(window._pendingDebrief&&!window._pendingDebrief._saved){ try{localStorage.setItem('bb_needs_position','1');}catch(e){} if(typeof updateLogbookPositionPrompt==='function')updateLogbookPositionPrompt(); } document.getElementById("dbf-overlay").style.display="none"; } async function saveDebriefData(extra){ if(!window._pendingDebrief)window._pendingDebrief={}; Object.assign(window._pendingDebrief,extra||{}); return window._pendingDebrief; } async function saveDebriefResults(){ var positionEl=document.getElementById('debrief-position'); var fileInput=document.getElementById('debrief-timing-file'); var position=positionEl&&positionEl.value?positionEl.value.trim():''; var file=fileInput&&fileInput.files&&fileInput.files[0]?fileInput.files[0]:null; var results={ finishing_position:position?parseInt(position,10):null, position:position?parseInt(position,10):null, has_timing_data:!!file, has_timing_upload:!!file, timing_upload_name:file?file.name:null }; if(file){ results.timing_upload_pending=true; // Future: send file for OCR / parsing } await saveDebriefData(results); if (typeof _fiMarkDebriefComplete === 'function') _fiMarkDebriefComplete(); if(results.finishing_position){ if($('lb-finish'))$('lb-finish').value=results.finishing_position; try{localStorage.removeItem('bb_needs_position');}catch(e){} }else{ try{localStorage.setItem('bb_needs_position','1');}catch(e){} } if(typeof updateLogbookPositionPrompt==='function')updateLogbookPositionPrompt(); return results; } async function saveDebriefPositionAndData(){ return saveDebriefResults(); } function updateLogbookPositionPrompt(){ var prompt=$('lb-position-prompt'); var finish=$('lb-finish'); if(!prompt)return; var need=!finish||!finish.value; var flagged=false; try{flagged=localStorage.getItem('bb_needs_position')==='1';}catch(e){} prompt.style.display=(need&&flagged)?'block':'none'; if(need&&flagged&&finish&&finish.value){ var fp=$('lb-finish-prompt'); if(fp&&!fp.value)fp.value=finish.value; } } function _collectTimingUploadMeta(prefix){ var fileInput=$(prefix+'-timing-upload'); if(!fileInput||!fileInput.files||!fileInput.files.length)return null; return{ has_timing_upload:true, timing_upload_name:fileInput.files[0].name, timing_upload_pending:true }; } function _normalizeNightBreakdown(b){ if(!b)return null; if(b.event&&b.results)return b; if(b.event_summary){ return{ event:{track:b.event_summary.track||'',date:b.event_summary.date||'',event_name:'Weekly Racing'}, results:{ finishing_position:b.event_summary.finishing_position||null, laps_completed:b.event_summary.laps_completed||0, cautions_encountered:b.event_summary.cautions||0, best_lap:null }, highlights:b.performance_highlights||[], hunter_insights:b.hunter_notes||[], hunter_card_takeaway:b.hunter_card_takeaway||null, public_friendly:b.public_shareable!==false }; } return b; } function _fmtBestLap(sec){ if(sec==null||sec===''||isNaN(Number(sec)))return null; var n=Number(sec); if(n>120)return String(n); return n.toFixed(2); } var RECAP_SENSITIVE_RE = /\b(psi|stagger|shock|rebound|compression|valving|\bcross\b|\bwedge\b|ballast|\blf\b|\brf\b|\blr\b|\brr\b|jetting|gear ratio|setup sheet|weight hint|\d+\.\d+\s*%\s*(left|rear|bite)?)/i; function sanitizeRecapText(raw, maxLen){ if(raw==null||raw==='')return ''; var text=String(raw).replace(//gi,'. ').replace(/<[^>]+>/g,' ').replace(/\s+/g,' ').trim(); if(!text)return ''; var parts=text.split(/[.!?]+/).map(function(p){return p.trim();}).filter(Boolean); var safe=parts.filter(function(p){return p&&!RECAP_SENSITIVE_RE.test(p);}); text=(safe.length?safe:(parts[0]?[parts[0]]:[])).join('. ').replace(/\s+/g,' ').trim(); if(!text||RECAP_SENSITIVE_RE.test(text))return ''; maxLen=maxLen||96; if(text.length>maxLen)text=text.slice(0,maxLen-1)+'…'; return text; } function _pickRankedRecapItems(candidates,min,max){ min=min||2;max=max||4; var seen={},out=[]; candidates.slice().sort(function(a,b){return b.score-a.score;}).forEach(function(item){ if(out.length>=max)return; var key=item.key||item.text; if(seen[key])return; seen[key]=true; if(item.text)out.push(item.text); }); return out.slice(0,max); } function _scoreHunterRecText(text){ var t=sanitizeRecapText(text,110); if(!t||t.length<14)return null; var score=40; if(/\b(next|before|try|focus|keep|save|repeat|priority)\b/i.test(t))score+=24; if(/\b(consistency|baseline|long run|feature|podium|win)\b/i.test(t))score+=12; if(t.length>100)score-=18; if(t.length<40)score+=6; return {text:t,score:score,key:'rec_'+t.slice(0,24)}; } function _collectRecapHighlightCandidates(source,ctx){ var c=[]; function add(text,score,key){ var t=sanitizeRecapText(text,88); if(!t)return; c.push({text:t,score:score,key:key||t}); } var position=ctx.position; if(source.result==='WIN'||position===1)add('Feature win — executed when it mattered',100,'win'); else if(position&&position<=3)add('P'+position+' in the feature — podium night',92,'podium'); else if(position&&position<=5)add('Top-five finish in the feature',78,'top5'); if(ctx.phoneConsistency!=null&&Number(ctx.phoneConsistency)>=0.65){ add('Strong lap-to-lap consistency in logged stints',86,'phone_consist_high'); }else if(ctx.phoneConsistency!=null&&Number(ctx.phoneConsistency)>=0.55){ add('Consistent pace across logged stints',74,'phone_consist'); } if(ctx.scaleFollow)add(ctx.scaleFollow.text,ctx.scaleFollow.score,ctx.scaleFollow.key); if(ctx.bestLap)add('Best lap '+ctx.bestLap+'s on the night',80,'best_lap'); if(source.pace==='FASTEST GROUP'||source.pace==='INSIDE TOP 5')add('Ran with the leaders on long green runs',76,'pace'); else if(source.result==='STRONG RUN')add('Strong runs when the track came to you',72,'strong_run'); if(source.corner==='FELT GOOD')add('Car felt balanced through the night',58,'felt_good'); if(ctx.lapsCompleted>=10&&!c.some(function(x){return x.key==='laps';})){ add(ctx.lapsCompleted+' laps logged across the night',52,'laps'); } if(!c.length)add('Race night captured — safe summary ready for recap',30,'fallback'); return c; } function _collectHunterTakeawayCandidates(source,ctx){ var c=[]; function add(text,score,key){ var t=sanitizeRecapText(text,110); if(!t||t.length<12)return; c.push({text:t,score:score,key:key||t}); } var position=ctx.position; var corner=String(source.corner||'').toUpperCase(); if(corner==='ENTRY'||corner==='MID-CORNER'||corner==='EXIT'){ var cornerLabel=corner.toLowerCase().replace('-corner',' corner').replace(/-/g,' '); if(source.changes&&String(source.changes).trim()&&source.changes!=='NOTHING CHANGED'){ add('Keep building on what worked through '+cornerLabel,88,'corner_fix'); }else{ add('Priority next visit: tighten up '+cornerLabel,84,'corner_focus'); } }else if(corner==='FELT GOOD'&&(source.result==='WIN'||position===1||(position&&position<=3))){ add('Save tonight as your baseline — repeat before chasing speed',90,'baseline'); }else if(corner==='FELT GOOD'){ add('Handling felt balanced — carry this routine forward',70,'felt_balanced'); } if(ctx.phoneConsistency!=null&&Number(ctx.phoneConsistency)>=0.6){ add('Consistency trend is there — same routine before hot laps',82,'phone_action'); }else if(ctx.phoneConsistency!=null&&Number(ctx.phoneConsistency)>0&&Number(ctx.phoneConsistency)<0.45){ add('Focus on repeatability before more setup changes',80,'phone_refine'); } if(ctx.scaleFollow)add('Scale work logged — note how the car responded before the next change',72,'scale_follow'); (ctx.recCandidates||[]).forEach(function(r){if(r)c.push(r);}); if(source.pace==='BACK MARKERS'||source.result==='ROUGH NIGHT'){ add('Compare best lap to the field before chasing handling',68,'rough_night'); } if(!c.length)add('Log more stints with the phone logger for deeper Hunter reads',20,'fallback'); return c; } function selectBestHunterCardTakeaway(candidates){ if(!candidates||!candidates.length)return sanitizeRecapText('Solid race night — keep logging stints for sharper Hunter reads.',100); var sorted=candidates.slice().sort(function(a,b){return b.score-a.score;}); return sorted[0].text; } function _scaleFollowSignal(carId){ var recent=typeof getMostRecentScaleChange==='function'?getMostRecentScaleChange():null; if(!recent)return null; try{ var learn=JSON.parse(localStorage.getItem('bb_scale_learn_'+(carId||'local'))||'{}'); var t=recent.type; if(t&&learn[t]&&learn[t].length>=2){ return {text:'Scale adjustments tracked — pattern building for this car',score:74,key:'scale_pattern'}; } }catch(e){} if(recent.deltas&&Math.abs(recent.deltas.cross||0)>=0.25){ return {text:'Logged scale change carried into the feature',score:68,key:'scale_logged'}; } return null; } function _raceNightToDebriefSource(rData, entry){ rData=rData||{}; entry=entry||{}; var pos=rData.result_pos||entry.finish||null; var result='STRONG RUN'; if(pos===1)result='WIN'; var corner='FELT GOOD'; var loose=['loose','very_loose']; var tight=['tight','very_tight']; if(loose.indexOf(rData.feel_exit)>=0)corner='EXIT'; else if(loose.indexOf(rData.feel_center)>=0)corner='MID-CORNER'; else if(tight.indexOf(rData.feel_entry)>=0)corner='ENTRY'; else if(tight.indexOf(rData.feel_center)>=0)corner='MID-CORNER'; var pace='MID-PACK'; if(pos===1)pace='FASTEST GROUP'; else if(pos&&pos<=5)pace='INSIDE TOP 5'; return{ ts:entry.ts||new Date().toISOString(), track:entry.track||(S.curTrack&&S.curTrack.name)||'', finishing_position:pos, position:pos, finish:pos, result:result, corner:corner, pace:pace, changes:rData.key_change||'', changes_txt:rData.key_change||'', debrief_note:rData.debrief_note||'', best_lap:entry.best_lap||null }; } function buildNightBreakdown(source){ source=source||window._pendingDebrief||{}; var trackName=source.track||(S.curTrack&&S.curTrack.name)||''; var dateStr=(source.ts||new Date().toISOString()).slice(0,10); var position=source.finishing_position||source.position||source.finish||null; var lapsCompleted=0; var cautions=0; var bestLap=null; try{ var rk=typeof _plRaceNightKey==='function'?_plRaceNightKey():null; var rData=rk&&typeof _raceLoad==='function'?_raceLoad(rk):null; if(rData&&rData.telemetry_stints){ rData.telemetry_stints.forEach(function(s){ lapsCompleted+=((s.laps&&s.laps.length)||s.greenLaps||0); cautions+=(s.cautions||0); if(s.bestLap&&(!bestLap||s.bestLap' +'
    '+(ev.track||'Race Night')+'
    ' +'
    '+(ev.date||'')+' · '+posTxt+' · '+(ev.event_name||'Weekly Racing')+'
    '; if(recapData.include_shirt){ previewHtml+='
    RECAP SHIRT
    ' +'
      '; (breakdown.highlights||[]).slice(0,3).forEach(function(h){previewHtml+='
    • '+h+'
    • ';}); previewHtml+='
    '; } if(recapData.include_hunter_card){ var cardLine=sanitizeRecapText(breakdown.hunter_card_takeaway||(breakdown.hunter_insights&&breakdown.hunter_insights[0])||'',110); previewHtml+='
    HUNTER CARD
    ' +'
    '+(cardLine||'—')+'
    '; } previewHtml+='
    ' +'' +'
    ' +'' +'' +'' +'
    '; sheet.innerHTML=previewHtml; overlay.appendChild(sheet); document.body.appendChild(overlay); $('recap-order-now-btn').onclick=function(){openMerchBuilderWithRecap(recapData,'order');}; $('recap-edit-design-btn').onclick=function(){openMerchBuilderWithRecap(recapData,'edit');}; } function addHunterCardToShirt(){ addHunterCard(); } function openMerchBuilderWithRecap(recapData,mode){ recapData=recapData||window._pendingRecapMerch; if(!recapData){toast('No recap data — run Create Merch first');return;} mode=mode||'order'; var addPage=$('recap-add-to-page'); recapData.add_to_driver_page=addPage?!!addPage.checked:!!recapData.add_to_driver_page; recapData.build_mode=mode; window._pendingRecapMerch=recapData; try{ sessionStorage.setItem('recap_merch_draft',JSON.stringify(recapData)); localStorage.setItem('bb_recap_merch_payload',JSON.stringify(recapData)); }catch(e){} toast(mode==='edit'?'Opening design editor…':'Opening merch builder…'); window.location.href='/merch?mode=recap'; } function _escRecapAttr(s){ return String(s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/' +'' +'' +''; } function populateRecapEditor(breakdown){ breakdown=_normalizeNightBreakdown(breakdown)||buildNightBreakdown(); window._lastNightBreakdown=breakdown; var res=breakdown.results||{}; var posEl=document.getElementById('recap-position'); if(posEl)posEl.value=res.finishing_position||''; var hWrap=document.getElementById('recap-highlights'); if(hWrap){ hWrap.innerHTML=(breakdown.highlights||[]).map(function(h,i){return _recapEditorRow(h,'highlight',i);}).join(''); } var nWrap=document.getElementById('recap-hunter-notes'); if(nWrap){ nWrap.innerHTML=(breakdown.hunter_insights||[]).map(function(n,i){return _recapEditorRow(n,'hunter',i);}).join(''); } } function addHighlight(){ syncBreakdownFromEditor(); var b=window._lastNightBreakdown; b.highlights=b.highlights||[]; b.highlights.push(''); populateRecapEditor(b); var inputs=document.querySelectorAll('#recap-highlights .recap-highlight-input'); if(inputs.length)inputs[inputs.length-1].focus(); } function addHunterNote(){ syncBreakdownFromEditor(); var b=window._lastNightBreakdown; b.hunter_insights=b.hunter_insights||[]; b.hunter_insights.push(''); populateRecapEditor(b); var inputs=document.querySelectorAll('#recap-hunter-notes .recap-hunter-input'); if(inputs.length)inputs[inputs.length-1].focus(); } function removeRecapItem(type,index){ syncBreakdownFromEditor(); var b=window._lastNightBreakdown; if(type==='highlight')b.highlights.splice(index,1); else b.hunter_insights.splice(index,1); populateRecapEditor(b); } async function createRecapShirt(){ var breakdown=getCustomizedBreakdown(); var recapDraft={ type:'recap_shirt', event:{track:breakdown.track_name,date:breakdown.date}, results:{finishing_position:breakdown.position}, highlights:breakdown.highlights, hunter_notes:breakdown.hunter_notes, driver_id:S.cur&&S.cur.id, driver_number:S.cur&&S.cur.car_number, driver_name:S.cur&&(S.cur.name||S.cur.driver_name), car_class:S.cur&&(S.cur.class||S.cur.car_class), created_from_post_race:true }; window._pendingRecapMerch=recapDraft; var addPageEl=document.getElementById('recap-add-to-driver-page'); recapDraft.add_to_driver_page=addPageEl?!!addPageEl.checked:true; var saved=await saveRecapMerchDraft(recapDraft); try{ sessionStorage.setItem('recap_merch_draft',JSON.stringify(recapDraft)); localStorage.setItem('bb_recap_merch_payload',JSON.stringify(recapDraft)); if(recapDraft.add_to_driver_page)localStorage.setItem('bb_recap_pending_publish','1'); }catch(e){} toast(saved?'Recap shirt draft saved':'Draft saved locally — sign in to sync'); if(recapDraft.add_to_driver_page){ showPublishRecapPanel(); return; } window.location.href='/merch?mode=recap'; } async function addHunterCard(){ var breakdown=getCustomizedBreakdown(); var cardNote=breakdown.hunter_card_takeaway||(breakdown.hunter_notes&&breakdown.hunter_notes[0])||''; cardNote=sanitizeRecapText(cardNote,110); var hunterCardDraft={ type:'hunter_card', event:{track:breakdown.track_name,date:breakdown.date}, notes:cardNote?[cardNote]:[], hunter_card_takeaway:cardNote, driver_id:S.cur&&S.cur.id, driver_number:S.cur&&S.cur.car_number, driver_name:S.cur&&(S.cur.name||S.cur.driver_name), created_from_post_race:true }; await saveBbDraft('hunter_card_draft',hunterCardDraft); openHunterCardOptions(hunterCardDraft); } function openHunterCardOptions(hunterCardDraft){ hunterCardDraft=hunterCardDraft||{}; var existing=document.getElementById('hunter-card-options-overlay'); if(existing)existing.remove(); var overlay=document.createElement('div'); overlay.id='hunter-card-options-overlay'; overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:1003;display:flex;align-items:center;justify-content:center;padding:20px'; overlay.onclick=function(e){if(e.target===overlay)overlay.remove();}; var ev=hunterCardDraft.event||{}; var sheet=document.createElement('div'); sheet.style.cssText='background:var(--dark2);border:1px solid rgba(220,38,38,.35);border-top:3px solid var(--red);max-width:400px;width:100%;padding:20px'; sheet.innerHTML='
    HUNTER CARD
    ' +'
    '+(ev.track||'Race Night')+'
    ' +'
    '+(ev.date||'')+'
    ' +'

    Add Hunter notes to your recap shirt, or build a standalone Hunter card.

    ' +'' +'' +''; overlay.appendChild(sheet); document.body.appendChild(overlay); document.getElementById('hco-add-shirt').onclick=function(){ var breakdown=getCustomizedBreakdown(); var recapDraft={ type:'recap_shirt', event:{track:breakdown.track_name,date:breakdown.date}, results:{finishing_position:breakdown.position}, highlights:breakdown.highlights, hunter_notes:breakdown.hunter_notes, include_hunter_card:true, driver_id:S.cur&&S.cur.id, driver_number:S.cur&&S.cur.car_number, driver_name:S.cur&&(S.cur.name||S.cur.driver_name), car_class:S.cur&&(S.cur.class||S.cur.car_class), created_from_post_race:true }; overlay.remove(); window._pendingRecapMerch=recapDraft; saveRecapMerchDraft(recapDraft); try{ sessionStorage.setItem('recap_merch_draft',JSON.stringify(recapDraft)); localStorage.setItem('bb_recap_merch_payload',JSON.stringify(recapDraft)); }catch(e){} window.location.href='/merch?mode=recap'; }; document.getElementById('hco-standalone').onclick=function(){ overlay.remove(); try{ sessionStorage.setItem('recap_merch_draft',JSON.stringify(hunterCardDraft)); localStorage.setItem('bb_recap_merch_payload',JSON.stringify(hunterCardDraft)); }catch(e){} window.location.href='/merch?mode=recap&hunter=1'; }; } function showPublishRecapPanel(){ var draft; try{draft=JSON.parse(sessionStorage.getItem('recap_merch_draft')||localStorage.getItem('bb_recap_merch_payload')||'{}');}catch(e){draft={};} if(!draft||!draft.driver_id){ try{draft=window._pendingRecapMerch||{};}catch(e2){draft={};} } window._pendingRecapMerch=draft; var existing=document.getElementById('recap-publish-overlay'); if(existing)existing.remove(); var overlay=document.createElement('div'); overlay.id='recap-publish-overlay'; overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.82);z-index:1004;display:flex;align-items:center;justify-content:center;padding:20px'; overlay.onclick=function(e){if(e.target===overlay)skipPublish();}; var sheet=document.createElement('div'); sheet.style.cssText='background:var(--dark2);border:1px solid rgba(59,130,246,.35);max-width:420px;width:100%;padding:20px;border-radius:10px'; sheet.innerHTML='
    ' +'Publish to Your Driver Page?' +'
    This will show your recap on your public page so fans can see it and buy the shirt.
    ' +'' +'' +'
    '; overlay.appendChild(sheet); document.body.appendChild(overlay); document.getElementById('recap-publish-yes').onclick=function(){publishRecapToDriverPage();}; document.getElementById('recap-publish-skip').onclick=function(){skipPublish();}; } async function publishRecapToDriverPage(){ var draft; try{draft=JSON.parse(sessionStorage.getItem('recap_merch_draft')||localStorage.getItem('bb_recap_merch_payload')||'{}');}catch(e){draft={};} draft=draft||window._pendingRecapMerch||{}; if(!draft.driver_id){toast('No recap draft found');skipPublish();return;} var recapEntry={ id:'r'+Date.now(), driver_id:draft.driver_id, recap_type:draft.type||'recap_shirt', event:draft.event||{}, results:draft.results||{}, highlights:(draft.highlights||[]).map(function(h){return sanitizeRecapText(h,88);}).filter(Boolean).slice(0,4), hunter_notes:(draft.hunter_notes||draft.notes||[]).map(function(h){return sanitizeRecapText(h,110);}).filter(Boolean).slice(0,2), hunter_card_takeaway:sanitizeRecapText(draft.hunter_card_takeaway||((draft.hunter_notes||draft.notes||[])[0])||'',110), visible:true, created_at:Date.now() }; var existingRecaps=[]; try{ var lr=await fetch(AU+'?action=load',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:S.token,car_id:S.cur.id,data_type:'driver_page_recaps',limit:1})}); var ld=await lr.json(); if(ld.success&&ld.data&&ld.data.length&&ld.data[0].data&&ld.data[0].data.recaps)existingRecaps=ld.data[0].data.recaps.slice(); }catch(e){} existingRecaps.unshift(recapEntry); existingRecaps=existingRecaps.filter(function(r){return r.visible!==false;}).slice(0,16); var saved=await saveBbDraft('driver_page_recaps',{recaps:existingRecaps}); try{ var authTok=(S&&S.token)||null; try{var auth=JSON.parse(localStorage.getItem('bb_auth')||'null');if(auth&&auth.token)authTok=auth.token;}catch(e0){} if(authTok){ var ar=await fetch('/driver-page-api',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({action:'append_recap',token:authTok,recap:recapEntry}) }); var arj=await ar.json(); if(arj.ok&&arj.recap)recapEntry=arj.recap; } }catch(e2){} try{localStorage.removeItem('bb_recap_pending_publish');}catch(e3){} var pub=document.getElementById('recap-publish-overlay'); if(pub)pub.remove(); toast(saved?'Added to your driver page.':'Recap saved — driver page sync pending.'); window.location.href='/merch?mode=recap'; } function skipPublish(){ try{localStorage.removeItem('bb_recap_pending_publish');}catch(e){} var pub=document.getElementById('recap-publish-overlay'); if(pub)pub.remove(); window.location.href='/merch?mode=recap'; } function _renderRecapDraftGuidance(){ return (typeof _renderGarageFlowStrip==='function'?_renderGarageFlowStrip('recap'):'') +'
    ' +'
    Your recap draft is ready
    ' +'
    Debrief saved — review highlights and Hunter notes below. Tweak anything that does not sound right, then share or create your recap shirt.
    ' +'
    '; } function _renderPostRaceLearningTeaser(breakdown){ breakdown=breakdown||{}; var line=breakdown.patterns_this_month; var body=line&&line.text?line.text:'Hunter is tracking patterns from your scale logs, phone data, and race nights.'; return '
    ' +'
    WHAT HUNTER HAS LEARNED
    ' +'
    '+_escapeRecHtml(body)+'
    ' +'' +'
    '; } function shareRecapDraft(){ syncBreakdownFromEditor(); var b=getCustomizedBreakdown(); var lines=[(b.track_name||'Race Night')+(b.date?' — '+b.date:'')]; if(b.position)lines.push('Finish: P'+b.position); if(b.highlights&&b.highlights.length){lines.push('');b.highlights.forEach(function(h){lines.push('• '+h);});} if(b.hunter_notes&&b.hunter_notes.length){lines.push('');b.hunter_notes.forEach(function(n){lines.push('→ '+n);});} var text=lines.join('\n'); if(navigator.share){ navigator.share({title:'Race Night Recap',text:text}).catch(function(){ if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(text).then(function(){toast('Recap copied — paste anywhere');}); }); }else if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(function(){toast('Recap copied — paste anywhere');}).catch(function(){toast('Could not copy recap');}); }else{toast('Copy highlights from the editor to share');} } function _renderPostRaceBreakdownHTML(breakdown){ breakdown=_normalizeNightBreakdown(breakdown)||buildNightBreakdown(); var ev=breakdown.event||{}; var res=breakdown.results||{}; return _renderRecapDraftGuidance() +_renderPostRaceLearningTeaser(breakdown) +'
    ' +'
    Post-Race Night Breakdown
    ' +'
    '+(ev.track||'Race Night')+' — '+(ev.date||'')+'
    ' +'
    ' +'
    ' +'
    ' +'
    ' +'
    ' +'
    ' +'
    ' +'
    ' +'' +'
    ' +'' +'' +'' +'' +'
    ' +'
    Safe summary only — no setup secrets or private notes.
    ' +'
    '; } function _renderNightBreakdownInner(breakdown){ return _renderPostRaceBreakdownHTML(breakdown); } function showNightBreakdownCard(breakdown){ breakdown=_normalizeNightBreakdown(breakdown)||buildNightBreakdown(); window._lastNightBreakdown=breakdown; try{localStorage.setItem('bb_last_night_breakdown',JSON.stringify(breakdown));}catch(e){} var mount=$('night-breakdown-panel'); if(!mount)return; mount.innerHTML='
    Post-Race Night Breakdown
    ' +'
    ' +_renderNightBreakdownInner(breakdown)+'
    '; mount.style.display='block'; populateRecapEditor(breakdown); setTimeout(function(){ if(mount&&mount.getBoundingClientRect)mount.scrollIntoView({behavior:'smooth',block:'start'}); },120); } function _continueRecapEditing(){ syncBreakdownFromEditor(); var ov=document.getElementById('night-breakdown-overlay'); if(ov)ov.remove(); if(typeof switchTab==='function')switchTab('logbook'); setTimeout(function(){ var panel=$('night-breakdown-panel'); if(panel){ panel.scrollIntoView({behavior:'smooth',block:'start'}); toast('Edit your recap draft — save when ready'); } },320); } function saveRecapDraftOnly(){ syncBreakdownFromEditor(); if (typeof _fiMarkDebriefComplete === 'function') _fiMarkDebriefComplete(); toast('Recap draft saved — pick up anytime in Logbook'); var ov=document.getElementById('night-breakdown-overlay'); if(ov)ov.remove(); } function showNightBreakdownOverlay(breakdown){ breakdown=_normalizeNightBreakdown(breakdown)||buildNightBreakdown(); window._lastNightBreakdown=breakdown; try{localStorage.setItem('bb_last_night_breakdown',JSON.stringify(breakdown));}catch(e){} var existing=document.getElementById('night-breakdown-overlay'); if(existing)existing.remove(); var overlay=document.createElement('div'); overlay.id='night-breakdown-overlay'; overlay.className='bb-recap-overlay'; overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.82);z-index:1001;display:flex;align-items:center;justify-content:center;padding:20px'; overlay.onclick=function(e){if(e.target===overlay)overlay.remove();}; var sheet=document.createElement('div'); sheet.className='bb-recap-sheet'; sheet.style.cssText='background:var(--dark2);border:1px solid rgba(200,150,10,.3);border-top:3px solid var(--gold);max-width:420px;width:100%;max-height:90vh'; sheet.onclick=function(e){e.stopPropagation();}; sheet.innerHTML='
    ' +'
    POST-RACE RECAP DRAFT
    ' +_renderNightBreakdownInner(breakdown) +'
    ' +'
    ' +'' +'' +'' +'
    ' +'
    ' +'' +'' +'
    '; overlay.appendChild(sheet); document.body.appendChild(overlay); populateRecapEditor(breakdown); if(typeof _bbMobileKeyboardScroll==='function')_bbMobileKeyboardScroll(sheet); showNightBreakdownCard(breakdown); } function _dbfRender(){ var s=_DBF.steps[_DBF.step]; var total=_DBF.steps.length; // Progress dots var prog=document.getElementById("dbf-prog"); prog.innerHTML=""; for(var i=0;i'; html+='
    '+s.sub.toUpperCase()+'
    '; var wide=s.opts.length===1||s.hasText?'wide':''; html+='
    '; s.opts.forEach(function(opt){ var sel=(_DBF.ans[s.key]===opt)?' sel':''; html+=''; }); html+='
    '; if(s.hasText){html+='';} document.getElementById("dbf-content").innerHTML=html; } function _dbfSel(key,val,btn){ _DBF.ans[key]=val; var parent=btn.parentElement; parent.querySelectorAll('.dbf-btn').forEach(function(b){b.classList.remove('sel');}); btn.classList.add('sel'); } function _dbfNext(){ var s=_DBF.steps[_DBF.step]; // Capture text if present if(s.hasText){var txt=document.getElementById("dbf-txt");if(txt)_DBF.ans[s.key+'_txt']=txt.value.trim();} // Require selection (allow skipping text) if(!_DBF.ans[s.key]&&!s.hasText){toast("Tap an answer to continue");return;} if(_DBF.step<_DBF.steps.length-1){ _DBF.step++;_dbfRender(); } else { _dbfFinish(); } } function _dbfBack(){if(_DBF.step>0){_DBF.step--;_dbfRender();}} async function _dbfFinish(){ var a=_DBF.ans; var now=new Date().toISOString(); // Build debrief record var debrief={ ts:now, car_id:S.cur&&S.cur.id, car_number:S.cur&&S.cur.car_number, car_class:S.cur&&(S.cur.class||S.cur.car_class), result:a.result, corner:a.corner, onset:a.onset, changes:a.changes_txt||a.changes, driver:a.driver, pace:a.pace, da:S.wx?Math.round(S.wx.density_altitude||0):null, track:($("lb-track-sel")&&$("lb-track-sel").value)||"" }; // Show summary in content area var nextBtn=document.getElementById("dbf-next-btn");nextBtn.style.display="none"; var backBtn=document.getElementById("dbf-back-btn");backBtn.style.display="none"; var prog=document.getElementById("dbf-prog"); prog.innerHTML=""; for(var i=0;i<_DBF.steps.length;i++){var d=document.createElement("div");d.className="dbf-dot done";prog.appendChild(d);} var sumHtml='
    DEBRIEF COMPLETE
    ' +'
    REVIEWING YOUR NIGHT
    ' +'
    '; var labels={result:"Result",corner:"Corner Issue",onset:"Onset",changes:"Changes",driver:"Driver",pace:"vs. Field"}; _DBF.steps.forEach(function(s){ var val=a[s.key]||(a[s.key+'_txt']?"(notes only)":"—"); sumHtml+='
    '+labels[s.key]+''+val+'
    '; }); sumHtml+='
    ' +'
    ' +'Quick Results' +'
    ' +'' +'' +'
    ' +'
    ' +'
    ' +'' +'
    Supports MRP, MyLaps, screenshots, or manual entry.
    ' +'
    ' +'
    ' +'
    SCALE FEEDBACK
    ' +'
    ' +'
    ' +'' +'' +'' +'' +'
    ' +'' +'' +'' +'
    ' +'' +''; document.getElementById("dbf-content").innerHTML=sumHtml; if(typeof _showScaleFeedbackPanel==='function')_showScaleFeedbackPanel('dbf','Link debrief feedback to your latest scale change.'); // Auto-fill logbook form with what we know if(a.result==="WIN"&&$("lb-finish")&&!$("lb-finish").value)$("lb-finish").value="1"; if(debrief.track&&$("lb-track-sel"))$("lb-track-sel").value=debrief.track; if(a.changes_txt&&$("lb-notes")&&!$("lb-notes").value)$("lb-notes").value=a.changes_txt; if(a.result==="WIN"){ var dp=$("debrief-position");if(dp&&!dp.value)dp.value="1"; } // Store pending debrief window._pendingDebrief=debrief; setTimeout(function(){ var dp=$("debrief-position"); if(dp&&$("lb-finish")&&$("lb-finish").value&&!dp.value)dp.value=$("lb-finish").value; },0); } async function _dbfSave(){ if(!window._pendingDebrief)return; if(typeof saveDebriefResults==='function')await saveDebriefResults(); window._pendingDebrief._saved=true; try{ var r=await fetch(AU+"?action=save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:S.token,car_id:S.cur.id,data_type:"debrief",data:window._pendingDebrief})}); var d=await r.json(); if(d.success){ toast("Debrief saved — your recap draft is open"); var savedDebrief=window._pendingDebrief; window._pendingDebrief=null; closeDebrief(); autoFillLogbook(); if(typeof switchTab==='function')switchTab('logbook'); var card=buildNightBreakdown(savedDebrief); if(typeof _recordRecapLearningSignal==='function')_recordRecapLearningSignal(savedDebrief); if(typeof showNightBreakdownOverlay==='function')showNightBreakdownOverlay(card); else if(typeof showNightBreakdownCard==='function')showNightBreakdownCard(card); setTimeout(function(){ var panel=$('night-breakdown-panel'); if(panel)panel.scrollIntoView({behavior:'smooth',block:'start'}); if(typeof loadHunterLearnedInsights==='function'&&S.cur&&S.cur.id)loadHunterLearnedInsights(S.cur.id,S.curTrack&&S.curTrack.id); },450); if(typeof fetchPostLogRecommendation==='function'&&savedDebrief)fetchPostLogRecommendation(savedDebrief,'debrief'); } else { toast("Save failed: "+(d.error||"")); } }catch(e){toast("Connection error — debrief not saved");} } function openAlignTool(){ if(!S.token||S._demo){_upgradeGate('Alignment Tool','cadet','Cadet','$8');return;} if(!_canDo('cadet')){_upgradeGate('Alignment Tool','cadet','Cadet','$8');return;} var panel=document.getElementById("align-panel"); var frame=document.getElementById("align-frame"); if(!panel||!frame)return; if(panel.style.display==="none"||!panel.style.display){ panel.style.display="block"; if(!frame.src||frame.src==="about:blank"||frame.src==="")frame.src="/align"; panel.scrollIntoView({behavior:"smooth",block:"start"}); } else { closeAlignTool(); } } function closeAlignTool(){ var panel=document.getElementById("align-panel"); var frame=document.getElementById("align-frame"); if(panel)panel.style.display="none"; if(frame){frame.src="about:blank";} // stops camera stream } // ── Setup Photo Log ───────────────────────────────────────────────────────── var _SETUP_PHOTO_PROMPT = 'You are Hunter, an AI dirt track crew chief. Analyze this photo of a race car suspension/setup. Describe what you see in 2-3 sentences: component positions, adjustments visible, anything notable. Be specific about what you observe (bar heights, spring colors, shock positions, ride height, etc). Keep it crew-chief terse.'; function _setupPhotoSnap() { if (!_canDo('warrior')) { _upgradeGate('Setup Photo Log','warrior','Warrior','$15'); return; } var inp = document.createElement('input'); inp.type = 'file'; inp.accept = 'image/*'; inp.capture = 'environment'; inp.onchange = function() { if (!inp.files || !inp.files[0]) return; var file = inp.files[0]; var reader = new FileReader(); reader.onload = function(ev) { var full = ev.target.result; var b64 = full.split(',')[1]; var mime = file.type || 'image/jpeg'; _setupPhotoProcess(full, b64, mime); }; reader.readAsDataURL(file); }; document.body.appendChild(inp); inp.click(); setTimeout(function() { if (inp.parentNode) inp.parentNode.removeChild(inp); }, 60000); } function _setupPhotoProcess(dataUrl, b64, mime) { var log = document.getElementById('setup-photo-log'); if (!log) return; var card = document.createElement('div'); card.style.cssText = 'background:var(--dark2);border:1px solid rgba(245,166,35,.2);padding:10px;margin-bottom:8px;'; var img = document.createElement('img'); img.src = dataUrl; img.style.cssText = 'width:100%;max-height:200px;object-fit:cover;margin-bottom:8px;'; card.appendChild(img); var status = document.createElement('div'); status.style.cssText = 'font-family:var(--mono);font-size:9px;color:var(--amber);letter-spacing:1px;'; status.textContent = 'HUNTER ANALYZING...'; card.appendChild(status); log.insertBefore(card, log.firstChild); fetch(HNTR, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ message: _SETUP_PHOTO_PROMPT, photos: [{data: b64, type: mime}], history: [], context: 'Car: ' + (S.cur ? (S.cur.name || S.cur.car_number) : 'unknown') + ' | Class: ' + (S.cur ? (S.cur.class || S.cur.car_class || '') : '') + ' | Track: ' + (S.curTrack ? S.curTrack.name : 'unknown'), user_id: S.user ? S.user.user_id : null, user_email: S.user ? S.user.email : null, ai_calls_this_session: _aiCallsThisSession }) }).then(function(r) { return r.json(); }).then(function(d) { _aiCallsThisSession++; var reply = d.response || d.reply || 'Could not analyze photo.'; status.style.color = 'var(--white)'; status.style.lineHeight = '1.6'; status.style.fontSize = '10px'; status.textContent = reply; // Save to cloud + localStorage var entry = { ts: new Date().toISOString(), photo: dataUrl.length < 500000 ? dataUrl : null, description: reply, track: S.curTrack ? S.curTrack.name : null, car_id: S.cur ? S.cur.id : null, setup_snapshot: Object.assign({}, _su) }; var carId = S.cur ? (S.cur.id || 'local') : 'demo'; var key = 'bb_photoLog_' + carId; var existing = []; try { existing = JSON.parse(localStorage.getItem(key) || '[]'); } catch(e) {} existing.unshift(entry); if (existing.length > 50) existing = existing.slice(0, 50); localStorage.setItem(key, JSON.stringify(existing)); _cloudSave('photo_log', carId, existing); _renderPhotoLog(); }).catch(function(e) { status.style.color = 'var(--red)'; status.textContent = 'Connection error — photo not analyzed.'; }); } function _renderPhotoLog() { var log = document.getElementById('setup-photo-log'); if (!log) return; var carId = S.cur ? (S.cur.id || 'local') : 'demo'; var entries = []; try { entries = JSON.parse(localStorage.getItem('bb_photoLog_' + carId) || '[]'); } catch(e) {} if (!entries.length) return; log.innerHTML = ''; var hdr = document.createElement('div'); hdr.style.cssText = 'font-family:var(--mono);font-size:8px;letter-spacing:2px;color:var(--muted);margin:8px 0 4px;'; hdr.textContent = 'SEASON LOG \u2014 ' + entries.length + ' ENTRIES'; log.appendChild(hdr); entries.slice(0, 10).forEach(function(e) { var card = document.createElement('div'); card.style.cssText = 'background:var(--dark2);border:1px solid rgba(255,255,255,.04);padding:8px;margin-bottom:4px;'; var top = document.createElement('div'); top.style.cssText = 'display:flex;justify-content:space-between;margin-bottom:4px;'; var date = document.createElement('div'); date.style.cssText = 'font-family:var(--mono);font-size:8px;color:var(--amber);'; date.textContent = e.ts ? new Date(e.ts).toLocaleDateString() : '?'; var track = document.createElement('div'); track.style.cssText = 'font-family:var(--mono);font-size:8px;color:var(--muted);'; track.textContent = e.track || ''; top.appendChild(date); top.appendChild(track); card.appendChild(top); if (e.photo) { var thumb = document.createElement('img'); thumb.src = e.photo; thumb.style.cssText = 'width:100%;max-height:80px;object-fit:cover;margin-bottom:4px;cursor:pointer;'; thumb.onclick = function() { var w = window.open(); if(w) w.document.write(''); }; card.appendChild(thumb); } var desc = document.createElement('div'); desc.style.cssText = 'font-family:var(--mono);font-size:9px;color:var(--white);line-height:1.5;'; desc.textContent = e.description || ''; card.appendChild(desc); log.appendChild(card); }); } // ── Surface Science Suite ──────────────────────────────────────────────────── function _getChecklistItems() { if (typeof _isReturningGarageUser === 'function' && !_isReturningGarageUser()) { return ['Add your car', 'Pick tonight\'s track', 'Get your first Hunter read']; } var ct = S.cur ? _getCarType(S.cur) : 'generic'; return (CK_ITEMS_BY_TYPE[ct] || CK_ITEMS).slice(); } function _ckStorageKey() { var ct = S.cur ? _getCarType(S.cur) : 'generic'; return 'bb_ck_' + ct; } function _buildMidWeekPlanner() { var mount = $('midweek-planner-mount'); if (!mount) return; mount.innerHTML = '
    ' + '
    MID-WEEK PLANNER
    ' + '
    Plan setup before race night
    ' + '
    ' + '' + '' + '
    ' + '
    '; var sel = document.getElementById('planner-track'); if (!sel) return; (S.tracks || []).slice().sort(function(a, b) { return a.name.localeCompare(b.name); }).forEach(function(t) { var o = document.createElement('option'); o.value = t.id; o.textContent = t.name + (t.city ? (' — ' + t.city + ', ' + t.state) : ''); sel.appendChild(o); }); if (S.curTrack && S.curTrack.id) sel.value = S.curTrack.id; } function _buildPhoneLoggerSettings() { var mount = $('phone-logger-settings-mount'); if (!mount) return; var enabled = localStorage.getItem('auto_phone_logger') === 'true'; var hapticsOn = localStorage.getItem('phone_haptics_enabled') !== 'false'; mount.innerHTML = '
    ' + '
    POCKET LOGGER
    ' + '' + '' + '
    Auto-start skips if a session was logged in the last 4 hours.
    ' + '
    '; } function saveAutoPhoneLoggerSetting() { var el = document.getElementById('auto-phone-logger'); if (!el) return; localStorage.setItem('auto_phone_logger', el.checked ? 'true' : 'false'); } function checkAutoPhoneLogger() { if (localStorage.getItem('auto_phone_logger') !== 'true') return; if (!S.cur || !S.curTrack) return; var lastSession = localStorage.getItem('last_phone_session'); if (lastSession) { var hoursSince = (Date.now() - parseInt(lastSession, 10)) / (1000 * 60 * 60); if (hoursSince < 4) return; } if (_pocketLogger && _pocketLogger.active) return; setTimeout(function() { if (_pocketLogger && _pocketLogger.active) return; if (typeof startPhoneDataLogger === 'function') { startPhoneDataLogger((S.curTrack && S.curTrack.name) ? 'practice' : 'Outing'); _plSetPhoneLoggerStatus('Auto-started phone logging...'); } }, 1500); } async function loadPlannerRecommendations() { var trackSelect = document.getElementById('planner-track'); var container = document.getElementById('planner-recommendations'); var text = document.getElementById('planner-rec-text'); if (!trackSelect || !container || !text) return; var trackId = trackSelect.value; if (!trackId) { toast('Select a track first'); return; } if (!S.cur || !S.cur.id) { toast('Select a car first'); return; } text.innerHTML = 'Loading recommendations…'; container.style.display = 'block'; var rec = null; try { var recRes = await fetch(HNTR, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ action: 'get_track_recommendations', user_id: S.user ? S.user.user_id : null, car_id: S.cur.id, track_id: trackId }, typeof _hunterRecPayloadForApi === 'function' ? _hunterRecPayloadForApi() : {})) }); var recData = await recRes.json(); rec = recData && recData.recommendation; if (rec && rec.message) { text.innerHTML = _renderHunterRecCard(rec, { label: 'TRACK REC', showActions: true }); } else { text.innerHTML = _renderHunterRecCard({ message: 'No specific recommendations yet for this track.' }, { label: 'TRACK REC', showActions: false }); } } catch (e) { text.innerHTML = _renderHunterRecCard({ message: 'Could not load track recommendations.' }, { label: 'TRACK REC', showActions: false }); } var skipPhoneAppend = rec && (rec.source === 'combined_phone_scale' || rec.source === 'contextual_bandit'); if (skipPhoneAppend) return; try { var phoneSessions = await _loadPhoneLoggerSessions(5, { allTracks: true, filterTrackId: trackId }); if (phoneSessions.length > 0) { var totalLaps = phoneSessions.reduce(function(sum, s) { return sum + ((s.analysis && s.analysis.detected_laps) || 0); }, 0); var totalCautions = phoneSessions.reduce(function(sum, s) { return sum + (s.cautions_removed || (s.analysis && s.analysis.detected_cautions) || 0); }, 0); text.innerHTML += '
    Phone data on this track: ' + phoneSessions.length + ' sessions · ' + totalLaps + ' clean laps · ' + totalCautions + ' cautions filtered
    '; } } catch (e) {} } function _eventMatchesTrack(event, track) { if (!event || !track) return false; if (event.track_id && track.id && event.track_id === track.id) return true; var en = String(event.track_name || '').toLowerCase().trim(); var tn = String(track.name || '').toLowerCase().trim(); var ts = String(track.short || '').toLowerCase().trim(); return !!en && (en === tn || (ts && en === ts)); } function _driverCheckinLabel() { if (!S.cur) return 'Driver'; var num = S.cur.car_number ? ('#' + S.cur.car_number + ' ') : ''; return (num + (S.cur.name || S.cur.driver_name || 'Driver')).trim(); } async function _saveEventCheckin(eventId) { if (!S || !S.token) { toast('Sign in to check in'); return false; } if (!S.cur || !S.cur.id) { toast('Select your car first'); return false; } var res = await fetch('/bb-magic?action=save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: S.token, car_id: S.cur.id, data_type: 'event_checkin', data: { event_id: eventId, driver_id: S.cur.id, driver_name: _driverCheckinLabel(), track_id: S.curTrack && S.curTrack.id, track_name: S.curTrack && S.curTrack.name, timestamp: Date.now() } }) }); var data = await res.json().catch(function() { return {}; }); if (!res.ok || data.error) { toast(data.error || 'Check-in failed'); return false; } return true; } async function checkInToSpecificEvent(eventId) { if (await _saveEventCheckin(eventId)) toast('Checked in!'); } async function checkInToEvent() { if (!S.curTrack || !S.curTrack.id) { alert('Please select your track first.'); return; } if (!S.cur) { alert('Select your car first.'); return; } var res = await fetch('/bb-magic?action=get_events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); var data = await res.json(); var events = data.events || []; var event = events.find(function(e) { return _eventMatchesTrack(e, S.curTrack) && (e.status === 'upcoming' || e.status === 'live'); }); if (!event) { alert('No upcoming or live event found for this track.'); return; } if (await _saveEventCheckin(event.id)) toast('Checked in to event!'); } async function loadUpcomingEventsForCheckIn() { var container = document.getElementById('upcoming-events-checkin'); var list = document.getElementById('upcoming-events-list-garage'); if (!container || !list) return; if (!S.curTrack || !S.curTrack.id) { container.style.display = 'none'; return; } try { var res = await fetch('/bb-magic?action=get_events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); var data = await res.json(); var upcoming = (data.events || []).filter(function(e) { return _eventMatchesTrack(e, S.curTrack) && e.status === 'upcoming'; }); if (!upcoming.length) { container.style.display = 'none'; return; } container.style.display = 'block'; list.innerHTML = ''; upcoming.forEach(function(event) { var div = document.createElement('div'); div.style.cssText = 'margin-bottom:8px;padding:10px;background:var(--dark3);border:1px solid rgba(255,255,255,.06);border-radius:8px'; div.innerHTML = '
    ' + String(event.event_name || '').replace(/' + '
    ' + new Date(event.event_date).toLocaleString() + '
    ' + ''; div.querySelector('.event-garage-checkin-btn').onclick = function() { checkInToSpecificEvent(event.id); }; list.appendChild(div); }); } catch (e) { container.style.display = 'none'; } } function _buildToolsTonightRead(mountId) { var m = $(mountId || 'tools-tonight-read'); if (!m) return; m.innerHTML = ''; if (!_fiCountCars || !_fiCountCars()) { m.innerHTML = '
    Add your car — Hunter will pull class profile, weather, and track data for a starting read.
    '; return; } if (!S.curTrack) { m.innerHTML = '
    Pick tonight\'s track to see weather, banking, and a setup read for your class.
    '; return; } var wx = S.wx || {}; var da = Math.round(wx.density_altitude || wx.densityAltitude || 0); var ct = S.cur ? _getCarType(S.cur) : 'generic'; var cls = S.cur ? (S.cur.class || S.cur.car_class || '') : ''; var track = S.curTrack ? S.curTrack.name : (wx.location || 'Set track'); var canon = cls ? _resolveCanonicalClassName(cls) : null; var prof = canon ? CLASS_PROFILES[canon] : null; var tips = []; if (da > 5500) tips.push({ pri: 1, t: 'High DA (' + da.toLocaleString() + ' ft) — engine may feel flat. Jet up or add timing if rules allow.' }); else if (da > 3500) tips.push({ pri: 2, t: 'Moderate DA — baseline jetting usually fine; watch temp gain in feature.' }); else if (da) tips.push({ pri: 4, t: 'Low DA tonight — engine makes power; protect tires from overheating early.' }); if (S.curTrack && S.curTrack.banking) { var bk = parseFloat(S.curTrack.banking); if (bk >= 14) tips.push({ pri: 2, t: 'High banking (' + bk + '°) — weight transfer to LR is aggressive. Stagger and LR bite matter.' }); else if (bk <= 6) tips.push({ pri: 2, t: 'Flat track — stagger and tire temp do most of the work. Entry line is everything.' }); } var rec = typeof hunterTonight === 'function' ? hunterTonight({}) : null; if (rec && rec.action) tips.unshift({ pri: 1, t: rec.action }); if (_su && _su.stagger_r) tips.push({ pri: 3, t: 'Active setup: ' + _su.stagger_r + '" stagger' + (_su.lf_psi ? ', LF ' + _su.lf_psi + ' psi' : '') + ' — sync before hot laps.' }); var card = document.createElement('div'); card.style.cssText = 'background:linear-gradient(135deg,rgba(208,25,14,.12),rgba(13,12,11,.95));border:1px solid rgba(208,25,14,.35);border-left:4px solid var(--red);padding:14px'; card.innerHTML = '
    HUNTER — TONIGHT AT ' + String(track).toUpperCase().substring(0, 28) + '
    ' + '
    #' + (S.cur && S.cur.car_number ? S.cur.car_number : '?') + ' · ' + (cls || ct).toString().substring(0, 24) + '
    ' + (prof && cls && cls !== canon ? '
    Setup profile: ' + prof.label + '
    ' : '') + '
    ' + (da ? da.toLocaleString() + ' ft DA' : 'Refresh weather for DA') + (wx.temp ? ' · ' + Math.round(wx.temp) + '°F' : '') + '
    '; if (!tips.length) tips.push({ pri: 3, t: _getGarageMode() === 'road' ? 'Road course — scale sheet + cold PSI. Pick NTK or AMP as your track.' : 'Select your car and track — Hunter will read weather, banking, and your setup.' }); tips.slice(0, 4).forEach(function(tip) { var row = document.createElement('div'); row.style.cssText = 'padding:7px 0;border-bottom:1px solid rgba(255,255,255,.06);font-size:12px;line-height:1.5;color:var(--white)'; row.textContent = tip.t; card.appendChild(row); }); var actions = document.createElement('div'); actions.style.cssText = 'display:flex;gap:8px;margin-top:10px;flex-wrap:wrap'; [['Race tab','race'],['Track walk','/trackwalk'],['Ask Hunter','hunter']].forEach(function(pair) { var b = document.createElement('button'); b.style.cssText = 'flex:1;min-width:90px;padding:8px;background:var(--dark2);border:1px solid rgba(255,255,255,.12);color:var(--white);font-family:Barlow Condensed;font-size:11px;font-weight:900;cursor:pointer'; b.textContent = pair[0]; b.onclick = function() { if (pair[1] === '/trackwalk') { var w = document.getElementById('rn-iframe-wrap'); var fr = document.getElementById('rn-iframe'); if (w && fr) { fr.src = '/trackwalk'; w.style.display = 'block'; } else if (typeof switchTab === 'function') switchTab('stuff'); return; } if (typeof switchTab === 'function') switchTab(pair[1]); if (pair[1] === 'hunter' && typeof sendToHunter === 'function') { setTimeout(function() { sendToHunter('What should I focus on tonight at ' + track + '?'); }, 300); } }; actions.appendChild(b); }); card.appendChild(actions); m.appendChild(card); var hasRead = false; try { hasRead = localStorage.getItem('bb_first_night_hunter_read') === '1'; } catch (e) {} if (hasRead && typeof _renderFiShareBar === 'function') { var shareWrap = document.createElement('div'); shareWrap.innerHTML = _renderFiShareBar('read', { insight: tips[0] && tips[0].t }); m.appendChild(shareWrap.firstElementChild || shareWrap); } } function _buildTirePressureTool() { var m = $('tire-psi-tool-mount'); if (!m) return; m.innerHTML = ''; var ct = S.cur ? _getCarType(S.cur) : 'generic'; if (ct === 'kart') return; var clsName = S.cur ? (S.cur.class || S.cur.car_class || '') : ''; var canon = clsName ? _resolveCanonicalClassName(clsName) : null; var prof = canon ? CLASS_PROFILES[canon] : null; var wx = S.wx || {}; var trackTemp = typeof _effectiveSurfaceTempF === 'function' ? _effectiveSurfaceTempF(wx) : Math.round(wx.temp || 75); var card = document.createElement('div'); card.style.cssText = 'background:var(--dark2);border:1px solid rgba(208,25,14,.1);padding:14px;margin-bottom:8px'; var h = '
    Tire Pressure Planner
    '; if (prof && clsName && clsName !== canon) { h += '
    CLASS PROFILE: ' + prof.label + ' · your series label maps here
    '; } else if (prof) { h += '
    ' + prof.label + '
    '; } h += '
    Cold PSI in the pits → estimated hot at track surface temp (IR gun overrides estimate). Rule of thumb: +0.02 psi per °F tire gain (adjust for compound).
    '; h += '
    TRACK SURFACE TEMP (°F)
    '; h += '
    '; ['LF', 'RF', 'LR', 'RR'].forEach(function(pos) { var k = pos.toLowerCase(); var v = (_su && _su[k + '_psi']) || ({ lf: 7, rf: 8, lr: 8.5, rr: 10.5 }[k] || ''); h += '
    ' + pos + ' COLD
    '; }); h += '
    '; h += '
    '; card.innerHTML = h; m.appendChild(card); function recalcHot() { var tt = parseFloat($('tp-track-temp') && $('tp-track-temp').value) || trackTemp; var gain = Math.max(0, tt - 65) * 0.02; var row = $('tp-hot-row'); if (!row) return; row.innerHTML = ''; ['lf', 'rf', 'lr', 'rr'].forEach(function(k) { var cold = parseFloat($('tp-' + k) && $('tp-' + k).value) || 0; var hot = cold ? (cold + gain).toFixed(1) : '--'; var d = document.createElement('div'); d.style.cssText = 'background:var(--dark);padding:5px;text-align:center'; d.innerHTML = '
    ' + k.toUpperCase() + ' HOT
    ' + hot + '
    '; row.appendChild(d); }); } ['tp-track-temp', 'tp-lf', 'tp-rf', 'tp-lr', 'tp-rr'].forEach(function(id) { var el = $(id); if (el) el.addEventListener('input', recalcHot); }); recalcHot(); var sync = $('tp-sync'); if (sync) sync.onclick = function() { if (typeof _su === 'undefined') return; ['lf', 'rf', 'lr', 'rr'].forEach(function(k) { var v = parseFloat($('tp-' + k) && $('tp-' + k).value); if (!isNaN(v)) _su[k + '_psi'] = v; }); var tt = parseFloat($('tp-track-temp') && $('tp-track-temp').value); if (!isNaN(tt) && typeof _saveManualSurfaceTempF === 'function') { _saveManualSurfaceTempF(tt, 'ir'); if (typeof _syncSurfaceTempToWx === 'function') _syncSurfaceTempToWx({ manual: tt, source: 'ir' }); } if (typeof saveG !== 'undefined') {} if (typeof _syncActiveSetupEverywhere === 'function') _syncActiveSetupEverywhere(true); else toast('PSI saved to active setup'); toast('Tire PSI synced'); }; } function _buildSurfaceScience(mountEl) { var el = mountEl || document.getElementById('surface-science'); if (!el) return; el.innerHTML = ''; if (!_canDo('cadet')) { el.innerHTML = '
    SURFACE SCIENCE
    Wismer-Luth traction index, dew point countdown, evaporation rate.
    Unlock with Cadet ($8/mo).
    '; return; } var wx = S.wx || {}; var temp = wx.temp || 0; var hum = wx.humidity || 0; var dp = wx.dewpoint || 0; var wind = wx.wind_speed || 0; var surf = typeof _effectiveSurfaceTempF === 'function' ? _effectiveSurfaceTempF(wx) : temp; var surfEst = typeof _estimateSurfaceTempF === 'function' ? _estimateSurfaceTempF(wx, _surfaceKindFromTrack()) : surf; var surfSrc = (wx.surface_temp_source === 'manual' || wx.surface_temp_source === 'ir') ? 'IR / MANUAL' : 'ESTIMATED'; var stEl = document.createElement('div'); stEl.style.cssText = 'background:var(--dark2);border:1px solid rgba(255,140,66,.22);border-left:3px solid #ff8c42;padding:12px;margin-bottom:6px;'; stEl.innerHTML = '
    ' + '
    TRACK SURFACE TEMP
    ' + '
    KEY METRIC · dirt · drag · road
    ' + '
    ' + surf + '\u00b0
    ' + '
    ' + surfSrc + ' · air ' + Math.round(temp) + '\u00b0F
    ' + '
    IR GUN / MANUAL (°F)
    ' + '
    ' + '
    ' + '
    Pavement/drag prep runs hot vs air. Clay surface temp drives drying — Hunter reads this on every rec.
    '; el.appendChild(stEl); var ssIn = $('ss-surf-temp'), ssBtn = $('ss-surf-save'); if (ssBtn) ssBtn.onclick = function() { var v = parseFloat(ssIn && ssIn.value); if (isNaN(v)) { toast('Enter IR reading'); return; } if (typeof _saveManualSurfaceTempF === 'function') _saveManualSurfaceTempF(v, 'ir'); if (typeof _syncSurfaceTempToWx === 'function') _syncSurfaceTempToWx({ manual: v, source: 'ir' }); toast('Surface temp logged: ' + Math.round(v) + '°F'); if (typeof _buildTirePressureTool === 'function') _buildTirePressureTool(); }; if (S.curTrack && S.curTrack.substrate) { var sub = S.curTrack.substrate; var subEl = document.createElement('div'); subEl.style.cssText = 'background:var(--dark2);border:1px solid rgba(200,150,10,.15);border-left:3px solid var(--amber);padding:12px;margin-bottom:6px;'; subEl.innerHTML = '
    NATIVE SUBSTRATE · USDA
    ' + '
    Permanent layer under promoter clay — same SSURGO data farmers use
    ' + (sub.clay_pct != null ? '
    ' + '
    '+sub.clay_pct+'%
    CLAY
    ' + '
    '+(sub.sand_pct!=null?sub.sand_pct:'—')+'%
    SAND
    ' + '
    '+(sub.silt_pct!=null?sub.silt_pct:'—')+'%
    SILT
    ' : '
    Substrate calibrating for this pin.
    ') + (sub.taxsuborder || sub.drainage ? '
    '+(sub.taxsuborder||'')+(sub.drainage?' · '+sub.drainage:'')+'
    ' : '') + '
    '+_substrateHint(sub)+'
    ' + '
    AI Eye: loose off-line sample = makeup · groove shot = moisture tonight
    '; el.appendChild(subEl); } // ── Wismer-Luth Traction Index ── // Simplified Wismer-Luth soil traction model: traction drops as moisture increases // Index: 0-100 where 100 = peak grip, below 40 = slippery var moistureEst = Math.min(100, Math.max(0, hum * 0.8 + (temp < dp + 3 ? 20 : 0))); var tractionIdx = Math.round(Math.max(0, 100 - moistureEst * 0.7 - (wind > 15 ? -5 : 0))); var tractionColor = tractionIdx > 65 ? '#2db87f' : tractionIdx > 40 ? 'var(--amber)' : 'var(--red-hot)'; var tractionLabel = tractionIdx > 65 ? 'GOOD GRIP' : tractionIdx > 40 ? 'MODERATE' : 'SLIPPERY'; var wl = document.createElement('div'); wl.style.cssText = 'background:var(--dark2);border:1px solid rgba(255,255,255,.04);padding:12px;margin-bottom:6px;'; wl.innerHTML = '
    ' + '
    TRACTION INDEX
    ' + '
    WISMER-LUTH MODEL
    ' + '
    ' + tractionIdx + '
    ' + '
    ' + '
    ' + tractionLabel + ' \u2014 Moisture est: ' + Math.round(moistureEst) + '%
    '; el.appendChild(wl); // ── Dew Point Countdown ── // How close is temp to dew point? If temp drops to dew point, track gets wet var dewGap = Math.round(temp - dp); var dewUrgent = dewGap <= 3; var dewWarn = dewGap <= 8; var dewColor = dewUrgent ? 'var(--red-hot)' : dewWarn ? 'var(--amber)' : '#2db87f'; var dewMsg = dewUrgent ? 'DEW IMMINENT \u2014 track will get slick fast' : dewWarn ? 'WATCH IT \u2014 cooling toward dew point' : 'CLEAR \u2014 no dew risk right now'; // Estimate time until dew point at ~2.2°F/hr cooling after sunset var hoursUntilDew = dewGap > 0 ? Math.round(dewGap / 2.2 * 10) / 10 : 0; var dewTime = dewGap <= 0 ? 'NOW' : hoursUntilDew < 1 ? 'UNDER 1 HOUR' : Math.round(hoursUntilDew) + 'h'; var dpEl = document.createElement('div'); dpEl.style.cssText = 'background:var(--dark2);border:1px solid rgba(255,255,255,.04);padding:12px;margin-bottom:6px;'; dpEl.innerHTML = '
    ' + '
    DEW POINT COUNTDOWN
    ' + '
    TEMP ' + Math.round(temp) + '\u00b0F \u2192 DEW ' + Math.round(dp) + '\u00b0F
    ' + '
    ' + dewGap + '\u00b0
    ' + '
    GAP
    ' + '
    ' + '
    ' + dewMsg + '
    ' + '
    ' + dewTime + '
    '; el.appendChild(dpEl); // ── Evaporation Rate ── // Penman-simplified: evap rate based on temp, humidity, wind // Higher temp + lower humidity + more wind = faster drying var satVP = 6.112 * Math.exp(17.67 * ((temp - 32) / 1.8) / (((temp - 32) / 1.8) + 243.5)); var actVP = satVP * (hum / 100); var vpd = satVP - actVP; // vapor pressure deficit (hPa) var evapRate = Math.round((vpd * 0.35 * (1 + wind * 0.15)) * 10) / 10; // mm/hr simplified var evapLabel = evapRate > 3 ? 'FAST DRYING' : evapRate > 1.5 ? 'MODERATE' : 'SLOW'; var evapColor = evapRate > 3 ? '#2db87f' : evapRate > 1.5 ? 'var(--amber)' : 'var(--red-hot)'; var evapNote = evapRate > 3 ? 'Track will transition quickly \u2014 prep for slick' : evapRate > 1.5 ? 'Normal transition pace \u2014 standard prep' : 'Track holds moisture \u2014 stay aggressive early'; var evEl = document.createElement('div'); evEl.style.cssText = 'background:var(--dark2);border:1px solid rgba(255,255,255,.04);padding:12px;margin-bottom:6px;'; evEl.innerHTML = '
    ' + '
    EVAPORATION RATE
    ' + '
    PENMAN MODEL \u2014 VPD ' + vpd.toFixed(1) + ' hPa
    ' + '
    ' + evapRate + '
    ' + '
    MM/HR
    ' + '
    ' + evapLabel + ' \u2014 ' + evapNote + '
    '; el.appendChild(evEl); // No weather loaded if (!temp && !hum) { el.innerHTML = '
    WAITING FOR WEATHER DATA...
    '; } // Track condition estimate on gauge cluster if (typeof window._updateCondStyle === 'function' && tractionIdx) { var condLbl = tractionIdx > 65 ? 'TACKY' : tractionIdx > 40 ? 'TRANSITION' : 'SLICK'; var dc = $('dx-cond'); if (dc) dc.textContent = condLbl; window._updateCondStyle(condLbl); var ev = $('dx-evap'); if (ev) ev.textContent = evapLabel + ' · ' + evapRate + ' mm/hr'; } } function wireToolsCalcs(){_buildGearCalc();_buildStaggerCalc();_buildDragTools();_buildRoadTools();_buildScaleSheet();} // ═══════════════════════════════════════════════════════════════════════ // ENHANCED GEAR CALCULATOR // ═══════════════════════════════════════════════════════════════════════ function _bestClassMatch(){ // Match user's car class to a GEAR_DB key via canonical profiles first var cls=S.cur?(S.cur.class||S.cur.car_class||''):'';var lc=cls.toLowerCase(); var canon=_resolveCanonicalClassName(cls); if(canon&&CLASS_PROFILES[canon]&&CLASS_PROFILES[canon].gear&&GEAR_DB[CLASS_PROFILES[canon].gear])return CLASS_PROFILES[canon].gear; if(canon&&GEAR_DB[canon])return canon; // Try exact match first for(var k in GEAR_DB){if(k.toLowerCase()===lc)return k;} // Fuzzy match if(/410.*wing/i.test(lc))return'410 Winged Sprint';if(/410.*non/i.test(lc)||/410.*open/i.test(lc))return'410 Non-Wing Sprint'; if(/360.*wing/i.test(lc))return'360 Winged Sprint';if(/360.*non/i.test(lc))return'360 Non-Wing Sprint'; if(/305/i.test(lc)&&/wing/i.test(lc))return'305 Winged Sprint';if(/305.*non/i.test(lc))return'305 Non-Wing Sprint'; if(/602/i.test(lc))return'602 Crate Late Model';if(/604/i.test(lc))return'604 Crate Late Model'; if(/super.*late/i.test(lc))return'Super Late Model';if(/limited.*late/i.test(lc))return'Limited Late Model'; if(/imca.*mod/i.test(lc))return'IMCA Modified';if(/ump.*mod/i.test(lc))return'UMP Modified';if(/sport.*mod/i.test(lc))return'Sport Modified';if(/b-mod|bmod/i.test(lc))return'B-Mod'; if(/junior sprint|jr\.?\s*sprint/i.test(lc))return'Junior Sprint'; if(/600.*wing/i.test(lc)||/micro.*wing/i.test(lc))return'600 Micro Sprint Winged';if(/600.*non/i.test(lc)||/micro.*non/i.test(lc))return'600 Micro Sprint Non-Wing'; if(/restricted.*micro|micro.*restricted/i.test(lc))return'Restricted Micro Sprint';if(/now600.*wing/i.test(lc))return'NOW600 A-Class Winged'; if(/young gun/i.test(lc))return'Young Guns'; if(/jr\s*1|junior\s*1\b/i.test(lc))return'Jr 1 Clone'; if(/jr\s*2|junior\s*2\b/i.test(lc))return'Jr 2 Clone'; if(/jr\s*3|junior\s*3\b/i.test(lc))return'Jr 3 Clone'; if(/pro clone/i.test(lc))return'Pro Clone Flat'; if(/outlaw\s*125|\b125\s*outlaw/i.test(lc))return'Outlaw 125'; if(/outlaw\s*250|\b250\s*outlaw/i.test(lc))return'Outlaw 250'; if(/outlaw\s*500|open outlaw/i.test(lc))return'Outlaw 500 Open'; if(/lo.?206.*sen|senior.*206|^lo206 senior$/i.test(lc))return'Kart LO206 Senior';if(/lo.?206.*jun|junior.*206|^lo206 junior$/i.test(lc))return'Kart LO206 Junior'; if(/lo.?206.*sport/i.test(lc))return'Kart LO206 Sportsman';if(/lo.?206|briggs/i.test(lc))return'Kart LO206 Senior'; if(/clone.*jun/i.test(lc))return'Kart Clone Junior';if(/clone/i.test(lc))return'Kart Clone'; if(/outlaw.*kart/i.test(lc))return'Outlaw 250'; if(/spec miata|road-spec-miata/i.test(lc))return'Spec Miata';if(/quarter.*mid/i.test(lc))return'Quarter Midget'; if(/legend/i.test(lc))return'Legend Car';if(/bandolero/i.test(lc))return'Bandolero'; if(/street.*stock/i.test(lc))return'Street Stock';if(/hobby/i.test(lc))return'Hobby Stock';if(/pure.*stock/i.test(lc))return'Pure Stock'; if(/compact|4.cyl/i.test(lc))return'Sport Compact';if(/lightning/i.test(lc))return'Lightning Sprint'; // Fallback by car type var ct2=S.cur?_getCarType(S.cur):'generic'; if(ct2==='sprint')return'410 Winged Sprint';if(ct2==='jrsprint')return'Junior Sprint';if(ct2==='micro')return'600 Micro Sprint Winged'; if(ct2==='latemodel')return'602 Crate Late Model';if(ct2==='modified')return'IMCA Modified'; if(ct2==='outlawkart')return'Outlaw 250';if(ct2==='flatkart')return'Pro Clone Flat'; if(ct2==='specmiata')return'Spec Miata';if(ct2==='roadkart')return'Kart LO206 Senior'; if(ct2==='kart')return'Kart LO206 Senior';if(ct2==='stock')return'Street Stock'; return'Street Stock'; } function _buildGearCalc(){ var m=$('gear-calc-mount');if(!m)return;m.innerHTML=''; var bestClass=_bestClassMatch();var gd=GEAR_DB[bestClass]; var ct=S.cur?_getCarType(S.cur):'generic'; var td=TIRE_PRESETS[ct]||24; var card=document.createElement('div'); card.style.cssText='background:var(--dark2);border:1px solid rgba(208,25,14,.1);padding:14px;margin-bottom:8px'; if(gd)td=gd.tire; var h='
    Gear Calculator
    '; // Class selector — populated from GEAR_DB h+='
    CLASS
    '; // Track size selector h+='
    '; // Recommendation panel h+=''; // Inputs h+='
    '; h+='
    DRIVE
    '; h+='
    DRIVEN
    '; h+='
    TIRE DIA
    '; h+='
    '; // Quick change gear set selector (for QC classes) h+=''; // Outputs h+='
    '; h+='
    RATIO
    --
    '; h+='
    ROLLOUT
    --
    '; h+='
    FDR
    --
    '; h+='
    '; // MPH at RPMs h+='
    '; [6000,7000,7500,8000].forEach(function(rpm){ h+='
    @'+rpm+'
    --
    '; }); h+='
    '; // Reverse lookup h+='
    REVERSE: TARGET MPH
    Enter target MPH
    '; card.innerHTML=h;m.appendChild(card); // Wire events function calcG(){ var dr=parseFloat($('gc-dr').value)||15,dn=parseFloat($('gc-dn').value)||62,dia=parseFloat($('gc-td').value)||22; var ratio=dn/dr;var circ=dia*Math.PI;var rollout=(circ/ratio); $('gc-ratio').textContent=ratio.toFixed(3); $('gc-roll').textContent=rollout.toFixed(1)+'"'; $('gc-fdr').textContent=ratio.toFixed(2); [6000,7000,7500,8000].forEach(function(rpm){ var mph=(rpm/ratio)*(circ/12)/5280*60; var el=$('gc-mph'+rpm);if(el)el.textContent=mph.toFixed(1); }); } ['gc-dr','gc-dn','gc-td'].forEach(function(id){var el=$(id);if(el)el.addEventListener('input',calcG);}); // Class selector — updates tire dia, track buttons, recommendations var _curGD=gd;var _selTrack=''; function updateClassUI(cls){ _curGD=GEAR_DB[cls]||null; if(_curGD){$('gc-td').value=_curGD.tire; // Show/hide QC set selector var qcw=$('gc-qc-wrap');if(qcw)qcw.style.display=_curGD.drive==='qc'?'block':'none'; // Build track size buttons var tb=$('gc-tracks');if(tb){tb.innerHTML=''; Object.keys(_curGD.tracks).forEach(function(ts){ var b=document.createElement('div');b.dataset.ts=ts; b.style.cssText='padding:5px 12px;font-family:var(--mono);font-size:9px;letter-spacing:1px;cursor:pointer;border:1px solid var(--dark4);color:var(--muted)'; b.textContent=ts+(ts.indexOf('/')>=0?' MI':' MI'); b.onclick=function(){ tb.querySelectorAll('div').forEach(function(d){d.style.borderColor='var(--dark4)';d.style.color='var(--muted)';}); b.style.borderColor='var(--gold)';b.style.color='var(--gold)'; _selTrack=ts;showRec(); }; tb.appendChild(b); }); // Auto-select from current track if(S.curTrack&&S.curTrack.size){var sz=S.curTrack.size.replace(/\s*mile/i,'').trim(); tb.querySelectorAll('div').forEach(function(d){if(d.dataset.ts===sz){d.click();}});} } } calcG(); } function showRec(){ var rec=$('gc-rec'),rt=$('gc-rec-text'),rn=$('gc-rec-note'); if(!rec||!_curGD||!_selTrack||!_curGD.tracks[_selTrack]){if(rec)rec.style.display='none';return;} var range=_curGD.tracks[_selTrack]; rec.style.display='block'; rt.textContent=range[0].toFixed(2)+' — '+range[1].toFixed(2)+' FDR'; var rpmRange=_curGD.rpm?'RPM range: '+_curGD.rpm[0]+'–'+_curGD.rpm[1]+'. ':''; rn.textContent=rpmRange+(_curGD.note||''); // Color the ratio output based on recommendation var curRatio=parseFloat($('gc-ratio').textContent); if(curRatio>=range[0]&&curRatio<=range[1]){$('gc-ratio').style.color='#2DB87F';} else if(curRatio>=range[0]-0.5&&curRatio<=range[1]+0.5){$('gc-ratio').style.color='var(--amber)';} else{$('gc-ratio').style.color='var(--red-hot)';} } var gcClass=$('gc-class'); if(gcClass)gcClass.onchange=function(){updateClassUI(gcClass.value);}; updateClassUI(bestClass); // Quick change gear set var gset=$('gc-set'); if(gset)gset.onchange=function(){var s=gset.value;if(s&&SPRINT_GEAR_SETS[s]){var fdr=SPRINT_GEAR_SETS[s];$('gc-dn').value=Math.round(fdr*parseFloat($('gc-dr').value));calcG();showRec();}}; // Reverse lookup — uses class RPM var tmph=$('gc-tmph'); if(tmph)tmph.addEventListener('input',function(){ var mph=parseFloat(tmph.value)||0;if(!mph){$('gc-need').textContent='Enter target MPH';return;} var dia=parseFloat($('gc-td').value)||22;var circ=dia*Math.PI; var useRpm=_curGD&&_curGD.rpm?Math.round((_curGD.rpm[0]+_curGD.rpm[1])/2):7000; var needRatio=useRpm*(circ/12)/5280*60/mph; $('gc-need').innerHTML='Need '+needRatio.toFixed(2)+' ratio @ '+useRpm+' RPM'; }); calcG(); } // ═══════════════════════════════════════════════════════════════════════ // STAGGER CALC — track × class matrix + hot growth + log // ═══════════════════════════════════════════════════════════════════════ function _buildStaggerCalc(){ var m=$('stagger-calc-mount');if(!m)return; var ct=S.cur?_getCarType(S.cur):'generic'; var tgt=_resolveStaggerTarget({carType:ct}); m.innerHTML=''; var card=document.createElement('div'); card.style.cssText='background:var(--dark2);border:1px solid rgba(208,25,14,.1);padding:14px;margin-bottom:8px'; var disc=_getGarageMode(); if(disc==='drag'){ card.innerHTML='
    Dial-in / ET
    Drag cars don\'t use inch stagger — use Hunter for dial-in, RT, staging depth, and 60-foot. Log passes in your run history.
    '; m.appendChild(card);return; } if (disc==='road'){card.innerHTML='
    Stagger
    Road course — no inch stagger. Use corner weights, brake bias (Miata), or seat/track width (kart).
    ';m.appendChild(card);return;} if(ct==='kart'||tgt.kart){ card.innerHTML='
    Stagger
    '+STAGGER_TARGETS.kart.note+'
    '; m.appendChild(card);return; } var carId=S.cur?(S.cur.id||'local'):'demo'; var logKey='bb_stagger_log_'+carId; var saved={};try{saved=JSON.parse(localStorage.getItem(logKey)||'{}');}catch(e){} var sc=tgt.sampleCirc||{}; var h='
    '; h+='
    Stagger Calculator
    '; h+='
    '; h+='
    '+tgt.note+'
    '; h+='
    '; h+='
    TONIGHT @ '+(tgt.trackLabel||'—').toUpperCase()+'
    '; h+='
    '; h+='
    HOT TARGET
    '+tgt.rec+'"
    '; h+='
    COLD SET
    '+tgt.coldRec+'"
    '; h+='
    '+(tgt.source==='bb-classes'?'CLASS RANGE':'FALLBACK')+'
    '+tgt.rear[0]+'–'+tgt.rear[1]+'"
    '; h+='
    '; if(tgt.source!=='bb-classes'&&tgt.source!=='class-profile'){ h+='
    Pick your exact class in Garage — stagger comes from bb-classes public records, not guesses.
    '; } if(tgt.anchor){ h+='
    MFR ANCHOR: '+tgt.anchor.mfr+' · '+tgt.anchor.doc+' · '+tgt.anchor.tire+'
    '; } h+='
    '; h+='
    '+tgt.notes.join(' · ')+'
    '; h+='
    After hot laps: RR +'+(tgt.growth.rr||0)+'" · LR +'+(tgt.growth.lr||0)+'" → stagger grows ~+'+tgt.growthNet.toFixed(3)+'"
    '; h+='
    '; h+='
    MEASURE COLD · FRONT: '+tgt.front[0]+'–'+tgt.front[1]+'"
    '; h+='
    '; [['RR','t-sc-rr',saved.rr||sc.rr],['LR','t-sc-lr',saved.lr||sc.lr],['RF','t-sc-rf',saved.rf||sc.rf||''],['LF','t-sc-lf',saved.lf||sc.lf||'']].forEach(function(row){ h+='
    '+row[0]+' CIRC (in)
    '; }); h+='
    '; h+='
    '; h+='
    REAR
    --
    '; h+='
    FRONT
    --
    '; h+='
    HOT EST
    --
    '; h+='
    '; h+='
    '; h+='
    '; h+=''; h+=''; h+='
    '; h+='
    '; card.innerHTML=h;m.appendChild(card); if(_su&&_su.stagger_r!=null){var v=$('t-sc-verdict');if(v)v.textContent='Notebook rear stagger: '+_su.stagger_r+'" · class target hot '+tgt.rec+'"';} var lastRear=null,lastFront=null; function cs(){ var rr=parseFloat($('t-sc-rr')&&$('t-sc-rr').value)||0,lr=parseFloat($('t-sc-lr')&&$('t-sc-lr').value)||0; var rf=parseFloat($('t-sc-rf')&&$('t-sc-rf').value)||0,lf=parseFloat($('t-sc-lf')&&$('t-sc-lf').value)||0; var rear=(rr&&lr)?(rr-lr):null,front=(rf&&lf)?(rf-lf):null; lastRear=rear;lastFront=front; var re=$('t-sc-rear'),fe=$('t-sc-front'),hot=$('t-sc-hot'),ver=$('t-sc-verdict'); if(re)re.textContent=rear!=null?rear.toFixed(3)+'"':'--'; if(fe)fe.textContent=front!=null?front.toFixed(3)+'"':'--'; if(hot&&rear!=null)hot.textContent=(rear+tgt.growthNet).toFixed(3)+'"'; if(re&&rear!=null){ if(rear>=tgt.rear[0]&&rear<=tgt.rear[1])re.style.color='#2DB87F'; else if(Math.abs(rear-tgt.coldRec)<=0.25)re.style.color='var(--amber)'; else re.style.color='var(--red-hot)'; } if(hot&&rear!=null){ var hotVal=rear+tgt.growthNet; if(Math.abs(hotVal-tgt.rec)<=0.25)hot.style.color='#2DB87F'; else if(hotVal>=tgt.rear[0]&&hotVal<=tgt.rear[1]+0.25)hot.style.color='var(--amber)'; else hot.style.color='var(--red-hot)'; } if(ver&&rear!=null){ var hotVal=rear+tgt.growthNet; var msg=''; if(Math.abs(hotVal-tgt.rec)<=0.25)msg='On class target '+tgt.rec+'" hot. '; else if(hotValtgt.rear[1])msg='Above class max '+tgt.rear[1]+'" — may be too free. '; else msg='Cold '+rear.toFixed(3)+'" → hot ~'+hotVal.toFixed(3)+'" (class target '+tgt.rec+'"). '; if(front!=null&&tgt.front[1]>0){ if(fronttgt.front[1]+0.125)msg+='Front stagger high.'; } ver.textContent=msg; } try{localStorage.setItem(logKey,JSON.stringify({rr:rr||saved.rr,lr:lr||saved.lr,rf:rf||saved.rf,lf:lf||saved.lf,ts:Date.now()}));}catch(e){} } ['t-sc-rr','t-sc-lr','t-sc-rf','t-sc-lf'].forEach(function(id){var el=$(id);if(el)el.addEventListener('input',cs);}); var fill=$('t-sc-fill'); if(fill)fill.onclick=function(){ var c=tgt.sampleCirc;if(!c.rr)return; if($('t-sc-rr'))$('t-sc-rr').value=c.rr; if($('t-sc-lr'))$('t-sc-lr').value=c.lr; if(c.rf&&$('t-sc-rf'))$('t-sc-rf').value=c.rf; if(c.lf&&$('t-sc-lf'))$('t-sc-lf').value=c.lf; cs();toast('Filled cold circumferences for '+tgt.rec+'" hot target'); }; var ap=$('t-sc-apply'); if(ap)ap.onclick=function(){ if(lastRear==null){toast('Enter RR and LR circumferences');return;} if(typeof _su!=='undefined'){ _su.stagger_r=Math.round(lastRear*1000)/1000; if(lastFront!=null)_su.stagger_f=Math.round(lastFront*1000)/1000; } if(typeof _syncActiveSetupEverywhere==='function')_syncActiveSetupEverywhere(true); var log=[];try{log=JSON.parse(localStorage.getItem(logKey+'_hist')||'[]');}catch(e){} log.unshift({ts:Date.now(),rear:lastRear,front:lastFront,hotEst:lastRear+tgt.growthNet,target:tgt.rec,track:tgt.trackLabel}); if(log.length>8)log=log.slice(0,8); localStorage.setItem(logKey+'_hist',JSON.stringify(log)); _cloudSave('stagger_log',carId,log); _renderStaggerLog(); toast('Stagger applied — cold '+lastRear.toFixed(3)+'" · hot est '+(lastRear+tgt.growthNet).toFixed(3)+'"'); }; function _renderStaggerLog(){ var el=$('t-sc-log');if(!el)return; var log=[];try{log=JSON.parse(localStorage.getItem(logKey+'_hist')||'[]');}catch(e){} if(!log.length){el.innerHTML='';return;} el.innerHTML='
    LOG
    '+log.slice(0,3).map(function(e){ return '
    '+new Date(e.ts).toLocaleDateString()+' cold '+e.rear.toFixed(3)+'" → hot ~'+e.hotEst.toFixed(3)+'" (tgt '+e.target+'")
    '; }).join(''); } _renderStaggerLog(); var ask=$('t-sc-ask'); if(ask)ask.onclick=function(){ switchTab('hunter'); var msg='Stagger check for tonight. '; if(lastRear!=null)msg+='I measured cold rear stagger '+lastRear.toFixed(3)+'" (hot est '+(lastRear+tgt.growthNet).toFixed(3)+'"). '; msg+='Tools class target is '+tgt.rec+'" hot ('+(tgt.classRange||tgt.rear[0]+'-'+tgt.rear[1]+'"')+') for '+(tgt.className||ct)+' at '+tgt.trackLabel+'. What should I change?'; setTimeout(function(){addMsg('u',msg);sendToHunter(msg);},300); }; cs(); } // ═══════════════════════════════════════════════════════════════════════ // DRAG TOOLS — run log, tree handicap (one garage; class picks mode) // ═══════════════════════════════════════════════════════════════════════ function _dragGarageState(){ try{return JSON.parse(localStorage.getItem('drag_garage')||'null')||{};}catch(e){return{};} } function _saveDragGarageState(g){try{localStorage.setItem('drag_garage',JSON.stringify(g));if(S.token&&window.RWGarage&&RWGarage.saveAccountBlob)RWGarage.saveAccountBlob(S.token,'drag_garage',g);}catch(e){}} function _parseDragNum(v){if(v==null||v==='')return null;var n=parseFloat(String(v).replace(/[^0-9.-]/g,''));return isNaN(n)?null:n;} function _buildDragTools(){ var m=$('drag-tools-mount');if(!m)return; m.innerHTML=''; if(_getGarageMode()!=='drag')return; var carId=S.cur?(S.cur.id||'local'):'demo'; var g=_dragGarageState(); if(!g.runs)g.runs={}; if(!g.runs[carId])g.runs[carId]=[]; var card=document.createElement('div'); card.style.cssText='background:var(--dark2);border:1px solid rgba(232,64,26,.2);padding:14px;margin-bottom:8px'; var h='
    Drag Run Log
    '; h+='
    Log passes here — Hunter drag mode reads this. Same subscription, same garage.
    '; h+='
    '; h+='
    DIAL
    '; h+='
    ET
    '; h+='
    '; h+='
    RT
    '; h+='
    60\'
    '; h+='
    MPH
    '; h+='
    SURFACE TEMP (°F)
    '; h+=''; h+='
    '; h+='
    TREE / HANDICAP
    '; h+='
    '; h+='
    YOUR DIAL
    '; h+='
    OPP DIAL
    '; h+='
    '; h+='
    Enter both dials for handicap head start.
    '; card.innerHTML=h;m.appendChild(card); if(g.active_dial!=null&&$('dg-dial'))$('dg-dial').value=Number(g.active_dial).toFixed(3); function renderLog(){ var el=$('dg-log');if(!el)return; var runs=(g.runs[carId]||[]).slice(0,5); if(!runs.length){el.textContent='No passes logged yet.';return;} el.innerHTML=runs.map(function(r){ var m=r.dial&&r.et?(Number(r.dial)-Number(r.et)).toFixed(3):'—'; return '
    '+new Date(r.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})+' dial '+r.dial+' ET '+r.et+' margin '+m+(r.rt?' RT '+r.rt:'')+'
    '; }).join(''); } function treeCalc(){ var y=_parseDragNum($('dg-tree-y')&&$('dg-tree-y').value),o=_parseDragNum($('dg-tree-o')&&$('dg-tree-o').value); var hs=$('dg-tree-out'),det=$('dg-tree-det'); if(!hs||!det)return; if(y==null||o==null){hs.textContent='—';det.textContent='Enter both dials for handicap head start.';return;} var diff=Math.abs(y-o),slower=y>o; hs.textContent=diff.toFixed(3)+'s head start'; det.textContent=(slower?'You leave first':'Opponent leaves first')+' by '+diff.toFixed(3)+'s · Your '+y.toFixed(3)+' vs '+o.toFixed(3); } renderLog();treeCalc(); ['dg-tree-y','dg-tree-o'].forEach(function(id){var el=$(id);if(el)el.addEventListener('input',treeCalc);}); var save=$('dg-save'); if(save)save.onclick=function(){ var dial=_parseDragNum($('dg-dial')&&$('dg-dial').value),et=_parseDragNum($('dg-et')&&$('dg-et').value); if(dial==null||et==null){toast('Dial and ET required');return;} var run={ts:Date.now(),dial:dial.toFixed(3),et:et.toFixed(3),rt:$('dg-rt')&&$('dg-rt').value||null,ft60:$('dg-60')&&$('dg-60').value||null,mph:$('dg-mph')&&$('dg-mph').value||null,surface_temp_f:$('dg-surf')&&$('dg-surf').value||null}; var st=_parseDragNum($('dg-surf')&&$('dg-surf').value); if(st!=null&&typeof _saveManualSurfaceTempF==='function'){_saveManualSurfaceTempF(st,'ir');if(typeof _syncSurfaceTempToWx==='function')_syncSurfaceTempToWx({manual:st,source:'ir'});} g.runs[carId].unshift(run); if(g.runs[carId].length>30)g.runs[carId]=g.runs[carId].slice(0,30); g.active_dial=dial;g.active_dial_source='run log'; _saveDragGarageState(g); if($('dg-tree-y')&&!$('dg-tree-y').value.trim())$('dg-tree-y').value=dial.toFixed(3); ['dg-et','dg-rt','dg-60','dg-mph'].forEach(function(id){var el=$(id);if(el)el.value='';}); renderLog();toast('Pass logged'); }; } function _buildRoadTools(){ var m=$('road-tools-mount');if(!m)return; m.innerHTML=''; if(_getGarageMode()!=='road')return; var cls=S.cur?(S.cur.class||S.cur.car_class||''):''; var isMiata=_isSpecMiataClass(cls); var card=document.createElement('div'); card.style.cssText='background:var(--dark2);border:1px solid rgba(107,140,255,.25);padding:14px;margin-bottom:8px'; var h='
    '+(isMiata?'Spec Miata Setup':'Road Kart Setup')+'
    '; h+='
    '+(isMiata?'Corner weight cross, cold PSI, brake bias. No aero — scale sheet is your main tool.':'Asphalt kart: seat mm, rear track width, axle, cold PSI. Gearing chart below.')+'
    '; var _rsurf=typeof _effectiveSurfaceTempF==='function'?_effectiveSurfaceTempF():''; h+='
    TRACK SURFACE TEMP (°F)
    Road grip and tire warm-up follow surface temp — log IR reading before sessions.
    '; if(!isMiata){ h+='
    '; h+='
    SEAT (mm)
    '; h+='
    REAR WIDTH (mm)
    '; } else { h+='
    BRAKE BIAS (% FRONT)
    '; } h+='
    NTK baseline gearing: Yamaha pipe 10/85 · Rotax Max 12/83 · LO206 Jr 15/24 — log what you run.
    '; h+=''; h+=''; card.innerHTML=h;m.appendChild(card); var sync=$('rt-sync'); if(sync)sync.onclick=function(){ if(typeof _su==='undefined')return; var st=parseFloat($('rt-surf')&&$('rt-surf').value); if(!isNaN(st)&&typeof _saveManualSurfaceTempF==='function'){_saveManualSurfaceTempF(st,'ir');if(typeof _syncSurfaceTempToWx==='function')_syncSurfaceTempToWx({manual:st,source:'ir'});} if(isMiata){var bb=parseFloat($('rt-bbias')&&$('rt-bbias').value);if(!isNaN(bb))_su.brake_bias=bb;} else{var sp=parseFloat($('rt-seat')&&$('rt-seat').value),rw=parseFloat($('rt-rw')&&$('rt-rw').value);if(!isNaN(sp))_su.seat_pos=sp;if(!isNaN(rw))_su.rear_track_width_mm=rw;} if(typeof _syncActiveSetupEverywhere==='function')_syncActiveSetupEverywhere(true); toast('Road setup synced'); }; } // ═══════════════════════════════════════════════════════════════════════ // ENHANCED SCALE SHEET WITH LEARNING SYSTEM // ═══════════════════════════════════════════════════════════════════════ function _calcScale(lf,rf,lr,rr){var t=lf+rf+lr+rr;if(t<50)return null;return{total:t,left:((lf+lr)/t*100),rear:((lr+rr)/t*100),cross:((rf+lr)/t*100),bite:lr-rr,wedge:(rf+lr)-(lf+rr),fsplit:rf-lf,rsplit:rr-lr};} function _scaleColor(val,range){if(!range)return'var(--white)';if(val>=range[0]&&val<=range[1])return'#2DB87F';if(val>=range[0]-2&&val<=range[1]+2)return'var(--amber)';return'var(--red-hot)';} function _bbViewportKbOffset() { if (!window.visualViewport) return 0; return Math.max(0, window.innerHeight - window.visualViewport.height); } function _bbSetKbOffset(px) { document.documentElement.style.setProperty('--bb-kb-offset', (px > 80 ? px + 16 : 0) + 'px'); } function _bbMobileKeyboardScroll(mount) { if (!mount || mount._kbWired) return; mount._kbWired = true; var raf = 0, focusEl = null; var apply = function() { raf = 0; var gap = _bbViewportKbOffset(); var active = document.activeElement; var open = gap > 80 && focusEl && mount.contains(focusEl); if (!open && active && mount.contains(active) && active.matches('input, select, textarea')) { focusEl = active; open = gap > 80; } _bbSetKbOffset(open ? gap : 0); mount.classList.toggle('bb-kb-open', open); if (open && focusEl) { try { var vr = focusEl.getBoundingClientRect(); var vh = window.visualViewport ? window.visualViewport.height : window.innerHeight; if (vr.bottom > vh - 80) focusEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } catch (_) {} } }; var schedule = function() { if (!raf) raf = requestAnimationFrame(apply); }; mount.addEventListener('focusin', function(e) { if (!e.target || !e.target.matches('input, select, textarea')) return; focusEl = e.target; schedule(); setTimeout(schedule, 120); }, true); mount.addEventListener('focusout', function() { setTimeout(function() { if (!mount.contains(document.activeElement)) { focusEl = null; _bbSetKbOffset(0); mount.classList.remove('bb-kb-open'); } }, 160); }, true); if (window.visualViewport) { window.visualViewport.addEventListener('resize', schedule); window.visualViewport.addEventListener('scroll', schedule); } } function _bbScaleKeyboardScroll(mount) { _bbMobileKeyboardScroll(mount); } function _bbSyncMobileThumbBars() { var scaleBar = document.getElementById('bb-scale-sticky-bar'); var scaleOn = $('t-tools') && $('t-tools').classList.contains('on'); if (scaleBar) scaleBar.classList.toggle('bb-scale-sticky-off', !scaleOn); var recBar = document.getElementById('bb-rec-thumb-bar'); if (!recBar) { recBar = document.createElement('div'); recBar.id = 'bb-rec-thumb-bar'; recBar.className = 'bb-rec-thumb-bar bb-rec-thumb-off'; document.body.appendChild(recBar); } var garageOn = $('t-garage') && $('t-garage').classList.contains('on'); var payload = S._activeRecPayload; if (!payload || !garageOn) { recBar.classList.add('bb-rec-thumb-off'); recBar.innerHTML = ''; return; } var p = typeof _normalizeHunterRecPayload === 'function' ? _normalizeHunterRecPayload(payload, 'ACTIVE REC') : { action: '' }; if (!p.action) { recBar.classList.add('bb-rec-thumb-off'); recBar.innerHTML = ''; return; } var recId = typeof _storeRecActionEntry === 'function' ? _storeRecActionEntry(payload) : 'rec'; recBar.classList.remove('bb-rec-thumb-off'); recBar.innerHTML = ''; } function _buildScaleSheet(){ var m=$('scale-sheet-mount');if(!m)return;m.innerHTML=''; var ct=S.cur?_getCarType(S.cur):'generic'; var tgt=SCALE_TARGETS[ct]||SCALE_TARGETS.generic; var carId=S.cur?(S.cur.id||'local'):'demo'; var sKey='bb_scale_'+carId; var sd=JSON.parse(localStorage.getItem(sKey)||'{"current":{},"previous":{},"adjustments":[]}'); var lKey='bb_scale_learn_'+carId; var ld=JSON.parse(localStorage.getItem(lKey)||'{}'); var card=document.createElement('div'); card.className='bb-scale-card'; var h=(typeof _renderGarageFlowStrip==='function'?_renderGarageFlowStrip('scale'):''); h+='
    Scale Sheet
    ' +'' +'
    '; h+='
    '; ['LF','RF','LR','RR'].forEach(function(pos){ var k=pos.toLowerCase();var v=sd.current[k]||''; h+=''; }); h+='
    '; h+='
    '; [{l:'CROSS %',k:'cross',c:'var(--red-hot)',pri:1},{l:'LEFT %',k:'left',c:'var(--white)',pri:1},{l:'REAR %',k:'rear',c:'var(--white)',pri:1},{l:'TOTAL',k:'total',c:'var(--gold)',pri:1}].forEach(function(o){ h+='
    '+o.l+'
    --
    '; }); h+='
    '; [{l:'BITE',k:'bite'},{l:'WEDGE',k:'wedge'},{l:'F SPL',k:'fsplit'},{l:'R SPL',k:'rsplit'}].forEach(function(o){ h+='
    '+o.l+'
    --
    '; }); h+='
    '; h+=''; h+='
    ' +'' +'' +'' +'
    '; h+=''; h+=''; h+='
    '; card.innerHTML=h;m.appendChild(card); var stickyBar=document.createElement('div'); stickyBar.id='bb-scale-sticky-bar'; stickyBar.className='bb-scale-sticky-bar'; stickyBar.innerHTML=''; m.appendChild(stickyBar); _bbScaleKeyboardScroll(m); if(typeof _bbSyncMobileThumbBars==='function')_bbSyncMobileThumbBars(); function recalc(){ var lf=parseFloat($('sc-lf').value)||0,rf=parseFloat($('sc-rf').value)||0,lr2=parseFloat($('sc-lr').value)||0,rr2=parseFloat($('sc-rr').value)||0; sd.current={lf:lf,rf:rf,lr:lr2,rr:rr2}; var r=_calcScale(lf,rf,lr2,rr2);if(!r)return; $('sc-v-cross').textContent=r.cross.toFixed(1)+'%';$('sc-v-cross').style.color=_scaleColor(r.cross,tgt.cross); $('sc-v-left').textContent=r.left.toFixed(1)+'%';$('sc-v-left').style.color=_scaleColor(r.left,tgt.left); $('sc-v-rear').textContent=r.rear.toFixed(1)+'%';$('sc-v-rear').style.color=_scaleColor(r.rear,tgt.rear); $('sc-v-total').textContent=Math.round(r.total); $('sc-v-bite').textContent=(r.bite>0?'+':'')+r.bite;$('sc-v-bite').style.color=_scaleColor(r.bite,tgt.bite); $('sc-v-wedge').textContent=(r.wedge>0?'+':'')+r.wedge; $('sc-v-fsplit').textContent=(r.fsplit>0?'+':'')+r.fsplit; $('sc-v-rsplit').textContent=(r.rsplit>0?'+':'')+r.rsplit; localStorage.setItem(sKey,JSON.stringify(sd)); if(sd.previous&&sd.previous.lf){ var p=_calcScale(sd.previous.lf,sd.previous.rf,sd.previous.lr,sd.previous.rr); if(p){ var dEl=$('sc-delta');dEl.style.display='block'; var dx=r.cross-p.cross,dl=r.left-p.left,dr2=r.rear-p.rear,db=r.bite-p.bite; dEl.innerHTML='Δ vs locked: Cross '+(dx>0?'+':'')+dx.toFixed(1)+'% · Left '+(dl>0?'+':'')+dl.toFixed(1)+'% · Rear '+(dr2>0?'+':'')+dr2.toFixed(1)+'% · Bite '+(db>0?'+':'')+db; } } } function applyCorners(obj){ if(!obj)return; ['lf','rf','lr','rr'].forEach(function(k){ var el=$('sc-'+k); if(el&&obj[k]!=null&&obj[k]!=='')el.value=obj[k]; }); recalc(); } ['sc-lf','sc-rf','sc-lr','sc-rr'].forEach(function(id){var el=$(id);if(el)el.addEventListener('input',recalc);}); recalc(); if(sd.previous&&sd.previous.lf){ var lockBtn=$('sc-lock');if(lockBtn){lockBtn.classList.add('locked');lockBtn.textContent='PREV LOCKED';} } $('sc-lock').onclick=function(){ sd.previous={lf:parseFloat($('sc-lf').value)||0,rf:parseFloat($('sc-rf').value)||0,lr:parseFloat($('sc-lr').value)||0,rr:parseFloat($('sc-rr').value)||0}; localStorage.setItem(sKey,JSON.stringify(sd)); toast('Previous weights locked. Make your change, re-scale, then Save / Log.'); $('sc-lock').classList.add('locked');$('sc-lock').textContent='PREV LOCKED'; }; $('sc-reset').onclick=function(){ if(!sd.previous||!sd.previous.lf){toast('Lock previous weights first');return;} applyCorners(sd.previous); toast('Restored to locked previous weights'); }; $('sc-log').onclick=function(){ var sel=$('sc-adj');var adjType=sel.value;if(!adjType){toast('Select what you changed first');return;} var adjLabel=adjType==='custom'?prompt('What did you change?'):sel.options[sel.selectedIndex].text; if(!adjLabel)return; if(!sd.previous||!sd.previous.lf){toast('Lock previous first, make change, then save.');return;} var before=_calcScale(sd.previous.lf,sd.previous.rf,sd.previous.lr,sd.previous.rr); var after=_calcScale(sd.current.lf,sd.current.rf,sd.current.lr,sd.current.rr); if(!before||!after)return; var deltas={cross:after.cross-before.cross,left:after.left-before.left,rear:after.rear-before.rear,bite:after.bite-before.bite}; var changeId='sc_'+Date.now(); sd.adjustments.push({id:changeId,ts:Date.now(),desc:adjLabel,type:adjType,before:sd.previous,after:sd.current,deltas:deltas}); if(sd.adjustments.length>20)sd.adjustments=sd.adjustments.slice(-20); localStorage.setItem(sKey,JSON.stringify(sd)); if(!ld[adjType])ld[adjType]=[]; ld[adjType].push({dc:deltas.cross,dl:deltas.left,dr:deltas.rear,db:deltas.bite,ts:Date.now(),id:changeId}); if(ld[adjType].length>10)ld[adjType]=ld[adjType].slice(-10); localStorage.setItem(lKey,JSON.stringify(ld)); _cloudSave('scale',carId,sd);_cloudSave('scale_learn',carId,ld); if(typeof _fetchScaleLearningUpdate==='function')_fetchScaleLearningUpdate({id:changeId,desc:adjLabel,type:adjType,deltas:deltas,before:sd.previous,after:sd.current},getActiveScaleRecommendation()); if(typeof _appendLocalLearningSignal==='function'&&S.cur&&S.cur.id){ var tid=typeof _resolveTrackId==='function'?_resolveTrackId():null; var hunterRec=typeof getActiveScaleRecommendation==='function'?getActiveScaleRecommendation():null; var direction='other';var followed=false; if(hunterRec&&hunterRec.suggestion){ var hs=hunterRec.suggestion; if(hs.left_rear_weight||Number(hs.lr||0)>=15)direction='lr'; else if(hs.right_front_wedge||Number(hs.rf||0)>=15)direction='rf'; if(direction==='lr')followed=/lr|ballast_left|rear/.test(adjType); if(direction==='rf')followed=/rf|jack|wedge/.test(adjType); } if(tid)_appendLocalLearningSignal(S.cur.id,tid,'scale_follow',{ts:Date.now(),direction:direction,followed:followed}); } card.classList.remove('bb-scale-saved'); void card.offsetWidth; card.classList.add('bb-scale-saved'); toast('✓ Saved — '+adjLabel+' · Cross '+(deltas.cross>0?'+':'')+deltas.cross.toFixed(1)+'%'); if(S._pendingRecLog){toast('Linked to Hunter rec: '+S._pendingRecLog);S._pendingRecLog=null;} setTimeout(function(){toast('Next: rate how the car felt in Debrief or scale feedback');},600); if(typeof _touchRecHistoryOutcome==='function'){ var recentRec=JSON.parse(localStorage.getItem('bb_rec_history_'+(S.cur&&S.cur.id?S.cur.id:'local'))||'[]')[0]; if(recentRec&&Date.now()-recentRec.ts<86400000)_touchRecHistoryOutcome(recentRec.rec||recentRec.text,'logged',adjLabel+(deltas.cross!=null?' · Cross '+(deltas.cross>0?'+':'')+deltas.cross.toFixed(1)+'%':'')); } sd.previous={lf:sd.current.lf,rf:sd.current.rf,lr:sd.current.lr,rr:sd.current.rr}; localStorage.setItem(sKey,JSON.stringify(sd)); sel.value=''; _showLearn();_showHist(); }; var stickyLog=$('sc-log-sticky'); if(stickyLog)stickyLog.onclick=function(){if($('sc-log'))$('sc-log').click();}; function _showLearn(){ var preds=[]; Object.keys(ld).forEach(function(t){ if(ld[t].length<3)return; var avg={dc:0,dl:0,dr:0,db:0}; ld[t].forEach(function(d){avg.dc+=d.dc;avg.dl+=d.dl;avg.dr+=d.dr;avg.db+=d.db;}); var n=ld[t].length;avg.dc/=n;avg.dl/=n;avg.dr/=n;avg.db/=n; var label=t;ADJ_TYPES.forEach(function(a){if(a.t===t)label=a.l;}); preds.push(label+' → Cross '+(avg.dc>0?'+':'')+avg.dc.toFixed(1)+'%, Bite '+(avg.db>0?'+':'')+avg.db.toFixed(0)+' ('+n+' samples)'); }); if(preds.length>0){$('sc-learn').style.display='block';$('sc-learn-list').innerHTML=preds.join('
    ');} } function _showHist(){ var hEl=$('sc-hist');if(!hEl)return; hEl.innerHTML=sd.adjustments.slice(-5).reverse().map(function(a){ return '
    '+new Date(a.ts).toLocaleTimeString().replace(/:\d{2}\s/,' ')+' '+a.desc+' → X'+(a.deltas.cross>0?'+':'')+a.deltas.cross.toFixed(1)+'%
    '; }).join(''); } _showLearn();_showHist(); if(typeof _fetchWeightRecommendation==='function')_fetchWeightRecommendation('sc-weight-rec'); $('sc-ask').onclick=function(){ var r=_calcScale(parseFloat($('sc-lf').value)||0,parseFloat($('sc-rf').value)||0,parseFloat($('sc-lr').value)||0,parseFloat($('sc-rr').value)||0); if(!r){toast('Enter corner weights first');return;} var msg='Scale reading: LF='+$('sc-lf').value+' RF='+$('sc-rf').value+' LR='+$('sc-lr').value+' RR='+$('sc-rr').value+'. Cross='+r.cross.toFixed(1)+'% Left='+r.left.toFixed(1)+'% Bite='+r.bite+'.'; var preds=[];Object.keys(ld).forEach(function(t){if(ld[t].length>=3){var avg=0;ld[t].forEach(function(d){avg+=d.dc;});avg/=ld[t].length;var label=t;ADJ_TYPES.forEach(function(a){if(a.t===t)label=a.l;});preds.push(label+'='+avg.toFixed(1)+'% cross');}}); if(preds.length)msg+=' My car patterns: '+preds.join(', ')+'.'; switchTab('hunter');addMsg('u',msg);sendToHunter(msg); }; } function buildChecklist(){var el=$("checklist");if(!el)return;var items=_getChecklistItems();var ckKey=_ckStorageKey();var saved=JSON.parse(localStorage.getItem(ckKey)||"{}");el.innerHTML="";items.forEach(function(item,i){var row=document.createElement("div");row.className="rn-check";row.style.cssText="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.03);cursor:pointer";var chk=document.createElement("div");var done=!!saved[i];chk.style.cssText="width:18px;height:18px;border:2px solid "+(done?"var(--green)":"var(--dark4)")+";background:"+(done?"var(--green)":"transparent")+";flex-shrink:0;display:flex;align-items:center;justify-content:center";chk.innerHTML=done?"":"";var lbl=document.createElement("div");lbl.style.cssText="font-family:var(--body);font-size:13px;color:"+(done?"var(--dark4)":"var(--white)")+";text-decoration:"+(done?"line-through":"none");lbl.textContent=item;row.appendChild(chk);row.appendChild(lbl);row.onclick=function(){saved[i]=!saved[i];localStorage.setItem(ckKey,JSON.stringify(saved));buildChecklist();};el.appendChild(row);});var done3=Object.values(saved).filter(Boolean).length;var prog2=$("ck-progress");if(prog2)prog2.textContent=done3+" / "+items.length;var reset=$("btn-resetck");if(reset)reset.onclick=function(){localStorage.removeItem(ckKey);buildChecklist();};} function toggleAcc(id){var body=$("body-"+id),arr=$("arr-"+id);if(!body)return;var open=body.classList.toggle("open");if(arr)arr.textContent=open?"▲":"▼";} // ── Camera Guide (TOOL 01) ───────────────────────────────────────────────── var _CAM={mode:null,scanMode:'moisture',stream:null,motionHandler:null,stableSamples:[],stableOk:false,captured:null,quality:null}; var _VISION_URL='https://zmrouoqututfndplboyc.supabase.co/functions/v1/bb-vision'; var _CAM_GUIDES={ track:{title:'Track Surface — AI Eye',icon:'🏁',ai:'vision',analyzeLabel:'ANALYZE SURFACE', scanModes:[ {id:'composition',label:'SOIL MAKEUP',sub:'Loose off-line sample'}, {id:'moisture',label:'RACE LINE',sub:'Groove moisture / grip tonight'} ], steps:['Pick scan type below — composition and moisture need different spots','Follow the checklist for that shot type','Hold steady until STABLE, then capture','AI compares tonight\'s read to USDA substrate baseline when on file'], overlay:'SAMPLE ZONE', hunterPrompt:'Analyze this track surface photo. Compare to USDA substrate baseline in context. Clay type, moisture, expected grip transition, and setup suggestions for tonight.'}, ride:{title:'Ride Height',icon:'📐',ai:'hunter',analyzeLabel:'COMPARE RIDE HEIGHT', steps:['Side profile — car on level ground in the pits','Frame the tire + rocker panel in one shot','Use the same spot before and after a change','Keep camera height at hub centerline'], overlay:'GROUND LINE', hunterPrompt:'Analyze this side-profile ride height photo. Estimate ride height at LF and LR if visible, note rake, and flag anything that looks off for a dirt car.'}, gauge:{title:'Gauge / Shock Ring',icon:'🔢',ai:'hunter',analyzeLabel:'READ GAUGE', steps:['Center the gauge face or shock adjustment ring','Fill the circle — no glare on the lens','Hold steady — numbers must be sharp','Works for tire pressure, shock clicks, scale display'], overlay:'', hunterPrompt:'Read the number on this gauge or shock adjustment ring. Reply with ONLY the reading and units (e.g. "32 PSI" or "7 clicks"). If unreadable, say what is blocking the read.'} }; function _camStepsForMode(g){ if(!g||!g.scanModes||_CAM.mode!=='track')return g.steps||[]; if(_CAM.scanMode==='composition')return [ 'Find a LOOSE patch OFF the race line — berm, infield, or freshly turned lane edge', 'Kick/scrape to expose broken-up aggregate — not the packed rubber groove', 'Kneel 12–18 in from the sample; fill frame with bare dirt/clay grain', 'Keep shadow and tire shine out — color and aggregate size drive the read' ]; return [ 'Shoot the ACTIVE race line or entry where cars actually run tonight', 'Include rubbered or prep-watered surface — sheen shows moisture layer', 'Hold phone 12–18 in from surface, parallel to ground', 'After hot laps, same spot shows transition vs intermission baseline' ]; } function _camRefreshGuideSteps(g){ var list=$('cam-guide-list');if(!list||!g)return; list.innerHTML=''; _camStepsForMode(g).forEach(function(s){var li=document.createElement('li');li.textContent=s;list.appendChild(li);}); var how=$('cam-scan-howto');if(how)how.innerHTML=_surfaceScanHowtoHtml(true); } function openCamGuide(mode){ if(!_CAM_GUIDES[mode]){toast('Unknown camera mode');return;} _camResetState(); _CAM.mode=mode; if(mode==='track'&&!_CAM.scanMode)_CAM.scanMode='moisture'; var g=_CAM_GUIDES[mode]; var modal=$('bb-cam-modal');if(!modal)return; $('cam-modal-title').textContent=g.icon+' '+g.title; $('cam-guide-title').textContent=g.title+' — shot checklist'; var modeWrap=$('cam-scan-modes'); if(modeWrap){ modeWrap.innerHTML=''; if(g.scanModes){ modeWrap.style.display='flex'; g.scanModes.forEach(function(m){ var b=document.createElement('button'); b.type='button'; var on=_CAM.scanMode===m.id; b.style.cssText='flex:1;padding:10px 8px;background:'+(on?'rgba(245,166,35,.12)':'var(--dark)')+';border:1px solid '+(on?'var(--amber)':'var(--dark4)')+';color:'+(on?'var(--amber)':'var(--muted)')+';font-family:Share Tech Mono;font-size:9px;cursor:pointer;line-height:1.4'; b.innerHTML='
    '+m.label+'
    '+m.sub+'
    '; b.onclick=function(){_CAM.scanMode=m.id;_camRefreshGuideSteps(g);Array.prototype.forEach.call(modeWrap.children,function(btn,i){var mm=g.scanModes[i];var sel=_CAM.scanMode===mm.id;btn.style.background=sel?'rgba(245,166,35,.12)':'var(--dark)';btn.style.borderColor=sel?'var(--amber)':'var(--dark4)';btn.style.color=sel?'var(--amber)':'var(--muted)';btn.firstChild.style.color=sel?'var(--white)':'var(--muted)';});}; modeWrap.appendChild(b); }); }else modeWrap.style.display='none'; } var how=$('cam-scan-howto');if(how){how.innerHTML=mode==='track'?_surfaceScanHowtoHtml(true):'';} _camRefreshGuideSteps(g); var svg=$('cam-overlay-svg');if(svg)svg.innerHTML=g.overlay; var analyzeBtn=$('cam-btn-analyze'); if(analyzeBtn){ analyzeBtn.textContent=g.analyzeLabel; if(_canDo('cadet')){ analyzeBtn.style.display='block'; analyzeBtn.onclick=function(){_camRunAnalysis();}; } else { analyzeBtn.style.display='block'; analyzeBtn.textContent='UNLOCK AI ANALYSIS — CADET+'; analyzeBtn.onclick=function(){_upgradeGate('Camera AI Analysis','cadet','Cadet','$5');}; } } $('cam-result-ai').style.display='none'; $('cam-result-ai').innerHTML=''; $('cam-result-quality').innerHTML=''; $('cam-result-preview').innerHTML=''; _camShowStep('guide'); modal.classList.add('on'); modal.setAttribute('aria-hidden','false'); document.body.style.overflow='hidden'; } function closeCamGuide(){ _camStopPreview(); _camStopStability(); var modal=$('bb-cam-modal');if(modal){modal.classList.remove('on');modal.setAttribute('aria-hidden','true');} document.body.style.overflow=''; _camResetState(); } function _camResetState(){ _CAM.captured=null;_CAM.quality=null;_CAM.stableOk=false;_CAM.stableSamples=[]; var still=$('cam-still'),feed=$('cam-feed'); if(still){still.style.display='none';still.src='';} if(feed){feed.style.display='block';} } function _camShowStep(step){ ['guide','capture','result'].forEach(function(s){ var el=$('cam-step-'+s);if(el)el.classList.toggle('on',s===step); }); if(step==='capture'){_camStartPreview();} else{_camStopPreview();_camStopStability();} } function _camPickFile(){ var inp=$('cam-file-inp');if(!inp)return; inp.value=''; inp.onchange=function(){ if(!inp.files||!inp.files[0])return; var f=inp.files[0]; if(f.size>15*1024*1024){toast('Photo too large (max 15MB)');return;} var reader=new FileReader(); reader.onload=function(ev){ _camProcessCapture(ev.target.result,f.type||'image/jpeg'); }; reader.readAsDataURL(f); }; inp.click(); } function _camStartPreview(){ _camStopPreview(); _camStopStability(); _CAM.stableOk=false; var capBtn=$('cam-btn-capture');if(capBtn)capBtn.disabled=true; var stab=$('cam-stability');if(stab){stab.className='cam-stability checking';stab.textContent='Starting camera…';} var feed=$('cam-feed');if(!feed)return; feed.style.display='block'; var still=$('cam-still');if(still)still.style.display='none'; if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia){ if(stab){stab.className='cam-stability bad';stab.textContent='No camera API — use upload instead';} return; } navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:'environment'},width:{ideal:1280},height:{ideal:960}},audio:false}) .then(function(stream){ _CAM.stream=stream;feed.srcObject=stream; return feed.play(); }).then(function(){ _camStartStability(); }).catch(function(){ if(stab){stab.className='cam-stability bad';stab.textContent='Camera blocked — tap upload instead';} }); } function _camStopPreview(){ if(_CAM.stream){ _CAM.stream.getTracks().forEach(function(t){t.stop();}); _CAM.stream=null; } var feed=$('cam-feed');if(feed)feed.srcObject=null; } function _camStartStability(){ _camStopStability(); _CAM.stableSamples=[]; var stab=$('cam-stability'); var capBtn=$('cam-btn-capture'); var hasMotion=typeof window.DeviceMotionEvent!=='undefined'; if(!hasMotion){ if(stab){stab.className='cam-stability ok';stab.textContent='No gyro — hold steady, then capture';} _CAM.stableOk=true; if(capBtn)capBtn.disabled=false; return; } if(stab){stab.className='cam-stability checking';stab.textContent='Hold phone steady…';} _CAM.motionHandler=function(ev){ var a=ev.accelerationIncludingGravity;if(!a)return; var mag=Math.sqrt((a.x||0)*(a.x||0)+(a.y||0)*(a.y||0)+(a.z||0)*(a.z||0)); _CAM.stableSamples.push(mag); if(_CAM.stableSamples.length>24)_CAM.stableSamples.shift(); if(_CAM.stableSamples.length<8)return; var mean=_CAM.stableSamples.reduce(function(s,v){return s+v;},0)/_CAM.stableSamples.length; var variance=_CAM.stableSamples.reduce(function(s,v){return s+(v-mean)*(v-mean);},0)/_CAM.stableSamples.length; var stable=variance<0.35; _CAM.stableOk=stable; if(stab){ stab.className='cam-stability '+(stable?'ok':'bad'); stab.textContent=stable?'STABLE — ready to capture':'Too much movement — brace your elbows'; } if(capBtn)capBtn.disabled=!stable; }; window.addEventListener('devicemotion',_CAM.motionHandler,{passive:true}); setTimeout(function(){ if(!_CAM.stableOk&&capBtn&&!capBtn.disabled)return; if(!_CAM.stableOk&&capBtn&&_CAM.stableSamples.length>=8){ capBtn.disabled=false; if(stab){stab.className='cam-stability ok';stab.textContent='Stable enough — capture when ready';} _CAM.stableOk=true; } },4000); } function _camStopStability(){ if(_CAM.motionHandler){ window.removeEventListener('devicemotion',_CAM.motionHandler); _CAM.motionHandler=null; } } function _camCapture(){ var feed=$('cam-feed');if(!feed||!feed.videoWidth){toast('Camera not ready');return;} var canvas=document.createElement('canvas'); canvas.width=feed.videoWidth;canvas.height=feed.videoHeight; var ctx=canvas.getContext('2d'); ctx.drawImage(feed,0,0); _camProcessCapture(canvas.toDataURL('image/jpeg',0.88),'image/jpeg'); } function _camProcessCapture(dataUrl,mime){ _camStopPreview(); _camStopStability(); _CAM.captured={dataUrl:dataUrl,b64:dataUrl.split(',')[1],mime:mime||'image/jpeg'}; _CAM.quality=null; var still=$('cam-still'),feed=$('cam-feed'); if(still){still.src=dataUrl;still.style.display='block';} if(feed)feed.style.display='none'; _camShowResultPreview(); _camShowStep('result'); } function _camAnalyzeQuality(dataUrl){ return new Promise(function(resolve){ var img=new Image(); img.onload=function(){ var w=320,h=Math.round(img.height*(320/img.width))||240; var c=document.createElement('canvas');c.width=w;c.height=h; var ctx=c.getContext('2d');ctx.drawImage(img,0,0,w,h); var id=ctx.getImageData(0,0,w,h),d=id.data,n=d.length; var sum=0,sumSq=0,count=0; for(var i=0;i35&&brightness<220; var sharpOk=blurScore>2.8; var pass=brightOk&&sharpOk; resolve({pass:pass,brightness:Math.round(brightness),blurScore:+blurScore.toFixed(2),brightOk:brightOk,sharpOk:sharpOk}); }; img.onerror=function(){resolve({pass:false,brightness:0,blurScore:0,brightOk:false,sharpOk:false});}; img.src=dataUrl; }); } function _camShowResultPreview(){ var prev=$('cam-result-preview'),qual=$('cam-result-quality'),ai=$('cam-result-ai'); if(!prev||!_CAM.captured)return; prev.innerHTML=''; qual.innerHTML='
    Quality gate
    Running local checks…
    '; if(ai){ai.style.display='none';ai.innerHTML='';} _camAnalyzeQuality(_CAM.captured.dataUrl).then(function(q){ _CAM.quality=q; var col=q.pass?'var(--green,#2DB87F)':'var(--red-hot)'; var verdict=q.pass?'PASS — safe to send to Hunter':'FAIL — retake for a sharper shot'; var tips=[]; if(!q.brightOk)tips.push(q.brightness<35?'Too dark — move to light or use flash':'Too bright — reduce glare'); if(!q.sharpOk)tips.push('Blurry — brace elbows, tap to focus, move closer'); qual.innerHTML='
    '+verdict+'
    ' +'
    ' +'Brightness: '+q.brightness+'/255 · Edge sharpness: '+q.blurScore+(q.blurScore>2.8?' ✓':' ✗') +(tips.length?'
    '+tips.join(' · ')+'':'') +'
    '; var analyzeBtn=$('cam-btn-analyze'); if(analyzeBtn){ analyzeBtn.disabled=!q.pass; analyzeBtn.style.opacity=q.pass?'1':'0.45'; } }); } function _camRetake(){ _CAM.captured=null;_CAM.quality=null; _camShowStep('capture'); } function _camRunAnalysis(){ if(!_CAM.captured||!_CAM.mode)return; if(!_canDo('cadet')){_upgradeGate('Camera AI Analysis','cadet','Cadet','$5');return;} if(_CAM.quality&&!_CAM.quality.pass){toast('Fix photo quality before sending');return;} var g=_CAM_GUIDES[_CAM.mode]; var aiEl=$('cam-result-ai'),btn=$('cam-btn-analyze'); if(aiEl){aiEl.style.display='block';aiEl.innerHTML='
    Hunter
    Analyzing…
    ';} if(btn){btn.disabled=true;btn.textContent='WORKING…';} if(g.ai==='vision'){_camVisionAnalyze();} else{_camHunterAnalyze(g.hunterPrompt);} } function _camVisionAnalyze(){ var parts=[]; if(S.curTrack)parts.push(S.curTrack.name); else if(S.nearTrack)parts.push(S.nearTrack.name); if(S.wx&&S.wx.temp_f)parts.push(S.wx.temp_f+'F'); if(S.wx&&S.wx.humidity)parts.push(S.wx.humidity+'%RH'); if(S.wx&&S.wx.da)parts.push('DA '+S.wx.da+'ft'); var scanMode=_CAM.scanMode||'moisture'; var ctxExtra=typeof buildContext==='function'?buildContext():''; var body={ photo:_CAM.captured.b64, dot_number:1, track_name:(S.curTrack&&S.curTrack.name)||(S.nearTrack&&S.nearTrack.name)||'Unknown', car_class:(S.cur&&S.cur.class)||(S.cur&&S.cur.car_class)||'Unknown', lane:scanMode==='composition'?'off_line_loose':'race_line', scan_mode:scanMode, scan_intent:scanMode==='composition'?'SOIL COMPOSITION — loose broken-up sample off race line':'RACE-NIGHT MOISTURE — packed groove read', substrate_baseline:S.curTrack&&S.curTrack.substrate?S.curTrack.substrate:null, context:ctxExtra, weather:parts.join(' · ')||undefined, user_id:S.user?S.user.user_id:null }; fetch(_VISION_URL,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}) .then(function(r){return r.json().then(function(d){return{ok:r.ok,data:d};});}) .then(function(res){ if(!res.ok)throw new Error((res.data&&res.data.error)||'Vision failed'); _camRenderVisionResults(res.data,scanMode); _camSaveLog({mode:'track',source:'bb-vision',scan_mode:scanMode,data:res.data}); _persistSurfaceScan({scan_mode:scanMode,image_b64:_CAM.captured.b64,mime:_CAM.captured.mime,track_name:body.track_name,analysis:{soil:res.data.soil,surface:res.data.surface,setup:res.data.setup}}); if(typeof _buildSurfaceScience==='function'){ var mount=$('tools-surface-science');if(mount)_buildSurfaceScience(mount); } }).catch(function(e){ _camHunterAnalyze(_CAM_GUIDES.track.hunterPrompt); toast('Vision unavailable — using Hunter chat'); }).finally(function(){ var btn=$('cam-btn-analyze');if(btn){btn.disabled=false;btn.textContent=_CAM_GUIDES[_CAM.mode].analyzeLabel;} }); } function _camRenderVisionResults(data,scanMode){ var aiEl=$('cam-result-ai');if(!aiEl)return; var soil=data.soil||{},surface=data.surface||{},setup=data.setup||{}; var modeLbl=(scanMode||_CAM.scanMode||'moisture')==='composition'?'SOIL MAKEUP':'RACE LINE'; var html='
    Surface read · '+modeLbl+'
    '; if(S.curTrack&&S.curTrack.substrate&&S.curTrack.substrate.clay_pct!=null){ html+='
    USDA baseline: '+S.curTrack.substrate.clay_pct+'% clay / '+S.curTrack.substrate.sand_pct+'% sand'+(S.curTrack.substrate.taxsuborder?' · '+S.curTrack.substrate.taxsuborder:'')+'
    '; } html+='
    '; if(soil.estimated_clay_type)html+='
    CLAY '+String(soil.estimated_clay_type).toUpperCase()+'
    '; if(surface.moisture_level)html+='
    MOISTURE '+String(surface.moisture_level).toUpperCase()+'
    '; if(surface.texture)html+='
    TEXTURE '+String(surface.texture).toUpperCase()+'
    '; if(setup.grip_prediction)html+='
    GRIP '+String(setup.grip_prediction).toUpperCase()+'
    '; var advice=setup.hunter_summary||surface.crew_chief_summary||''; if(advice)html+='
    '+advice+'
    '; html+='
    '; aiEl.innerHTML=html; aiEl.style.display='block'; if(S.curTrack&&setup.grip_prediction){ try{ var tk='bb_surface_'+((S.curTrack.id||S.curTrack.name||'track').toString().replace(/\W/g,'_')); var snap=JSON.parse(localStorage.getItem(tk)||'{}'); snap.last_cam_scan={ts:Date.now(),grip:setup.grip_prediction,moisture:surface.moisture_level,clay:soil.estimated_clay_type}; localStorage.setItem(tk,JSON.stringify(snap)); }catch(ex){} } } function _camHunterAnalyze(prompt){ fetch(HNTR,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ message:prompt, photos:[{data:_CAM.captured.b64,type:_CAM.captured.mime}], history:S.hist?S.hist.slice(-4):[], context:typeof buildContext==='function'?buildContext():'', user_id:S.user?S.user.user_id:null, user_email:S.user?S.user.email:null, ai_calls_this_session:_aiCallsThisSession })}).then(function(r){return r.json();}).then(function(d){ _aiCallsThisSession++; var reply=d.response||d.reply||'Could not analyze photo.'; var aiEl=$('cam-result-ai'); if(aiEl){ aiEl.innerHTML='
    Hunter
    '+reply+'
    '; aiEl.style.display='block'; } _camSaveLog({mode:_CAM.mode,source:'hunter-api',reply:reply}); if(_CAM.mode==='gauge'){ var m=reply.match(/(\d+(?:\.\d+)?)\s*(psi|PSI|clicks|CLICKS|lb|in|mm)/i); if(m&&$('tire-cold-lf'))toast('Reading: '+m[0]); } }).catch(function(){ toast('Analysis failed — try again or describe to Hunter'); }).finally(function(){ var btn=$('cam-btn-analyze');if(btn){btn.disabled=false;btn.textContent=_CAM_GUIDES[_CAM.mode].analyzeLabel;} }); } function _camSaveLog(entry){ entry.ts=new Date().toISOString(); entry.track=S.curTrack?S.curTrack.name:null; entry.thumb=_CAM.captured&&_CAM.captured.dataUrl.length<400000?_CAM.captured.dataUrl:null; var carId=S.cur?(S.cur.id||'local'):'demo'; var key='bb_camGuide_'+carId; var log=[];try{log=JSON.parse(localStorage.getItem(key)||'[]');}catch(e){} log.unshift(entry); if(log.length>20)log=log.slice(0,20); localStorage.setItem(key,JSON.stringify(log)); _cloudSave('cam_guide',carId,log); _renderCamGuideLog(); } function _renderCamGuideLog(){ var mount=$('cam-guide-log');if(!mount)return; var carId=S.cur?(S.cur.id||'local'):'demo'; var log=[];try{log=JSON.parse(localStorage.getItem('bb_camGuide_'+carId)||'[]');}catch(e){} if(!log.length){mount.innerHTML='';return;} var html='
    RECENT SCANS
    '; log.slice(0,3).forEach(function(e){ var modeLbl=(e.mode||'?').toUpperCase(); var when=e.ts?new Date(e.ts).toLocaleString([],{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}):''; var summary=''; if(e.data&&e.data.setup&&e.data.setup.grip_prediction)summary=e.data.setup.grip_prediction; else if(e.reply)summary=e.reply.length>60?e.reply.slice(0,60)+'…':e.reply; html+='
    ' +''+modeLbl+' · '+when+(summary?'
    '+summary+'':'')+'
    '; }); mount.innerHTML=html; } function openPrintMarkers(){window.open("/align#print","_blank");} /** ─── Track analysis instrumentation (LiDAR + thermal) — garage layer only ─── */ var _THERMAL_GEAR_CATALOG=[ {id:"thermal-master-p3",brand:"Thermal Master",model:"P3 / Thor series",tier:"pro",price:"$289–$449",resolution:"256×192 IR (up to 25fps)",connector:"USB-C (Android + iPhone adapter)",pros:["High resolution for the price","Good for surface temp maps","Popular in automotive"],cons:["Needs app (ThermViewer)","iPhone needs OTG/Lightning adapter"],why:"Best balance for reading groove vs cushion temps during drying — spot which lane is warming first.",links:[{label:"Thermal Master store",url:"https://www.thermalmaster.com/"},{label:"Amazon search",url:"https://www.amazon.com/s?k=Thermal+Master+P3+thermal+camera"}]}, {id:"topdon-tc002",brand:"TOPDON",model:"TC002 / TC004",tier:"value",price:"$149–$259",resolution:"256×192",connector:"USB-C",pros:["Plug-and-play with TOPDON app","Solid mid-tier","Good for tire/pyrometer cross-check"],cons:["Lower build than pro units","App required for export"],why:"Entry path for experimental tire/surface temp logging without a standalone pyrometer.",links:[{label:"TOPDON TC series",url:"https://www.topdon.com/"},{label:"Amazon search",url:"https://www.amazon.com/s?k=TOPDON+TC002+thermal"}]}, {id:"hikmicro-p2",brand:"HIKMICRO",model:"P2 Pro / Mini",tier:"pro",price:"$399–$899",resolution:"256×192 (P2 Pro)",connector:"USB-C / WiFi models",pros:["Industrial-grade sensor","Excellent sensitivity","Export stills + video"],cons:["Higher price","Bulkier than phone dongles"],why:"When you want repeatable thermal reads for drying studies — same spot every heat.",links:[{label:"HIKMICRO",url:"https://www.hikmicro.com/"},{label:"Amazon search",url:"https://www.amazon.com/s?k=HIKMICRO+P2+Pro+thermal"}]}, {id:"flir-one",brand:"FLIR",model:"ONE Pro (legacy)",tier:"legacy",price:"$299+ (discontinued/new-old-stock)",resolution:"160×120 (Gen 3 Pro)",connector:"Lightning / USB-C",pros:["FLIR MSX overlay","Known brand"],cons:["Often discontinued","Lower res than newer dongles"],why:"Mentioned for completeness — fine for qualitative reads, not ideal for quantitative experiments.",links:[{label:"FLIR",url:"https://www.flir.com/"}]} ]; function _instrScansKey(carId){return"bb_instr_scans_"+(carId||"local");} function _instrLoadScans(carId){try{return JSON.parse(localStorage.getItem(_instrScansKey(carId))||"[]");}catch(e){return[];}} function _instrSaveScans(carId,rows){try{localStorage.setItem(_instrScansKey(carId),JSON.stringify(rows));}catch(e){toast("Storage full — could not save scan metadata");}} function _instrDetectDevice(){ var ua=navigator.userAgent||"",ios=/iPhone|iPad|iPod/i.test(ua),android=/Android/i.test(ua); var pref=null;try{pref=localStorage.getItem("bb_lidar_device_pref");}catch(e){} var lidarLikely=false,lidarReason=""; if(pref==="yes"){lidarLikely=true;lidarReason="You marked this device as LiDAR-capable";} else if(pref==="no"){lidarLikely=false;lidarReason="LiDAR marked unavailable on this device";} else if(ios){ lidarLikely=/iPhone/.test(ua); lidarReason="iPhone detected — LiDAR on Pro/Pro Max (12+). Use Scan / Polycam, then import USDZ here."; if(/iPhone1[2-9],|iPhone16,|iPhone17,|iPhone15,[23]|iPhone14,[67]|iPhone13,[34]/.test(ua))lidarReason="LiDAR-capable iPhone likely (Pro series heuristic)."; } return{ios:ios,android:android,mobile:ios||android,lidar_likely:lidarLikely,lidar_reason:lidarReason,thermal_ready:true}; } function _instrSetLidarPref(yes){try{localStorage.setItem("bb_lidar_device_pref",yes?"yes":"no");}catch(e){}if(typeof _buildTrackAnalysisTools==="function"){var m=$("track-analysis-tools-mount");if(m)_buildTrackAnalysisTools(m);}toast(yes?"LiDAR enabled for this device":"LiDAR hidden for this device");} function _instrCurrentContext(extra){ extra=extra||{};var carId=S.cur?(S.cur.id||"local"):"local"; var walkKey=typeof _walkKey==="function"?_walkKey():null; var draft=typeof _logLoadDrkExpDraft==="function"?_logLoadDrkExpDraft():{}; return{car_id:carId,track:S.curTrack?(S.curTrack.short||S.curTrack.name):null,track_id:S.curTrack&&S.curTrack.id||null,walk_key:extra.walk_key||walkKey,experiment_id:extra.experiment_id||draft.experiment_id||null,arm:extra.arm||draft.arm||null,section:extra.section||null}; } function _instrAddScan(entry){ var carId=S.cur?(S.cur.id||"local"):"local",rows=_instrLoadScans(carId); rows.unshift(entry);if(rows.length>80)rows=rows.slice(0,80);_instrSaveScans(carId,rows); if(typeof _logRefreshTrackConditionIntel==="function")_logRefreshTrackConditionIntel(); return entry; } function _instrImportScanFile(kind,ctx){ ctx=ctx||_instrCurrentContext(); var inp=document.createElement("input");inp.type="file";inp.style.display="none"; if(kind==="lidar")inp.accept=".usd,.usdz,.json,.ply,.zip,model/vnd.usdz+zip,application/json"; else inp.accept="image/*,.csv,.json"; inp.onchange=function(){ if(!inp.files||!inp.files[0])return; var f=inp.files[0],ext=(f.name.split(".").pop()||"").toLowerCase(); if(kind==="lidar"&&f.size>8*1024*1024){toast("Large scan — saving metadata only (keep file in Files app)");} var base={id:"instr_"+Date.now(),kind:kind,ts:new Date().toISOString(),file_name:f.name,file_type:f.type||ext,file_size:f.size,track:ctx.track,track_id:ctx.track_id,walk_key:ctx.walk_key,experiment_id:ctx.experiment_id,arm:ctx.arm,section:ctx.section||null,notes:"",meta:{import_source:"garage"}}; if(kind==="thermal"&&f.type.indexOf("image/")===0&&f.size<900000){ var reader=new FileReader();reader.onload=function(ev){base.thumb=ev.target.result;_instrAddScan(base);toast("Thermal image linked");if(typeof _renderInstrScanList==="function")_renderInstrScanList();};reader.readAsDataURL(f);return; } if(ext==="json"&&f.size<200000){ var r2=new FileReader();r2.onload=function(ev){try{base.meta=Object.assign(base.meta||{},JSON.parse(ev.target.result));}catch(e){}base.meta_json=true;_instrAddScan(base);toast((kind==="lidar"?"LiDAR":"Scan")+" metadata linked");if(typeof _renderInstrScanList==="function")_renderInstrScanList();};r2.readAsText(f);return; } _instrAddScan(base);toast((kind==="lidar"?"LiDAR":"Thermal")+" scan registered — "+f.name); if(typeof _renderInstrScanList==="function")_renderInstrScanList(); }; document.body.appendChild(inp);inp.click();setTimeout(function(){if(inp.parentNode)document.body.removeChild(inp);},60000); } function _instrCollectRefs(trackName){ var carId=S.cur?(S.cur.id||"local"):"local",rows=_instrLoadScans(carId),refs=[],i,r; for(i=0;i
    '+g.tier.toUpperCase()+'
    ' +'
    '+g.resolution+' · '+g.connector+' · '+g.price+'
    ' +'
    '+g.why+'
    ' +'
    + '+g.pros.join(" · ")+'
    '+g.cons.join(" · ")+'
    '; var links=document.createElement("div");links.style.cssText="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px"; (g.links||[]).forEach(function(l){var a=document.createElement("a");a.href=l.url;a.target="_blank";a.rel="noopener";a.textContent=l.label;a.style.cssText="font-family:var(--mono);font-size:8px;color:var(--amber);text-decoration:none;padding:4px 8px;border:1px solid rgba(245,166,35,.25)";links.appendChild(a);}); card.appendChild(links);inner.appendChild(card); }); modal.appendChild(inner);document.body.appendChild(modal);document.body.style.overflow="hidden"; } function _renderInstrScanList(mount){ if(!mount)mount=$("instr-scan-list"); if(!mount)return; var carId=S.cur?(S.cur.id||"local"):"local",rows=_instrLoadScans(carId),track=S.curTrack&&(S.curTrack.short||S.curTrack.name); var filtered=track?rows.filter(function(r){return!r.track||r.track===track;}):rows; if(!filtered.length){mount.innerHTML='
    No linked scans yet.
    ';return;} mount.innerHTML=filtered.slice(0,6).map(function(r){ var when=r.ts?new Date(r.ts).toLocaleString([],{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):""; return'
    ' +''+(r.kind==="lidar"?"LiDAR":"THERMAL").toUpperCase()+' · ' +(r.section?r.section+" · ":"")+(r.file_name||"scan")+'
    '+when+(r.experiment_id?" · "+r.experiment_id:"")+'
    '; }).join(""); } function _buildTrackAnalysisTools(mount){ if(!mount)return; var dev=_instrDetectDevice(),carId=S.cur?(S.cur.id||"local"):"local",rows=_instrLoadScans(carId); var lidarN=rows.filter(function(r){return r.kind==="lidar";}).length,thermN=rows.filter(function(r){return r.kind==="thermal";}).length; mount.innerHTML=""; var gearBtn=document.createElement("button");gearBtn.type="button";gearBtn.textContent="★ Recommended Track Analysis Tools — thermal dongles + LiDAR workflow";gearBtn.style.cssText="width:100%;padding:14px 12px;font-family:var(--head);font-size:13px;font-weight:900;background:rgba(251,146,60,.12);border:2px solid rgba(251,146,60,.4);color:#fb923c;cursor:pointer;margin-bottom:10px;text-align:left;line-height:1.4"; gearBtn.onclick=function(){_openThermalGearModal();}; mount.appendChild(gearBtn); var wrap=document.createElement("div");wrap.style.cssText="background:var(--dark2);border:1px solid rgba(56,189,248,.18);padding:12px;margin-bottom:10px"; var hdr=document.createElement("div");hdr.style.cssText="font-family:var(--mono);font-size:8px;color:#38bdf8;letter-spacing:1px;margin-bottom:8px";hdr.textContent="INSTRUMENTATION"; wrap.appendChild(hdr); var devLine=document.createElement("div");devLine.style.cssText="font-family:var(--mono);font-size:8px;color:var(--muted);line-height:1.55;margin-bottom:10px"; devLine.textContent=(dev.lidar_likely?"📡 LiDAR: "+dev.lidar_reason:"📱 "+dev.lidar_reason)+" · 🌡 Thermal import ready · "+lidarN+" LiDAR · "+thermN+" thermal linked"; wrap.appendChild(devLine); if(dev.ios){ var prefRow=document.createElement("div");prefRow.style.cssText="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap"; [{l:"This iPhone has LiDAR",v:true},{l:"No LiDAR",v:false}].forEach(function(o){ var b=document.createElement("button");b.type="button";b.textContent=o.l;b.style.cssText="padding:6px 8px;font-family:var(--mono);font-size:7px;background:var(--dark);border:1px solid rgba(255,255,255,.12);color:var(--muted);cursor:pointer"; b.onclick=function(){_instrSetLidarPref(o.v);};prefRow.appendChild(b); }); wrap.appendChild(prefRow); } var btnRow=document.createElement("div");btnRow.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px"; [{lbl:"Import LiDAR scan",fn:function(){_instrImportScanFile("lidar");}},{lbl:"Import thermal image",fn:function(){_instrImportScanFile("thermal");}}].forEach(function(spec){ var b=document.createElement("button");b.type="button";b.textContent=spec.lbl;b.style.cssText="padding:10px 8px;font-family:var(--mono);font-size:8px;background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.25);color:#7dd3fc;cursor:pointer";b.onclick=spec.fn;btnRow.appendChild(b); }); wrap.appendChild(btnRow); var gearBtn2=document.createElement("button");gearBtn2.type="button";gearBtn2.textContent="Compare dongles & specs →";gearBtn2.style.cssText="width:100%;padding:8px;font-family:var(--mono);font-size:8px;background:rgba(251,146,60,.06);border:1px solid rgba(251,146,60,.25);color:#fb923c;cursor:pointer;margin-bottom:10px"; gearBtn2.onclick=function(){_openThermalGearModal();}; wrap.appendChild(gearBtn2); var listMount=document.createElement("div");listMount.id="instr-scan-list";wrap.appendChild(listMount); mount.appendChild(wrap); _renderInstrScanList(listMount); } function loadDashboard(){try{loadWx();}catch(e){}_buildToolsTonightRead();if(typeof loadUpcomingEventsForCheckIn==="function")loadUpcomingEventsForCheckIn();_buildMidWeekPlanner();_buildPhoneLoggerSettings();_buildSurfaceScience($("tools-surface-science"));if(typeof _logApplyTrackIntelEventPreset==="function")_logApplyTrackIntelEventPreset(true);if(typeof _buildTrackConditionIntelPanel==="function")_buildTrackConditionIntelPanel($("track-condition-intel-mount"));if(typeof _buildTrackAnalysisTools==="function")_buildTrackAnalysisTools($("track-analysis-tools-mount"));if(typeof _buildCrewChiefReportPanel==="function")_buildCrewChiefReportPanel($("crew-chief-report-mount"));wireToolsCalcs();_buildTirePressureTool();buildChecklist();_renderCamGuideLog();if(S.cur&&S.cur.id&&typeof loadHunterLearnedInsights==="function")loadHunterLearnedInsights(S.cur.id,S.curTrack&&S.curTrack.id);if(typeof _buildRecommendationHistory==="function")_buildRecommendationHistory();} initAuth(); // ── Parts Tracker ────────────────────────────────────────────────────────── function _buildTireLog(container) { var existing=container.querySelector('.bb-tire-log');if(existing)existing.remove(); var ct=S.cur?_getCarType(S.cur):'generic'; var brands=TIRE_BRANDS_BY_CLASS[ct]||TIRE_BRANDS_BY_CLASS.generic; var carId=S.cur?(S.cur.id||'local'):'demo';var key='bb_tire_'+carId; var data=JSON.parse(localStorage.getItem(key)||'null')||{ lf:{pos:'LF',compound:'',compound_type:'medium',dur_new:null,dur_cur:null,heats:0,prep:'',siping:'',pyro_in:null,pyro_mid:null,pyro_out:null,prep_hrs:null,prep_date:'',install_date:''}, rf:{pos:'RF',compound:'',compound_type:'medium',dur_new:null,dur_cur:null,heats:0,prep:'',siping:'',pyro_in:null,pyro_mid:null,pyro_out:null,prep_hrs:null,prep_date:'',install_date:''}, lr:{pos:'LR',compound:'',compound_type:'medium',dur_new:null,dur_cur:null,heats:0,prep:'',siping:'',pyro_in:null,pyro_mid:null,pyro_out:null,prep_hrs:null,prep_date:'',install_date:''}, rr:{pos:'RR',compound:'',compound_type:'medium',dur_new:null,dur_cur:null,heats:0,prep:'',siping:'',pyro_in:null,pyro_mid:null,pyro_out:null,prep_hrs:null,prep_date:'',install_date:''}, history:[] }; if(!data.history)data.history=[]; function save(){localStorage.setItem(key,JSON.stringify(data));_cloudSave('tire',carId,data);} var IS='width:100%;background:var(--dark);border:1px solid var(--dark4);color:var(--white);font-family:Share Tech Mono;font-size:9px;padding:5px 6px;box-sizing:border-box;outline:none;'; var sec=document.createElement('div');sec.className='bb-tire-log';sec.style.cssText='margin-top:16px;'; // Header var hdr=document.createElement('div');hdr.style.cssText='display:flex;align-items:center;gap:8px;margin-bottom:8px;'; var lbl=document.createElement('span');lbl.style.cssText='font-family:Barlow Condensed;font-size:14px;font-weight:900;letter-spacing:2px;color:var(--muted);text-transform:uppercase;';lbl.textContent='Tire Log'; var heatBtn=document.createElement('button');heatBtn.style.cssText='margin-left:auto;background:var(--dark2);border:1px solid var(--dark4);color:var(--amber);font-family:Share Tech Mono;font-size:10px;padding:5px 12px;cursor:pointer;'; heatBtn.textContent='+ HEAT';heatBtn.onclick=function(){['lf','rf','lr','rr'].forEach(function(p){data[p].heats=(data[p].heats||0)+1;});save();_buildTireLog(container);}; var archBtn=document.createElement('button');archBtn.style.cssText='background:var(--red);border:none;color:var(--white);font-family:Share Tech Mono;font-size:9px;padding:5px 10px;cursor:pointer;letter-spacing:1px;clip-path:polygon(4px 0%,100% 0%,calc(100% - 4px) 100%,0% 100%)'; archBtn.textContent='ARCHIVE SET';archBtn.onclick=function(){ var result=prompt('Race result? (e.g. P3 Feature)'); var archived={ts:Date.now(),tires:{},result:result||'',track:S.curTrack?S.curTrack.name:''}; ['lf','rf','lr','rr'].forEach(function(p){archived.tires[p]={compound:data[p].compound,compound_type:data[p].compound_type,dur_new:data[p].dur_new,dur_cur:data[p].dur_cur,heats:data[p].heats,prep:data[p].prep,install_date:data[p].install_date};}); data.history.push(archived);if(data.history.length>20)data.history=data.history.slice(-20); ['lf','rf','lr','rr'].forEach(function(p){data[p]={pos:data[p].pos,compound:'',compound_type:'medium',dur_new:null,dur_cur:null,heats:0,prep:'',prep_hrs:null,prep_date:'',install_date:''};}); save();_buildTireLog(container);toast('Set archived'+(result?' — '+result:'')); }; hdr.appendChild(lbl);hdr.appendChild(heatBtn);hdr.appendChild(archBtn);sec.appendChild(hdr); // Tire cards var grid=document.createElement('div');grid.style.cssText='display:grid;grid-template-columns:1fr 1fr;gap:6px;'; ['lf','rf','lr','rr'].forEach(function(pos){ var t=data[pos];if(!t.compound_type)t.compound_type='medium';if(!t.prep)t.prep='';if(!t.install_date)t.install_date=''; var card=document.createElement('div');card.style.cssText='background:var(--dark2);border:1px solid rgba(255,255,255,.04);padding:10px;'; // Position label var posDiv=document.createElement('div');posDiv.style.cssText='font-family:Barlow Condensed;font-size:22px;font-weight:900;color:var(--amber);margin-bottom:4px;';posDiv.textContent=t.pos;card.appendChild(posDiv); // Brand dropdown — class-specific if(!t.brand)t.brand=brands[0]||'hoosier_dirt'; var brandSel=document.createElement('select');brandSel.style.cssText=IS+'margin-bottom:3px;color:var(--gold);font-size:8px;'; brands.forEach(function(b){var op=document.createElement('option');op.value=b;op.textContent=TIRE_BRAND_LABELS[b]||b;if(t.brand===b)op.selected=true;brandSel.appendChild(op);}); card.appendChild(brandSel); // Compound dropdown — populated from TIRE_DB by brand var compSel=document.createElement('select');compSel.style.cssText=IS+'margin-bottom:3px;color:var(--amber);'; function fillCompounds(brand){ compSel.innerHTML=''; var tires=TIRE_DB[brand]||[]; tires.forEach(function(tr){var op=document.createElement('option');op.value=tr.code;op.textContent=tr.code+' ('+tr.duro+') '+tr.type;if(t.compound===tr.code)op.selected=true;compSel.appendChild(op);}); var cust=document.createElement('option');cust.value='_custom';cust.textContent='Custom...';compSel.appendChild(cust); } fillCompounds(t.brand); brandSel.onchange=function(){data[pos].brand=brandSel.value;fillCompounds(brandSel.value);data[pos].compound='';data[pos].dur_new=null;save();_buildTireLog(container);}; compSel.onchange=function(){ var v=compSel.value; if(v==='_custom'){var cname=prompt('Compound name:');if(cname){data[pos].compound=cname;}return;} data[pos].compound=v; // Auto-fill new durometer from database var tires=TIRE_DB[data[pos].brand]||[]; var match=tires.filter(function(tr){return tr.code===v;})[0]; if(match&&match.duro){data[pos].dur_new=match.duro;data[pos].compound_type=match.type.indexOf('soft')>=0?'soft':match.type.indexOf('hard')>=0?'hard':'medium';} save();_buildTireLog(container); }; card.appendChild(compSel); // Target duro from DB var dbTires=TIRE_DB[t.brand]||[];var dbMatch=dbTires.filter(function(tr){return tr.code===t.compound;})[0]; if(dbMatch){ var tgtDiv=document.createElement('div');tgtDiv.style.cssText='font-family:Share Tech Mono;font-size:7px;color:var(--muted);margin-bottom:3px;line-height:1.4;'; tgtDiv.textContent='TARGET: '+dbMatch.duro+' duro | '+dbMatch.heat+' heat | '+dbMatch.use; card.appendChild(tgtDiv); } var kbMatch=typeof lookupTireCompoundKB==='function'?lookupTireCompoundKB(t.brand,t.compound):null; if(kbMatch&&kbMatch.setupImplications&&kbMatch.setupImplications.length){ var kbDiv=document.createElement('div');kbDiv.style.cssText='font-family:var(--body);font-size:9px;color:#94a3b8;line-height:1.45;margin-bottom:4px;padding:5px 6px;background:rgba(107,140,255,.06);border:1px solid rgba(107,140,255,.15)'; kbDiv.innerHTML='KB '+kbMatch.setupImplications[0]; card.appendChild(kbDiv); } // Durometer row — NEW and CURRENT var durRow=document.createElement('div');durRow.style.cssText='display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:3px;'; function mkIn(ph,val,field){var inp=document.createElement('input');inp.type='number';inp.placeholder=ph;inp.value=val||'';inp.step='0.5';inp.style.cssText=IS;inp.onchange=function(){data[pos][field]=parseFloat(inp.value)||null;save();_buildTireLog(container);};return inp;} var durNewWrap=document.createElement('div'); var durNewLbl=document.createElement('div');durNewLbl.style.cssText='font-family:Share Tech Mono;font-size:6px;color:var(--muted);letter-spacing:1px;margin-bottom:1px';durNewLbl.textContent='NEW DURO'; durNewWrap.appendChild(durNewLbl);durNewWrap.appendChild(mkIn('New',t.dur_new,'dur_new')); var durCurWrap=document.createElement('div'); var durCurLbl=document.createElement('div');durCurLbl.style.cssText='font-family:Share Tech Mono;font-size:6px;color:var(--muted);letter-spacing:1px;margin-bottom:1px';durCurLbl.textContent='CURRENT'; durCurWrap.appendChild(durCurLbl);durCurWrap.appendChild(mkIn('Current',t.dur_cur,'dur_cur')); durRow.appendChild(durNewWrap);durRow.appendChild(durCurWrap);card.appendChild(durRow); // Tire life bar + wear indicator if(t.dur_new&&t.dur_cur){ var delta=t.dur_cur-t.dur_new;var warnThresh=dbMatch?Math.max(4,dbMatch.duro*0.15):8; var life=Math.max(0,Math.min(100,(1-Math.abs(delta)/warnThresh)*100)); var barWrap=document.createElement('div');barWrap.style.cssText='height:4px;background:var(--dark4);margin-bottom:3px;overflow:hidden;'; var barFill=document.createElement('div');barFill.style.cssText='height:100%;width:'+life+'%;background:'+(life>60?'#2DB87F':life>30?'var(--amber)':'var(--red-hot)')+';transition:width .3s'; barWrap.appendChild(barFill);card.appendChild(barWrap); var wearDiv=document.createElement('div');wearDiv.style.cssText='font-family:Share Tech Mono;font-size:8px;color:'+(life>60?'var(--muted)':life>30?'var(--amber)':'var(--red-hot)')+';margin-bottom:3px;'; wearDiv.textContent='WEAR: '+delta.toFixed(1)+' | LIFE: '+Math.round(life)+'%'+(life<=30?' — REPLACE':'');card.appendChild(wearDiv); } // Prep var prepIn=document.createElement('input');prepIn.type='text';prepIn.placeholder='Prep compound';prepIn.value=t.prep||''; prepIn.style.cssText=IS+'margin-bottom:3px;font-size:8px;';prepIn.onchange=function(){data[pos].prep=prepIn.value.trim();save();};card.appendChild(prepIn); var sipIn=document.createElement('input');sipIn.type='text';sipIn.placeholder='Siping / grooves';sipIn.value=t.siping||'';sipIn.style.cssText=IS+'margin-bottom:3px;font-size:8px;';sipIn.onchange=function(){data[pos].siping=sipIn.value.trim();save();};card.appendChild(sipIn); var pyroLbl=document.createElement('div');pyroLbl.style.cssText='font-family:Share Tech Mono;font-size:6px;color:var(--muted);letter-spacing:1px;margin:4px 0 2px';pyroLbl.textContent='PYROMETER (IN/MID/OUT °F)'; card.appendChild(pyroLbl); var pyroRow=document.createElement('div');pyroRow.style.cssText='display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;margin-bottom:4px'; [{ph:'In',f:'pyro_in'},{ph:'Mid',f:'pyro_mid'},{ph:'Out',f:'pyro_out'}].forEach(function(pf){var inp=document.createElement('input');inp.type='number';inp.placeholder=pf.ph;inp.value=t[pf.f]||'';inp.step='1';inp.style.cssText=IS+'font-size:8px;text-align:center';inp.onchange=function(){data[pos][pf.f]=parseFloat(inp.value)||null;save();};pyroRow.appendChild(inp);}); card.appendChild(pyroRow); // Dates row var dateRow=document.createElement('div');dateRow.style.cssText='display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:4px;'; function mkDate(ph,val,field){var inp=document.createElement('input');inp.type='date';inp.value=val||'';inp.title=ph; inp.style.cssText=IS+'font-size:8px;color:var(--muted);';inp.onchange=function(){data[pos][field]=inp.value;save();};return inp;} dateRow.appendChild(mkDate('Prep date',t.prep_date,'prep_date'));dateRow.appendChild(mkDate('Install date',t.install_date,'install_date'));card.appendChild(dateRow); // Heats + reset var heatRow=document.createElement('div');heatRow.style.cssText='display:flex;align-items:center;gap:6px;'; var heatLbl=document.createElement('div');heatLbl.style.cssText='font-family:Share Tech Mono;font-size:9px;color:var(--muted);flex:1;';heatLbl.textContent='HEATS: '+(t.heats||0); var resetBtn=document.createElement('button');resetBtn.style.cssText='background:none;border:1px solid var(--dark4);color:var(--muted);font-family:Share Tech Mono;font-size:7px;padding:2px 6px;cursor:pointer;'; resetBtn.textContent='NEW';resetBtn.onclick=function(){data[pos]={pos:t.pos,compound:'',compound_type:'medium',dur_new:null,dur_cur:null,heats:0,prep:'',prep_hrs:null,prep_date:'',install_date:''};save();_buildTireLog(container);}; heatRow.appendChild(heatLbl);heatRow.appendChild(resetBtn);card.appendChild(heatRow); grid.appendChild(card); }); sec.appendChild(grid); // History section if(data.history.length>0){ var histDiv=document.createElement('div');histDiv.style.cssText='margin-top:8px;'; var histBtn=document.createElement('div');histBtn.style.cssText='font-family:Share Tech Mono;font-size:8px;letter-spacing:1px;color:var(--muted);cursor:pointer;padding:4px 0;';histBtn.textContent='▼ HISTORY ('+data.history.length+' sets)'; var histList=document.createElement('div');histList.style.cssText='display:none;max-height:120px;overflow-y:auto;'; histBtn.onclick=function(){var vis=histList.style.display==='none';histList.style.display=vis?'block':'none';histBtn.textContent=(vis?'▲':'▼')+' HISTORY ('+data.history.length+' sets)';}; data.history.slice().reverse().forEach(function(h){ var row=document.createElement('div');row.style.cssText='background:var(--dark);padding:6px 8px;margin-bottom:2px;font-family:Share Tech Mono;font-size:8px;color:var(--muted);'; var date=new Date(h.ts).toLocaleDateString('en-US',{month:'short',day:'numeric'}); var tires=Object.keys(h.tires||{}).map(function(p){var tt=h.tires[p];return tt.pos+':'+((tt.compound||'?').substring(0,6))+' d'+(tt.dur_cur||'?')+'/'+tt.heats+'h';}).join(' '); row.textContent=date+(h.track?' '+h.track:'')+(h.result?' — '+h.result:'')+' | '+tires; histList.appendChild(row); }); histDiv.appendChild(histBtn);histDiv.appendChild(histList);sec.appendChild(histDiv); } container.appendChild(sec); } function _buildPartsTracker(container) { var carId = S.cur ? (S.cur.id || 'local') : 'demo'; var key = 'bb_parts_' + carId; var data = JSON.parse(localStorage.getItem(key) || 'null') || { biscuits: {nights:0, threshold:50, label:'BISCUITS (CHAIN GUIDE)', unit:'nights'}, chain: {nights:0, threshold:30, label:'CHAIN', unit:'nights'}, sprocket_d:{nights:0, threshold:60, label:'DRIVE SPROCKET', unit:'nights'}, sprocket_r:{nights:0, threshold:60, label:'REAR SPROCKET', unit:'nights'}, bearings: {nights:0, threshold:40, label:'REAR BEARINGS', unit:'nights'}, clutch: {nights:0, threshold:20, label:'CLUTCH PACK', unit:'nights'}, }; var sec = document.createElement('div'); sec.style.cssText = 'margin-top:16px;'; var hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:8px;'; var lbl = document.createElement('span'); lbl.style.cssText = 'font-family:Barlow Condensed;font-size:14px;font-weight:900;letter-spacing:2px;color:var(--muted);text-transform:uppercase;'; lbl.textContent = 'Parts Tracker'; var addNightBtn = document.createElement('button'); addNightBtn.style.cssText = 'margin-left:auto;background:var(--dark2);border:1px solid var(--dark4);color:var(--amber);font-family:Share Tech Mono;font-size:10px;padding:5px 12px;cursor:pointer;'; addNightBtn.textContent = '+ NIGHT'; addNightBtn.onclick = function() { Object.keys(data).forEach(function(k) { data[k].nights++; }); localStorage.setItem(key, JSON.stringify(data));_cloudSave('parts',carId,data); _buildPartsTracker(container); }; hdr.appendChild(lbl); hdr.appendChild(addNightBtn); sec.appendChild(hdr); var grid = document.createElement('div'); grid.style.cssText = 'display:flex;flex-direction:column;gap:4px;'; Object.keys(data).forEach(function(k) { var part = data[k]; var pct = Math.min(1, part.nights / part.threshold); var isWarn = pct >= 0.8; var isDue = pct >= 1; var row = document.createElement('div'); row.style.cssText = 'background:var(--dark2);border:1px solid ' + (isDue ? 'rgba(208,25,14,.4)' : isWarn ? 'rgba(245,166,35,.3)' : 'rgba(255,255,255,.04)') + ';padding:8px 10px;display:flex;align-items:center;gap:8px;'; var pLbl = document.createElement('div'); pLbl.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--white);flex:1;'; pLbl.textContent = part.label; var pNights = document.createElement('div'); pNights.style.cssText = 'font-family:Barlow Condensed;font-size:18px;font-weight:700;color:' + (isDue ? 'var(--red-hot)' : isWarn ? 'var(--amber)' : 'var(--muted)') + ';min-width:32px;text-align:right;'; pNights.textContent = part.nights; var pUnit = document.createElement('div'); pUnit.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);min-width:28px;'; pUnit.textContent = '/ ' + part.threshold; // Progress bar var barWrap = document.createElement('div'); barWrap.style.cssText = 'flex:1;max-width:60px;height:3px;background:rgba(255,255,255,.06);'; var bar = document.createElement('div'); bar.style.cssText = 'height:100%;width:' + Math.round(pct*100) + '%;background:' + (isDue ? 'var(--red)' : isWarn ? 'var(--amber)' : 'var(--muted)') + ';'; barWrap.appendChild(bar); // Reset button var resetBtn = document.createElement('button'); resetBtn.style.cssText = 'background:none;border:1px solid rgba(255,255,255,.08);color:var(--muted);font-family:Share Tech Mono;font-size:8px;padding:3px 7px;cursor:pointer;'; resetBtn.textContent = 'RESET'; resetBtn.onclick = (function(partKey) { return function(e) { e.stopPropagation(); data[partKey].nights = 0; localStorage.setItem(key, JSON.stringify(data));_cloudSave('parts',carId,data); _buildPartsTracker(container); }; })(k); if (isDue) { var dueBadge = document.createElement('div'); dueBadge.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--red-hot);letter-spacing:1px;animation:blink 1s infinite;'; dueBadge.textContent = 'REPLACE'; row.appendChild(pLbl); row.appendChild(pNights); row.appendChild(pUnit); row.appendChild(barWrap); row.appendChild(dueBadge); row.appendChild(resetBtn); } else { row.appendChild(pLbl); row.appendChild(pNights); row.appendChild(pUnit); row.appendChild(barWrap); row.appendChild(resetBtn); } grid.appendChild(row); }); sec.appendChild(grid); // Remove existing parts section if re-rendering var existing = container.querySelector('.bb-parts-tracker'); if (existing) existing.remove(); sec.className = 'bb-parts-tracker'; container.appendChild(sec); } // ── SFI / Safety Date Tracker ────────────────────────────────────────────── function _buildSFITracker(container) { var carId = S.cur ? (S.cur.id || 'local') : 'demo'; var key = 'bb_sfi_' + carId; var saved = JSON.parse(localStorage.getItem(key) || '{}'); var items = [ {k:'belt', label:'HANS / BELTS', sfi:'SFI 16.1/38.1', life:365*2, unit:'2yr'}, {k:'helmet', label:'HELMET', sfi:'Snell SA2020+', life:365*5, unit:'5yr'}, {k:'suit', label:'FIRE SUIT', sfi:'SFI 3.2A/5+', life:365*5, unit:'5yr'}, {k:'gloves', label:'GLOVES', sfi:'SFI 3.3/5', life:365*3, unit:'3yr'}, {k:'boots', label:'BOOTS / SHOES', sfi:'SFI 3.3/5', life:365*3, unit:'3yr'}, {k:'neck_col',label:'NECK COLLAR', sfi:'SFI 3.3', life:365*3, unit:'3yr'}, ]; var sec = document.createElement('div'); sec.style.cssText = 'margin-top:16px;'; var hdr = document.createElement('div'); hdr.style.cssText = 'font-family:Barlow Condensed;font-size:14px;font-weight:900;letter-spacing:2px;color:var(--muted);text-transform:uppercase;margin-bottom:8px;'; hdr.textContent = 'Safety / SFI Dates'; sec.appendChild(hdr); var grid = document.createElement('div'); grid.style.cssText = 'display:flex;flex-direction:column;gap:4px;'; var now = Date.now(); items.forEach(function(item) { var purchaseTs = saved[item.k] || null; var expiresTs = purchaseTs ? purchaseTs + item.life * 86400000 : null; var daysLeft = expiresTs ? Math.round((expiresTs - now) / 86400000) : null; var isExpired = daysLeft !== null && daysLeft <= 0; var isWarn = daysLeft !== null && daysLeft > 0 && daysLeft <= 60; var row = document.createElement('div'); row.style.cssText = 'background:var(--dark2);border:1px solid ' + (isExpired ? 'rgba(208,25,14,.4)' : isWarn ? 'rgba(245,166,35,.3)' : 'rgba(255,255,255,.04)') + ';padding:8px 10px;display:flex;align-items:center;gap:8px;'; var rLbl = document.createElement('div'); rLbl.style.cssText = 'flex:1;'; var rName = document.createElement('div'); rName.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--white);'; rName.textContent = item.label; var rSFI = document.createElement('div'); rSFI.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--muted);margin-top:1px;'; rSFI.textContent = item.sfi; rLbl.appendChild(rName); rLbl.appendChild(rSFI); var rStatus = document.createElement('div'); rStatus.style.cssText = 'font-family:Barlow Condensed;font-size:16px;font-weight:700;min-width:80px;text-align:right;color:' + (isExpired ? 'var(--red-hot)' : isWarn ? 'var(--amber)' : purchaseTs ? '#4CAF50' : 'var(--muted)') + ';'; rStatus.textContent = isExpired ? 'EXPIRED' : daysLeft !== null ? daysLeft + 'd left' : 'NOT SET'; var setBtn = document.createElement('button'); setBtn.style.cssText = 'background:none;border:1px solid rgba(255,255,255,.1);color:var(--muted);font-family:Share Tech Mono;font-size:8px;padding:3px 7px;cursor:pointer;'; setBtn.textContent = purchaseTs ? 'REDATE' : 'SET DATE'; setBtn.onclick = (function(k2) { return function() { var inp = document.createElement('input'); inp.type = 'date'; inp.style.cssText = 'position:fixed;bottom:60px;left:50%;transform:translateX(-50%);background:var(--dark2);border:1px solid var(--red);color:var(--white);font-size:16px;padding:8px;z-index:9999;'; inp.valueAsDate = new Date(); inp.onchange = function() { if (!inp.value) return; saved[k2] = new Date(inp.value).getTime(); localStorage.setItem(key, JSON.stringify(saved));_cloudSave('sfi',carId,saved); if (inp.parentNode) inp.parentNode.removeChild(inp); _buildSFITracker(container); }; inp.onblur = function() { setTimeout(function() { if (inp.parentNode) inp.parentNode.removeChild(inp); }, 200); }; document.body.appendChild(inp); inp.focus(); inp.click(); }; })(item.k); row.appendChild(rLbl); row.appendChild(rStatus); row.appendChild(setBtn); grid.appendChild(row); }); sec.appendChild(grid); var existing = container.querySelector('.bb-sfi-tracker'); if (existing) existing.remove(); sec.className = 'bb-sfi-tracker'; container.appendChild(sec); } // ═══════════════════════════════════════════════════════════════════════════ // TRACK WALK — 22-point dirt mapping system // ═══════════════════════════════════════════════════════════════════════════ var WALK_POINTS = [ {id:'e1_t1_en', label:'T1 ENTRY', zone:'END 1', banking:true}, {id:'e1_t1_hi', label:'T1 HIGH', zone:'END 1', banking:true}, {id:'e1_t1_ap', label:'T1 APEX', zone:'END 1', banking:true}, {id:'e1_t1_lo', label:'T1 LOW', zone:'END 1', banking:true}, {id:'e1_chute', label:'T1/T2 CHUTE', zone:'END 1', banking:false}, {id:'e1_t2_hi', label:'T2 HIGH', zone:'END 1', banking:true}, {id:'e1_t2_ap', label:'T2 APEX', zone:'END 1', banking:true}, {id:'e1_t2_lo', label:'T2 LOW', zone:'END 1', banking:true}, {id:'e1_t2_ex', label:'T2 EXIT', zone:'END 1', banking:true}, {id:'fs_hi', label:'F/STR HIGH', zone:'FRONT STR', banking:false}, {id:'fs_lo', label:'F/STR LOW', zone:'FRONT STR', banking:false}, {id:'e2_t3_en', label:'T3 ENTRY', zone:'END 2', banking:true}, {id:'e2_t3_hi', label:'T3 HIGH', zone:'END 2', banking:true}, {id:'e2_t3_ap', label:'T3 APEX', zone:'END 2', banking:true}, {id:'e2_t3_lo', label:'T3 LOW', zone:'END 2', banking:true}, {id:'e2_chute', label:'T3/T4 CHUTE', zone:'END 2', banking:false}, {id:'e2_t4_hi', label:'T4 HIGH', zone:'END 2', banking:true}, {id:'e2_t4_ap', label:'T4 APEX', zone:'END 2', banking:true}, {id:'e2_t4_lo', label:'T4 LOW', zone:'END 2', banking:true}, {id:'e2_t4_ex', label:'T4 EXIT', zone:'END 2', banking:true}, {id:'bs_hi', label:'B/STR HIGH', zone:'BACK STR', banking:false}, {id:'bs_lo', label:'B/STR LOW', zone:'BACK STR', banking:false} ]; var WALK_TAGS = [ 'WET','TACKY','DRYING','SLICK','DRY', 'SMOOTH','ROUGH','RUTTED','MARBLED', 'BLUE GROOVE','RUBBER','CUSHION' ]; // Soil composition constants var SOIL_TYPES = [ {v:'dark_loam',l:'DARK LOAM',d:'Rich black dirt — holds moisture deep, rubbers heavily, big grip swing'}, {v:'red_clay',l:'RED CLAY',d:'Mineral clay — slick extremes, transitions fast, very prep-sensitive'}, {v:'sandy_loam',l:'SANDY LOAM',d:'Sand-based — no water or rubber retention, gets rough early, consistent'}, {v:'caliche',l:'CALICHE / LIMESTONE',d:'Hard base — fast, no rubber buildup, minimal grip change all night'}, {v:'mixed',l:'MIXED',d:'Variable composition — assess zone by zone'} ]; var SOIL_BINDING = [{v:'high',l:'HIGH BIND'},{v:'moderate',l:'MODERATE'},{v:'low',l:'LOW BIND'}]; var SOIL_MOISTURE = [ {v:'surface_only',l:'SURFACE ONLY',d:'Dries in 3-5 laps'}, {v:'uniform',l:'UNIFORM',d:'Holds through feature'}, {v:'deep',l:'DEEP RESERVE',d:'Slow release, stable all night'}, {v:'bone_dry',l:'BONE DRY',d:'No moisture — dust, rubber only path'} ]; // Soil AI prompt — returns JSON only var _SOIL_PROMPT = 'You are analyzing a dirt oval racing track surface photo. THREE-LAYER MODEL: (1) USDA substrate = permanent native soil under the track — provided in context when available; (2) promoter racing clay = amended surface layer NOT visible in SSURGO; (3) race-night surface = what you see tonight. SAMPLING RULE: If the photo shows LOOSE broken-up material off the race line, classify SOIL COMPOSITION (clay/sand/aggregate/binding). If the photo shows the PACKED rubbered groove, classify MOISTURE/GRIP for tonight only — do not treat groove color as native substrate. Respond with ONLY a valid JSON object, no markdown. JSON fields: soil_type (dark_loam|red_clay|sandy_loam|caliche|mixed), clay_content (high|moderate|low), aggregate_size (fine|medium|coarse), moisture_depth (surface_only|uniform|deep|bone_dry), binding_character (high|moderate|low), transition_rate (fast|moderate|slow), scan_target (composition|moisture), night_arc (one sentence: what this track will do from hot laps to feature), setup_note (one sentence: chassis adjustment to anticipate). Base analysis on: color, visible aggregate size, texture, moisture sheen, rubber pattern — and reconcile with substrate baseline in context when present.'; // Load/save track-level soil fingerprint (persists by track, not by date) function _soilLoad(trackSlug) { if (!trackSlug) return null; try { return JSON.parse(localStorage.getItem('bb_soil_' + trackSlug) || 'null'); } catch(e) { return null; } } function _soilSave(trackSlug, data) { if (!trackSlug) return; try { localStorage.setItem('bb_soil_' + trackSlug, JSON.stringify(data)); } catch(e) {} if(S.token&&S.cur&&S.cur.id)_cloudSave('soil_'+trackSlug,S.cur.id,data); } function _walkKey() { var carId = S.cur ? (S.cur.id || 'local') : 'demo'; var ts = S.curTrack ? S.curTrack.name.toLowerCase().replace(/[^a-z0-9]/g,'_') : 'no_track'; var d = new Date(); return 'bb_walk_' + carId + '_' + ts + '_' + d.getFullYear() + String(d.getMonth()+1).padStart(2,'0') + String(d.getDate()).padStart(2,'0'); } function _walkLoad(key) { try { return JSON.parse(localStorage.getItem(key) || 'null') || {points:{}}; } catch(e) { return {points:{}}; } } function _walkSave(key, data) { try { localStorage.setItem(key, JSON.stringify(data)); } catch(e) {} if (typeof _maybeRefreshTrackWalkUnlock === 'function') _maybeRefreshTrackWalkUnlock(data, key); if (typeof _logRefreshTrackConditionIntel === 'function') _logRefreshTrackConditionIntel(); } function _walkCount(data) { if (!data || !data.points) return 0; return Object.keys(data.points).filter(function(k) { var p = data.points[k]; return p && ((p.surface && p.surface.length > 0) || p.penetrometer || p.banking || p.note); }).length; } // ── Entry button in Extras tab ───────────────────────────────────────────── function _buildWalkSection(container) { var existing = container.querySelector('.bb-walk-entry'); if (existing) existing.remove(); if (_canDo('chief') && S.curTrack) _buildChiefWalkIntel(container, S.curTrack); var key = _walkKey(); var data = _walkLoad(key); var count = _walkCount(data); var sec = document.createElement('div'); sec.className = 'bb-walk-entry'; sec.style.cssText = 'margin-bottom:16px;'; var btn = document.createElement('button'); btn.className = 'bb-walk-entry-btn'; btn.type = 'button'; var row1 = document.createElement('div'); row1.style.cssText = 'display:flex;align-items:baseline;gap:10px;margin-bottom:4px;'; var title = document.createElement('span'); title.style.cssText = 'font-family:Barlow Condensed;font-size:22px;font-weight:900;letter-spacing:2px;color:var(--white);'; title.textContent = count > 0 ? 'CONTINUE WALK' : 'START TRACK WALK'; var badge = document.createElement('span'); badge.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--red);letter-spacing:1px;border:1px solid rgba(208,25,14,.3);padding:2px 6px;'; badge.textContent = 'CADET+'; row1.appendChild(title); row1.appendChild(badge); var sub = document.createElement('div'); sub.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--muted);line-height:1.6;'; if (count > 0) { sub.textContent = count + ' / 22 points — ' + (S.curTrack ? S.curTrack.name : 'tap to continue'); } else { sub.textContent = '22-point dirt mapping · your walk feeds Chief intel for everyone'; } btn.appendChild(row1); btn.appendChild(sub); btn.onclick = function() { _buildTrackWalk(); }; sec.appendChild(btn); if (typeof _instrDetectDevice === 'function') { var instrStrip = document.createElement('div'); instrStrip.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:8px'; [{ lbl: 'Link LiDAR scan', kind: 'lidar' }, { lbl: 'Link thermal', kind: 'thermal' }].forEach(function(spec) { var ib = document.createElement('button'); ib.type = 'button'; ib.textContent = spec.lbl; ib.style.cssText = 'padding:8px;font-family:Share Tech Mono;font-size:8px;background:rgba(56,189,248,.06);border:1px solid rgba(56,189,248,.2);color:#7dd3fc;cursor:pointer'; ib.onclick = function() { var ctx = typeof _instrCurrentContext === 'function' ? _instrCurrentContext({ walk_key: key }) : { walk_key: key }; if (typeof _instrImportScanFile === 'function') _instrImportScanFile(spec.kind, ctx); }; instrStrip.appendChild(ib); }); sec.appendChild(instrStrip); var gearL = document.createElement('button'); gearL.type = 'button'; gearL.textContent = 'Recommended thermal dongles →'; gearL.style.cssText = 'width:100%;margin-top:6px;padding:8px;font-family:Share Tech Mono;font-size:8px;background:rgba(251,146,60,.06);border:1px solid rgba(251,146,60,.22);color:#fb923c;cursor:pointer'; gearL.onclick = function() { if (typeof _openThermalGearModal === 'function') _openThermalGearModal(); }; sec.appendChild(gearL); } if (S.cur && S.cur.id && S.curTrack && typeof _renderWalkEntryRewardBlock === 'function') { var trackId = S.curTrack.id || _trackSlug(S.curTrack.name); var rewardHtml = _renderWalkEntryRewardBlock(S.cur.id, trackId, S.curTrack.name); if (rewardHtml) { var rewardWrap = document.createElement('div'); rewardWrap.innerHTML = rewardHtml; if (rewardWrap.firstElementChild) sec.appendChild(rewardWrap.firstElementChild); } } container.insertBefore(sec, container.firstChild); } // ── Main walk overlay ────────────────────────────────────────────────────── function _buildTrackWalk() { if (document.getElementById('bb-walk-modal')) return; var key = _walkKey(); var data = _walkLoad(key); if (!data.points) data.points = {}; if (S.curTrack) data.track = {name:S.curTrack.name, size:S.curTrack.size, banking:S.curTrack.banking}; if (S.cur) data.car = {id:S.cur.id, number:S.cur.car_number, div:S.cur.class_id}; _walkSave(key, data); var modal = document.createElement('div'); modal.id = 'bb-walk-modal'; modal.style.cssText = 'position:fixed;inset:0;background:var(--dark);z-index:9998;display:flex;flex-direction:column;overflow:hidden;'; // Header var hdr = document.createElement('div'); hdr.style.cssText = 'background:var(--dark2);border-bottom:2px solid var(--red);padding:12px 16px;display:flex;align-items:center;gap:10px;flex-shrink:0;'; var hInfo = document.createElement('div'); hInfo.style.cssText = 'flex:1;'; var hTitle = document.createElement('div'); hTitle.style.cssText = 'font-family:Barlow Condensed;font-size:20px;font-weight:900;letter-spacing:2px;color:var(--white);'; hTitle.textContent = 'TRACK WALK'; var hSub = document.createElement('div'); hSub.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--muted);'; hSub.textContent = S.curTrack ? S.curTrack.name.toUpperCase() + (S.curTrack.size ? ' · ' + S.curTrack.size : '') + (S.curTrack.banking ? ' · ' + S.curTrack.banking + '° BANKING' : '') : 'NO TRACK SELECTED — select a track first for full analysis'; hInfo.appendChild(hTitle); hInfo.appendChild(hSub); var closeBtn = document.createElement('button'); closeBtn.style.cssText = 'background:none;border:1px solid var(--dark4);color:var(--muted);font-family:Share Tech Mono;font-size:11px;padding:9px 14px;cursor:pointer;flex-shrink:0;min-height:44px;'; closeBtn.textContent = 'CLOSE'; closeBtn.onclick = function() { modal.remove(); }; hdr.appendChild(hInfo); hdr.appendChild(closeBtn); modal.appendChild(hdr); // Progress strip var progBar = document.createElement('div'); progBar.style.cssText = 'background:var(--dark2);padding:10px 16px 12px;border-bottom:1px solid var(--dark4);flex-shrink:0;'; var progTop = document.createElement('div'); progTop.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;'; var progLbl = document.createElement('span'); progLbl.id = 'walk-prog-lbl'; progLbl.style.cssText = 'font-family:Share Tech Mono;font-size:10px;color:var(--muted);'; var progPct = document.createElement('span'); progPct.id = 'walk-prog-pct'; progPct.style.cssText = 'font-family:Barlow Condensed;font-size:18px;font-weight:900;color:var(--red);'; progTop.appendChild(progLbl); progTop.appendChild(progPct); var barTrack = document.createElement('div'); barTrack.className = 'bb-walk-progress-bar'; var barFill = document.createElement('div'); barFill.id = 'walk-prog-fill'; barFill.className = 'bb-walk-progress-fill'; barFill.style.width = '0%'; barTrack.appendChild(barFill); var dotsWrap = document.createElement('div'); dotsWrap.id = 'walk-dots'; dotsWrap.style.cssText = 'display:flex;gap:5px;flex-wrap:wrap;margin-top:10px;'; progBar.appendChild(progTop); progBar.appendChild(barTrack); progBar.appendChild(dotsWrap); modal.appendChild(progBar); if (typeof _instrDetectDevice === 'function') { var instrBar = document.createElement('div'); instrBar.style.cssText = 'background:rgba(56,189,248,.04);border-bottom:1px solid rgba(56,189,248,.12);padding:8px 16px;display:flex;gap:6px;flex-wrap:wrap;flex-shrink:0;'; var devW = _instrDetectDevice(); var devNote = document.createElement('span'); devNote.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);flex:1;min-width:140px;line-height:1.5'; devNote.textContent = devW.lidar_likely ? 'LiDAR device — import USDZ after Scan/Polycam' : 'Import LiDAR/thermal linked to this walk'; instrBar.appendChild(devNote); [{ lbl: 'LiDAR', kind: 'lidar' }, { lbl: 'Thermal', kind: 'thermal' }].forEach(function(spec) { var wb = document.createElement('button'); wb.type = 'button'; wb.textContent = spec.lbl; wb.style.cssText = 'padding:6px 10px;font-family:Share Tech Mono;font-size:8px;background:var(--dark);border:1px solid rgba(56,189,248,.25);color:#7dd3fc;cursor:pointer'; wb.onclick = function() { var ctx = typeof _instrCurrentContext === 'function' ? _instrCurrentContext({ walk_key: key }) : { walk_key: key }; if (typeof _instrImportScanFile === 'function') _instrImportScanFile(spec.kind, ctx); }; instrBar.appendChild(wb); }); modal.appendChild(instrBar); } // Voice hint for Android var isAndroid = /android/i.test(navigator.userAgent); var hasSR = !!(window.SpeechRecognition || window.webkitSpeechRecognition); if (hasSR) { var hint = document.createElement('div'); hint.style.cssText = 'background:rgba(245,166,35,.07);border-bottom:1px solid rgba(245,166,35,.15);padding:7px 16px;font-family:Share Tech Mono;font-size:9px;color:var(--amber);flex-shrink:0;'; hint.textContent = (isAndroid ? 'VOICE READY — ' : '') + 'Tap MIC next to any field and speak the value. Tap again to cancel.'; modal.appendChild(hint); } // Scrollable zone cards var scroll = document.createElement('div'); scroll.style.cssText = 'flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:10px 10px 110px;'; // Group points by zone var zones = []; WALK_POINTS.forEach(function(pt) { var z = zones.find(function(zz){return zz.name === pt.zone;}); if (!z) { z = {name:pt.zone, points:[]}; zones.push(z); } z.points.push(pt); }); // Soil characterization panel (top of walk, persists by track) var tSlug = S.curTrack ? _trackSlug(S.curTrack.name) : null; if (tSlug) { _walkBuildSoilPanel(scroll, tSlug, data, key); } zones.forEach(function(zone) { var zDiv = document.createElement('div'); zDiv.style.cssText = 'margin-bottom:14px;'; var zLbl = document.createElement('div'); zLbl.style.cssText = 'font-family:Barlow Condensed;font-size:11px;font-weight:900;letter-spacing:3px;color:var(--red);margin-bottom:5px;padding-bottom:4px;border-bottom:1px solid rgba(208,25,14,.18);'; zLbl.textContent = zone.name; zDiv.appendChild(zLbl); zone.points.forEach(function(pt) { zDiv.appendChild(_walkCard(pt, data, key)); }); scroll.appendChild(zDiv); }); modal.appendChild(scroll); // Footer — Hunter + Submit buttons var footer = document.createElement('div'); footer.style.cssText = 'position:absolute;bottom:0;left:0;right:0;background:var(--dark2);border-top:1px solid var(--dark4);padding:10px 12px;padding-bottom:max(10px,env(safe-area-inset-bottom,10px));display:flex;flex-direction:column;gap:6px;'; var hunterBtn = document.createElement('button'); hunterBtn.id = 'walk-hunter-btn'; hunterBtn.style.cssText = 'width:100%;background:rgba(208,25,14,.12);border:1px solid rgba(208,25,14,.35);color:var(--red);font-family:Barlow Condensed;font-size:16px;font-weight:900;letter-spacing:2px;padding:14px;cursor:pointer;opacity:.35;transition:opacity .2s;'; hunterBtn.disabled = true; hunterBtn.textContent = 'RUN HUNTER ANALYSIS — LOG 6+ POINTS'; hunterBtn.onclick = function() { _walkRunHunter(data, key); }; var submitBtn = document.createElement('button'); submitBtn.id = 'walk-submit-btn'; submitBtn.style.cssText = 'width:100%;display:none;background:rgba(200,150,10,.1);border:1px solid rgba(200,150,10,.35);color:var(--gold);font-family:Barlow Condensed;font-size:13px;font-weight:900;letter-spacing:2px;padding:10px;cursor:pointer;transition:all .2s;'; submitBtn.textContent = 'SUBMIT WALK — SHARE INTEL'; submitBtn.onclick = function() { _walkSubmit(_walkLoad(key), key); }; footer.appendChild(hunterBtn); footer.appendChild(submitBtn); modal.appendChild(footer); document.body.appendChild(modal); _walkRefreshProgress(data, key); } // ── Point card ───────────────────────────────────────────────────────────── function _walkCard(pt, data, key) { var ptData = (data.points && data.points[pt.id]) || {}; var filled = (ptData.surface && ptData.surface.length > 0) || ptData.penetrometer || ptData.banking || ptData.note; var card = document.createElement('div'); card.id = 'walk-card-' + pt.id; card.style.cssText = 'background:var(--dark2);border:1px solid rgba(255,255,255,.04);margin-bottom:5px;'; // ── Card header (tap to expand) ────────────────────────────────────────── var cardHdr = document.createElement('div'); cardHdr.style.cssText = 'display:flex;align-items:center;padding:10px 12px;cursor:pointer;gap:8px;min-height:48px;user-select:none;-webkit-user-select:none;'; var dot = document.createElement('div'); dot.setAttribute('data-dot','1'); dot.className = 'walk-point-dot'; dot.style.cssText = 'width:10px;height:10px;border-radius:50%;flex-shrink:0;background:' + (filled ? 'var(--red)' : 'rgba(255,255,255,.12)') + ';transition:background .25s,transform .2s;' + (filled ? 'transform:scale(1.2);' : ''); var ptLbl = document.createElement('div'); ptLbl.style.cssText = 'font-family:Barlow Condensed;font-size:15px;font-weight:900;letter-spacing:1px;color:var(--white);flex:1;'; ptLbl.textContent = pt.label; var summary = document.createElement('div'); summary.setAttribute('data-summary','1'); summary.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);text-align:right;max-width:120px;'; var sumParts = []; if (ptData.surface && ptData.surface.length) sumParts.push(ptData.surface[0]); if (ptData.penetrometer) sumParts.push(ptData.penetrometer + 'psi'); if (ptData.banking) sumParts.push(ptData.banking + '°'); summary.textContent = sumParts.join(' · '); var chev = document.createElement('div'); chev.style.cssText = 'font-size:9px;color:var(--dark4);transition:transform .2s;flex-shrink:0;'; chev.textContent = '▼'; cardHdr.appendChild(dot); cardHdr.appendChild(ptLbl); cardHdr.appendChild(summary); cardHdr.appendChild(chev); card.appendChild(cardHdr); // ── Card body ──────────────────────────────────────────────────────────── var body = document.createElement('div'); body.style.cssText = 'padding:0 12px 14px;display:none;border-top:1px solid rgba(255,255,255,.04);'; // Surface tags var tagBlock = document.createElement('div'); tagBlock.style.cssText = 'margin-top:10px;'; var tagLbl = document.createElement('div'); tagLbl.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);letter-spacing:1px;margin-bottom:7px;'; tagLbl.textContent = 'SURFACE CONDITION (current state)'; var tagRow = document.createElement('div'); tagRow.style.cssText = 'display:flex;flex-wrap:wrap;gap:5px;'; WALK_TAGS.forEach(function(tag) { var active = (ptData.surface || []).indexOf(tag) > -1; var tb = document.createElement('button'); tb.type = 'button'; tb.className = 'walk-tag-btn'; tb.style.cssText = _walkTagStyle(active); tb.textContent = tag; tb.setAttribute('data-active', active ? '1' : '0'); tb.onclick = function() { var nowActive = tb.getAttribute('data-active') === '1'; nowActive = !nowActive; tb.setAttribute('data-active', nowActive ? '1' : '0'); tb.style.cssText = _walkTagStyle(nowActive); var d2 = _walkLoad(key); if (!d2.points) d2.points = {}; if (!d2.points[pt.id]) d2.points[pt.id] = {}; var tags = d2.points[pt.id].surface || []; var idx = tags.indexOf(tag); if (nowActive && idx === -1) tags.push(tag); if (!nowActive && idx > -1) tags.splice(idx, 1); d2.points[pt.id].surface = tags; _walkSave(key, d2); data.points = d2.points; _walkMarkFilled(pt.id, d2, dot, summary); _walkRefreshProgress(d2, key); }; tagRow.appendChild(tb); }); tagBlock.appendChild(tagLbl); tagBlock.appendChild(tagRow); body.appendChild(tagBlock); // Penetrometer body.appendChild(_walkNumField('PENETROMETER (psi)', pt.id, 'penetrometer', ptData.penetrometer, data, key, dot, summary)); // Banking — turn points with Walk Scan if (pt.banking) { body.appendChild(_walkBankField(pt.id, ptData.banking, data, key, dot, summary)); } // Note + mic var noteBlock = document.createElement('div'); noteBlock.style.cssText = 'margin-top:12px;'; var noteLbl = document.createElement('div'); noteLbl.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);letter-spacing:1px;margin-bottom:6px;'; noteLbl.textContent = 'NOTE'; var noteRow = document.createElement('div'); noteRow.style.cssText = 'display:flex;gap:6px;'; var noteTA = document.createElement('textarea'); noteTA.placeholder = 'Observations, bumps, rubber, anything...'; noteTA.value = ptData.note || ''; noteTA.style.cssText = 'flex:1;background:var(--dark);border:1px solid var(--dark4);color:var(--white);font-family:Share Tech Mono;font-size:11px;padding:9px;outline:none;resize:none;min-height:54px;line-height:1.5;'; noteTA.onchange = function() { var d2 = _walkLoad(key); if (!d2.points) d2.points = {}; if (!d2.points[pt.id]) d2.points[pt.id] = {}; d2.points[pt.id].note = noteTA.value.trim(); _walkSave(key, d2); data.points = d2.points; _walkMarkFilled(pt.id, d2, dot, summary); _walkRefreshProgress(d2, key); }; var noteMicWrap = document.createElement('div'); noteMicWrap.style.cssText = 'display:flex;flex-direction:column;gap:3px;flex-shrink:0;'; var noteMicBtn = _walkMicBtn(); var noteMicSt = document.createElement('div'); noteMicSt.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--amber);text-align:center;min-height:10px;width:52px;'; noteMicBtn.onclick = function() { _walkAudible(noteTA, noteMicBtn, noteMicSt, 'text'); }; noteMicWrap.appendChild(noteMicBtn); noteMicWrap.appendChild(noteMicSt); noteRow.appendChild(noteTA); noteRow.appendChild(noteMicWrap); noteBlock.appendChild(noteLbl); noteBlock.appendChild(noteRow); body.appendChild(noteBlock); // ── Per-point soil override (for zones that differ from track baseline) ── if (pt.banking) { // only on turn points where soil matters most var soilOverrideBlock = document.createElement('div'); soilOverrideBlock.style.cssText = 'margin-top:10px;padding-top:10px;border-top:1px solid rgba(255,255,255,.04);'; var soilOvLbl = document.createElement('div'); soilOvLbl.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--dark4);letter-spacing:1px;margin-bottom:6px;display:flex;align-items:center;justify-content:space-between;'; var soilOvLeft = document.createElement('span'); soilOvLeft.textContent = 'SOIL DIFFERS HERE?'; var soilOvRight = document.createElement('span'); soilOvRight.style.cssText = 'color:var(--amber);cursor:pointer;'; soilOvRight.textContent = ptData.soil ? ('✔ ' + (ptData.soil.soil_type||'').replace('_',' ').toUpperCase()) : 'OVERRIDE'; soilOvLbl.appendChild(soilOvLeft); soilOvLbl.appendChild(soilOvRight); var soilOvBtn = document.createElement('button'); soilOvBtn.style.cssText = 'background:none;border:1px solid rgba(255,255,255,.08);color:var(--muted);font-family:Share Tech Mono;font-size:8px;padding:6px 10px;cursor:pointer;width:100%;text-align:left;'; soilOvBtn.textContent = ptData.soil ? (ptData.soil.night_arc || 'Soil analyzed') : '📷 Photo this spot to override track soil'; var soilOvInput = document.createElement('input'); soilOvInput.type = 'file'; soilOvInput.accept = 'image/*'; soilOvInput.setAttribute('capture','environment'); soilOvInput.style.display = 'none'; soilOvInput.onchange = function() { if (!soilOvInput.files || !soilOvInput.files[0]) return; soilOvBtn.textContent = 'Analyzing soil…'; soilOvBtn.disabled = true; var reader = new FileReader(); reader.onload = function(ev) { var b64 = ev.target.result.split(',')[1]; var mime = soilOvInput.files[0].type; _walkSoilAnalyzePoint(b64, mime, pt.id, data, key, soilOvBtn, soilOvRight); }; reader.readAsDataURL(soilOvInput.files[0]); }; soilOvBtn.onclick = function() { soilOvInput.click(); }; soilOverrideBlock.appendChild(soilOvLbl); soilOverrideBlock.appendChild(soilOvBtn); soilOverrideBlock.appendChild(soilOvInput); body.appendChild(soilOverrideBlock); } card.appendChild(body); // Toggle cardHdr.onclick = function() { var open = body.style.display !== 'none'; body.style.display = open ? 'none' : 'block'; chev.style.transform = open ? '' : 'rotate(180deg)'; if (filled) { card.style.border = '1px solid rgba(208,25,14,.22)'; } }; return card; } // ── Soil analysis helpers ──────────────────────────────────────────────────── function _walkSoilAnalyzePoint(b64, mime, ptId, data, key, btn, labelEl) { fetch(HNTR, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ message: _SOIL_PROMPT, photos: [{data: b64, type: mime}], history: [], context: (typeof buildContext === 'function' ? buildContext() : '') + ' | scan_target:composition', user_id: S.user ? S.user.user_id : null, user_email: S.user ? S.user.email : null, ai_calls_this_session: _aiCallsThisSession }) }).then(function(r){return r.json();}).then(function(d){ _aiCallsThisSession++; var reply = d.response || d.reply || ''; var soil = null; // Extract JSON from response try { var m = reply.match(/{[sS]*}/); if (m) soil = JSON.parse(m[0]); } catch(e) {} if (!soil) { if(btn){btn.textContent='Could not parse soil data — try again';btn.disabled=false;} return; } var d2 = _walkLoad(key); if (!d2.points) d2.points = {}; if (!d2.points[ptId]) d2.points[ptId] = {}; d2.points[ptId].soil = soil; _walkSave(key, d2); data.points = d2.points; if (btn) { btn.textContent = soil.night_arc || soil.soil_type; btn.disabled = false; btn.style.color = 'var(--amber)'; btn.style.borderColor = 'rgba(245,166,35,.3)'; } if (labelEl) labelEl.textContent = '✔ ' + (soil.soil_type||'').replace(/_/g,' ').toUpperCase(); }).catch(function(){ if(btn){btn.textContent='Connection error — try again';btn.disabled=false;} }); } function _walkSoilAnalyzeTrack(b64, mime, trackSlug, displayEl, statusEl) { if (statusEl) { statusEl.textContent = 'Analyzing soil composition…'; statusEl.style.color = 'var(--amber)'; } fetch(HNTR, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ message: _SOIL_PROMPT, photos: [{data: b64, type: mime}], history: [], context: (typeof buildContext === 'function' ? buildContext() : '') + ' | scan_target:composition | full track soil characterization', user_id: S.user ? S.user.user_id : null, user_email: S.user ? S.user.email : null, ai_calls_this_session: _aiCallsThisSession }) }).then(function(r){return r.json();}).then(function(d){ _aiCallsThisSession++; var reply = d.response || d.reply || ''; var soil = null; try { var m = reply.match(/{[sS]*}/); if (m) soil = JSON.parse(m[0]); } catch(e) {} if (!soil) { if(statusEl){statusEl.textContent='Could not parse — try again or set manually';statusEl.style.color='var(--red)';} return; } soil.manual = false; soil.analyzed_at = Date.now(); _soilSave(trackSlug, soil); _walkRenderSoilResult(soil, displayEl, statusEl); }).catch(function(){ if(statusEl){statusEl.textContent='Connection error — set manually below';statusEl.style.color='var(--red)';} }); } function _walkRenderSoilResult(soil, displayEl, statusEl) { if (!soil || !displayEl) return; if (statusEl) { statusEl.textContent = soil.manual ? 'Set manually' : '✔ Analyzed'; statusEl.style.color = '#4CAF50'; } displayEl.innerHTML = ''; displayEl.style.cssText = 'background:rgba(208,25,14,.06);border:1px solid rgba(208,25,14,.2);border-left:3px solid var(--red);padding:10px;margin-top:8px;'; var typeRow = document.createElement('div'); typeRow.style.cssText = 'display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px;'; var typeEl = document.createElement('div'); typeEl.style.cssText = 'font-family:Barlow Condensed;font-size:18px;font-weight:900;color:var(--white);letter-spacing:1px;'; typeEl.textContent = (soil.soil_type||'').replace(/_/g,' ').toUpperCase(); var bindEl = document.createElement('div'); bindEl.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--amber);'; bindEl.textContent = (soil.binding_character||'').toUpperCase() + ' BIND · ' + (soil.transition_rate||'').toUpperCase() + ' TRANSITION'; typeRow.appendChild(typeEl); typeRow.appendChild(bindEl); displayEl.appendChild(typeRow); var arcEl = document.createElement('div'); arcEl.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--muted);line-height:1.6;margin-bottom:4px;'; arcEl.textContent = soil.night_arc || ''; displayEl.appendChild(arcEl); if (soil.setup_note) { var setupEl = document.createElement('div'); setupEl.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--amber);border-top:1px solid rgba(255,255,255,.06);padding-top:5px;margin-top:5px;'; setupEl.textContent = '▶ ' + soil.setup_note; displayEl.appendChild(setupEl); } var detailRow = document.createElement('div'); detailRow.style.cssText = 'display:flex;gap:10px;margin-top:6px;'; [{l:'CLAY',v:soil.clay_content},{l:'AGGREGATE',v:soil.aggregate_size},{l:'MOISTURE',v:soil.moisture_depth}].forEach(function(f){ var chip = document.createElement('div'); chip.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--dark4);'; chip.textContent = f.l + ': ' + (f.v||'—').replace(/_/g,' '); detailRow.appendChild(chip); }); displayEl.appendChild(detailRow); } function _walkBuildSoilPanel(scroll, trackSlug, data, key) { var panel = document.createElement('div'); panel.style.cssText = 'margin-bottom:14px;background:var(--dark2);border:1px solid var(--dark4);border-top:2px solid var(--red);padding:12px;'; var hdrRow = document.createElement('div'); hdrRow.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;'; var hdrLbl = document.createElement('div'); hdrLbl.style.cssText = 'font-family:Barlow Condensed;font-size:16px;font-weight:900;letter-spacing:2px;color:var(--white);'; hdrLbl.textContent = 'SOIL COMPOSITION'; var hdrSub = document.createElement('div'); hdrSub.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--muted);'; hdrSub.textContent = 'Predicts the night arc — persists for this track'; hdrRow.appendChild(hdrLbl); hdrRow.appendChild(hdrSub); panel.appendChild(hdrRow); var howWrap = document.createElement('div'); howWrap.innerHTML = _surfaceScanHowtoHtml(true); panel.appendChild(howWrap); var statusEl = document.createElement('div'); statusEl.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);margin-bottom:8px;'; var existing = _soilLoad(trackSlug); statusEl.textContent = existing ? ('✔ On file' + (existing.analyzed_at ? ' — ' + new Date(existing.analyzed_at).toLocaleDateString() : '')) : 'Not analyzed yet for this track'; panel.appendChild(statusEl); var resultEl = document.createElement('div'); if (existing) { _walkRenderSoilResult(existing, resultEl, statusEl); } panel.appendChild(resultEl); // Action buttons row var btnRow = document.createElement('div'); btnRow.style.cssText = 'display:flex;gap:6px;margin-top:8px;'; // Camera / photo button var photoInput = document.createElement('input'); photoInput.type = 'file'; photoInput.accept = 'image/*'; photoInput.setAttribute('capture','environment'); photoInput.style.display = 'none'; var photoBtn = document.createElement('button'); photoBtn.style.cssText = 'flex:2;background:rgba(208,25,14,.1);border:1px solid rgba(208,25,14,.3);color:var(--white);font-family:Share Tech Mono;font-size:9px;font-weight:700;padding:10px 6px;cursor:pointer;letter-spacing:1px;'; photoBtn.textContent = '📷 PHOTO ANALYZE SOIL'; photoInput.onchange = function() { if (!photoInput.files || !photoInput.files[0]) return; var reader = new FileReader(); reader.onload = function(ev) { _walkSoilAnalyzeTrack(ev.target.result.split(',')[1], photoInput.files[0].type, trackSlug, resultEl, statusEl); }; reader.readAsDataURL(photoInput.files[0]); }; photoBtn.onclick = function() { photoInput.click(); }; btnRow.appendChild(photoInput); btnRow.appendChild(photoBtn); // Manual set button var manualBtn = document.createElement('button'); manualBtn.style.cssText = 'flex:1;background:none;border:1px solid var(--dark4);color:var(--muted);font-family:Share Tech Mono;font-size:9px;padding:10px 6px;cursor:pointer;letter-spacing:1px;'; manualBtn.textContent = 'SET MANUALLY'; var manualOpen = false; var manualPanel = document.createElement('div'); manualPanel.style.cssText = 'display:none;margin-top:8px;'; // Soil type select var stLbl = document.createElement('div'); stLbl.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--muted);margin-bottom:4px;margin-top:4px;'; stLbl.textContent = 'SOIL TYPE'; var stSel = document.createElement('select'); stSel.style.cssText = 'width:100%;background:var(--dark);border:1px solid var(--dark4);color:var(--white);font-family:Share Tech Mono;font-size:11px;padding:8px;outline:none;margin-bottom:6px;'; stSel.innerHTML = ''; SOIL_TYPES.forEach(function(t){ var o=document.createElement('option');o.value=t.v;o.textContent=t.l+' — '+t.d;if(existing&&existing.soil_type===t.v)o.selected=true;stSel.appendChild(o); }); // Moisture depth select var mdLbl = document.createElement('div'); mdLbl.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--muted);margin-bottom:4px;'; mdLbl.textContent = 'MOISTURE DEPTH (current)'; var mdSel = document.createElement('select'); mdSel.style.cssText = 'width:100%;background:var(--dark);border:1px solid var(--dark4);color:var(--white);font-family:Share Tech Mono;font-size:11px;padding:8px;outline:none;margin-bottom:6px;'; mdSel.innerHTML = ''; SOIL_MOISTURE.forEach(function(m){ var o=document.createElement('option');o.value=m.v;o.textContent=m.l+' — '+m.d;if(existing&&existing.moisture_depth===m.v)o.selected=true;mdSel.appendChild(o); }); // Binding var bdLbl = document.createElement('div'); bdLbl.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--muted);margin-bottom:4px;'; bdLbl.textContent = 'BINDING CHARACTER'; var bdSel = document.createElement('select'); bdSel.style.cssText = 'width:100%;background:var(--dark);border:1px solid var(--dark4);color:var(--white);font-family:Share Tech Mono;font-size:11px;padding:8px;outline:none;margin-bottom:8px;'; bdSel.innerHTML = ''; SOIL_BINDING.forEach(function(b){ var o=document.createElement('option');o.value=b.v;o.textContent=b.l;if(existing&&existing.binding_character===b.v)o.selected=true;bdSel.appendChild(o); }); // Save manual var saveMBtn = document.createElement('button'); saveMBtn.style.cssText = 'width:100%;background:rgba(208,25,14,.12);border:1px solid rgba(208,25,14,.3);color:var(--white);font-family:Share Tech Mono;font-size:9px;font-weight:700;padding:10px;cursor:pointer;letter-spacing:1px;'; saveMBtn.textContent = 'SAVE SOIL CHARACTERIZATION'; saveMBtn.onclick = function() { if (!stSel.value) { toast('Pick a soil type'); return; } var soilType = SOIL_TYPES.find(function(t){return t.v===stSel.value;}); // Derive night arc and transition rate from selections var arcMap = { dark_loam: 'Rubbers heavily by Heat 2, major grip swing, setup needs to loosen by feature', red_clay: 'Fast-transitioning — critical prep sensitivity, check surface between every heat', sandy_loam: 'No rubber retention, gets rough early, setup stays consistent but handling gets choppy', caliche: 'Hard and fast all night, minimal grip change, rubber buildup minimal, look for line', mixed: 'Variable by zone — assess each turn independently' }; var trMap = {dark_loam:'moderate',red_clay:'fast',sandy_loam:'slow',caliche:'slow',mixed:'moderate'}; var bindMap = {dark_loam:'high',red_clay:'moderate',sandy_loam:'low',caliche:'low',mixed:'moderate'}; var soilData = { soil_type: stSel.value, clay_content: (stSel.value==='dark_loam'||stSel.value==='red_clay') ? 'high' : stSel.value==='mixed' ? 'moderate' : 'low', aggregate_size: stSel.value==='caliche' ? 'coarse' : 'medium', moisture_depth: mdSel.value || 'uniform', binding_character: bdSel.value || bindMap[stSel.value] || 'moderate', transition_rate: trMap[stSel.value] || 'moderate', night_arc: arcMap[stSel.value] || '', setup_note: 'Monitor chassis balance through heat cycle — adjust as track transitions', manual: true, analyzed_at: Date.now() }; _soilSave(trackSlug, soilData); _walkRenderSoilResult(soilData, resultEl, statusEl); manualPanel.style.display = 'none'; manualOpen = false; manualBtn.textContent = 'EDIT MANUAL'; }; manualPanel.appendChild(stLbl); manualPanel.appendChild(stSel); manualPanel.appendChild(mdLbl); manualPanel.appendChild(mdSel); manualPanel.appendChild(bdLbl); manualPanel.appendChild(bdSel); manualPanel.appendChild(saveMBtn); manualBtn.onclick = function() { manualOpen = !manualOpen; manualPanel.style.display = manualOpen ? 'block' : 'none'; manualBtn.textContent = manualOpen ? 'CANCEL' : 'SET MANUALLY'; }; btnRow.appendChild(manualBtn); panel.appendChild(btnRow); panel.appendChild(manualPanel); scroll.insertBefore(panel, scroll.firstChild); } function _walkTagStyle(active) { return 'background:' + (active ? 'rgba(208,25,14,.18)' : 'none') + ';border:1px solid ' + (active ? 'rgba(208,25,14,.45)' : 'rgba(255,255,255,.1)') + ';color:' + (active ? 'var(--red-hot)' : 'var(--muted)') + ';font-family:Share Tech Mono;font-size:11px;padding:10px 14px;cursor:pointer;min-height:48px;transition:all .15s;'; } // Reusable numeric field with mic button function _walkNumField(label, ptId, field, currentVal, data, key, dot, summary) { var wrap = document.createElement('div'); wrap.style.cssText = 'margin-top:12px;'; var lbl = document.createElement('div'); lbl.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);letter-spacing:1px;margin-bottom:6px;'; lbl.textContent = label; var row = document.createElement('div'); row.style.cssText = 'display:flex;gap:8px;align-items:flex-start;'; var inp = document.createElement('input'); inp.type = 'number'; inp.inputMode = 'decimal'; inp.placeholder = '—'; inp.value = currentVal || ''; inp.style.cssText = 'width:100px;background:var(--dark);border:1px solid var(--dark4);color:var(--white);font-family:Barlow Condensed;font-size:28px;font-weight:700;padding:10px;text-align:center;outline:none;'; inp.onchange = function() { var v = parseFloat(inp.value); if (!isNaN(v) && v > 0) { var d2 = _walkLoad(key); if (!d2.points) d2.points = {}; if (!d2.points[ptId]) d2.points[ptId] = {}; d2.points[ptId][field] = v; _walkSave(key, d2); data.points = d2.points; _walkMarkFilled(ptId, d2, dot, summary); _walkRefreshProgress(d2, key); } }; var micWrap = document.createElement('div'); micWrap.style.cssText = 'display:flex;flex-direction:column;gap:3px;align-items:center;'; var micBtn = _walkMicBtn(); var micSt = document.createElement('div'); micSt.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--amber);text-align:center;min-height:10px;width:52px;overflow:hidden;'; micBtn.onclick = function() { _walkAudible(inp, micBtn, micSt, 'number'); }; micWrap.appendChild(micBtn); micWrap.appendChild(micSt); row.appendChild(inp); row.appendChild(micWrap); wrap.appendChild(lbl); wrap.appendChild(row); return wrap; } function _walkMicBtn() { var btn = document.createElement('button'); btn.style.cssText = 'background:var(--dark);border:1px solid rgba(245,166,35,.3);color:var(--amber);font-family:Share Tech Mono;font-size:10px;font-weight:700;letter-spacing:1px;width:52px;height:52px;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;'; btn.textContent = 'MIC'; btn.setAttribute('data-mic','idle'); return btn; } function _walkMarkFilled(ptId, data, dotEl, summaryEl) { var p = data.points && data.points[ptId]; var filled = p && ((p.surface && p.surface.length > 0) || p.penetrometer || p.banking || p.note); if (dotEl) dotEl.style.background = filled ? 'var(--red)' : 'rgba(255,255,255,.12)'; if (summaryEl) { var parts = []; if (p && p.surface && p.surface.length) parts.push(p.surface[0]); if (p && p.penetrometer) parts.push(p.penetrometer + 'psi'); if (p && p.banking) parts.push(p.banking + '°'); summaryEl.textContent = parts.join(' · '); } var card = document.getElementById('walk-card-' + ptId); if (card && filled) card.style.border = '1px solid rgba(208,25,14,.2)'; } // ── Progress refresh ─────────────────────────────────────────────────────── function _walkRefreshProgress(data, key) { var count = _walkCount(data); var remaining = Math.max(0, 22 - count); var lbl = document.getElementById('walk-prog-lbl'); if (lbl) lbl.textContent = count + ' / 22 logged · ' + remaining + ' left'; var pct = document.getElementById('walk-prog-pct'); var pctVal = Math.round(count / 22 * 100); if (pct) pct.textContent = pctVal + '%'; var fill = document.getElementById('walk-prog-fill'); if (fill) fill.style.width = pctVal + '%'; var dotsWrap = document.getElementById('walk-dots'); if (dotsWrap) { dotsWrap.innerHTML = ''; WALK_POINTS.forEach(function(pt) { var p = data.points && data.points[pt.id]; var done = p && ((p.surface && p.surface.length > 0) || p.penetrometer || p.banking || p.note); var d = document.createElement('div'); d.className = 'walk-point-dot'; d.title = pt.label; d.style.cssText = 'width:10px;height:10px;border-radius:50%;background:' + (done ? 'var(--red)' : 'rgba(255,255,255,.1)') + ';flex-shrink:0;transition:background .2s,transform .15s;'; if (done) d.style.transform = 'scale(1.15)'; dotsWrap.appendChild(d); }); } var hunterBtn = document.getElementById('walk-hunter-btn'); if (hunterBtn) { var ok = count >= 6; hunterBtn.disabled = !ok; hunterBtn.style.opacity = ok ? '1' : '.35'; hunterBtn.textContent = ok ? 'RUN HUNTER ANALYSIS — ' + count + ' PTS' : 'RUN HUNTER ANALYSIS — LOG 6+ POINTS'; } var submitBtn2 = document.getElementById('walk-submit-btn'); if (submitBtn2 && !submitBtn2._submitted) { var canSub = count >= 20 && S.token && !S._demo; submitBtn2.style.display = canSub ? 'block' : 'none'; if (canSub) submitBtn2.textContent = count >= 22 ? 'SUBMIT FULL WALK — SHARE INTEL' : 'SUBMIT WALK (' + count + '/22) — SHARE INTEL'; } if (typeof _maybeGrantTrackWalkReward === 'function') _maybeGrantTrackWalkReward(data, key); } // ── Voice input ──────────────────────────────────────────────────────────── function _walkAudible(inputEl, btnEl, statusEl, mode) { var SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SR) { toast('Voice not supported — type the value'); return; } // Toggle off if already running if (window._walkSR && btnEl.getAttribute('data-mic') === 'listening') { try { window._walkSR.abort(); } catch(e) {} return; } var rec = new SR(); window._walkSR = rec; rec.lang = 'en-US'; rec.interimResults = true; rec.maxAlternatives = 3; rec.continuous = false; btnEl.setAttribute('data-mic','listening'); btnEl.style.background = 'rgba(208,25,14,.3)'; btnEl.style.color = 'var(--white)'; btnEl.textContent = '●'; if (statusEl) statusEl.textContent = 'READY'; rec.onresult = function(e) { var interim = '', final_t = ''; for (var i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) final_t += e.results[i][0].transcript; else interim += e.results[i][0].transcript; } var shown = (final_t || interim).toUpperCase().substring(0,12); if (statusEl) statusEl.textContent = shown || 'READY'; if (final_t) { if (mode === 'number') { var num = _walkParseNumber(final_t); if (num !== null) { inputEl.value = num; inputEl.dispatchEvent(new Event('change')); if (statusEl) statusEl.textContent = '✔ ' + num; } else { if (statusEl) statusEl.textContent = 'RETRY?'; } } else { var existing = inputEl.value ? inputEl.value.trim() + ' ' : ''; inputEl.value = existing + final_t.trim(); inputEl.dispatchEvent(new Event('change')); if (statusEl) statusEl.textContent = '✔'; } } }; rec.onerror = function(e) { var msg = {'no-speech':'NO SPEECH','not-allowed':'MIC BLOCKED','aborted':'STOPPED'}[e.error] || e.error.toUpperCase(); if (statusEl) statusEl.textContent = msg; _walkMicReset(btnEl); window._walkSR = null; }; rec.onend = function() { _walkMicReset(btnEl); window._walkSR = null; }; try { rec.start(); } catch(e) { toast('Could not start mic'); _walkMicReset(btnEl); window._walkSR = null; } } function _walkMicReset(btnEl) { btnEl.setAttribute('data-mic','idle'); btnEl.style.background = 'var(--dark)'; btnEl.style.color = 'var(--amber)'; btnEl.textContent = 'MIC'; } // Spoken number parser — handles digits AND words // "forty two" -> 42, "one twenty seven" -> 127, "14.5" -> 14.5 function _walkParseNumber(text) { var t = text.toLowerCase().trim(); // Try direct float first var direct = parseFloat(t.replace(/[^0-9.]/g,'')); if (!isNaN(direct) && t.match(/^d/)) return direct; // Word map var ones = {zero:0,one:1,two:2,three:3,four:4,five:5,six:6,seven:7,eight:8,nine:9, ten:10,eleven:11,twelve:12,thirteen:13,fourteen:14,fifteen:15, sixteen:16,seventeen:17,eighteen:18,nineteen:19}; var tens = {twenty:20,thirty:30,forty:40,fifty:50,sixty:60,seventy:70,eighty:80,ninety:90}; var words = t.replace(/-/g,' ').split(/s+/); var sum = 0, curr = 0; words.forEach(function(w) { if (ones[w] !== undefined) curr += ones[w]; else if (tens[w] !== undefined) curr += tens[w]; else if (w === 'hundred') curr *= 100; else if (w === 'point' || w === 'decimal') {} // ignore — handle decimals below }); sum += curr; if (sum > 0) return sum; // Last resort: grab first number sequence var m = t.match(/d+.?d*/); return m ? parseFloat(m[0]) : null; } // ── Hunter track prediction (stub — full Tier 3 in next build) ───────────── async function _hunterTrackPredict(zones, trackName) { if (!_canDo('chief')) return; var wx = S.wx || {}; var da = wx.density_altitude ? Math.round(wx.density_altitude) : null; var temp = wx.temp ? Math.round(wx.temp) : null; var pred = (da !== null && temp !== null) ? _daPredict(da, temp, new Date().getHours(), 20) : null; var parts = ['Full 22-point track walk at ' + (trackName || 'this track')]; var zLabels = {e1:'End 1',e2:'End 2',fs:'Front straight',bs:'Back straight'}; ['e1','e2','fs','bs'].forEach(function(z) { var zd = zones[z]; if (zd && zd.grip !== null) parts.push(zLabels[z] + ': grip ' + zd.grip + '/100' + (zd.dominant_tag ? ' (' + zd.dominant_tag + ')' : '')); }); if (zones.e1 && zones.e2 && zones.e1.grip !== null && zones.e2.grip !== null) { var diff = zones.e1.grip - zones.e2.grip; if (Math.abs(diff) > 8) parts.push('Track balance: End ' + (diff > 0 ? '1 grippier' : '2 grippier') + ' by ' + Math.abs(diff) + ' pts'); } if (da !== null) parts.push('Current DA: ' + da.toLocaleString() + 'ft at ' + temp + '°F'); if (pred && Math.abs(pred.delta) > 50) parts.push('Race-time DA est: ' + pred.race.toLocaleString() + 'ft (drops ' + Math.abs(pred.delta) + 'ft by race)'); var cls = S.cur ? (S.cur.class || S.cur.car_class || 'unknown') : 'unknown'; parts.push('Class: ' + cls); parts.push('Give me specific starting setup numbers for tonight: tire pressures, stagger, wing angle if applicable. Factor in track condition trend as rubber builds in the feature.'); var prompt = parts.join('. '); var ov = document.querySelector('[data-chief-overlay="1"]'); var predDiv = ov ? ov.querySelector('.chief-predict') : null; try { var r = await fetch(HNTR, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({message:prompt, history:[], context:buildContext(), user_id:S.user?S.user.user_id:null, user_email:S.user?S.user.email:null, ai_calls_this_session:_aiCallsThisSession})}); var d = await r.json(); var reply = d.response || d.reply || null; if (!d.routed) _aiCallsThisSession++; if (reply) { if (predDiv) { predDiv.style.cssText = 'margin:14px 0 0;max-width:320px;background:rgba(200,150,10,.08);border-left:2px solid var(--gold);padding:10px 12px;text-align:left;border-radius:0'; var safeReply = reply.replace(/&/g,'&').replace(//g,'>'); predDiv.innerHTML = '
    HUNTER SAYS
    ' + safeReply + '
    '; } else { S.hist.push({r:'u', t:'[Walk predict]'}); S.hist.push({r:'h', t:reply}); } } } catch(e) { if (predDiv) predDiv.innerHTML = ''; } } function _walkRunHunter(data, key) { var count = _walkCount(data); if (count < 6) return; var zones = _walkZoneSummary(data); S._walkSummary = zones; if(S.curTrack&&typeof fetchEngineRec==='function')fetchEngineRec(true); var trackName = (data.track && data.track.name) || (S.curTrack && S.curTrack.name) || 'this track'; var wx = S.wx || {}; var temp = Math.round(wx.temp || wx.tempF || 70); var da = Math.round(wx.density_altitude || 0); var dew = Math.round(wx.dewpoint || wx.dew_point || 0); var dewSpread = temp - dew; var wind = Math.round(wx.wind_speed || wx.windMph || 0); var windDir = (wx.wind_dir || wx.windDir || 'N').toUpperCase(); var humid = Math.round(wx.humidity || 0); var hour = new Date().getHours(); var raceHour = S._raceStartHour || (hour < 18 ? 20 : hour + 2); // Corner-by-corner detail var cornerLines = []; WALK_POINTS.forEach(function(pt) { var p = data.points && data.points[pt.id]; if (!p) return; var parts2 = [pt.label + ':']; if (p.surface && p.surface.length) parts2.push(p.surface.join('/')); if (p.penetrometer) parts2.push(p.penetrometer + 'psi(' + _walkGripIndex(p.penetrometer) + '/100)'); if (p.banking) parts2.push(p.banking + '° bank'); if (p.note) parts2.push('"' + p.note.substring(0, 60) + '"'); if (parts2.length > 1) cornerLines.push(parts2.join(' ')); }); // Wind exposure var windNote = ''; if (wind > 5) { var wAngle = {N:0,NE:45,E:90,SE:135,S:180,SW:225,W:270,NW:315}[windDir] || 0; var exposed = (wAngle >= 315 || wAngle < 45) ? 'End 1' : (wAngle >= 135 && wAngle < 225) ? 'End 2' : (wAngle >= 45 && wAngle < 135) ? 'Front str' : 'Back str'; windNote = 'WIND ' + wind + 'mph ' + windDir + ': ' + exposed + ' dries first.'; } // Surface evolution var tempDrop = Math.max(0, Math.round((raceHour - hour) * 2.5)); var featureDew = dewSpread - tempDrop; var evolution = []; if (dewSpread > 15) evolution.push('NOW: dry, fast dust.'); else if (dewSpread > 8) evolution.push('NOW: moderate. Tacky early, transitioning.'); else evolution.push('NOW: heavy/tacky. Dew close.'); if (featureDew < 3) evolution.push('BY FEATURE: dew settling. Track gets heavy. Loosen the car.'); else if (featureDew < 8) evolution.push('BY FEATURE: rubber building. Car may tighten.'); else evolution.push('BY FEATURE: still drying. Grip on rubber line only.'); if (hour >= 19 && humid > 70) evolution.push('High humidity + post-sunset = heavier each session.'); // Assemble var msg = []; msg.push('TRACK WALK at ' + trackName + ' — ' + count + '/22 pts'); msg.push('Weather: ' + temp + 'F, DA ' + da + 'ft, dew ' + dew + 'F (spread ' + dewSpread + '), wind ' + wind + 'mph ' + windDir + ', ' + humid + '%RH'); msg.push(''); msg.push('CORNER BY CORNER:'); cornerLines.forEach(function(l) { msg.push(l); }); if (windNote) { msg.push(''); msg.push(windNote); } msg.push(''); msg.push('SURFACE EVOLUTION:'); evolution.forEach(function(e) { msg.push(e); }); msg.push(''); msg.push('Based on this data — what setup adjustments for each end? What changes between heats and the feature?'); var modal = document.getElementById('bb-walk-modal'); if (modal) modal.remove(); if (typeof openChat === 'function') openChat(); setTimeout(function() { sendToHunter(msg.join('\n')); }, 250); } // ── Submit walk to shared pool (direct REST — no edge function needed) ──────── async function _walkSubmit(data, key) { if (!S.token || S._demo) { toast('Sign in to share walk data'); return; } var count = _walkCount(data); var is_full = count >= 22; var zones = _walkZoneSummary(data); var track_slug = S.curTrack ? _trackSlug(S.curTrack.name) : 'unknown'; var track_name = S.curTrack ? S.curTrack.name : ''; var uid = S.token.slice(-12).replace(/[^a-z0-9]/gi,'x'); var d = new Date().toISOString().slice(0,10).replace(/-/g,''); var slug = '_tw_'+track_slug+'_'+uid+'_'+d; var btn = document.getElementById('walk-submit-btn'); if (btn) { btn.disabled = true; btn.textContent = 'SUBMITTING…'; btn._submitted = true; } try { var payload = JSON.stringify({track_slug:track_slug,track_name:track_name,car_class:S.cur?(S.cur.class||S.cur.car_class||''):'',zones:zones,is_full:is_full,point_count:count,submitted_at:new Date().toISOString(),night_date:d}); var ok = await _sbUpsert(slug, payload); if (ok) { var others = []; var chief_for_night = false; if (is_full) { var rows = await _sbQuery('_tw_'+track_slug+'_%'); others = (rows||[]).filter(function(r){try{var p=JSON.parse(r.html);return p.is_full&&p.night_date===d&&r.slug!==slug;}catch(e){return false;}}); chief_for_night = others.length === 0; } var all_full = (others.length + 1); chief_for_night = others.length >= 2; if (chief_for_night) { _grantNightChief(track_name); _hunterTrackPredict(zones,track_name); } else if (is_full) { toast('Walk '+all_full+'/3 at '+(track_name||'this track')+' — '+(2-others.length)+' more needed for Chief intel'); } else { toast('Walk shared \u2713 — powering Chief intel at '+(track_name||'this track')); } if (is_full && S.cur && S.cur.id && typeof _recordTrackWalkComplete === 'function') { var walkEntry = _recordTrackWalkComplete(S.cur.id, track_slug, track_name, data, { walkKey: key, shared: true }); if (walkEntry && typeof _showTrackWalkRewardToast === 'function') _showTrackWalkRewardToast(walkEntry); } if (btn) { btn.textContent = 'SUBMITTED \u2713'; btn.style.opacity = '.5'; } } else { toast('Submit failed — try again'); if (btn) { btn.disabled = false; btn._submitted = false; btn.textContent = 'RETRY SUBMIT'; } } } catch(e) { toast('Connection error — walk not submitted'); if (btn) { btn.disabled = false; btn._submitted = false; btn.textContent = 'RETRY SUBMIT'; } } } // ── Chief for the night ───────────────────────────────────────────────────── function _grantNightChief(trackName) { var until = new Date(); until.setHours(23,59,59,999); localStorage.setItem('bb_night_chief', String(until.getTime())); var ov = document.createElement('div'); ov.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(13,12,11,.95);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px;'; ov.innerHTML = '
    First Full Walk Tonight
    ' + '
    CHIEF
    ' + '
    FOR THE NIGHT
    ' + '
    You mapped '+(trackName||'the track')+' first.
    All Chief features unlocked until midnight.
    ' + '
    HUNTER READING THE TRACK…
    '+''; ov.dataset.chiefOverlay='1'; document.body.appendChild(ov); _updateNightChiefBadge(); } function _updateNightChiefBadge() { var badge = document.getElementById('nc-badge'); if (!badge) return; var nc = parseInt(localStorage.getItem('bb_night_chief')||'0'); badge.style.display = (nc > Date.now()) ? 'inline' : 'none'; } // ── Chief aggregated intel panel ──────────────────────────────────────────── function _buildChiefWalkIntel(container, track) { var existing = container.querySelector('.bb-chief-intel'); if (existing) existing.remove(); var track_slug = track ? (track.slug || track.name.replace(/[^a-z0-9]/gi,'-').toLowerCase()) : null; if (!track_slug) return; var panel = document.createElement('div'); panel.className = 'bb-chief-intel'; panel.style.cssText = 'margin-bottom:12px;background:var(--dark2);border:1px solid rgba(200,150,10,.25);border-top:3px solid var(--gold);padding:14px;'; var hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;align-items:baseline;gap:8px;margin-bottom:10px;'; var title = document.createElement('div'); title.style.cssText = 'font-family:Barlow Condensed;font-size:16px;font-weight:900;letter-spacing:2px;color:var(--gold);'; title.textContent = 'TRACK INTEL'; var badge = document.createElement('div'); badge.style.cssText = 'font-family:Share Tech Mono;font-size:7px;letter-spacing:1px;color:var(--dark);background:var(--gold);padding:2px 6px;'; badge.textContent = 'CHIEF'; var sub = document.createElement('div'); sub.style.cssText = 'margin-left:auto;font-family:Share Tech Mono;font-size:8px;color:var(--muted);'; sub.textContent = 'Loading…'; hdr.appendChild(title); hdr.appendChild(badge); hdr.appendChild(sub); panel.appendChild(hdr); var body = document.createElement('div'); body.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--muted);letter-spacing:1px;'; body.textContent = 'Fetching crowdsourced data…'; panel.appendChild(body); container.insertBefore(panel, container.firstChild); _sbQuery('_tw_'+track_slug+'_%').then(function(rows){ var cutoff = new Date(Date.now() - 90*86400000).toISOString(); var walks = (rows||[]).map(function(r){try{return JSON.parse(r.html);}catch(e){return null;}}) .filter(function(w){return w && w.submitted_at >= cutoff && w.zones;}); if (!walks.length) { sub.textContent = 'No walks yet'; body.innerHTML = '
    No track walks submitted for '+(track.name||track_slug)+' yet.
    Be the first — your data powers Chief intel.
    '; return; } var fullWalks = walks.filter(function(w){return w.is_full;}); if (fullWalks.length < 3) { sub.textContent = fullWalks.length + '/3 full walks'; body.innerHTML = '
    Chief intel needs 3 independent full walks to verify.
    '+fullWalks.length+' of 3 confirmed at this track.
    '; return; } sub.textContent = walks.length + (walks.length === 1 ? ' walk' : ' walks') + ' · 90 days'; // Aggregate zones var zKeys = ['e1','e2','fs','bs']; var zones = {}; zKeys.forEach(function(z){ var grips = walks.map(function(w){return w.zones[z]&&w.zones[z].grip!=null?w.zones[z].grip:null;}).filter(function(v){return v!=null;}); var tags = walks.map(function(w){return w.zones[z]&&w.zones[z].dominant_tag;}).filter(Boolean); var tagCounts = {}; tags.forEach(function(t){tagCounts[t]=(tagCounts[t]||0)+1;}); var topTag = Object.entries(tagCounts).sort(function(a,b){return b[1]-a[1];})[0]; zones[z] = { grip_avg: grips.length ? Math.round(grips.reduce(function(a,b){return a+b;},0)/grips.length) : null, top_surface: topTag ? topTag[0] : null, n: grips.length }; }); // Aggregate banking var bpAgg = {}; walks.forEach(function(w){ var bp = w.zones && w.zones.banking_profiles; if (!bp) return; Object.entries(bp).forEach(function(e){ var turn=e[0], prof=e[1]; if (!bpAgg[turn]) bpAgg[turn]={}; bpAgg[turn][prof.type]=(bpAgg[turn][prof.type]||0)+1; }); }); var banking = {}; Object.entries(bpAgg).forEach(function(e){ var turn=e[0], counts=e[1]; banking[turn]=Object.entries(counts).sort(function(a,b){return b[1]-a[1];})[0][0]; }); // Render var zLabels = {e1:'END 1', e2:'END 2', fs:'FRONT STR', bs:'BACK STR'}; var grid = '
    '; zKeys.forEach(function(z){ var zd = zones[z]; var grip = zd && zd.grip_avg != null ? zd.grip_avg : '--'; var surf = zd && zd.top_surface ? zd.top_surface : '—'; var color = grip === '--' ? 'var(--dark4)' : grip >= 70 ? 'var(--green,#2DB87F)' : grip >= 50 ? 'var(--amber)' : 'var(--red-hot)'; grid += '
    ' + '
    '+zLabels[z]+'
    ' + '
    '+grip+'
    ' + '
    GRIP IDX · '+surf+'
    ' + '
    '; }); grid += '
    '; if (Object.keys(banking).length) { grid += '
    '; Object.entries(banking).forEach(function(e){ grid += ''+e[0].toUpperCase()+': '+e[1]+''; }); grid += '
    '; } body.innerHTML = grid; }).catch(function(){ sub.textContent = 'Offline'; body.textContent = 'Could not load intel.'; }); } // Pre-computation — runs client-side, zero tokens function _walkGripIndex(psi) { if (!psi || psi <= 0) return null; if (psi < 90) return Math.round((psi / 90) * 62); if (psi < 130) return Math.round(62 + ((psi-90)/40) * 16); if (psi < 200) return Math.round(78 - ((psi-130)/70) * 24); return Math.round(Math.max(10, 54 - ((psi-200)/100) * 28)); } function _walkZoneSummary(data) { function zoneStats(ids) { var psiVals = [], gripVals = [], tags = []; ids.forEach(function(id) { var p = data.points && data.points[id]; if (!p) return; if (p.penetrometer) { psiVals.push(p.penetrometer); gripVals.push(_walkGripIndex(p.penetrometer)); } if (p.surface) tags = tags.concat(p.surface); }); var avgGrip = gripVals.length ? Math.round(gripVals.reduce(function(a,b){return a+b;},0)/gripVals.length) : null; var tagCounts = {}; tags.forEach(function(t){tagCounts[t]=(tagCounts[t]||0)+1;}); var topTag = Object.keys(tagCounts).sort(function(a,b){return tagCounts[b]-tagCounts[a];})[0] || null; return {grip:avgGrip, dominant_tag:topTag, n:psiVals.length}; } var e1pts = ['e1_t1_en','e1_t1_hi','e1_t1_ap','e1_t1_lo','e1_chute','e1_t2_hi','e1_t2_ap','e1_t2_lo','e1_t2_ex']; var e2pts = ['e2_t3_en','e2_t3_hi','e2_t3_ap','e2_t3_lo','e2_chute','e2_t4_hi','e2_t4_ap','e2_t4_lo','e2_t4_ex']; var fsPts = ['fs_hi','fs_lo'], bsPts = ['bs_hi','bs_lo']; var apexBanking = {}; ['e1_t1_ap','e1_t2_ap','e2_t3_ap','e2_t4_ap'].forEach(function(id){ var p = data.points && data.points[id]; if (p && p.banking) apexBanking[id] = p.banking; }); return { e1: zoneStats(e1pts), e2: zoneStats(e2pts), fs: zoneStats(fsPts), bs: zoneStats(bsPts), banking: apexBanking, track: data.track || null, car: data.car || null, total_points: _walkCount(data) }; } // ── Walk Scan Banking System ─────────────────────────────────────────────── // Phone-as-body-tilt inclinometer. User stands on the track, phone held // naturally at chest/waist. Body conforms to slope. SET ZERO calibrates // out natural body lean. No mud contact required. function _walkBankField(ptId, currentVal, data, key, dot, summary) { var wrap = document.createElement('div'); wrap.style.cssText = 'margin-top:12px;'; var lbl = document.createElement('div'); lbl.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);letter-spacing:1px;margin-bottom:7px;display:flex;align-items:center;justify-content:space-between;'; var lblLeft = document.createElement('span'); lblLeft.textContent = 'BANKING (degrees)'; var lblRight = document.createElement('span'); lblRight.style.cssText = 'color:var(--amber);font-size:7px;cursor:pointer;'; lblRight.textContent = 'WHAT IS WALK SCAN?'; lblRight.onclick = function() { toast('Stand on the track. Tap SET ZERO. Walk to your spot. Tap CAPTURE.'); }; lbl.appendChild(lblLeft); lbl.appendChild(lblRight); // Input row: number field + SCAN button var row = document.createElement('div'); row.style.cssText = 'display:flex;gap:8px;align-items:flex-start;'; var inp = document.createElement('input'); inp.type = 'number'; inp.inputMode = 'decimal'; inp.placeholder = '—'; inp.value = currentVal || ''; inp.style.cssText = 'width:100px;background:var(--dark);border:1px solid var(--dark4);color:var(--white);font-family:Barlow Condensed;font-size:28px;font-weight:700;padding:10px;text-align:center;outline:none;'; inp.onchange = function() { var v = parseFloat(inp.value); if (!isNaN(v) && v >= 0) { var d2 = _walkLoad(key); if (!d2.points) d2.points = {}; if (!d2.points[ptId]) d2.points[ptId] = {}; d2.points[ptId].banking = v; _walkSave(key, d2); data.points = d2.points; _walkMarkFilled(ptId, d2, dot, summary); _walkRefreshProgress(d2, key); } }; var scanBtn = document.createElement('button'); scanBtn.style.cssText = 'background:var(--dark);border:1px solid rgba(245,166,35,.35);color:var(--amber);font-family:Share Tech Mono;font-size:9px;font-weight:700;letter-spacing:1px;padding:0 14px;height:52px;cursor:pointer;flex-shrink:0;white-space:nowrap;'; scanBtn.textContent = 'WALK SCAN'; row.appendChild(inp); row.appendChild(scanBtn); wrap.appendChild(lbl); wrap.appendChild(row); // ── Live scanner panel (hidden until activated) ────────────────────────── var panel = document.createElement('div'); panel.style.cssText = 'display:none;margin-top:8px;background:var(--dark);border:1px solid rgba(245,166,35,.3);border-top:2px solid var(--amber);padding:12px;'; // SVG arc gauge var svg = document.createElementNS('http://www.w3.org/2000/svg','svg'); svg.setAttribute('width','140'); svg.setAttribute('height','80'); svg.setAttribute('viewBox','0 0 140 80'); svg.style.cssText = 'display:block;margin:0 auto 8px;'; // Track arc (background) var arcBg = document.createElementNS('http://www.w3.org/2000/svg','path'); arcBg.setAttribute('d','M 10 75 A 60 60 0 0 1 130 75'); arcBg.setAttribute('stroke','rgba(255,255,255,.1)'); arcBg.setAttribute('stroke-width','3'); arcBg.setAttribute('fill','none'); svg.appendChild(arcBg); // Active arc var arcActive = document.createElementNS('http://www.w3.org/2000/svg','path'); arcActive.setAttribute('d','M 70 75 A 0 0 0 0 1 70 75'); arcActive.setAttribute('stroke','var(--amber)'); arcActive.setAttribute('stroke-width','3'); arcActive.setAttribute('fill','none'); arcActive.setAttribute('data-arc','1'); svg.appendChild(arcActive); // Needle var needle = document.createElementNS('http://www.w3.org/2000/svg','line'); needle.setAttribute('x1','70'); needle.setAttribute('y1','75'); needle.setAttribute('x2','70'); needle.setAttribute('y2','22'); needle.setAttribute('stroke','var(--amber)'); needle.setAttribute('stroke-width','2'); needle.setAttribute('stroke-linecap','round'); needle.setAttribute('data-needle','1'); svg.appendChild(needle); // Center dot var cdot = document.createElementNS('http://www.w3.org/2000/svg','circle'); cdot.setAttribute('cx','70'); cdot.setAttribute('cy','75'); cdot.setAttribute('r','5'); cdot.setAttribute('fill','var(--amber)'); svg.appendChild(cdot); // Degree marks 0/10/20/30° [0, 15, 30, 45].forEach(function(deg) { var rad = (deg / 60) * Math.PI; var tx = 70 + 55 * Math.sin(rad); var ty = 75 - 55 * Math.cos(rad); var t2 = document.createElementNS('http://www.w3.org/2000/svg','text'); t2.setAttribute('x', tx.toFixed(0)); t2.setAttribute('y', (ty + 4).toFixed(0)); t2.setAttribute('fill','rgba(255,255,255,.3)'); t2.setAttribute('font-size','7'); t2.setAttribute('text-anchor','middle'); t2.setAttribute('font-family','monospace'); t2.textContent = deg + '°'; svg.appendChild(t2); }); panel.appendChild(svg); // Big live readout var liveNum = document.createElement('div'); liveNum.style.cssText = 'font-family:Barlow Condensed;font-size:44px;font-weight:900;color:var(--amber);text-align:center;line-height:1;margin-bottom:4px;'; liveNum.textContent = '--'; panel.appendChild(liveNum); var liveHint = document.createElement('div'); liveHint.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);text-align:center;margin-bottom:10px;'; liveHint.textContent = 'Lay phone FLAT on track surface • tap CAPTURE when stable'; panel.appendChild(liveHint); // Control buttons var ctrlRow = document.createElement('div'); ctrlRow.style.cssText = 'display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;'; var zeroBtn = document.createElement('button'); zeroBtn.style.cssText = 'background:none;border:1px solid var(--dark4);color:var(--muted);font-family:Share Tech Mono;font-size:9px;padding:10px 4px;cursor:pointer;letter-spacing:1px;'; zeroBtn.textContent = 'RESET'; var captureBtn = document.createElement('button'); captureBtn.style.cssText = 'background:rgba(245,166,35,.15);border:1px solid rgba(245,166,35,.4);color:var(--amber);font-family:Share Tech Mono;font-size:10px;font-weight:700;padding:10px 4px;cursor:pointer;letter-spacing:1px;'; captureBtn.textContent = 'CAPTURE'; var stopBtn = document.createElement('button'); stopBtn.style.cssText = 'background:none;border:1px solid rgba(208,25,14,.3);color:var(--red);font-family:Share Tech Mono;font-size:9px;padding:10px 4px;cursor:pointer;letter-spacing:1px;'; stopBtn.textContent = 'DONE'; ctrlRow.appendChild(zeroBtn); ctrlRow.appendChild(captureBtn); ctrlRow.appendChild(stopBtn); panel.appendChild(ctrlRow); // Zero status var zeroStatus = document.createElement('div'); zeroStatus.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--muted);text-align:center;margin-top:5px;'; zeroStatus.textContent = 'Place phone flat on track surface — reading stabilizes in 1–2 sec'; panel.appendChild(zeroStatus); wrap.appendChild(panel); // ── Scanner logic — flat-on-surface accelerometer inclinometer ─────────── // Phone lies flat ON the track. Gravity vector angle from vertical = slope angle. // atan2(sqrt(ax^2+ay^2), |az|) is orientation-agnostic: works in any rotation. var motionHandler = null; var readings = []; // rolling buffer, last 20 samples var stableTimer = null; function _computeAngle(e) { var g = e.accelerationIncludingGravity; if (!g) return null; var ax = g.x || 0, ay = g.y || 0, az = g.z || 0; var horiz = Math.sqrt(ax * ax + ay * ay); var angle = Math.atan2(horiz, Math.abs(az)) * 180 / Math.PI; return Math.round(angle * 10) / 10; } function _isStable(buf) { if (buf.length < 10) return false; var mn = buf[0], mx = buf[0]; for (var i = 1; i < buf.length; i++) { if (buf[i] < mn) mn = buf[i]; if (buf[i] > mx) mx = buf[i]; } return (mx - mn) < 0.6; } function updateDisplay(angle) { if (angle === null) return; readings.push(angle); if (readings.length > 20) readings.shift(); var avg = readings.reduce(function(a, b) { return a + b; }, 0) / readings.length; avg = Math.round(avg * 10) / 10; liveNum.textContent = avg.toFixed(1) + '°'; liveNum.style.color = avg < 8 ? 'var(--muted)' : avg < 16 ? 'var(--amber)' : 'var(--red-hot)'; _walkUpdateNeedle(needle, arcActive, avg); // Stability indicator if (_isStable(readings)) { zeroStatus.textContent = '● STABLE — tap CAPTURE'; zeroStatus.style.color = '#4CAF50'; captureBtn.style.borderColor = '#4CAF50'; } else { zeroStatus.textContent = 'hold still… (' + readings.length + '/10 samples)'; zeroStatus.style.color = 'var(--muted)'; captureBtn.style.borderColor = 'rgba(245,166,35,.4)'; } } function startSensor() { readings = []; motionHandler = function(e) { updateDisplay(_computeAngle(e)); }; window.addEventListener('devicemotion', motionHandler, true); panel.style.display = 'block'; scanBtn.style.background = 'rgba(245,166,35,.15)'; scanBtn.textContent = 'SCANNING…'; } function stopSensor() { if (motionHandler) { window.removeEventListener('devicemotion', motionHandler, true); motionHandler = null; } panel.style.display = 'none'; scanBtn.style.background = 'var(--dark)'; scanBtn.textContent = 'WALK SCAN'; readings = []; captureBtn.style.borderColor = 'rgba(245,166,35,.4)'; } scanBtn.onclick = function() { if (motionHandler) { stopSensor(); return; } if (!window.DeviceMotionEvent) { toast('Motion sensor not available — enter degrees manually'); return; } if (typeof DeviceMotionEvent.requestPermission === 'function') { DeviceMotionEvent.requestPermission() .then(function(r) { r === 'granted' ? startSensor() : toast('Allow motion in iOS Settings → Safari → Motion & Orientation'); }) .catch(function() { toast('Could not request motion permission'); }); } else { startSensor(); } }; // Zero btn repurposed: recalibrate (clear buffer, restart averaging) zeroBtn.onclick = function() { readings = []; zeroStatus.textContent = 'buffer cleared — hold phone flat and still'; zeroStatus.style.color = 'var(--amber)'; }; captureBtn.onclick = function() { var val = parseFloat(liveNum.textContent); if (isNaN(val) || val < 0) { toast('No reading yet — place phone flat on track first'); return; } inp.value = val.toFixed(1); inp.dispatchEvent(new Event('change')); captureBtn.style.background = 'rgba(76,175,80,.15)'; captureBtn.style.color = '#4CAF50'; captureBtn.textContent = '✔ ' + val.toFixed(1) + '°'; setTimeout(function() { captureBtn.style.background = 'rgba(245,166,35,.15)'; captureBtn.style.color = 'var(--amber)'; captureBtn.textContent = 'CAPTURE'; }, 1800); _walkCheckProgressive(ptId, val, data, key); }; stopBtn.onclick = function() { stopSensor(); }; return wrap; } // Update SVG needle and arc fill function _walkUpdateNeedle(needle, arcEl, degrees) { var clamped = Math.min(degrees, 55); var rad = (clamped / 60) * Math.PI; // 60 degrees = full sweep var nx = 70 + 48 * Math.sin(rad); var ny = 75 - 48 * Math.cos(rad); needle.setAttribute('x2', nx.toFixed(2)); needle.setAttribute('y2', ny.toFixed(2)); // Active arc from 0 to current angle if (clamped > 0.5) { var arcRad = (clamped / 60) * Math.PI; var ex = 70 + 60 * Math.sin(arcRad); var ey = 75 - 60 * Math.cos(arcRad); var large = clamped > 30 ? 1 : 0; arcEl.setAttribute('d', 'M 10 75 A 60 60 0 ' + large + ' 1 ' + ex.toFixed(2) + ' ' + ey.toFixed(2)); } else { arcEl.setAttribute('d', 'M 10 75 A 0 0 0 0 1 10 75'); } } // After a capture, detect progressive banking across the turn function _walkCheckProgressive(ptId, newAngle, data, key) { // Determine which turn this point belongs to var turnMap = { 'e1_t1_en':'t1','e1_t1_hi':'t1','e1_t1_ap':'t1','e1_t1_lo':'t1', 'e1_t2_hi':'t2','e1_t2_ap':'t2','e1_t2_lo':'t2','e1_t2_ex':'t2', 'e2_t3_en':'t3','e2_t3_hi':'t3','e2_t3_ap':'t3','e2_t3_lo':'t3', 'e2_t4_hi':'t4','e2_t4_ap':'t4','e2_t4_lo':'t4','e2_t4_ex':'t4' }; var turn = turnMap[ptId]; if (!turn) return; // Gather all banking readings for this turn var turnPoints = Object.keys(turnMap).filter(function(k){return turnMap[k]===turn;}); var vals = []; turnPoints.forEach(function(id) { var v = id === ptId ? newAngle : (data.points && data.points[id] && data.points[id].banking); if (v && !isNaN(v)) vals.push({id:id, v:parseFloat(v)}); }); if (vals.length < 3) return; // Need at least 3 points for a profile var result = _detectProgressiveBanking(vals); if (!result) return; // Store on data if (!data.banking_profiles) data.banking_profiles = {}; data.banking_profiles[turn] = result; _walkSave(key, data); // Show toast with insight var msgs = { 'uniform': 'T' + turn.slice(1) + ': Uniform banking (±2°) — consistent load through corner', 'progressive':'T' + turn.slice(1) + ': Progressive banking — load builds entry to exit, Hunter notified', 'peaked': 'T' + turn.slice(1) + ': Peaked mid-corner — aggressive load at apex, light entry/exit', 'feathered': 'T' + turn.slice(1) + ': Feathered banking — steepest at entry, flattens out', 'variable': 'T' + turn.slice(1) + ': Variable banking — unpredictable load, be precise with line' }; var msg = msgs[result.type] || ('T' + turn.slice(1) + ': ' + result.type); toast(msg); } // Classify banking profile from ordered low-to-high measurements function _detectProgressiveBanking(vals) { if (vals.length < 2) return null; var angles = vals.map(function(x){return x.v;}); var min = Math.min.apply(null, angles); var max = Math.max.apply(null, angles); var spread = max - min; var mid = angles[Math.floor(angles.length / 2)]; if (spread <= 2) return {type:'uniform', spread:spread, min:min, max:max}; // Peaked: middle higher than both ends if (angles.length >= 3 && mid > angles[0] + 2 && mid > angles[angles.length-1] + 2) { return {type:'peaked', spread:spread, min:min, max:max, peak:mid}; } // Feathered: starts steep, drops if (angles[0] > angles[angles.length-1] + 3) { return {type:'feathered', spread:spread, min:min, max:max}; } // Progressive: generally increasing low to high var increasing = angles.filter(function(v,i){return i===0||v>=angles[i-1]-1;}).length >= angles.length - 1; if (increasing && spread > 2) return {type:'progressive', spread:spread, min:min, max:max}; return {type:'variable', spread:spread, min:min, max:max}; } // Updated zone summary includes progressive banking profiles // Replaces the existing _walkZoneSummary — patched version appends profile + soil data var _walkZoneSummaryOrig = _walkZoneSummary; _walkZoneSummary = function(data) { var base = _walkZoneSummaryOrig(data); base.banking_profiles = data.banking_profiles || {}; // Build per-turn readable summary for Hunter base.banking_narrative = {}; var turnLabels = {t1:'T1',t2:'T2',t3:'T3',t4:'T4'}; Object.keys(base.banking_profiles).forEach(function(turn) { var p = base.banking_profiles[turn]; base.banking_narrative[turnLabels[turn]] = p.type + ' ' + p.min.toFixed(0) + '-' + p.max.toFixed(0) + '°'; }); // Soil composition — track-level fingerprint var tSlug = S.curTrack ? _trackSlug(S.curTrack.name) : null; var trackSoil = tSlug ? _soilLoad(tSlug) : null; if (trackSoil) { base.soil_fingerprint = { type: trackSoil.soil_type, clay: trackSoil.clay_content, binding: trackSoil.binding_character, moisture_depth: trackSoil.moisture_depth, transition_rate: trackSoil.transition_rate, night_arc: trackSoil.night_arc, setup_note: trackSoil.setup_note }; } // Collect per-point soil overrides var pointSoils = []; if (data.points) { Object.keys(data.points).forEach(function(ptId) { var pt = data.points[ptId]; if (pt && pt.soil) pointSoils.push({point: ptId, soil: pt.soil.soil_type, arc: pt.soil.night_arc}); }); } if (pointSoils.length) base.soil_overrides = pointSoils; return base; }; // ── Track-to-track setup comparison ─────────────────────────────────────── function _buildTrackCompare(el) { var existing = el.querySelector('.bb-track-compare'); if (existing) existing.remove(); if (!S._setupEntries || S._setupEntries.length < 2) return; var currentTrack = S.curTrack ? S.curTrack.name : null; var trackMap = {}; S._setupEntries.forEach(function(entry) { var tn = entry.data && entry.data._track; if (tn && tn !== currentTrack) { if (!trackMap[tn]) trackMap[tn] = entry.data; // first (most recent) per track } }); var tracks = Object.keys(trackMap); if (!tracks.length) return; var sec = document.createElement('div'); sec.className = 'bb-track-compare'; sec.style.cssText = 'margin-top:10px;padding:12px;background:var(--dark2);border:1px solid var(--dark4);border-left:2px solid rgba(200,150,10,.3);'; var hdrRow = document.createElement('div'); hdrRow.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:8px;'; var hdrLbl = document.createElement('span'); hdrLbl.style.cssText = 'font-family:Barlow Condensed;font-size:12px;font-weight:900;letter-spacing:2px;color:var(--muted);flex-shrink:0;'; hdrLbl.textContent = 'COMPARE TO'; var sel = document.createElement('select'); sel.style.cssText = 'flex:1;background:var(--dark);border:1px solid var(--dark4);color:var(--white);font-family:Share Tech Mono;font-size:10px;padding:6px 8px;outline:none;min-height:36px;'; var def = document.createElement('option'); def.value = ''; def.textContent = 'Select another track…'; sel.appendChild(def); tracks.forEach(function(tn) { var opt = document.createElement('option'); opt.value = tn; opt.textContent = tn; sel.appendChild(opt); }); hdrRow.appendChild(hdrLbl); hdrRow.appendChild(sel); sec.appendChild(hdrRow); var diffPanel = document.createElement('div'); sec.appendChild(diffPanel); sel.onchange = function() { diffPanel.innerHTML = ''; var tn = sel.value; if (!tn) return; var other = trackMap[tn]; if (!other) return; var FIELDS = [ {k:'psi_lf', l:'LF PSI'}, {k:'psi_rf', l:'RF PSI'}, {k:'psi_lr', l:'LR PSI'}, {k:'psi_rr', l:'RR PSI'}, {k:'stagger_r', l:'R STAG'}, {k:'stagger_f', l:'F STAG'}, {k:'bite', l:'BITE'}, {k:'panhard', l:'PANHARD'}, {k:'bar_lr', l:'LR BAR'}, {k:'bar_rr', l:'RR BAR'}, {k:'nose_wing', l:'NOSE WNG'}, {k:'j_ladder', l:'J-LADR'}, {k:'sway_bar', l:'SWAY'}, {k:'brake_bias',l:'BRAKE'}, ]; var diffs = []; FIELDS.forEach(function(f) { var cur = _su[f.k], oth = other[f.k]; if (cur === undefined || cur === null || oth === undefined || oth === null) return; var dc = parseFloat(cur), do2 = parseFloat(oth); if (!isNaN(dc) && !isNaN(do2)) { var delta = dc - do2; if (Math.abs(delta) < 0.01) return; diffs.push({l:f.l, cur:cur, oth:oth, delta:delta, big:Math.abs(delta) > 2}); } else { if (String(cur) === String(oth)) return; diffs.push({l:f.l, cur:cur, oth:oth, delta:null, big:true}); } }); if (!diffs.length) { var same = document.createElement('div'); same.style.cssText = 'font-family:Share Tech Mono;font-size:9px;color:var(--muted);padding:6px 0;'; same.textContent = 'Setups match — nothing different between these two tracks'; diffPanel.appendChild(same); return; } // Column headers var curLabel = (currentTrack || 'NOW').substring(0,14).toUpperCase(); var othLabel = tn.substring(0,14).toUpperCase(); var grid = document.createElement('div'); grid.style.cssText = 'display:grid;grid-template-columns:72px 1fr 1fr 44px;gap:3px 8px;'; ['',' '+curLabel,' '+othLabel,' Δ'].forEach(function(txt) { var hd = document.createElement('div'); hd.style.cssText = 'font-family:Share Tech Mono;font-size:7px;color:var(--dark4);padding-bottom:4px;border-bottom:1px solid rgba(255,255,255,.05);letter-spacing:1px;overflow:hidden;white-space:nowrap;'; hd.textContent = txt; grid.appendChild(hd); }); diffs.forEach(function(r) { var dColor = r.delta === null ? 'var(--amber)' : r.big ? 'var(--red-hot)' : 'var(--amber)'; var deltaStr = r.delta === null ? '≠' : (r.delta > 0 ? '+' : '') + (Math.abs(r.delta) < 10 ? r.delta.toFixed(1) : Math.round(r.delta)); var lc = document.createElement('div'); lc.style.cssText = 'font-family:Share Tech Mono;font-size:8px;color:var(--muted);display:flex;align-items:center;'; lc.textContent = r.l; var cc = document.createElement('div'); cc.style.cssText = 'font-family:Barlow Condensed;font-size:15px;font-weight:700;color:var(--white);'; cc.textContent = r.cur; var oc = document.createElement('div'); oc.style.cssText = 'font-family:Barlow Condensed;font-size:15px;font-weight:700;color:rgba(255,255,255,.4);'; oc.textContent = r.oth; var dc = document.createElement('div'); dc.style.cssText = 'font-family:Barlow Condensed;font-size:15px;font-weight:900;color:' + dColor + ';'; dc.textContent = deltaStr; grid.appendChild(lc); grid.appendChild(cc); grid.appendChild(oc); grid.appendChild(dc); }); diffPanel.appendChild(grid); // Ask Hunter link if (diffs.length >= 2) { var hunterLink = document.createElement('button'); hunterLink.style.cssText = 'margin-top:8px;background:none;border:none;color:var(--red);font-family:Share Tech Mono;font-size:8px;letter-spacing:1px;cursor:pointer;padding:0;'; hunterLink.textContent = '▶ ASK HUNTER WHY THESE TRACKS DIFFER'; hunterLink.onclick = function() { var parts = ['Compare my setup at ' + (currentTrack||'current track') + ' vs ' + tn + '.']; diffs.slice(0,5).forEach(function(r){ parts.push(r.l + ': ' + r.cur + ' vs ' + r.oth + (r.delta !== null ? ' (delta ' + (r.delta>0?'+':'') + r.delta.toFixed(1) + ')' : '')); }); parts.push('Why might these tracks need different setups?'); if (typeof openChat === 'function') openChat(); setTimeout(function(){ sendToHunter(parts.join(' ')); }, 200); }; diffPanel.appendChild(hunterLink); } }; el.appendChild(sec); }