-- wx
#--
Track Detected
Tap to start race night
PLAY TRACK MEMORY: Draw your line on the real aerial for this track (micros + midgets tonight) →
TEMP
HUMID
DEW PT
DENSITY ALT
WIND
RACE NIGHT LOADING...
YOU'RE READY TO RACE
2 MINUTES. THAT'S ENOUGH.
Hunter already has your class and car. Deep geometry (PIVOT, pickups, shocks) is optional — add it later when you want sharper recs.
Step 1 of 3 — Division
WELCOME TO BLACK BOOK
What do you race? Hunter will tune your setup sheet to your exact class.
Step 2 of 3 — Chassis
Or
Step 3 of 7 — Front Suspension
Some chassis ship torsion and can be converted to coil springs. This changes your setup sheet.
Step 3 of 3 — Weight (optional engine next)
Step 5 of 7 — Engine
Step 9 of 10 — Geometry
J-ladder, panhard height, torsion bar, birdcage. If nothing changed, tap Skip.
What Makes This Car Different
Stiffness, frame style (270 mid-rail vs downtube), quirks. Skip if stock / unknown.
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
SET
FEET
--
-2k
8k
0
HUMID
--
%
DEW PT
--
°F
TRACK EST
SET WX
-- 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
RACE DATA TOOLS
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; function _mrpClean(raw) { return String(raw || '') .replace(//gi, '') .replace(//gi, '') .replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/©/g,'©'); } var cleaned = ''; try { var pr = await fetch('/mrp-proxy?type=url&track_id=' + encodeURIComponent(url)); if (pr.ok) cleaned = _mrpClean(await pr.text()); } catch(e) {} if (!cleaned || cleaned.length < 200) { try { var r = await fetch(MRP_PROXY + encodeURIComponent(url)); var j = await r.json(); cleaned = _mrpClean(j.contents || ''); } catch(e) { cleaned = ''; } } if (cleaned) _mrpCache[url] = { text: cleaned, ts: Date.now() }; return cleaned; } // ── 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 && typeof mrpFindNearbyEvent === 'function' && S.curTrack) { try { await mrpFindNearbyEvent(); } catch(e) {} 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 from first class with posted lineups var refClass = races.classes.find(function(c) { return !c.isPending; }); if (refClass) { var mh = refClass.heats || 2; var md = refClass.hasDash ? 1 : 0; var mc = refClass.hasConsi ? 1 : 0; if (typeof window._bbFmtGateApply === 'function') window._bbFmtGateApply(mh, md, mc, 'mrp'); else { var h = document.getElementById('fmt-heats'); var d = document.getElementById('fmt-dash'); var c = document.getElementById('fmt-consi'); if (h) h.value = mh; if (d) d.value = md; if (c) c.value = mc; } var fillBtn = document.createElement('button'); fillBtn.style.cssText = 'font-family:var(--mono);font-size:9px;color:var(--green);background:none;border:1px solid rgba(45,184,127,.25);padding:4px 12px;cursor:pointer;letter-spacing:1px;margin-bottom:8px'; fillBtn.textContent = '\u2713 MRP FORMAT APPLIED \u2014 TAP TO RE-APPLY'; fillBtn.onclick = function() { if (typeof window._bbFmtGateApply === 'function') window._bbFmtGateApply(mh, md, mc, 'mrp'); else { var h2 = document.getElementById('fmt-heats'); var d2 = document.getElementById('fmt-dash'); var c2 = document.getElementById('fmt-consi'); if (h2) h2.value = mh; if (d2) d2.value = md; if (c2) c2.value = mc; } toast('Format re-applied from MyRacePass'); }; box.appendChild(fillBtn); var lockMrp = document.createElement('button'); lockMrp.type = 'button'; lockMrp.style.cssText = 'width:100%;margin-bottom:10px;padding:12px;background:rgba(208,25,14,.15);border:1px solid rgba(208,25,14,.4);color:var(--white);font-family:Barlow Condensed;font-size:14px;font-weight:900;letter-spacing:1px;cursor:pointer;'; lockMrp.textContent = 'LOCK MRP FORMAT & START HOT LAPS \u25BA'; lockMrp.onclick = function() { if (typeof window._bbFmtGateApply === 'function') window._bbFmtGateApply(mh, md, mc, 'mrp'); var lockBtn = document.querySelector('.rn-format button.fg'); if (lockBtn) lockBtn.click(); else toast('Format applied \u2014 tap Lock Format above'); }; box.appendChild(lockMrp); } // 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 (!_isKartFamilyAlgo()) 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 (!_isKartFamilyAlgo()) 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 (!_isKartFamilyAlgo()) 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() { /* Youth-aware: outlawkart / flatkart / micro must not collapse to generic kart */ if (typeof S !== 'undefined' && S.cur && typeof _getCarType === 'function') { var gt = _getCarType(S.cur); if (gt === 'outlawkart' || gt === 'flatkart') return gt; if (gt === 'micro' || gt === 'micro270' || gt === 'microturf') return gt === 'microturf' ? 'micro' : gt; if (gt === 'kart' || gt === 'quartermidget') return 'kart'; if (gt === 'jrsprint') return 'jrsprint'; if (gt === 'sprint' || gt === 'lightningsprint') return 'sprint'; if (gt === 'latemodel') return 'latemodel'; if (gt === 'modified' || gt === 'modlite') return 'modified'; if (gt === 'stock') return 'stock'; } var cls = (S.cur && (S.cur.class || S.cur.car_class || S.cur.name || S.cur.class_name || '')) || ''; var ch = (S.cur && S.cur.chassis_name || '') + ''; var lc = (cls + ' ' + ch).toLowerCase(); if (/outlaw|minecon|toigo|cage kart|young gun/i.test(lc)) return 'outlawkart'; if (/flat kart|lo.?206|pro clone|qrc|vector/i.test(lc) && !/outlaw|micro/i.test(lc)) return 'flatkart'; if (/micro|600|now600|restricted.micro|hyper/i.test(lc)) return 'micro'; if (/kart|clone|quarter.mid|cadet|kid kart/i.test(lc)) return 'kart'; 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 _isYouthAlgoType(t) { t = t || _algoCarType(); return t === 'outlawkart' || t === 'flatkart' || t === 'micro' || t === 'micro270' || t === 'kart' || t === 'jrsprint'; } function _isKartFamilyAlgo() { var t = _algoCarType(); return t === 'kart' || t === 'flatkart' || t === 'outlawkart'; } 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) { var _stCt = _algoCarType(); if (_stCt === 'flatkart' || _stCt === 'kart') { var rw = (S.cur && (S.cur.rear_track_width_mm || S.cur.track_width)) || (_su && _su.rear_track_width_mm) || 44; var ax = (S.cur && S.cur.axle_stiffness) || (_su && _su.axle_stiffness) || 'medium'; var flatNote = _stCt === 'flatkart' ? 'Vector DO / LO206: seat position first, then rear track mm. Front stagger typically 1/2\" max 1-1/2\". PSI dirt start 6/8/6/8 (Carlson). Left ~57% · cross 53-54%.' : 'Widen rear 2-5mm to free the kart. Narrow 2-5mm to tighten. LO206 left side 56-62% — move seat before adding lead.'; return { algo: true, answer: _stCt === 'flatkart' ? 'FLAT KART: seat + rear track width (not big-car stagger)' : 'KART ROTATION: rear track width, not inch stagger', detail: 'Rear width ' + rw + ' · Axle: ' + ax, action: flatNote, confidence: 'HIGH', source: _stCt === 'flatkart' ? '/chassis/vector' : null }; } if (_stCt === 'outlawkart') { var oTgt = _resolveStaggerTarget({ track: (ctx && ctx.track) || S.curTrack, carType: 'outlawkart' }); var oRec = (oTgt && oTgt.rec != null) ? oTgt.rec : 1.25; var oR = (oTgt && oTgt.rear) ? (oTgt.rear[0] + '-' + oTgt.rear[1] + '"') : '0.75-2"'; var curO = S.cur && (S.cur.stagger || S.cur.stagger_r); return { algo: true, answer: 'OUTLAW KART stagger target ~' + oRec + '" (class window ' + oR + ')', detail: 'Phantom MINecon Box Stock teaches scale first (nose 45 / left 56.5 / cross 56.5), then stagger. Current: ' + (curO != null ? curO + '"' : 'not set'), action: '1. Scale to sheet before chasing stagger.\n2. Box stock / youth: 0.75-1.5". Open outlaw 1-2.5".\n3. More rear stagger frees mid; less plants RR drive-off.\n4. Re-check after tire heat — youth tires move fast.\nSource: /chassis/phantom', confidence: 'HIGH', source: '/chassis/phantom' }; } if (_stCt === 'micro' || _stCt === 'micro270') { var mTgt = _resolveStaggerTarget({ track: (ctx && ctx.track) || S.curTrack, carType: _stCt }); var mRec = (mTgt && mTgt.rec != null) ? mTgt.rec : 5; var mR = (mTgt && mTgt.rear) ? (mTgt.rear[0] + '-' + mTgt.rear[1] + '"') : '4-6"'; return { algo: true, answer: 'MICRO stagger ~' + mRec + '" (window ' + mR + ')', detail: 'Hyper: measure RR circ − LR circ. Prestretch new tires 20 psi then race pressure. 2007+ Hyper WB 62-63".', action: '1. Square rear axle within 1/32" before trusting stagger.\n2. Winged 600 mid-window 4-6".\n3. After hot stint, re-measure — then wing 0.25° steps.\n4. Source: /chassis/hyper Hyper Basic Micro Sprint Setup.', confidence: 'HIGH', source: '/chassis/hyper' }; } 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.'; } // Phase 2: use historical memory avg if present var mem = (typeof _getTrackMemorySummary === 'function' && S.curTrack) ? _getTrackMemorySummary(S.curTrack.name) : null; var histAdj = ''; var rec = tgt.rec; if (mem && mem.avgStagger) { var h = parseFloat(mem.avgStagger); if (h && Math.abs(h - rec) > 0.3) { rec = ((h + rec) / 2).toFixed(1); histAdj = ' (your history avg ' + mem.avgStagger + '" at this track)'; } } return { algo: true, answer: 'STAGGER: ' + rec + '" tonight (class ' + tgt.rear[0] + '–' + tgt.rear[1] + '")' + histAdj, detail: tgt.notes.join(' · '), action: 'Measure cold in pits. Class target ' + rec + '" hot (RR−LR). Source: ' + (tgt.source === 'bb-classes' ? tgt.className + ' bb-classes' : 'matrix fallback') + (tgt.anchor ? ' · See ' + tgt.anchor.mfr + ' setup sheet.' : '') + wingWind + (mem && mem.avgStagger ? '\nHistorical at this track: avg ' + mem.avgStagger + '" from ' + (mem.visits||0) + ' visits' : ''), 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(); // Walk + vision context for tight diagnosis (grip affects fix priority) var walkNote = ''; if (S && S._walkSummary) { var ws = S._walkSummary; var g1 = ws.e1 && ws.e1.grip, g2 = ws.e2 && ws.e2.grip; if (g1 != null && g2 != null) { var avgG = Math.round((g1 + g2) / 2); if (avgG < 50) walkNote = ' (low grip ' + avgG + ' from walk — prioritize contact patch/pressure)'; } } else { try { var vKey = 'bb_surface_' + ((S.curTrack && (S.curTrack.id || S.curTrack.name) || 'track') + '').replace(/\W/g, '_'); var vs = JSON.parse(localStorage.getItem(vKey) || '{}'); if (vs.last_cam_scan && vs.last_cam_scan.analysis) walkNote = ' (vision surface logged)'; } catch(e) {} } // 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(' / ')+walkNote,detail:kE,action:kF.slice(0,3).map(function(f,i){return(i+1)+'. '+f.change+' — '+f.why;}).join('\n')+sn+walkNote,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(' / ')+walkNote,detail:sE+' Check class rules.',action:sF.slice(0,3).map(function(f,i){return(i+1)+'. '+f.change+' — '+f.why;}).join('\n')+walkNote,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(' / ') + walkNote, detail: explain, action: fixes.slice(0,3).map((f,i)=>(i+1)+'. '+f.change+' — '+f.why).join('\n') + wingNote + walkNote, 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(); // Walk + vision context for loose diagnosis var walkNote = ''; if (S && S._walkSummary) { var ws = S._walkSummary; var g1 = ws.e1 && ws.e1.grip, g2 = ws.e2 && ws.e2.grip; if (g1 != null && g2 != null) { var avgG = Math.round((g1 + g2) / 2); if (avgG < 50) walkNote = ' (low grip ' + avgG + ' from walk — prioritize rear grip fixes)'; } } else { try { var vKey = 'bb_surface_' + ((S.curTrack && (S.curTrack.id || S.curTrack.name) || 'track') + '').replace(/\W/g, '_'); var vs = JSON.parse(localStorage.getItem(vKey) || '{}'); if (vs.last_cam_scan && vs.last_cam_scan.analysis) walkNote = ' (vision surface logged)'; } catch(e) {} } 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(' / ')+walkNote,detail:kE,action:kF.slice(0,3).map(function(f,i){return(i+1)+'. '+f.change+' — '+f.why;}).join('\n')+sn+walkNote,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(' / ')+walkNote,detail:sE+' Check class rules.',action:sF.slice(0,3).map(function(f,i){return(i+1)+'. '+f.change+' — '+f.why;}).join('\n')+walkNote,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(' / ') + walkNote, detail: explain, action: fixes.slice(0,3).map((f,i)=>(i+1)+'. '+f.change+' — '+f.why).join('\n') + wingNote + walkNote, 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; // Walk grip from 22pt + Walk Scan (higher grip index = more bite) var walkGripAdj = 0; var walkGripNote = ''; if (S && S._walkSummary) { var g1 = S._walkSummary.e1 && S._walkSummary.e1.grip; var g2 = S._walkSummary.e2 && S._walkSummary.e2.grip; var wg = (g1 != null && g2 != null) ? Math.round((g1 + g2) / 2) : (g1 != null ? g1 : g2); if (wg != null) { if (wg < 45) { walkGripAdj = +0.5; walkGripNote = 'walk grip ' + wg + ' (soft) → +0.5 psi'; } else if (wg > 70) { walkGripAdj = -0.5; walkGripNote = 'walk grip ' + wg + ' → -0.5 psi'; } else { walkGripNote = 'walk grip ' + wg; } } } // Vision surface adj + note (harder = +psi for contact, tacky = -) var visionAdj = 0; var visionNote = ''; try { var vsKey = 'bb_surface_' + ((S.curTrack && (S.curTrack.id || S.curTrack.name) || 'track') + '').replace(/\W/g, '_'); var vs = JSON.parse(localStorage.getItem(vsKey) || '{}'); if (vs.last_cam_scan && vs.last_cam_scan.analysis && vs.last_cam_scan.analysis.surface) { var sStr = JSON.stringify(vs.last_cam_scan.analysis.surface || '').toLowerCase(); if (/hard|slick|dry|rough/.test(sStr)) { visionAdj = +0.25; visionNote = 'vision hard → +0.25 psi'; } else if (/tacky|wet|cush/.test(sStr)) { visionAdj = -0.25; visionNote = 'vision tacky → -0.25 psi'; } else visionNote = 'vision surface logged'; } } catch (e) {} var psi = { lf: Math.round((base.lf + daAdj + tempAdj + surfAdj + walkGripAdj + visionAdj) * 2) / 2, rf: Math.round((base.rf + daAdj + tempAdj + surfAdj + walkGripAdj + visionAdj) * 2) / 2, lr: Math.round((base.lr + daAdj + tempAdj + walkGripAdj + visionAdj) * 2) / 2, rr: Math.round((base.rr + daAdj + tempAdj + walkGripAdj + visionAdj) * 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'); if (walkGripNote) notes.push(walkGripNote); if (visionNote) notes.push(visionNote); 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', ]; } } // Walk data powers between-heats advice (grip for tires, variance for caution) if (S && S._walkSummary) { var ws = S._walkSummary; var avgG = null; if (ws.e1 && ws.e1.grip != null && ws.e2 && ws.e2.grip != null) avgG = Math.round((ws.e1.grip + ws.e2.grip)/2); if (avgG != null && avgG < 50) { advice.push('Walk grip ' + avgG + ' (low) — +0.5 psi all around between heats; watch early fade.'); } else if (avgG != null && avgG > 70) { advice.push('Walk grip ' + avgG + ' — good bite, small changes only.'); } if (ws.banking_consistency != null && ws.banking_consistency > 2) { advice.push('Walk banking variance high — track inconsistent, conservative tweaks.'); } } // Vision surface scan power for between heats (if no/full walk) try { var vKey = 'bb_surface_' + ((S.curTrack && (S.curTrack.id || S.curTrack.name) || 'track') + '').replace(/\W/g, '_'); var vSnap = JSON.parse(localStorage.getItem(vKey) || '{}'); if (vSnap.last_cam_scan && vSnap.last_cam_scan.analysis) { advice.push('Vision surface logged — use for grip read before feature.'); } } catch (e) {} 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.'; } // Phase 1 memory note from rec + logbook history var mem = _getTrackMemorySummary(track.name); if (mem && mem.count > 0) { histNote += ' Memory: ' + mem.note; } 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.' + (mem ? '\nYour history here suggests starting close to prior successful nights.' : ''), confidence: S._walkSummary ? 'HIGH — walk data included' : 'HIGH', }; } // ── History/comparison algorithm ───────────────────────────────────────────── function hunterHistory(ctx) { var hist = S.lastTrackSetup; var track = S.curTrack; var trackName = track ? track.name : null; // Phase 1: pull richer memory from rec history var mem = trackName ? _getTrackMemorySummary(trackName) : null; if (!hist || !hist.setup) { var base = { algo: true, answer: 'No saved setup found for ' + (trackName || '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', }; if (mem && mem.count > 0) { base.detail = mem.note; base.action = 'From your rec history here: ' + mem.count + ' prior suggestions (mostly ' + mem.dominant + '). ' + base.action; } return base; } 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) + ')'); } }); var ans = 'COMPARED TO LAST VISIT: ' + diffs.length + ' change' + (diffs.length!==1?'s':''); var det = 'Last visit: ' + (hist.date||'') + (hist.result?' · Result: '+hist.result:''); if (mem && mem.count > 0) { det += ' · ' + mem.note; } return { algo: true, answer: ans, detail: det, action: diffs.length ? diffs.join('\n') : 'Setup identical to last visit.' + (mem ? '\nMemory: ' + mem.note : ''), 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', }; } // HUNTER DIRT KART PACK — Kart 6T3_Ben + Bob's 4 Cycle + neutral physics // Auto-generated 2026-07-11T14:03:17.863Z var _HUNTER_KART_RULES = [{"id":"kart-stagger-roll-speed","topic":"kart_stagger","src":"KART6T3-SEED","fact":"On dirt oval karts, excess rear stagger can kill straightaway roll speed even when it helps rotation — more stagger is not always faster.","act":"If the kart rotates but is slow on the straights, reduce rear stagger 1/8–1/4\" and re-check hot numbers before more geometry.","conf":"HIGH","m":["stagger","roll speed","killing roll","too much stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart-psi-primary","topic":"kart_tires","src":"KART6T3-SEED","fact":"Dirt kart air pressure is a primary handling knob — small PSI changes affect bite and heat more than on full-size cars because of light mass and tire construction.","act":"Change one corner 0.5–1 psi at a time. Log cold and hot. Pair PSI with stagger — do not chase both in the same session without notes.","conf":"HIGH","m":["kart air pressure","kart psi","tire pressure kart","air pressures"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart-tight-not-push","topic":"kart_handling","src":"KART6T3-SEED","fact":"Kart language matters: \"tight\" and \"push\" are often used interchangeably by drivers but can mean different corners of the problem — confirm entry/mid/exit before changing setup.","act":"Ask where: entry, center, exit. Then change one thing (PSI, stagger, or front geometry) — not three.","conf":"HIGH","m":["tight isn","tight vs push","push not tight","tight isnt"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart-lead-vs-toe","topic":"kart_geometry","src":"KART6T3-SEED","fact":"Lead (front axle position / lead-trail) and toe both affect turn-in on dirt karts — order of adjustment matters; fix the primary front-end relationship before fine toe.","act":"Set lead/front geometry baseline first, then toe. Log both. One change per run on a new track.","conf":"HIGH","m":["lead or toe","lead vs toe","kart toe","kart lead"],"cls":["kart","flatkart"]},{"id":"kart-surfacing-grit","topic":"kart_tires","src":"KART6T3-SEED","fact":"Tire surfacing grit changes how quickly a kart tire works and how it grips on dirt — coarser vs finer grit is a prep choice tied to track moisture and rubber.","act":"Match grit to track: green/tacky vs slick/rubbered. Log brand + grit + PSI together. Do not re-surface mid-program without a plan.","conf":"MEDIUM","m":["surfacing","grit","surface tires","tire prep kart"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart-gearing-basics","topic":"kart_gearing","src":"KART6T3-SEED","fact":"Dirt kart gearing (sprocket ratio / rollout) must match track size and engine powerband — wrong gearing looks like a chassis problem.","act":"Before big chassis changes for \"no power off\" or \"bog,\" verify gearing vs track length. Log front/rear tooth counts.","conf":"HIGH","m":["kart gearing","sprocket","gear ratio kart","rollout"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart-north-south-tracks","topic":"kart_track","src":"KART6T3-SEED","fact":"Regional dirt kart tracks differ in moisture, clay, banking, and rubber — northern vs southern setups are not interchangeable without re-baselining.","act":"When traveling, reset PSI/stagger baseline for local conditions. Do not copy home-track stagger blindly.","conf":"MEDIUM","m":["north vs south","track changes","traveling kart","regional setup"],"cls":["kart","flatkart"]},{"id":"kart-front-end-map","topic":"kart_geometry","src":"KART6T3-SEED","fact":"Dirt kart front end has multiple interrelated adjustments (spindle, kingpin, lead, camber/caster family) — changing one without logging the stack creates mystery handling.","act":"Photograph/log front-end settings before any change. One adjustment per session when diagnosing turn-in.","conf":"HIGH","m":["front end","kart front","whats what front","what's what"],"cls":["kart","flatkart"]},{"id":"kart-rear-bearing","topic":"kart_maintenance","src":"KART6T3-SEED","fact":"Worn rear axle bearings create handling that looks like stagger or chassis problems — free-play and heat at the hub are red flags.","act":"If one side is mysteriously free or binds, check rear bearings before more setup. Maintenance beats setup chasing.","conf":"HIGH","m":["rear bearing","axle bearing","bearing maintenance"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart-axle-pin-side","topic":"kart_maintenance","src":"KART6T3-SEED","fact":"Rear axle pin side and method affect how the axle is located under load — wrong pin practice is a safety and consistency issue.","act":"Follow class rules and chassis practice for pin side. After axle work, re-check rear track width and stagger.","conf":"MEDIUM","m":["pinning axle","axle pin","which side pin","pin rear axle"],"cls":["kart","flatkart"]},{"id":"kart-clone-engine","topic":"kart_engine","src":"KART6T3-SEED","fact":"Clone engine installs on dirt karts require correct mounts, chain line, clutch clearance, and throttle/kill routing — engine swaps change weight distribution.","act":"After a clone install, re-scale the kart and re-baseline gearing/PSI. Do not assume old setup still fits.","conf":"HIGH","m":["clone engine","install clone","clone power","dirt kart engine"],"cls":["kart","flatkart"]},{"id":"kart-tire-brands","topic":"kart_tires","src":"KART6T3-SEED","fact":"Dirt kart tire brands/compounds behave differently for heat and bite — brand rules and local availability often matter more than internet \"best tire\" lists.","act":"Run what the class and local winners use, then tune PSI/stagger. Switching brands mid-night resets your notes.","conf":"MEDIUM","m":["tire brand","burris","hoosier kart","which tire kart"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-2NNS2V0HR5k-2","topic":"kart_engine","src":"KART6T3-2NNS2V0HR5k","fact":"Uh, real quick before we get into the video and stuff, I didn't I'm sorry I don't recall her name, but when I was at John's race, uh, his dad's race, the Bill Lfield memorial a couple weeks ago running clone and everything and the open, uh, a little girl had brought this over to me and gave this to me and I just awesome.","act":"Dirt kart context from Kart 6T3_Ben (Older tires and Duro). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-2yiemDlI_Vs-6","topic":"kart_maintenance","src":"KART6T3-2yiemDlI_Vs","fact":"I I want to say that Ward Carting uh Word Cart Supply the Infiniti was the first to design the clamshell rear bearing hanger.","act":"Dirt kart context from Kart 6T3_Ben (Coming back 2 Karting 2026?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["bearing"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-2yiemDlI_Vs-7","topic":"kart_handling","src":"KART6T3-2yiemDlI_Vs","fact":"Um I also my my friends at Motorworks there the engines I run Kyle P's they are a uh they are distributor of uh Foot Loose Racing chassis.","act":"Dirt kart context from Kart 6T3_Ben (Coming back 2 Karting 2026?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["loose"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-3ZQTzHg12es-3","topic":"kart_engine","src":"KART6T3-3ZQTzHg12es","fact":"I'm sure that is, but how it usually goes is people start in Predator or they start in clone.","act":"Dirt kart context from Kart 6T3_Ben (UAS 4 Stroke Over 2 Stroke?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-3ZQTzHg12es-5","topic":"kart_engine","src":"KART6T3-3ZQTzHg12es","fact":"know, Black Hat Speed has got a a class or not them in particular, they got a little bit of a thing going down there in Virginia area with the limited mods in the clone.","act":"Dirt kart context from Kart 6T3_Ben (UAS 4 Stroke Over 2 Stroke?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-3ZQTzHg12es-6","topic":"kart_engine","src":"KART6T3-3ZQTzHg12es","fact":"It's a, you know, it it I just think that when you go from a clone to a two-stroke, there's so much to learn to make a two-stroke perform correctly that it's just easier to go to a four- stroke.","act":"Dirt kart context from Kart 6T3_Ben (UAS 4 Stroke Over 2 Stroke?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-98kaUcGQQM0-7","topic":"kart_geometry","src":"KART6T3-98kaUcGQQM0","fact":"When you're adjusting your front end, not only should you have it loaded, but you're going to want to set um you're going to want to set your uh your cambers with the driver in the seat and then set your toe because when you start moving this, this is going to change the angle in which that So, that's actually going to move a little bit and and affect your toe.","act":"Dirt kart context from Kart 6T3_Ben (Dirt Kart Front End: What’s What?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["toe","toe"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-98kaUcGQQM0-8","topic":"kart_geometry","src":"KART6T3-98kaUcGQQM0","fact":"You're going to have this uh this spindle, your left front spindle, is going to be square to the axle.","act":"Dirt kart context from Kart 6T3_Ben (Dirt Kart Front End: What’s What?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-98kaUcGQQM0-9","topic":"kart_geometry","src":"KART6T3-98kaUcGQQM0","fact":"But what I want to tell you you know, assure you here or not assure you, but let you know is you're going to go ahead set your caster.","act":"Dirt kart context from Kart 6T3_Ben (Dirt Kart Front End: What’s What?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["caster"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-98kaUcGQQM0-10","topic":"kart_geometry","src":"KART6T3-98kaUcGQQM0","fact":"Then you're going to set your camber with your camber gauge with a driver in the seat on the scales perfectly level.","act":"Dirt kart context from Kart 6T3_Ben (Dirt Kart Front End: What’s What?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["camber","camber"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Avm73ZylspY-6","topic":"kart_stagger","src":"KART6T3-Avm73ZylspY","fact":"I run at minimum an inch and a half rear stagger along with front stagger, but an inch and a half rear stagger is quite significant on a track like that.","act":"Dirt kart context from Kart 6T3_Ben (I have a “Bad Push”). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger","stagger","stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Avm73ZylspY-7","topic":"kart_stagger","src":"KART6T3-Avm73ZylspY","fact":"And I won't say get off the corner because that was always a thing that if you ran more rear stagger, it didn't seem to get off the corner as good.","act":"Dirt kart context from Kart 6T3_Ben (I have a “Bad Push”). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Avm73ZylspY-8","topic":"kart_stagger","src":"KART6T3-Avm73ZylspY","fact":"Now that I'm, you know, running in excess of, you know, an inch and a quarter to inch and a half or stagger, it still gets the off the corner just fine, but it also helps it rotate in the center of the corner.","act":"Dirt kart context from Kart 6T3_Ben (I have a “Bad Push”). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Avm73ZylspY-10","topic":"kart_handling","src":"KART6T3-Avm73ZylspY","fact":"If you get too much left, you may be causing a four-wheel drift, which is really more so a looseish condition, not so much a tight.","act":"Dirt kart context from Kart 6T3_Ben (I have a “Bad Push”). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["tight"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-bg1anV-oUy4-5","topic":"kart_tires","src":"KART6T3-bg1anV-oUy4","fact":"A like I just said, sometimes it can grow or shrink your tire so you have to be very mindful of what kind of tire pressure you're running inside of the tire when you're torching in some prep.","act":"Dirt kart context from Kart 6T3_Ben (Torching tires: explained). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["pressure"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-cUhCsp528XI-2","topic":"kart_geometry","src":"KART6T3-cUhCsp528XI","fact":"I did it for a long time that way, but we're talking about pinning the axle.","act":"Dirt kart context from Kart 6T3_Ben (Pinning Kart Rear Axle: which side?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-cUhCsp528XI-3","topic":"kart_geometry","src":"KART6T3-cUhCsp528XI","fact":"That is locking either side or both sides or everything or or anything on your axle so it doesn't shift.","act":"Dirt kart context from Kart 6T3_Ben (Pinning Kart Rear Axle: which side?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-cUhCsp528XI-4","topic":"kart_geometry","src":"KART6T3-cUhCsp528XI","fact":"Uh, all the forces are going to try and shove the axle out of the rear bearings, and they need to be pinned.","act":"Dirt kart context from Kart 6T3_Ben (Pinning Kart Rear Axle: which side?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-cUhCsp528XI-5","topic":"kart_geometry","src":"KART6T3-cUhCsp528XI","fact":"We are uh checking out the back end of the shatter chassis and we are in fact talking about where to pin your axle.","act":"Dirt kart context from Kart 6T3_Ben (Pinning Kart Rear Axle: which side?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-cUhCsp528XI-8","topic":"kart_geometry","src":"KART6T3-cUhCsp528XI","fact":"So, as I was saying, back on the UAS, there is a ton of force that is trying to shove the axle this way.","act":"Dirt kart context from Kart 6T3_Ben (Pinning Kart Rear Axle: which side?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-cUhCsp528XI-9","topic":"kart_maintenance","src":"KART6T3-cUhCsp528XI","fact":"I either, you know, I'd put one on this side of the bearing to keep it from for, you know, going this way.","act":"Dirt kart context from Kart 6T3_Ben (Pinning Kart Rear Axle: which side?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["bearing"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-cUhCsp528XI-10","topic":"kart_geometry","src":"KART6T3-cUhCsp528XI","fact":"I'm just convinced that locking both sides of your axle is just a way better idea.","act":"Dirt kart context from Kart 6T3_Ben (Pinning Kart Rear Axle: which side?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-dLHp7OX53yo-1","topic":"kart_geometry","src":"KART6T3-dLHp7OX53yo","fact":"I they have packages for front end bearing and or in bearings in general.","act":"Dirt kart context from Kart 6T3_Ben (Changing Kart Bearings-Front End Components). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["bearing"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-dLHp7OX53yo-10","topic":"kart_engine","src":"KART6T3-dLHp7OX53yo","fact":"And then the 53 titanium ones, which are actually aluminum hubs with titanium, uh, nuts and bolts on them, they are going to go on the Recon, which is my clone cart.","act":"Dirt kart context from Kart 6T3_Ben (Changing Kart Bearings-Front End Components). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-EANj0Z85N60-7","topic":"kart_tires","src":"KART6T3-EANj0Z85N60","fact":"The uh the the tire compound seemed to be what that track was asking for, at least for my setup, and the the increased air pressure got me more into the track.","act":"Dirt kart context from Kart 6T3_Ben (Kart Air Pressures). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["pressure"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-EANj0Z85N60-9","topic":"kart_handling","src":"KART6T3-EANj0Z85N60","fact":"That is the same you know that correlation that that that that feeling of hard fast really bit up tight track moves over to the dirt side.","act":"Dirt kart context from Kart 6T3_Ben (Kart Air Pressures). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["tight"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-eklcY5LBNxg-3","topic":"kart_stagger","src":"KART6T3-eklcY5LBNxg","fact":"[laughter] When you run a lot of rear stagger, uh you you got to put the uh cross to it in order to get your numbers up a little bit.","act":"Dirt kart context from Kart 6T3_Ben (2025/2026 Shatter Karts: All New Blast Chassis !). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-eklcY5LBNxg-6","topic":"kart_geometry","src":"KART6T3-eklcY5LBNxg","fact":"57 and a half left 63 cross 46 and a half nose negative 2.75 right front camber positive 7/8 of a degree left front camber and then the casters are 7 and 10.","act":"Dirt kart context from Kart 6T3_Ben (2025/2026 Shatter Karts: All New Blast Chassis !). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["camber","camber"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-FFo7ZOHflX4-5","topic":"kart_engine","src":"KART6T3-FFo7ZOHflX4","fact":"I'm starting to be able to tell when a when a clone isn't behaving, and this behaved awesome.","act":"Dirt kart context from Kart 6T3_Ben (Back on The Bullring - Back on Burris). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-FFo7ZOHflX4-9","topic":"kart_engine","src":"KART6T3-FFo7ZOHflX4","fact":"I I just want to be a little better at this clone stuff.","act":"Dirt kart context from Kart 6T3_Ben (Back on The Bullring - Back on Burris). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-fQ3-qVHq3Ug-1","topic":"kart_gearing","src":"KART6T3-fQ3-qVHq3Ug","fact":"to to have the equal ratio here you need to change your rear gear to a 54.","act":"Dirt kart context from Kart 6T3_Ben (Gearing Change: Quick Refrence). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["gear"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-fYeJB8GoXzI-1","topic":"kart_geometry","src":"KART6T3-fYeJB8GoXzI","fact":"Anyway, they asked a specific question about toe and lead, and I thought we could really talk about it today.","act":"Dirt kart context from Kart 6T3_Ben (Lead or Toe: Which First?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["toe","lead"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-fYeJB8GoXzI-3","topic":"kart_geometry","src":"KART6T3-fYeJB8GoXzI","fact":"So, when a manufacturer, say, wants a 16th of an inch toe this spindle backwards.","act":"Dirt kart context from Kart 6T3_Ben (Lead or Toe: Which First?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["toe"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-fYeJB8GoXzI-4","topic":"kart_geometry","src":"KART6T3-fYeJB8GoXzI","fact":"It starts to you know to a degree or not affect your caster a little bit.","act":"Dirt kart context from Kart 6T3_Ben (Lead or Toe: Which First?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["caster"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-fYeJB8GoXzI-6","topic":"kart_geometry","src":"KART6T3-fYeJB8GoXzI","fact":"So, the question that was was brought to me was that do you set your toe before or after you set your lead?","act":"Dirt kart context from Kart 6T3_Ben (Lead or Toe: Which First?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["toe","lead"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-fYeJB8GoXzI-7","topic":"kart_geometry","src":"KART6T3-fYeJB8GoXzI","fact":"Say you wanted you want to take this axle and take it backwards.","act":"Dirt kart context from Kart 6T3_Ben (Lead or Toe: Which First?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-fYeJB8GoXzI-8","topic":"kart_geometry","src":"KART6T3-fYeJB8GoXzI","fact":"My initial thought was that you would set the lead and then set the toe with the rear axle.","act":"Dirt kart context from Kart 6T3_Ben (Lead or Toe: Which First?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["lead","toe","axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-fYeJB8GoXzI-9","topic":"kart_geometry","src":"KART6T3-fYeJB8GoXzI","fact":"If we took this all the way back here, and now your axle is faced at an extreme angle, and then you set your toe like this, you're essentially dog tracking your kart.","act":"Dirt kart context from Kart 6T3_Ben (Lead or Toe: Which First?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle","toe"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-iuoqhp93QZg-9","topic":"kart_tires","src":"KART6T3-iuoqhp93QZg","fact":"It really depends on what you're doing to the outside, you know, tire pressure, how aggressive you're being with prep.","act":"Dirt kart context from Kart 6T3_Ben (Track Tac Series: Tire Tuff Original Brown). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["pressure"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-IXc-bCYt0f0-2","topic":"kart_geometry","src":"KART6T3-IXc-bCYt0f0","fact":"I blown out a caster on my buggy on my uh one-man cart lift pretty much when I got there.","act":"Dirt kart context from Kart 6T3_Ben (Rained out? What to do with Tires). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["caster"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-IXc-bCYt0f0-3","topic":"kart_geometry","src":"KART6T3-IXc-bCYt0f0","fact":"They showed up, gave me a brand new caster, Harbor Freight, you know, they just \"Hey man, can I get a shout-out?\" And I was like, \"You betcha.\" Uh big big big thank you to those guys.","act":"Dirt kart context from Kart 6T3_Ben (Rained out? What to do with Tires). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["caster"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-llUPwsYoJzI-3","topic":"kart_engine","src":"KART6T3-llUPwsYoJzI","fact":"I went down to Alagany Memorial Speedway, uh, AMS, this past weekend to run some clone and, uh, this kid came up and gave me this.","act":"Dirt kart context from Kart 6T3_Ben (Tire Prep: Dropping Duro Internally?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Llw7A0GG9hg-1","topic":"kart_engine","src":"KART6T3-Llw7A0GG9hg","fact":"So, a little bit ago, a gentleman asked for a video on how to install a clone, and I thought, you know what?","act":"Dirt kart context from Kart 6T3_Ben (Installing a Clone Engine on a Dirt kart). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Llw7A0GG9hg-2","topic":"kart_engine","src":"KART6T3-Llw7A0GG9hg","fact":"For those of us who have, you know, like I said before, yes, this is like year three or something of me running a clone, but I didn't know anything about him.","act":"Dirt kart context from Kart 6T3_Ben (Installing a Clone Engine on a Dirt kart). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Llw7A0GG9hg-3","topic":"kart_engine","src":"KART6T3-Llw7A0GG9hg","fact":"In the in the idea of the karting 101, you're brand new to the sport, you've never put a clone on a chassis before, I thought that was an absolute great idea.","act":"Dirt kart context from Kart 6T3_Ben (Installing a Clone Engine on a Dirt kart). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Llw7A0GG9hg-4","topic":"kart_engine","src":"KART6T3-Llw7A0GG9hg","fact":"Um I'm going to try and do my best to show you what it takes in order to mount a clone on a kart.","act":"Dirt kart context from Kart 6T3_Ben (Installing a Clone Engine on a Dirt kart). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Llw7A0GG9hg-5","topic":"kart_engine","src":"KART6T3-Llw7A0GG9hg","fact":"I I mean, basically, I'm going to show you what it's like for a clone, but I think the premise still holds true to a Predator, but you take a take that for what it is.","act":"Dirt kart context from Kart 6T3_Ben (Installing a Clone Engine on a Dirt kart). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Llw7A0GG9hg-6","topic":"kart_engine","src":"KART6T3-Llw7A0GG9hg","fact":"I have uh three clone engines, as you're about to see here, uh to select from.","act":"Dirt kart context from Kart 6T3_Ben (Installing a Clone Engine on a Dirt kart). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-LNED_xUiwCM-2","topic":"kart_stagger","src":"KART6T3-LNED_xUiwCM","fact":"You're going to think, you know, put this stagger on it, put these percents on it, your cross, your left, your nose, all those things.","act":"Dirt kart context from Kart 6T3_Ben (Kart Setup: Consider This). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-LNED_xUiwCM-3","topic":"kart_stagger","src":"KART6T3-LNED_xUiwCM","fact":"everything you can think of from stagger to uh camber and caster.","act":"Dirt kart context from Kart 6T3_Ben (Kart Setup: Consider This). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger","camber","caster"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-LNED_xUiwCM-4","topic":"kart_geometry","src":"KART6T3-LNED_xUiwCM","fact":"Uh caster would be the this the switch that allows the in this is in my mind.","act":"Dirt kart context from Kart 6T3_Ben (Kart Setup: Consider This). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["caster"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-LNED_xUiwCM-5","topic":"kart_geometry","src":"KART6T3-LNED_xUiwCM","fact":"Not only how far down this spindle is, but the amount of camber that's in it controls this lever from happening.","act":"Dirt kart context from Kart 6T3_Ben (Kart Setup: Consider This). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["camber"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-LNED_xUiwCM-6","topic":"kart_stagger","src":"KART6T3-LNED_xUiwCM","fact":"Now there's other things like you know um you know I I mentioned stagger but also effects on that are sidewall heights.","act":"Dirt kart context from Kart 6T3_Ben (Kart Setup: Consider This). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-LOER9vrD5uA-5","topic":"kart_tires","src":"KART6T3-LOER9vrD5uA","fact":"My theory is when it comes to the springtime, you're going to be surfacing them anyway.","act":"Dirt kart context from Kart 6T3_Ben (Winter Tire Storage? What Do You Do?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["surfacing"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-MFFLRPgNYLs-2","topic":"kart_stagger","src":"KART6T3-MFFLRPgNYLs","fact":"You're not going to you you might play with rear stagger a little bit or something like that.","act":"Dirt kart context from Kart 6T3_Ben (Quick Tip Tuesday: Tire Pressure). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-MFFLRPgNYLs-3","topic":"kart_tires","src":"KART6T3-MFFLRPgNYLs","fact":"So say if you're running five on your right, four PSI on your left, you're going to increase that.","act":"Dirt kart context from Kart 6T3_Ben (Quick Tip Tuesday: Tire Pressure). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["psi"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-mnCSzL31x1M-2","topic":"kart_tires","src":"KART6T3-mnCSzL31x1M","fact":"I was just merely showing what a different grit of sandpaper will do to a tire when you surface the tire.","act":"Dirt kart context from Kart 6T3_Ben (Surfacing Kart Tires: What Grits Do?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["grit"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-mnCSzL31x1M-5","topic":"kart_tires","src":"KART6T3-mnCSzL31x1M","fact":"Um this isn't about tire cutting, this is only surfacing, but it's been a while since I've done a surfacing video.","act":"Dirt kart context from Kart 6T3_Ben (Surfacing Kart Tires: What Grits Do?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["surfacing","surfacing"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-mnCSzL31x1M-6","topic":"kart_tires","src":"KART6T3-mnCSzL31x1M","fact":"I just thought this would be, you know, especially for the new crowd, the people who have never surfaced a tire, the people who are curious, \"Hey Ben, what grit do I start with?\" So we are going to start off with uh what I got over there?","act":"Dirt kart context from Kart 6T3_Ben (Surfacing Kart Tires: What Grits Do?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["grit"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-mnCSzL31x1M-10","topic":"kart_tires","src":"KART6T3-mnCSzL31x1M","fact":"So, uh people do ask me, \"What kind of pressure you run at?\" I always suggest whatever you're running on track is perfectly fine.","act":"Dirt kart context from Kart 6T3_Ben (Surfacing Kart Tires: What Grits Do?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["pressure"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-MQAx612PVTk-7","topic":"kart_tires","src":"KART6T3-MQAx612PVTk","fact":"If you nail tires, if you have somebody that is fantastic with knowing tire pressure, what date code, what kind of durometer, the prep situation, you have all that stuff.","act":"Dirt kart context from Kart 6T3_Ben (Tires Over Setup?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["pressure"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-MQAx612PVTk-8","topic":"kart_tires","src":"KART6T3-MQAx612PVTk","fact":"But the pressure points, the four tires that are hitting the ground, that setup matters for that track.","act":"Dirt kart context from Kart 6T3_Ben (Tires Over Setup?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["pressure"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-nG0mDbllY44-4","topic":"kart_engine","src":"KART6T3-nG0mDbllY44","fact":"He you know, it's like I said, if you're if you're a clone guy and you and you you're you're a Predator guy and you wanted to clone racing, but the the price scares you.","act":"Dirt kart context from Kart 6T3_Ben (Clone Power - Budget Price!). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone","clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-nG0mDbllY44-5","topic":"kart_engine","src":"KART6T3-nG0mDbllY44","fact":"They It's just like, I'd love to run those clone glasses, but there's no way I'm paying, you know, 1,400, 1,900 an engine.","act":"Dirt kart context from Kart 6T3_Ben (Clone Power - Budget Price!). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-NxCNMtmqeec-8","topic":"kart_tires","src":"KART6T3-NxCNMtmqeec","fact":"Whether it was air pressure or because I went to a different set of tires with nothing in them and nothing on them, I don't know.","act":"Dirt kart context from Kart 6T3_Ben (Attack of the Clones Reaper Race Selinsgrove PA). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["pressure"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-PA9gl248OKk-3","topic":"kart_stagger","src":"KART6T3-PA9gl248OKk","fact":"I will tell you this, if you add too much rear stagger, you will have a tendency to be too free and well, the opposite is the same with too much stagger.","act":"Dirt kart context from Kart 6T3_Ben (Tight isn’t a Push?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger","stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-PA9gl248OKk-4","topic":"kart_handling","src":"KART6T3-PA9gl248OKk","fact":"Now, tight is just it's not a push because it will still turn.","act":"Dirt kart context from Kart 6T3_Ben (Tight isn’t a Push?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["tight"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-PA9gl248OKk-5","topic":"kart_handling","src":"KART6T3-PA9gl248OKk","fact":"A tight cart is something you you go into the turn and you put wheel into it, but it takes an excessive amount of force to get it to do what it You can put that cart where you want.","act":"Dirt kart context from Kart 6T3_Ben (Tight isn’t a Push?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["tight"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-PA9gl248OKk-7","topic":"kart_handling","src":"KART6T3-PA9gl248OKk","fact":"The The most ideal cart, you wouldn't barely move your hands at all and it will just do what it does and it will be fast, but that's not the case when you have a tight cart.","act":"Dirt kart context from Kart 6T3_Ben (Tight isn’t a Push?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["tight"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-QAQ2xRwr3SU-2","topic":"kart_engine","src":"KART6T3-QAQ2xRwr3SU","fact":"You know, even, you know, you've heard that clone carbs, they can get bunged up easy.","act":"Dirt kart context from Kart 6T3_Ben (Clone Carb Clean Tutorial). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-QAQ2xRwr3SU-7","topic":"kart_handling","src":"KART6T3-QAQ2xRwr3SU","fact":"I think that don't go crazy tight and also uh don't don't just kind of you just like oh that that should be good.","act":"Dirt kart context from Kart 6T3_Ben (Clone Carb Clean Tutorial). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["tight"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-QAQ2xRwr3SU-8","topic":"kart_handling","src":"KART6T3-QAQ2xRwr3SU","fact":"You want things nice and tight so they're not going to come loose on you while you're racing.","act":"Dirt kart context from Kart 6T3_Ben (Clone Carb Clean Tutorial). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["tight","loose"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-QAQ2xRwr3SU-9","topic":"kart_handling","src":"KART6T3-QAQ2xRwr3SU","fact":"And I can go ahead and give it a nice a nice tight feel like that is tight.","act":"Dirt kart context from Kart 6T3_Ben (Clone Carb Clean Tutorial). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["tight","tight"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-QAQ2xRwr3SU-10","topic":"kart_engine","src":"KART6T3-QAQ2xRwr3SU","fact":"I really, you know, I I had no idea how to take apart the carburetor on a clone.\" Now, I do.","act":"Dirt kart context from Kart 6T3_Ben (Clone Carb Clean Tutorial). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-r76fouiRoHg-3","topic":"kart_gearing","src":"KART6T3-r76fouiRoHg","fact":"Essentially, the engine is connected to the rear axle without any clutch.","act":"Dirt kart context from Kart 6T3_Ben (My 2025 UAS Grand Nationals Experience). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-r76fouiRoHg-4","topic":"kart_geometry","src":"KART6T3-r76fouiRoHg","fact":"[Music] And that created a um where I lifted off the throttle, the rear rear axle was locking up.","act":"Dirt kart context from Kart 6T3_Ben (My 2025 UAS Grand Nationals Experience). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-r76fouiRoHg-5","topic":"kart_tires","src":"KART6T3-r76fouiRoHg","fact":"It was making it exceptionally loose into the corner just like skidding the rear tires and causing me to spin out there.","act":"Dirt kart context from Kart 6T3_Ben (My 2025 UAS Grand Nationals Experience). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["loose"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv3cYnwFYak-1","topic":"kart_engine","src":"KART6T3-Rv3cYnwFYak","fact":"I just Ah, [sighs] me and clone racing with their their I'm getting better, but something's got to give.","act":"Dirt kart context from Kart 6T3_Ben (How to Make your Kart into a UAS Kart). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv3cYnwFYak-2","topic":"kart_engine","src":"KART6T3-Rv3cYnwFYak","fact":"What are some of the things, if you have a base chassis, if you're a clone racer, if you're a predator racer, what are the base things that you have to do to your cart in order to make it accept that much horsepower?","act":"Dirt kart context from Kart 6T3_Ben (How to Make your Kart into a UAS Kart). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv3cYnwFYak-6","topic":"kart_engine","src":"KART6T3-Rv3cYnwFYak","fact":"I kind of just threw it together in a panic, but >> [laughter] >> we're going to go over a general idea of what you need to convert your basic chassis, your clone chassis, Predator chassis, whatever whatever you're riding into something UAS.","act":"Dirt kart context from Kart 6T3_Ben (How to Make your Kart into a UAS Kart). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv3cYnwFYak-7","topic":"kart_engine","src":"KART6T3-Rv3cYnwFYak","fact":"They twist the frames unlike a clone will ever think of doing.","act":"Dirt kart context from Kart 6T3_Ben (How to Make your Kart into a UAS Kart). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv9oqAaGVyY-2","topic":"kart_gearing","src":"KART6T3-Rv9oqAaGVyY","fact":"He was new to the sport, he was new to a track, whatever the situation was, he was asking me about gear issues.","act":"Dirt kart context from Kart 6T3_Ben (Karting Gearing). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["gear"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv9oqAaGVyY-3","topic":"kart_gearing","src":"KART6T3-Rv9oqAaGVyY","fact":"Now, when it comes to gear ratios, we'll we'll just go nuts and bolts karting 101 type of thing.","act":"Dirt kart context from Kart 6T3_Ben (Karting Gearing). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["gear"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv9oqAaGVyY-5","topic":"kart_engine","src":"KART6T3-Rv9oqAaGVyY","fact":"A a clone typically we'll say like 6,800 to 7,200 RPM.","act":"Dirt kart context from Kart 6T3_Ben (Karting Gearing). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv9oqAaGVyY-7","topic":"kart_engine","src":"KART6T3-Rv9oqAaGVyY","fact":"So, my my my clone experience is very limited even though I've been running a couple years.","act":"Dirt kart context from Kart 6T3_Ben (Karting Gearing). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv9oqAaGVyY-8","topic":"kart_gearing","src":"KART6T3-Rv9oqAaGVyY","fact":"Um I would need to change three teeth on the rear gear up to accommodate for one tooth on this driver.","act":"Dirt kart context from Kart 6T3_Ben (Karting Gearing). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["gear"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv9oqAaGVyY-9","topic":"kart_gearing","src":"KART6T3-Rv9oqAaGVyY","fact":"You can find karting gear ratios all over the place.","act":"Dirt kart context from Kart 6T3_Ben (Karting Gearing). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["gear"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-Rv9oqAaGVyY-10","topic":"kart_gearing","src":"KART6T3-Rv9oqAaGVyY","fact":"If you don't know how to how to do that, you're going to divide the uh you're going to divide the the driver into the rear gear.","act":"Dirt kart context from Kart 6T3_Ben (Karting Gearing). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["gear"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-s-EqDtygOr4-2","topic":"kart_handling","src":"KART6T3-s-EqDtygOr4","fact":"You know, in in chassis manufacturing, we want to keep our seats loose.","act":"Dirt kart context from Kart 6T3_Ben (Caylor Custom Karts). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["loose"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-t34H6VmNWOo-1","topic":"kart_gearing","src":"KART6T3-t34H6VmNWOo","fact":"I reached out to him, find out some gearing, blah blah blah, saying that, hey, I'm coming I'm I'm probably coming down to the Clone Wars race.","act":"Dirt kart context from Kart 6T3_Ben (A New Way to Shrink Kart Tires?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-t34H6VmNWOo-2","topic":"kart_stagger","src":"KART6T3-t34H6VmNWOo","fact":"And I thought, what I've been doing I've been shrinking tires the same way for as long as I've paid attention to stagger.","act":"Dirt kart context from Kart 6T3_Ben (A New Way to Shrink Kart Tires?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-t34H6VmNWOo-3","topic":"kart_stagger","src":"KART6T3-t34H6VmNWOo","fact":"Uh, as I started off in racing, I don't remember ever checking stagger.","act":"Dirt kart context from Kart 6T3_Ben (A New Way to Shrink Kart Tires?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-t34H6VmNWOo-4","topic":"kart_stagger","src":"KART6T3-t34H6VmNWOo","fact":"He, you know, the the tires a stagger much the same as any, you know, any father would do for their kid.","act":"Dirt kart context from Kart 6T3_Ben (A New Way to Shrink Kart Tires?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-t34H6VmNWOo-5","topic":"kart_stagger","src":"KART6T3-t34H6VmNWOo","fact":"But as time rolled on, I eventually figured out, hey, stagger is a thing.","act":"Dirt kart context from Kart 6T3_Ben (A New Way to Shrink Kart Tires?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-VBarxamZFWE-2","topic":"kart_handling","src":"KART6T3-VBarxamZFWE","fact":"Um excellent foot loose I you know they're they were it's kind of the little brand that can you see them really making a stink in the world.","act":"Dirt kart context from Kart 6T3_Ben (Best Kart Chassis 2025? Dirt Kart Manufacturers). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["loose"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-VBarxamZFWE-4","topic":"kart_geometry","src":"KART6T3-VBarxamZFWE","fact":"What I really like is if you notice on the back side of their chassis, the rails that they actually mount their uh axle to are movable.","act":"Dirt kart context from Kart 6T3_Ben (Best Kart Chassis 2025? Dirt Kart Manufacturers). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-VBarxamZFWE-7","topic":"kart_geometry","src":"KART6T3-VBarxamZFWE","fact":"That that replace your components, replace your bearings, your spindles, uh you can get wear points, your rear axle, your your rear axle bearing, all the all the components that make it work are more important than worrying about being flexed out on a chassis.","act":"Dirt kart context from Kart 6T3_Ben (Best Kart Chassis 2025? Dirt Kart Manufacturers). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle","axle","bearing"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-VBarxamZFWE-8","topic":"kart_tires","src":"KART6T3-VBarxamZFWE","fact":"I have run against a lot of people who are on a lot older stuff, be it UAS, be it clone or anything like that, and because they're on point with their maintenance, because their their components are up to date, because they're on point with their tires, they are still competitive, they could still win.","act":"Dirt kart context from Kart 6T3_Ben (Best Kart Chassis 2025? Dirt Kart Manufacturers). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["clone"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-VEzbT9chbPI-4","topic":"kart_maintenance","src":"KART6T3-VEzbT9chbPI","fact":"I just want to give a a little like hands-on tutorial for the for somebody who has never taken a bearing shield off, never looked at the bearings, never even thought about it, maybe.","act":"Dirt kart context from Kart 6T3_Ben (Rear Bearing Maintenance). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["bearing"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-VEzbT9chbPI-8","topic":"kart_geometry","src":"KART6T3-VEzbT9chbPI","fact":"Um, I chose this side specifically because those of us who use lock collars on our our rear axle, this is on this side.","act":"Dirt kart context from Kart 6T3_Ben (Rear Bearing Maintenance). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-VEzbT9chbPI-9","topic":"kart_maintenance","src":"KART6T3-VEzbT9chbPI","fact":"But I wanted to show the the the the bigger problem area bearing because I need to explain if you need to get the uh dust cover off, this has to come out of the way.","act":"Dirt kart context from Kart 6T3_Ben (Rear Bearing Maintenance). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["bearing"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-WKtBrq7dpdA-3","topic":"kart_geometry","src":"KART6T3-WKtBrq7dpdA","fact":"I think in the long run, if you're somebody who insists on replacing your rear axle, titanium is going to be a a good purchase because it unless you bend it, it should last longer than a standard steel.","act":"Dirt kart context from Kart 6T3_Ben (Five Three Titanium Revolver Series Hubs). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-WKtBrq7dpdA-7","topic":"kart_geometry","src":"KART6T3-WKtBrq7dpdA","fact":"Um, and I and I think you should if you if you're really trying to spare weight, if you're really trying to maximize your horsepower, the the the less your rear axle weighs, the more horsepower is going to apply to it.","act":"Dirt kart context from Kart 6T3_Ben (Five Three Titanium Revolver Series Hubs). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-WKtBrq7dpdA-9","topic":"kart_tires","src":"KART6T3-WKtBrq7dpdA","fact":"But if you're looking to maximize horsepower, if you're looking to get weight off your rear axle, if you're looking to just get weight off the cart entirely, titanium's the way to go.","act":"Dirt kart context from Kart 6T3_Ben (Five Three Titanium Revolver Series Hubs). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-WKtBrq7dpdA-10","topic":"kart_geometry","src":"KART6T3-WKtBrq7dpdA","fact":"Uh if you kind of, you know, rear axle, if you were to get everything, it might make a significant dent in it, but don't break the bank at it at it.","act":"Dirt kart context from Kart 6T3_Ben (Five Three Titanium Revolver Series Hubs). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["axle"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-yUkbeX5D7Bo-1","topic":"kart_stagger","src":"KART6T3-yUkbeX5D7Bo","fact":"I don't know if it's the turn in the center is good for me, what it is, if it like, you know, I like a I like to run a bit more rear stagger and these chassis kind of do that for me.","act":"Dirt kart context from Kart 6T3_Ben (What Kart chassis do you ride?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-zkwr8ipHmns-2","topic":"kart_stagger","src":"KART6T3-zkwr8ipHmns","fact":"But, there are some people that believe that having the least amount of stagger will increase your straightaway speed.","act":"Dirt kart context from Kart 6T3_Ben (Is Stagger Killing Your Roll Speed?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]},{"id":"kart6t3-zkwr8ipHmns-3","topic":"kart_stagger","src":"KART6T3-zkwr8ipHmns","fact":"Stagger is the difference in size between your right sides and your left sides.","act":"Dirt kart context from Kart 6T3_Ben (Is Stagger Killing Your Roll Speed?). Apply only if it matches your class rules. Log PSI/stagger/gearing with the change.","conf":"MEDIUM","m":["stagger"],"cls":["kart","flatkart","outlawkart"]}]; function hunterKartKnowledge(q, ctx) { if (!q || typeof q !== 'string') return null; var ql = q.toLowerCase(); var kartish = /kart|flat kart|box stock|clone|uas|wka|burris|rollout|sprocket|surfacing grit|lead or toe|dirt kart|4cycle|bob'?s/i.test(q) || (ctx && ctx.cls && /kart/i.test(String(ctx.cls.name || ctx.cls.class_name || ctx.cls || ''))) || (typeof _algoCarType === 'function' && _algoCarType() === 'kart') || (typeof RW_CAR_TYPE !== 'undefined' && /kart/i.test(String(RW_CAR_TYPE||''))); var tech = /stagger|roll speed|kart psi|air pressure|gear ratio|sprocket|surfacing|clone engine|rear bearing|axle pin|front end|lead or toe|tight isn|clutch|scale|cross weight/i.test(ql); if (!kartish && !tech) return null; if (!/kart|stagger|psi|pressure|gear|sprocket|tight|loose|toe|lead|caster|camber|clone|bearing|axle|tire|surfacing|grit|setup|handle|roll|clutch|scale|cross|seat/i.test(ql)) return null; var hits = []; for (var i = 0; i < _HUNTER_KART_RULES.length; i++) { var r = _HUNTER_KART_RULES[i]; var score = 0; if (r.m && r.m.length) { for (var j = 0; j < r.m.length; j++) { if (r.m[j] && ql.indexOf(String(r.m[j]).toLowerCase()) !== -1) score += 3; } } if (r.topic && ql.indexOf(r.topic.replace(/_/g, ' ')) !== -1) score += 2; if (r.topic === 'kart_stagger' && /stagger|roll speed/.test(ql)) score += 3; if (r.topic === 'kart_gearing' && /gear|sprocket|rollout|clutch/.test(ql)) score += 3; if (r.topic === 'kart_tires' && /tire|psi|pressure|surfacing|grit|burris|duro|prep/.test(ql)) score += 3; if (r.topic === 'kart_geometry' && /toe|lead|caster|camber|front end|scale|seat|cross/.test(ql)) score += 3; if (r.topic === 'kart_handling' && /tight|loose|push|handle/.test(ql)) score += 2; if (r.topic === 'kart_engine' && /clone|engine|motor|carb/.test(ql)) score += 3; if (r.topic === 'kart_maintenance' && /bearing|axle|pin|maintenance|chain/.test(ql)) score += 3; if (score > 0) hits.push({ r: r, score: score }); } if (!hits.length) return null; hits.sort(function(a, b) { return b.score - a.score; }); var top = hits.slice(0, 3).map(function(h) { return h.r; }); var primary = top[0]; var actionLines = top.map(function(r, idx) { return (idx + 1) + '. ' + r.act + (r.src ? ' [' + r.src + ']' : ''); }); return { algo: true, answer: primary.fact.length > 140 ? primary.fact.slice(0, 137) + '…' : primary.fact, detail: 'Dirt kart knowledge · Kart 6T3 + Bobs 4 Cycle · ' + top.length + ' rule(s) · not a brand ad', action: actionLines.join('\n') + '\n\nGARAGE: Log gearing, PSI, stagger, and front-end on your kart → https://racer.wiki/garage', confidence: primary.conf || 'MEDIUM', source: 'HUNTER_KART_PACK', rules_used: top.map(function(r) { return r.id; }) }; } // HUNTER SHOCK KNOWLEDGE PACK — neutral technical rules (CSI MMQB + multi-brand physics) // Auto-generated 2026-07-11T13:29:12.915Z — DO NOT dump into AI system prompts. Local algo only. // brand_policy: neutral_learning var _HUNTER_SHOCK_RULES = [{"id":"torsion-rate-same-number-feel","topic":"torsion_bars","src":"CSI-MMQB-Ep-245","fact":"Two torsion bars stamped with the same rate number can feel different because of material, heat treat, bar diameter vs wall, effective length, and how the arm/spline interfaces load the bar.","act":"When driver says 'same rate, different feel', log brand/lot/length/diameter and treat as a new baseline — do not assume rate stamp equals identical roll stiffness. Re-check ride heights and stop settings after any bar swap.","conf":"HIGH","m":["torsion bar feel","same rate different","bar feels different","torsion rate"],"cls":["sprint","micro","midget"]},{"id":"torsion-hidden-details","topic":"torsion_bars","src":"CSI-MMQB-Ep-244","fact":"Bar straightness, spline quality, and rate consistency bar-to-bar matter more for repeatable setup than chasing a slightly different advertised rate.","act":"Prefer a matched straight set over mixed-brand rates. Log cold rate check if available. If one corner keeps drifting setup, inspect bar for bent/out-of-round before changing geometry.","conf":"HIGH","m":["torsion bar quality","bar straightness","matched torsion","hidden details torsion"],"cls":["sprint","micro"]},{"id":"coilover-vs-torsion-platform","topic":"platform","src":"CSI-MMQB-Ep-246/247","fact":"Coil-overs and torsion bars both control roll and wheel rate — the chassis design, arm length, motion ratio, and available adjustment window decide which works better for a given car, not brand marketing.","act":"Do not recommend platform swap mid-season without chassis-specific data. For torsion cars: bars + stops + arms first. For coil cars: spring rate + shock + perch. Compare motion ratio before equating spring rate to torsion stamp.","conf":"HIGH","m":["coilover or torsion","coil over vs torsion","which works better torsion","convert to coil"],"cls":["sprint","micro","modified","latemodel"]},{"id":"shock-mount-orientation","topic":"shock_mounting","src":"CSI-MMQB-Ep-243","fact":"Shock body-up vs rod-up changes unsprung mass, heat path, reservoir behavior, and how adjusters are accessed — mount direction is a design choice that must match shock type and chassis brackets.","act":"Before changing valving for heat or fade, verify mount orientation matches manufacturer intent for that shock. Log body-up/rod-up in the car's shock inventory. Wrong orientation can look like a 'bad valve job'.","conf":"HIGH","m":["mount my shocks","shock orientation","body up","rod up","which way shock"],"cls":["sprint","micro","modified","latemodel","stock"]},{"id":"bump-rubber-in-cup","topic":"bump_rubbers","src":"CSI-CATALOG-NXT-Bump-Cup","fact":"A bump rubber on a flat washer can mushroom under load and lose progressive force; running in a cup keeps the rubber shape so force is applied where intended and increases effective drive.","act":"If running bump rubber for drive: use a cup that fully supports the rubber. Soft micro-cellular (e.g. ~25G class) for light cars/micros; harder stacks for heavier open cars. Log free gap to bump engagement.","conf":"HIGH","m":["bump rubber","bump cup","bump stop","drive off bump"],"cls":["micro","sprint","midget"]},{"id":"soft-bump-micro-305","topic":"bump_rubbers","src":"CSI-CATALOG-Speedthane-25G","fact":"Softest progressive bump packages suit light cars (600 micros, 305 wing) where a hard pack spikes load and kills grip.","act":"On micros/305: start soft progressive bump before stiff packs. If car spikes or pushes mid, soften pack or increase free gap before adding spring.","conf":"MEDIUM","m":["soft bump","25g bump","micro bump rubber","305 bump"],"cls":["micro","sprint"]},{"id":"shock-five-elements","topic":"shock_valving","src":"CSI-MMQB-Ep-209","fact":"A racing shock is a system of fluid, gas charge/volume, piston, shim stack (or equivalent valve), and seals/friction — changing one without logging the others breaks repeatability.","act":"When diagnosing shocks: check fluid level/condition, gas pressure, piston type, stack notes, and seal friction before blaming clicks. One variable at a time. Prefer dyno sheet if available (racer.wiki/shock-dyno).","conf":"HIGH","m":["what makes a shock","shock oil","shim stack","revalve","shock rebuild","gas pressure shock"],"cls":["all"]},{"id":"springs-before-shocks","topic":"shock_valving","src":"MULTI-BRAND-PHYSICS","fact":"Springs and geometry set the platform; shocks control the rate of weight transfer. Shock chasing before spring/geometry is stable wastes sessions.","act":"Order: geometry/ride heights → springs/bars → cross/wedge/PSI/stagger → shock clicks last. One click at a time; re-check after heat cycles.","conf":"HIGH","m":["shock clicks","change shocks first","shock chase","what first setup"],"cls":["all"]},{"id":"rebound-exit-control","topic":"shock_valving","src":"MULTI-BRAND-PHYSICS","fact":"Rebound controls how fast a corner unloads. Too much LR rebound can keep weight on the rear too long or make the car feel dead; too little can unload violently and go loose off.","act":"Loose off: try +1 RR rebound or -1 LR rebound depending on which corner is unloading wrong; confirm with tire temps. Tight off: free the unload path (softer rear rebound or geometry) before stacking bite.","conf":"HIGH","m":["rebound","shock rebound","loose off rebound","tight off rebound"],"cls":["all"]},{"id":"compression-rough-track","topic":"shock_valving","src":"MULTI-BRAND-PHYSICS","fact":"On rough tracks, excessive low-speed compression packs the car and reduces grip; too little lets the chassis hit stops and spike.","act":"Rough: soften low-speed compression one step on the abused corners first; protect with bump pack rather than max compression. Log which end is hammering.","conf":"HIGH","m":["rough track shock","compression rough","holes in track","packing the car"],"cls":["all"]},{"id":"slick-soft-platform","topic":"track_tuning","src":"MULTI-BRAND-PHYSICS","fact":"As the track slicks, mechanical grip falls — teams usually free the car (less cross/wedge, manage stagger growth, softer roll resistance) rather than stacking more bite that pushes mid.","act":"Slick feature plan: protect rear tires, re-check hot stagger, reduce cross/wedge if mid-push appears, shocks fine-tune only after PSI/stagger. Soft bump can add progressive drive without static wedge.","conf":"HIGH","m":["slick track","track went slick","rubbered up","feature slick"],"cls":["all"]},{"id":"heavy-tacky-more-platform","topic":"track_tuning","src":"MULTI-BRAND-PHYSICS","fact":"Heavy/tacky tracks reward platform control — cars can take more cross/wedge and stiffer roll control without immediate push.","act":"Heavy: RF wedge/cross before exotic spring swaps; stiffer front platform often calms entry. Plan a feature step-down as it dries.","conf":"HIGH","m":["heavy track","tacky","green track","moisture"],"cls":["all"]},{"id":"nxt-adjustability-logging","topic":"shock_valving","src":"CSI-CATALOG-NXT","fact":"Adjustable shocks and cockpit knobs only help if every change is logged with track phase (hot lap / heat / feature) and driver feel.","act":"Log click counts cold and after each session. One adjustment per run when possible. Cockpit knobs: pre-agree direction (+ = tighter/stiffer) with driver.","conf":"MEDIUM","m":["adjustable shock","cockpit knob","shock clicks","nxt shock","double adjustable"],"cls":["sprint","micro","midget","latemodel","modified"]},{"id":"dyno-before-guess","topic":"maintenance","src":"RACER-WIKI-TOOL","fact":"Two shocks with the same click count can dyno differently after rebuilds, heat, or wear. Force-velocity data beats guessing.","act":"Send racer to racer.wiki/shock-dyno for phone-based trend testing; pair with written click logs. Mismatched corners are a common fake 'setup' problem.","conf":"HIGH","m":["dyno shock","shock dyno","test my shocks","mismatched shocks"],"cls":["all"]},{"id":"torsion-retainer-security","topic":"maintenance","src":"CSI-CATALOG-Retainer","fact":"Threaded retainers prevent bar walk-out under load — a common failure mode when bars are swapped quickly between sessions.","act":"After every torsion bar change: confirm retainer torque/engagement both sides before rolling. Walk-out looks like sudden handling change mid-race.","conf":"HIGH","m":["torsion retainer","bar walked out","torsion bar came out","retainer"],"cls":["sprint","micro"]},{"id":"gas-shock-cover-heat","topic":"maintenance","src":"CSI-CATALOG-Cover","fact":"Covers protect from clay and debris but can trap heat on mono-tube gas shocks if airflow is blocked for long runs.","act":"If shocks fade late in features, check cover fit and body temp by corner. Fade often heat/oil aeration — not more click on compression.","conf":"MEDIUM","m":["shock cover","shock fade","shock heat","fading shocks"],"cls":["sprint","micro","midget"]},{"id":"neutral-brand-policy","topic":"policy","src":"RACER-WIKI-POLICY","fact":"Hunter learns technical principles from any manufacturer content. Product names are source tags, not endorsements.","act":"Answer with physics and process first. Mention brand-derived facts only with source tags. Compare concepts across brands when relevant.","conf":"HIGH","m":["best shock brand","should i buy csi","csi vs afco","best shocks"],"cls":["all"]},{"id":"springs-carry-shocks-control","topic":"platform","src":"CSI-MMQB-Ep-226","fact":"Springs support vehicle weight and set ride height/platform. Shocks control the RATE of weight transfer — they do not hold the car up. Stiffer spring rate and stiffer shock rate are not the same change.","act":"If the car sits wrong or rolls too much, change springs/bars first. If the car takes a set too fast/slow or packs/unloads wrong, change shocks. Never equate one click of shock to a spring rate step.","conf":"HIGH","m":["spring vs shock","shocks control","springs carry","stiffer shock","stiffer spring"],"cls":["all"]},{"id":"bump-gap-winged-lr","topic":"bump_rubbers","src":"CSI-MMQB-Ep-242","fact":"On modern winged dirt cars (micros through 410), LR bump rubber gap is a primary setup number. A quarter-inch gap error changes when progressive force engages and can look like a spring or wedge problem.","act":"Log free gap to bump engagement cold. Re-measure after ride height or spring changes. If drive-off changes with no wedge change, check bump gap before more bite.","conf":"HIGH","m":["bump gap","bumper rubber gap","gap measurement","left rear bump"],"cls":["micro","sprint","midget"]},{"id":"deflective-disc-shims","topic":"shock_valving","src":"CSI-MMQB-Ep-216","fact":"Most modern quarter midget, micro, midget, and sprint shocks use deflective disc (shim stack) valving rather than older bypass-spring designs — stacks let tuners hit specific force-velocity curves.","act":"When reading a dyno sheet, interpret low-speed vs high-speed separately. Shim stack changes are not the same as clicker changes on an adjustable. Log stack notes if you rebuild.","conf":"HIGH","m":["shim stack","deflective disc","what shims do","valving stack"],"cls":["micro","sprint","midget","quarter midget"]},{"id":"rebuild-interval-20-25","topic":"maintenance","src":"CSI-MMQB-Ep-215","fact":"A practical freshen interval for race shocks is about 20–25 race nights before service — sooner after a wreck or if fade/leakage appears.","act":"Ask how many race nights since last rebuild. If 20+ or post-crash, recommend service before more setup changes. Pair with racer.wiki/shock-dyno trend check.","conf":"HIGH","m":["when to rebuild shocks","freshen shocks","how often rebuild","service interval shocks"],"cls":["all"]},{"id":"bleed-air-out","topic":"maintenance","src":"CSI-MMQB-Ep-218","fact":"Air left in a shock during assembly ruins the force curve — proper bleeding is mandatory whether you have a bleed machine or a careful manual process.","act":"If a freshly rebuilt corner feels soft or inconsistent vs the others, suspect incomplete bleed before changing the stack. Dyno or side-by-side stroke test.","conf":"HIGH","m":["bleed shocks","air in shock","how to bleed","bleeding shocks"],"cls":["all"]},{"id":"torsion-arm-angles","topic":"torsion_bars","src":"CSI-MMQB-Ep-229","fact":"Torsion arm angles change effective wheel rate and how the bar loads through travel — arm angle is part of the rate the car feels, not just the stamped bar number.","act":"When logging torsion setup, record bar rate AND arm angle/position both sides. A rate swap without matching arm angles is an incomplete change.","conf":"HIGH","m":["torsion arm","arm angle","torsion angle","arm angles"],"cls":["sprint","micro","midget"]},{"id":"shaft-up-vs-body-up","topic":"shock_mounting","src":"CSI-MMQB-Ep-243","fact":"Shaft-up vs body-up mounting has real trade-offs: unsprung weight, heat, adjuster access, and how reservoirs behave. Choice must match the shock design and chassis brackets.","act":"Do not reverse mount direction casually to chase handling — verify manufacturer intent. Log orientation in shock inventory for every corner.","conf":"HIGH","m":["shaft up","body up","mount shocks","shock mounting direction"],"cls":["all"]},{"id":"basevalve-role","topic":"shock_valving","src":"CSI-MMQB-Ep-217","fact":"Base valves (where present) are a primary compression control path in the shock architecture — they work with the piston stack, not instead of it.","act":"Compression complaints after a rebuild: confirm basevalve/stack pairing was documented. Changing only the main piston stack may not match the original compression curve.","conf":"MEDIUM","m":["basevalve","base valve","compression valve"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"14QIK4Xv2m4-3","topic":"shock_valving","src":"CSI-MMQB-14QIK4Xv2m4","fact":"So last week we talked about um shock tuning um with Shim Stack and we'd mentioned if you really want to change the low speed, you would look at the bleed.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 215: What is Shock Bleed?. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["shim stack"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"14QIK4Xv2m4-6","topic":"shock_valving","src":"CSI-MMQB-14QIK4Xv2m4","fact":"And that will allow the shock to bleed by on both compression and rebound.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 215: What is Shock Bleed?. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["compression","rebound"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"14QIK4Xv2m4-7","topic":"shock_valving","src":"CSI-MMQB-14QIK4Xv2m4","fact":"So, keep in mind if you're going to increase the size of the bleed hole on this guy, um it is going to bleed more on compression and rebound.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 215: What is Shock Bleed?. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["compression","rebound"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"14QIK4Xv2m4-8","topic":"shock_valving","src":"CSI-MMQB-14QIK4Xv2m4","fact":"We'll show you what one of those looks like, but on that piston, we have bleed shims, and we can increase bleed on just the compression or just the rebound.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 215: What is Shock Bleed?. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["compression","rebound"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"14QIK4Xv2m4-12","topic":"shock_valving","src":"CSI-MMQB-14QIK4Xv2m4","fact":"Um, now sometimes there there'll be two or three bleeds um in this in this shock and uh when we get back to the dyno, I'll explain what the different uh bleeds would do.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 215: What is Shock Bleed?. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["dyno"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"1vKnTa3U2DQ-2","topic":"torsion_bars","src":"CSI-MMQB-1vKnTa3U2DQ","fact":"So gun drilling uh the torsion bar is made of a different material than the tubular bar and you can only get that material in solid.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 194: Torsion Bars: Gun Drilled versus Tubular. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"1vKnTa3U2DQ-5","topic":"maintenance","src":"CSI-MMQB-1vKnTa3U2DQ","fact":"They do dyno slightly different, but as far as the manufacturing process, um there's a much larger tolerance on a tubular bar.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 194: Torsion Bars: Gun Drilled versus Tubular. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["dyno"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"1vKnTa3U2DQ-6","topic":"track_tuning","src":"CSI-MMQB-1vKnTa3U2DQ","fact":"So, if this is turned down to a certain diameter to give you a certain spring rate, but then you have one bar that has um a 10,000 bigger hole through it because of the variance in the tubing.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 194: Torsion Bars: Gun Drilled versus Tubular. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["spring rate"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"1vKnTa3U2DQ-9","topic":"maintenance","src":"CSI-MMQB-1vKnTa3U2DQ","fact":"um a thou or two, you're not really going to notice a difference on the bar dyno, but if it's 10 thou difference, you will notice a spring rate difference.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 194: Torsion Bars: Gun Drilled versus Tubular. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["dyno","spring rate"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"1vKnTa3U2DQ-10","topic":"platform","src":"CSI-MMQB-1vKnTa3U2DQ","fact":"That's going to be a huge difference in spring rate as well.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 194: Torsion Bars: Gun Drilled versus Tubular. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["spring rate"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"1vKnTa3U2DQ-11","topic":"platform","src":"CSI-MMQB-1vKnTa3U2DQ","fact":"Um, as I mentioned in that video, different effective lengths will change the spring rate um as much or or or more than um different thicknesses of bars.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 194: Torsion Bars: Gun Drilled versus Tubular. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["spring rate"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"2XGtlqVtNYg-1","topic":"torsion_bars","src":"CSI-MMQB-2XGtlqVtNYg","fact":"Tonight we're going to talk about all the hidden details inside a torsion bar.","act":"Apply only if it matches current class/track. Source: MMQB 244: The Hidden Details Inside a Torsion Bar. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"2XGtlqVtNYg-2","topic":"torsion_bars","src":"CSI-MMQB-2XGtlqVtNYg","fact":"Um key dimensions, what's important with a torsion bar.","act":"Apply only if it matches current class/track. Source: MMQB 244: The Hidden Details Inside a Torsion Bar. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"2XGtlqVtNYg-3","topic":"bump_rubbers","src":"CSI-MMQB-2XGtlqVtNYg","fact":"Before we do, I want to answer a question that came in uh on a previous video, and that was how do we determine what what bump rubber to select for what application.","act":"Apply only if it matches current class/track. Source: MMQB 244: The Hidden Details Inside a Torsion Bar. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["bump rubber"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"2XGtlqVtNYg-5","topic":"bump_rubbers","src":"CSI-MMQB-2XGtlqVtNYg","fact":"One, how tall of a bump rubber can we run?","act":"Apply only if it matches current class/track. Source: MMQB 244: The Hidden Details Inside a Torsion Bar. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["bump rubber"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"2XGtlqVtNYg-6","topic":"bump_rubbers","src":"CSI-MMQB-2XGtlqVtNYg","fact":"Um and then two, what rate bump rubber should we be running for how much we're going to get into it.","act":"Apply only if it matches current class/track. Source: MMQB 244: The Hidden Details Inside a Torsion Bar. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["bump rubber"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"2XGtlqVtNYg-7","topic":"bump_rubbers","src":"CSI-MMQB-2XGtlqVtNYg","fact":"Obviously, your lighter, smaller cars like a micro sprint, you're going to run a softer bump rubber than you would on a sprint car.","act":"Apply only if it matches current class/track. Source: MMQB 244: The Hidden Details Inside a Torsion Bar. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["bump rubber"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"2XGtlqVtNYg-8","topic":"bump_rubbers","src":"CSI-MMQB-2XGtlqVtNYg","fact":"We're going to run a softer bump rubber on a 305 than we do with a 410 just because we have so much more downforce and wing speed with a 410 and we're squeezing the bump rubber a lot harder.","act":"Apply only if it matches current class/track. Source: MMQB 244: The Hidden Details Inside a Torsion Bar. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["bump rubber"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"2XGtlqVtNYg-9","topic":"bump_rubbers","src":"CSI-MMQB-2XGtlqVtNYg","fact":"Um so, those are some things when whenever somebody calls and they want to know what how tall of a bump rubber they should get and what rate, class, how much available travel they have, and what do they typically travel on a normal um race without a bump rubber.","act":"Apply only if it matches current class/track. Source: MMQB 244: The Hidden Details Inside a Torsion Bar. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["bump rubber"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"2XGtlqVtNYg-10","topic":"torsion_bars","src":"CSI-MMQB-2XGtlqVtNYg","fact":"Uh without further ado, we're going to jump into uh tonight's video and uh it's about critical dimensions on a torsion bar and what you should know.","act":"Apply only if it matches current class/track. Source: MMQB 244: The Hidden Details Inside a Torsion Bar. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"2XGtlqVtNYg-11","topic":"torsion_bars","src":"CSI-MMQB-2XGtlqVtNYg","fact":"Uh tonight we're going to talk about the critical measurements uh on a torsion bar.","act":"Apply only if it matches current class/track. Source: MMQB 244: The Hidden Details Inside a Torsion Bar. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"4C8NYl2OzUs-2","topic":"torsion_bars","src":"CSI-MMQB-4C8NYl2OzUs","fact":"But, without further ado, uh here is the first part of coil over springs versus torsion bars.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 246 Coil Overs or Torsion Bars Which One Works Better Part 1. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["coil over"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"4C8NYl2OzUs-6","topic":"torsion_bars","src":"CSI-MMQB-4C8NYl2OzUs","fact":"Um your coil spring is going to have less friction in the whole system than the torsion bar, a more linear motion and less mass being twisted.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 246 Coil Overs or Torsion Bars Which One Works Better Part 1. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"4C8NYl2OzUs-8","topic":"torsion_bars","src":"CSI-MMQB-4C8NYl2OzUs","fact":"Um drive drivers often say that torsion bar cars have more feel, they're easier to control on a slick race track and to put the power down smoother.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 246 Coil Overs or Torsion Bars Which One Works Better Part 1. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"4C8NYl2OzUs-11","topic":"torsion_bars","src":"CSI-MMQB-4C8NYl2OzUs","fact":"There's a lot you can do in a spring that you can't really do to a torsion bar.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 246 Coil Overs or Torsion Bars Which One Works Better Part 1. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"775Pooxo0oQ-1","topic":"torsion_bars","src":"CSI-MMQB-775Pooxo0oQ","fact":"And in tonight's topic is how to check your suspension if you don't have a shock dyno, a torsion bar dyno, a spring rater.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 169: How to check shocks with no dyno.. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["dyno","torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"775Pooxo0oQ-3","topic":"maintenance","src":"CSI-MMQB-775Pooxo0oQ","fact":"The next one came from a video where we explain how to read your shock dyno sheets.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 169: How to check shocks with no dyno.. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["dyno"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"775Pooxo0oQ-5","topic":"maintenance","src":"CSI-MMQB-775Pooxo0oQ","fact":"So, you don't have a shock dyno, but you want to put your best foot forward and make sure your shocks are as good as you can check them without one.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 169: How to check shocks with no dyno.. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["dyno"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"775Pooxo0oQ-6","topic":"shock_valving","src":"CSI-MMQB-775Pooxo0oQ","fact":"Um Upon that initial compression, if there's a dead spot, that could be a sign of uh a loss of some oil.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 169: How to check shocks with no dyno.. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["compression"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"775Pooxo0oQ-10","topic":"shock_mounting","src":"CSI-MMQB-775Pooxo0oQ","fact":"Um but if your if your bearings look good, you don't feel any catches when you're stroking the shock all the way down, you've spun the the shaft on the way out, the shaft doesn't appear to be bent, um that's about as good as you're going to get for checking it without a dyno.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 169: How to check shocks with no dyno.. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["dyno"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"775Pooxo0oQ-12","topic":"torsion_bars","src":"CSI-MMQB-775Pooxo0oQ","fact":"If you have a torsion bar car and you don't have a torsion bar dyno or you don't have anybody near you, um torsion bar dynos aren't as common.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 169: How to check shocks with no dyno.. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar","dyno"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"ak45oCvwlPU-2","topic":"torsion_bars","src":"CSI-MMQB-ak45oCvwlPU","fact":"And when you make a big shock change, now the spring rate of your spring or torsion bar um don't match the tire load anymore, and you've created another issue.","act":"Apply only if it matches current class/track. Source: MMQB 237 Why Big Changes Usually Make The Car Worse. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["spring rate","torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"AWwiRcvvQcA-1","topic":"shock_valving","src":"CSI-MMQB-AWwiRcvvQcA","fact":"So, uh, the first one came in, what does changing your high-speed compression look like, um, on a a shock dyno?","act":"Apply only if it matches current class/track. Source: MMQB Ep. 205: Deep Dive on Jacobs Ladders. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["compression","dyno"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"AWwiRcvvQcA-2","topic":"shock_valving","src":"CSI-MMQB-AWwiRcvvQcA","fact":"So rather than it being dead linear, it'll ramp up at a higher rate, and that's when we're uh increasing high-speed compression.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 205: Deep Dive on Jacobs Ladders. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["compression"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"AWwiRcvvQcA-6","topic":"track_tuning","src":"CSI-MMQB-AWwiRcvvQcA","fact":"Um, and typically a softer spring rate does help you get through bumps better than um, a really stiff, harsh one.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 205: Deep Dive on Jacobs Ladders. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["spring rate"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"AWwiRcvvQcA-7","topic":"shock_valving","src":"CSI-MMQB-AWwiRcvvQcA","fact":"And then we have to control that with the rebound in the shock.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 205: Deep Dive on Jacobs Ladders. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["rebound"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"b4hFEFCkaI8-2","topic":"shock_valving","src":"CSI-MMQB-b4hFEFCkaI8","fact":"Um, typically we start with our high flow piston uh with like a ring shim type setup um which will allows us to get um a little bit of a digressive type curve uh on the rebound side so we can have more low- speed rebound control without a ton of high-speed.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 212: All about Base Valves. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["rebound"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"bEJUWcGwCks-1","topic":"torsion_bars","src":"CSI-MMQB-bEJUWcGwCks","fact":"That's also a great indicator of if a torsion bar's went bad.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 198: Jets & Needles: What do they do?. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"bEJUWcGwCks-4","topic":"shock_valving","src":"CSI-MMQB-bEJUWcGwCks","fact":"If it's open, then it will adjust compression and rebound evenly.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 198: Jets & Needles: What do they do?. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["compression","rebound"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"bEJUWcGwCks-5","topic":"shock_valving","src":"CSI-MMQB-bEJUWcGwCks","fact":"But, most of the time there's a one-way check that is only going to allow the shock to add extra bleed in either the compression or rebound orientation depending on the way the check's in there.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 198: Jets & Needles: What do they do?. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["compression","rebound"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"bEJUWcGwCks-11","topic":"shock_valving","src":"CSI-MMQB-bEJUWcGwCks","fact":"Um so, each click of your shock is going to take a pretty big jump to get to that uh adjustment range.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 198: Jets & Needles: What do they do?. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["click"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"bEJUWcGwCks-12","topic":"shock_valving","src":"CSI-MMQB-bEJUWcGwCks","fact":"This needle, which let's say that's like a 2°, um it's going to have a much smaller adjustment range, but each click of your shock is going to be a much finer adjustment.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 198: Jets & Needles: What do they do?. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["click"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"BFQMqNqeIYE-2","topic":"track_tuning","src":"CSI-MMQB-BFQMqNqeIYE","fact":"Um, and then of course while you're down there you got to hit Speedway Indoor Karting, um, which has a really fun slick track and multi-level road course.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 144 Navigating the PRI Show. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["slick track"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"BFQMqNqeIYE-8","topic":"shock_mounting","src":"CSI-MMQB-BFQMqNqeIYE","fact":"Scott, uh adjustable front shock mounts, what are the downfalls of topping my front shocks out too early?","act":"Apply only if it matches current class/track. Source: MMQB Ep. 144 Navigating the PRI Show. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["adjustable"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"cdNFG9NzagY-3","topic":"maintenance","src":"CSI-MMQB-cdNFG9NzagY","fact":"Um I brought a dyno sheet in here, probably a little hard for everybody to see, but if you're familiar with our dyno sheets, um they all look the same.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 1 Our first attempt!. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["dyno"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"cdNFG9NzagY-4","topic":"maintenance","src":"CSI-MMQB-cdNFG9NzagY","fact":"And uh I'm just going to go through a couple of the things that are really important to know when you're reading that dyno sheet.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 1 Our first attempt!. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["dyno"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"cdNFG9NzagY-5","topic":"shock_valving","src":"CSI-MMQB-cdNFG9NzagY","fact":"So, um first of all, as you look at the actual graph, any the column on the left, any positive number, so from zero up in a positive direction is the compression side of the graph.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 1 Our first attempt!. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["compression"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"cdNFG9NzagY-6","topic":"shock_valving","src":"CSI-MMQB-cdNFG9NzagY","fact":"And then as you see the negative numbers in that left-hand column, that's the rebound side of the graph.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 1 Our first attempt!. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["rebound"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"cdNFG9NzagY-7","topic":"shock_valving","src":"CSI-MMQB-cdNFG9NzagY","fact":"Again, hard to see, I apologize, over to 3 in per second, and then go up and see what your rebound force is and what your compression force is.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 1 Our first attempt!. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["rebound","compression"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"cdNFG9NzagY-10","topic":"shock_valving","src":"CSI-MMQB-cdNFG9NzagY","fact":"We also include the 1-in rebound number with it on the left rear stuff.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 1 Our first attempt!. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["rebound"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"cdNFG9NzagY-11","topic":"maintenance","src":"CSI-MMQB-cdNFG9NzagY","fact":"So, on that dyno sheet as we had said, the graph is the first bit you get from us and then all of those numbers which are ultra confusing for everybody.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 1 Our first attempt!. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["dyno"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"cdNFG9NzagY-12","topic":"shock_valving","src":"CSI-MMQB-cdNFG9NzagY","fact":"So, what we want you to look at with those numbers, I apologize, I'm looking at it backwards, are these two columns, CA which is compression average, RA which is rebound average.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 1 Our first attempt!. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["compression","rebound"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"CqUVzo5JZFg-1","topic":"torsion_bars","src":"CSI-MMQB-CqUVzo5JZFg","fact":"Tonight is part two of coil over springs versus torsion bars and this is Monday morning quarterback episode 247.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 247: Coil Overs or Torsion Bars Which One Works Better Part 2. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["coil over"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"CqUVzo5JZFg-5","topic":"torsion_bars","src":"CSI-MMQB-CqUVzo5JZFg","fact":"Some drivers think it's easier to apply um the power to the racetrack um especially in slick conditions with a torsion bar versus a coil spring.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 247: Coil Overs or Torsion Bars Which One Works Better Part 2. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["torsion bar"],"cls":["sprint","micro","midget","modified","latemodel"]},{"id":"CqUVzo5JZFg-8","topic":"maintenance","src":"CSI-MMQB-CqUVzo5JZFg","fact":"Obviously, where they're connecting to your race car, so where your front axle position is, will change your overall spring rate, but um moving angles of shocks and stuff isn't going to change the the spring rate like it will with a coil spring.","act":"Apply only if it matches current class/track. Source: MMQB Ep. 247: Coil Overs or Torsion Bars Which One Works Better Part 2. Cross-check with springs/geometry before shock clicks.","conf":"MEDIUM","m":["spring rate"],"cls":["sprint","micro","midget","modified","latemodel"]}]; function hunterShockKnowledge(q, ctx) { if (!q || typeof q !== 'string') return null; var ql = q.toLowerCase(); // Intent gate — only fire on suspension/shock vocabulary if (!/shock|rebound|compression|valv|torsion|bump|shim|piston|dyno|coil\s*over|spring rate|mount.*(shock|body|rod)|adjustable shock|clicks?\b|gas pressure|shock oil|retainer|fade/i.test(q) && !/shock|torsion|bump|valv|rebound|compression/i.test(ql)) { // still allow brand-compare questions if (!/csi|afco|integra|penske|bilstein|qa1|best shock|shock brand/i.test(ql)) return null; } var hits = []; for (var i = 0; i < _HUNTER_SHOCK_RULES.length; i++) { var r = _HUNTER_SHOCK_RULES[i]; var score = 0; if (r.m && r.m.length) { for (var j = 0; j < r.m.length; j++) { if (r.m[j] && ql.indexOf(String(r.m[j]).toLowerCase()) !== -1) score += 3; } } // topic keywords if (r.topic && ql.indexOf(r.topic.replace(/_/g, ' ')) !== -1) score += 2; if (r.topic === 'torsion_bars' && /torsion|bar rate/.test(ql)) score += 2; if (r.topic === 'shock_valving' && /rebound|compression|valv|shim|click/.test(ql)) score += 2; if (r.topic === 'bump_rubbers' && /bump/.test(ql)) score += 2; if (r.topic === 'shock_mounting' && /mount|body up|rod up/.test(ql)) score += 2; if (r.topic === 'maintenance' && /rebuild|dyno|oil|fade|retainer/.test(ql)) score += 2; if (r.topic === 'policy' && /brand|best shock|vs |versus|buy/.test(ql)) score += 2; if (score > 0) hits.push({ r: r, score: score }); } if (!hits.length) return null; hits.sort(function(a, b) { return b.score - a.score; }); var top = hits.slice(0, 3).map(function(h) { return h.r; }); var primary = top[0]; var actionLines = top.map(function(r, idx) { return (idx + 1) + '. ' + r.act + (r.src ? ' [' + r.src + ']' : ''); }); var conf = primary.conf || 'MEDIUM'; return { algo: true, answer: primary.fact.length > 140 ? primary.fact.slice(0, 137) + '…' : primary.fact, detail: 'Neutral suspension knowledge · ' + top.length + ' rule' + (top.length > 1 ? 's' : '') + ' matched · not a brand ad', action: actionLines.join('\n') + ( /dyno|test my shock/i.test(q) ? '\n→ Phone dyno tool: https://racer.wiki/shock-dyno' : ''), confidence: conf, source: 'HUNTER_SHOCK_PACK', rules_used: top.map(function(r) { return r.id; }) }; } // ── 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); if (typeof hunterKartKnowledge === 'function') { var _kk = hunterKartKnowledge(q, fullCtx); if (_kk) return _kk; } if (typeof hunterShockKnowledge === 'function') { var _sk = hunterShockKnowledge(q, fullCtx); if (_sk) return _sk; } 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 _bbFanUid(){ var u=localStorage.getItem('bb_fan_uid'); if(!u){u='bb-'+Math.random().toString(36).slice(2,12);try{localStorage.setItem('bb_fan_uid',u);}catch(e){}} return u; } function _syncFanPhotoToBreaks(dataUrl,track,story){ if(!dataUrl)return; fetch('https://zmrouoqututfndplboyc.supabase.co/functions/v1/hero-api?fn=breaks&action=sync_photo',{ method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({ photo_b64:dataUrl, track_name:track||'Race Night', event_date:new Date().toISOString().slice(0,10), story_line:(story||'').slice(0,120), uid:_bbFanUid() }) }).then(function(r){return r.json();}).then(function(d){ if(d.ok&&d.photo&&d.photo.id){ try{localStorage.setItem('bb_fan_latest_photo_id',d.photo.id);}catch(e){} } }).catch(function(){}); } 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 track=(S.curTrack&&S.curTrack.name)||'Unknown';var photos=JSON.parse(localStorage.getItem('bb_fan_photos')||'[]');photos.unshift({data:reader.result,ts:Date.now(),track:track});if(photos.length>20)photos=photos.slice(0,20);try{localStorage.setItem('bb_fan_photos',JSON.stringify(photos));}catch(e){toast('Storage full');return;}_syncFanPhotoToBreaks(reader.result,track,'');toast('Photo saved — in tonight\'s break pool');};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
' +'
Your pit photo → printed hero card or wearable button. Gilco ships.
' +'' +''; el.appendChild(wrap); setTimeout(function(){ var lb=document.getElementById('hero-launch-btn'); if(lb)lb.onclick=function(){ var photos=[];try{photos=JSON.parse(localStorage.getItem('bb_fan_photos')||'[]');}catch(e){} if(!photos.length){toast('Snap a pit photo first (Race Night Photos above)');return;} var track=(S.curTrack&&S.curTrack.name)||''; var date=new Date().toISOString().slice(0,10); var story=''; var storyKeys=[];for(var si=0;si=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 v=variant||"empty"; 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 _ccIsStreetStockGarageClass(){ var c=S.cur||{},cls=String(c.class||c.car_class||"").toLowerCase(); var ct=typeof _getCarType==="function"?_getCarType(c):""; if(ct==="stock")return true; return /hobby stock|street stock|pure stock|factory stock|bomber|economy stock|sport compact|limited sport stock|wissota street|imca stock/i.test(cls)&&!/modified|late model|sport mod|micro|midget|sprint/i.test(cls); } 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 _ccPersonalityCoachHtml(){ if(typeof _personalityEligible!=='function'||!_personalityEligible())return ''; var CCR=window.CrewChiefReport; var vc=typeof _logVehicleContextForCrewChief==='function'?_logVehicleContextForCrewChief():{}; var line=CCR&&CCR.pickPersonalityCoachLine?CCR.pickPersonalityCoachLine(vc,''):null; if(!line)line=_personalityIsFilled(vc.chassis_personality)?'Car personality on file':'Log what makes YOUR car different — stiffness, frame style, quirks'; var filled=_personalityIsFilled(S.cur&&S.cur.chassis_personality); var s='
'; s+='
MY CAR · PERSONALITY
'+(filled?'On file':'Optional')+'
'; s+='
'+_ccEsc(line)+'
'; s+='
'; return s; } try{window._openPersonalityEditor=_openPersonalityEditor;}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+='
DATA CAPTURE · '+_ccEsc(payload.tierLabel||"Baseline")+'
'+Math.round(payload.completeness||0)+'% on file
'; 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 pcls=payload.pivotStale?'alert':(payload.pivotComplete?'good':'warn'); 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 pcls=pivot.stale?'alert':(pivot.complete?'good':'warn'); 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); } var _SCENARIO_STORE_KEY='bb_scenario_runs_v1'; function _loadScenarioStore(){ try{ var raw=JSON.parse(localStorage.getItem(_SCENARIO_STORE_KEY)||'{"version":2,"entries":[],"compares":[],"favorites":[]}'); if(window.CrewChiefReport&&window.CrewChiefReport.loadScenarioStore)return window.CrewChiefReport.loadScenarioStore(raw); return raw&&Array.isArray(raw.entries)?raw:{version:2,entries:[],compares:[],favorites:[]}; }catch(e){return{version:2,entries:[],compares:[],favorites:[]};} } function _saveScenarioStore(store){ try{localStorage.setItem(_SCENARIO_STORE_KEY,JSON.stringify(store));}catch(e){} } function _scenarioStoreForCurrentCar(){ var store=_loadScenarioStore(); var carId=(S.cur&&S.cur.id)||'local'; if(window.CrewChiefReport&&window.CrewChiefReport.filterScenarioStoreForCar)return window.CrewChiefReport.filterScenarioStoreForCar(store,carId); return{version:2,entries:(store.entries||[]).filter(function(e){return String(e.carId||'local')===String(carId);}),compares:store.compares||[],favorites:store.favorites||[]}; } function _ccScenarioContext(out,report){ var vc=typeof _logVehicleContextForCrewChief==='function'?_logVehicleContextForCrewChief():{}; var tc=(report&&report.track_condition)||{}; var trackState=tc.track_state||tc.state||null; var profile=(out&&out.profile)||''; var CCR=window.CrewChiefReport; if(CCR&&CCR.captureScenarioContext)return CCR.captureScenarioContext(vc,trackState,profile); return{trackState:trackState}; } function _ccOutcomeColor(outcome){ if(outcome==='helped')return'#39d0a3'; if(outcome==='hurt')return'#f87171'; return'#94a3b8'; } function _ccOutcomeTone(outcome){ if(outcome==='helped')return'good'; if(outcome==='hurt')return'alert'; return'muted'; } function _ccOutcomeLabel(outcome){ if(outcome==='helped')return'Helped'; if(outcome==='hurt')return'Hurt'; return'Unclear'; } function _ccRenderOutcomeButtons(mountId,entryId,prefix){ return '
' +'How did it go?' +'' +'' +'' +'
'; } function _ccScenarioEligible(out){ if(!out||!out.scenarioPanel||!out.scenarioPanel.eligible)return false; if(_ccIsGrassrootsGarageClass()&&!_ccIsStreetStockGarageClass())return false; if(_ccIsAdvancedGarageClass()&&!_ccIsGrassrootsGarageClass())return true; if(_ccIsStreetStockGarageClass()&&out.outputDepth==='mid')return true; return false; } function _ccRunScenarioQuery(query,out,report){ var CCR=window.CrewChiefReport; if(!CCR||!CCR.evaluateScenario)return null; var vc=typeof _logVehicleContextForCrewChief==='function'?_logVehicleContextForCrewChief():{}; var tc=(report&&report.track_condition)||{}; var trackState=tc.track_state||tc.state||null; var profile=(out&&out.profile)||'maxim_sprint_winged'; return CCR.evaluateScenario({query:query,vc:vc,trackState:trackState,profile:profile,sessionLog:vc.session_log||[]}); } function _ccRenderScenarioPanel(out,report,mountId){ if(!_ccScenarioEligible(out))return ''; var CCR=window.CrewChiefReport; var panel=out.scenarioPanel||{}; var vc=typeof _logVehicleContextForCrewChief==='function'?_logVehicleContextForCrewChief():{}; var chips=panel.quickChips||(CCR&&CCR.resolveScenarioQuickChips&&CCR.resolveScenarioQuickChips(out.profile||'',vc))||(CCR&&CCR.SCENARIO_QUICK_CHIPS)||[]; var suggestions=panel.suggestions||out.scenarioSuggestions||[]; var compareHistory=panel.compareHistory||[]; var favorites=panel.favoriteComparisons||[]; var recent=panel.recentScenarios||(function(){var cs=_scenarioStoreForCurrentCar();return(cs.entries||[]).slice(-3).reverse();})(); var pending=panel.pendingOutcomes||[]; var outcomeSummaries=panel.outcomeSummaries||[]; var compareHint=panel.compareHint||'Compare with "X vs Y"'; var h='
'; h+='
DECISION SUPPORT · SCENARIO
'; h+='
'+(panel.contextLine?_ccEsc(panel.contextLine)+' · ':'')+'Directional reads from your logged feel — one lever per run. '+_ccEsc(compareHint)+'.
'; if(pending.length){ h+='
TAG RECENT CHANGES
'; pending.forEach(function(e){ var lbl=(e.parsed&&e.parsed.label)||e.query||'change'; h+='
'; h+='
'+_ccEsc(lbl)+' · applied
'; h+=_ccRenderOutcomeButtons(mountId,e.id,'pending'); h+='
'; }); } if(outcomeSummaries.length){ h+='
YOUR OUTCOME MEMORY
'; h+='
'; outcomeSummaries.forEach(function(s){ h+='
'+_ccEsc(s.line||'')+'
'; }); h+='
'; } if(out.scenarioHighlights&&out.scenarioHighlights.length){ h+='
[YOUR PLAN] '+out.scenarioHighlights.map(function(x){return _ccEsc(x.label||'');}).join(' · ')+'
'; } if(out.scenarioInfluenceNote){ h+='
'+_ccEsc(out.scenarioInfluenceNote)+'
'; } if(suggestions.length){ h+='
SUGGESTED FOR YOUR CAR
'; h+='
'; suggestions.slice(0,4).forEach(function(s){ var lbl=s.type==='compare'?'⇄ ':'→ '; var sub=s.reason?String(s.reason).slice(0,120):''; var eff=s.preview&&s.preview.effect&&s.preview.effect.label?s.preview.effect.label:(s.preview&&s.preview.confidence&&s.preview.confidence.label?s.preview.confidence.label:''); var memBadge=s.memoryInfluenced?' [MEMORY]':''; h+=''; }); h+='
'; } if(favorites.length){ h+='
SAVED COMPARES
'; h+='
'; favorites.slice(0,4).forEach(function(f){ h+=''; }); h+='
'; } if(compareHistory.length){ h+='
RECENT COMPARES
'; h+='
'; compareHistory.slice(0,5).forEach(function(c){ var pick=c.recommendedDirection||c.winnerLabel||''; h+='
'; }); h+='
'; } if(recent.length){ h+='
RECENT SCENARIOS
'; h+='
'; recent.forEach(function(e){ var lbl=(e.parsed&&e.parsed.label)||e.query||'scenario'; var oc=e.outcome?_ccOutcomeLabel(e.outcome):''; var ocTone=e.outcome?_ccOutcomeTone(e.outcome):''; h+='
'; h+=''+_ccEsc(lbl)+(e.applied?' · applied':'')+(oc?' · '+_ccEsc(oc)+'':'')+''; if(e.id){ h+=''; h+=''; } h+='
'; }); h+='
'; } h+='
CUSTOM · QUICK CHIPS
'; h+='
'; chips.slice(0,8).forEach(function(c){ h+=''; }); h+='
'; h+='
'; h+='
Tap a suggestion, chip, or type your own move.
'; h+='
'; return h; } function _ccWireScenarioPanel(out,report,mountId){ var root=document.getElementById(mountId); if(!root||!_ccScenarioEligible(out))return; var input=document.getElementById(mountId+'-input'); var result=document.getElementById(mountId+'-result'); var runBtn=document.getElementById(mountId+'-run'); if(!input||!result||!runBtn)return; var _lastScenarioResult=null; var _lastAppliedEntryId=null; function wireOutcomeButtons(scope){ if(!scope)return; scope.querySelectorAll('.cc-outcome-btn').forEach(function(btn){ btn.onclick=function(){ var CCR=window.CrewChiefReport; if(!CCR||!CCR.recordScenarioOutcome)return; var entryId=btn.getAttribute('data-entry'); var outcome=btn.getAttribute('data-outcome'); var store=_loadScenarioStore(); var carId=(S.cur&&S.cur.id)||'local'; var ctx=_ccScenarioContext(out,report); store=CCR.recordScenarioOutcome(store,carId,entryId,outcome,ctx); _saveScenarioStore(store); toast('Outcome saved — suggestions will learn from this'); btn.parentElement.innerHTML='Tagged: '+_ccOutcomeLabel(outcome)+''; }; }); scope.querySelectorAll('.cc-outcome-edit').forEach(function(btn){ btn.onclick=function(){ var entryId=btn.getAttribute('data-entry'); var wrap=document.createElement('div'); wrap.innerHTML=_ccRenderOutcomeButtons(mountId,entryId,'edit'); btn.parentElement.appendChild(wrap); wireOutcomeButtons(wrap); }; }); scope.querySelectorAll('.cc-outcome-del').forEach(function(btn){ btn.onclick=function(){ var CCR=window.CrewChiefReport; if(!CCR||!CCR.deleteScenarioEntry)return; var store=_loadScenarioStore(); var carId=(S.cur&&S.cur.id)||'local'; store=CCR.deleteScenarioEntry(store,carId,btn.getAttribute('data-entry')); _saveScenarioStore(store); toast('Removed from history'); btn.closest('div').remove(); }; }); } wireOutcomeButtons(root); function renderResult(res){ _lastScenarioResult=res&&res.ok&&!res.compare?res:null; if(!res||!res.ok){ result.innerHTML=_ccPanelBox('warn','Could not run scenario',[res&&res.error?res.error:'Unknown error','Try one lever: bar, wing, LR weight, cross, stagger, psi, wedge, or compound.','Compare: stagger out 1/4 vs cross up 0.3'],null); return; } if(res.compare&&res.a&&res.b){ var leftLines=[res.a.parsed&&res.a.parsed.label,res.a.effect&&res.a.effect.label,res.a.effectSummary]; if(res.a.helps&&res.a.helps.length)leftLines.push('Helps: '+res.a.helps.slice(0,2).join(' · ')); if(res.a.hurts&&res.a.hurts.length)leftLines.push('Watch: '+res.a.hurts.slice(0,2).join(' · ')); var rightLines=[res.b.parsed&&res.b.parsed.label,res.b.effect&&res.b.effect.label,res.b.effectSummary]; if(res.b.helps&&res.b.helps.length)rightLines.push('Helps: '+res.b.helps.slice(0,2).join(' · ')); if(res.b.hurts&&res.b.hurts.length)rightLines.push('Watch: '+res.b.hurts.slice(0,2).join(' · ')); var pickLines=[res.recommendedDirection?('Recommended: '+res.recommendedDirection):res.winnerLabel,res.recommendedReasoning||res.summary]; if(res.confidence&&res.confidence.label)pickLines.push('Confidence: '+res.confidence.label+(res.confidenceExplanation?' — '+res.confidenceExplanation:'')); result.innerHTML='
' +_ccPanelBox(res.a.effect&&res.a.effect.color?res.a.effect.color:'info','Option A',leftLines.filter(Boolean),null) +_ccPanelBox(res.b.effect&&res.b.effect.color?res.b.effect.color:'info','Option B',rightLines.filter(Boolean),null) +'
'+_ccPanelBox('ok',res.winner==='tie'?'Close call':'Recommended direction',pickLines.filter(Boolean),_CC_DISCIPLINE+' · one lever per run') +''; var carId=(S.cur&&S.cur.id)||'local'; if(window.CrewChiefReport&&window.CrewChiefReport.recordCompareRun){ var store=_loadScenarioStore(); store=window.CrewChiefReport.recordCompareRun(store,res,carId); _saveScenarioStore(store); } var favBtn=document.getElementById(mountId+'-fav'); if(favBtn&&window.CrewChiefReport&&window.CrewChiefReport.toggleCompareFavorite){ favBtn.onclick=function(){ var store=_loadScenarioStore(); store=window.CrewChiefReport.toggleCompareFavorite(store,carId,res.query); _saveScenarioStore(store); favBtn.textContent='Saved'; favBtn.disabled=true; }; } return; } var kind=res.effect&&res.effect.color?res.effect.color:'info'; var lines=[res.parsed&&res.parsed.label?res.parsed.label:res.query,res.effectSummary,res.rationale]; if(res.helps&&res.helps.length)lines.push('Likely helps: '+res.helps.join(' · ')); if(res.hurts&&res.hurts.length)lines.push('Likely hurts: '+res.hurts.join(' · ')); if(res.confidence&&res.confidence.label)lines.push('Confidence: '+res.confidence.label+(res.confidence.hint?' — '+res.confidence.hint:'')); if(res.confidenceExplanation)lines.push(res.confidenceExplanation); if(res.risks&&res.risks.length)lines.push('Risks: '+res.risks.join(' · ')); if(res.tradeoffs&&res.tradeoffs.length)lines.push('Trade-offs: '+res.tradeoffs.join(' · ')); if(res.tags&&res.tags.length)lines.push('Sources: '+res.tags.map(function(t){return '['+t+']';}).join(' ')); var box=_ccPanelBox(kind,res.effect&&res.effect.label?res.effect.label:'Scenario read',lines.filter(Boolean),_CC_DISCIPLINE+' · one lever per run'); result.innerHTML=box+'
Tap after the change is on the car, then tag Helped / Hurt / Unclear so suggestions improve.
'; var applyBtn=document.getElementById(mountId+'-apply'); if(applyBtn&&window.CrewChiefReport&&window.CrewChiefReport.recordScenarioRun){ applyBtn.onclick=function(){ if(!_lastScenarioResult||!_lastScenarioResult.parsed){toast('Run a scenario first');return;} var store=_loadScenarioStore(); var carId=(S.cur&&S.cur.id)||'local'; var ctx=_ccScenarioContext(out,report); var CCR=window.CrewChiefReport; if(CCR.markScenarioApplied){ store=CCR.markScenarioApplied(store,carId,_lastScenarioResult.parsed,ctx); var hasApplied=(store.entries||[]).some(function(e){return e.carId===carId&&e.parsed&&e.parsed.lever===_lastScenarioResult.parsed.lever&&e.applied;}); if(!hasApplied){ store=CCR.recordScenarioRun(store,{carId:carId,trackState:(report&&report.track_condition&&report.track_condition.track_state)||null,query:_lastScenarioResult.query,parsed:_lastScenarioResult.parsed,effect:_lastScenarioResult.effect,confidence:_lastScenarioResult.confidence,applied:true,applyContext:ctx}); } }else{ store=CCR.recordScenarioRun(store,{carId:carId,trackState:(report&&report.track_condition&&report.track_condition.track_state)||null,query:_lastScenarioResult.query,parsed:_lastScenarioResult.parsed,effect:_lastScenarioResult.effect,confidence:_lastScenarioResult.confidence,applied:true,applyContext:ctx}); } _lastAppliedEntryId=null; for(var i=(store.entries||[]).length-1;i>=0;i--){ var ent=store.entries[i]; if(String(ent.carId||'local')===String(carId)&&ent.applied&&ent.parsed&&ent.parsed.lever===_lastScenarioResult.parsed.lever){_lastAppliedEntryId=ent.id;break;} } _saveScenarioStore(store); applyBtn.textContent='Linked for this car'; applyBtn.disabled=true; var outcomeEl=document.getElementById(mountId+'-outcome'); if(outcomeEl&&_lastAppliedEntryId){ outcomeEl.innerHTML=_ccRenderOutcomeButtons(mountId,_lastAppliedEntryId,'apply'); wireOutcomeButtons(outcomeEl); }else if(outcomeEl){ outcomeEl.textContent='Linked — regenerate report, then tag outcome from TAG RECENT CHANGES above.'; } toast('Linked — regenerate report to see scenario tags'); }; } } function runQuery(q){ var res=_ccRunScenarioQuery(q,out,report); renderResult(res); } runBtn.onclick=function(){runQuery(input.value);}; input.onkeydown=function(e){if(e.key==='Enter')runQuery(input.value);}; root.querySelectorAll('.cc-sc-chip,.cc-sc-suggest,.cc-sc-fav,.cc-sc-hist').forEach(function(btn){ btn.onclick=function(){ input.value=btn.getAttribute('data-q')||''; runQuery(input.value); }; }); } function _ccGenerateRecommendations(report){ if(_ccLastRecsFromApi)return _ccLastRecsFromApi; if(!window.CrewChiefReport||!window.CrewChiefReport.recommend)return null; return window.CrewChiefReport.recommend(report,{scenarioStore:_loadScenarioStore(),carId:(S.cur&&S.cur.id)||'local'}); } function _ccRecTagAndColor(r){ var CCR=window.CrewChiefReport; if(CCR&&CCR.recTag){ var raw=CCR.recTag(r)||'PRIOR'; var tag=String(raw).replace(/^\[|\]$/g,''); var tone='prior'; if(/YOUR DATA|TRY FIRST|BOOK|HIGH/i.test(tag))tone='good'; else if(/STOCK|BASICS|RACEY/i.test(tag))tone='warn'; else if(/SCENARIO/i.test(tag))tone='scenario'; else if(/BUILDER|ADV CHASSIS|PRINCIPLE|VALIDATE|MODERATE|PERSONALITY/i.test(tag))tone='builder'; else if(/EVOLUTION|PLAN AHEAD|TRACK SNAP/i.test(tag))tone='intel'; else if(/A-B-A|CONFLICT/i.test(tag))tone='aba'; else if(/REF|SCAFFOLD|PUBLIC|PRIOR/i.test(tag))tone='ref'; else if(r.confidence==='HIGH')tone='good'; else if(r.confidence==='MODERATE')tone='moderate'; return {tag:tag,tone:tone}; } 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 tone=isPrior?(isScaffold?'ref':(isChassis?'builder':(isPub?'intel':'prior'))):(r.confidence==='HIGH'?'good':r.confidence==='MODERATE'?'moderate':'warn'); if(isPrior&&isSnap){tag='TRACK SNAP';tone='intel';} else if(isPrior&&isConflict){tag='CONFLICT';tone='conflict';} else if(isPrior&&isBuilder){tag='BUILDER · '+((r.flags.builder_mfr||'SIGNAL').toUpperCase());tone='builder';} else if(isPrior&&isAbaDisc){tag=r.lever&&r.lever.indexOf('aba_interpret')===0?'A-B-A · READ':'A-B-A';tone='aba';} else if(isPrior&&isLmEvo){tag=r.flags.advanced_forward_plan?'PLAN AHEAD · LM':'EVOLUTION · LM';tone='intel';} else if(isPrior&&isEvo){tag=r.flags.advanced_forward_plan?'PLAN AHEAD':'EVOLUTION · ADV';tone='intel';} if(r.flags&&r.flags.try_first){tag='TRY FIRST · '+tag;tone='good';} return {tag:tag,tone:tone}; } 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 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 isStock=_ccIsStreetStockGarageClass(); var isMidTier=isStock||(!isAdv&&!_ccIsGrassrootsGarageClass()&&out.outputDepth==='mid'); var s=''; if(out.tryFirst&&out.tryFirst.action){ var tfHeadline=out.tryFirstHeadline||(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:(isStock?'One lever · log hot psi after lap 8':'Race-night priority · one lever'); s+=_ccPanelBox("ok","Try first tonight"+tierBadge,[ tfHeadline, 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||isStock||isMidTier)&&out.dataQualityNote&&out.dataQualityNote.headline){ var dqLines=[out.dataQualityNote.headline]; if(out.dataQualityNote.gaps&&out.dataQualityNote.gaps.length){ dqLines.push('Add: '+out.dataQualityNote.gaps.join(' · ')); } 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"); } var scenarioMountId='cc-scenario-'+Date.now(); if(_ccScenarioEligible(out)){ s+=_ccRenderScenarioPanel(out,report,scenarioMountId); } s+='
Mode: '+_ccEsc(out.mode)+' · '+_ccEsc(out.summary||"")+_ccBc39LiveChip()+'
'; if((isAdv||isStock)&&(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){ var rg=out.recommendationGroups; if(rg.bookNow&&rg.bookNow.length){ s+='
BOOK TONIGHT
'; rg.bookNow.forEach(function(r){s+=_ccRecCard(r);}); } if(rg.validate&&rg.validate.length){ s+='
VALIDATE NEXT
'; rg.validate.forEach(function(r){s+=_ccRecCard(r);}); } if(rg.canWait&&rg.canWait.length){ s+='
CAN WAIT · REFERENCE
'; s+='
'; rg.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;})); if(out.recommendationGroups){ (out.recommendationGroups.bookNow||[]).concat(out.recommendationGroups.validate||[]).forEach(function(r){shown.add(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; if(_ccScenarioEligible(out))_ccWireScenarioPanel(out,report,scenarioMountId); } 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+=_ccPersonalityCoachHtml(); 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.className="cc-wrap"; var ccBanner=null; if(_logIsBc39Mode()){ ccBanner=document.createElement("div"); ccBanner.className="cc-bc39-banner"; 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); if(typeof _maybePromptPersonality==='function')setTimeout(_maybePromptPersonality,1200); var statusEl=document.createElement("div"); statusEl.className="cc-meta"; 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.className="cc-bar"; var sel=document.createElement("select");sel.id="cc-block-sel";sel.className="cc-select"; var genBtn=document.createElement("button");genBtn.type="button";genBtn.textContent="Generate";genBtn.className="cc-gen"; 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.className="cc-export";}); 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.className="cc-meta"; 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.className="cc-tabs"; 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.className="cc-tab"+(t.id===activeTab?" on":""); b.onclick=function(){activeTab=t.id;Object.keys(tabBtns).forEach(function(k){tabBtns[k].className="cc-tab"+(k===activeTab?" on":"");});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}; function _bbTryInstallScreenshotHooks(attempt){ if(!window._bbScreenshotCapture)return; if(typeof window._bbInstallScreenshotHooks!=='function'){ if((attempt||0)<80)setTimeout(function(){_bbTryInstallScreenshotHooks((attempt||0)+1);},100); return; } window._bbInstallScreenshotHooks({tabBtns:tabBtns,outRecs:outRecs,mount:mount}); } _bbTryInstallScreenshotHooks(0); 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, chassis_personality:(function(){ var p=c.chassis_personality||null; if(typeof p==='string'){try{p=JSON.parse(p);}catch(e){p=null;}} return p; })(), 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, prevPos: null, awayFromSf: false, sfArmed: 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 _plDeriveSpeedMph(lat, lon, ts) { var pl = _pocketLogger; if (!pl.prevPos || !ts) return 0; var dt = (ts - pl.prevPos.ts) / 1000; if (dt <= 0 || dt > 8) return 0; var dist = _plHaversineM(pl.prevPos.lat, pl.prevPos.lon, lat, lon); return _plMpsToMph(dist / dt); } function _plSeedAwayFromSf(lat, lon) { var pl = _pocketLogger; if (pl.sf && _plHaversineM(lat, lon, pl.sf.lat, pl.sf.lon) > _plSfRadiusM() * 2.5) { pl.awayFromSf = true; } } 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; _pocketLogger.sfArmed = false; } 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, maxLonG: 0, maxBrakeG: 0, maxRpm: 0, sfAuto: false, motionSamples: [] }; } 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, gps: [] }; } function _plClassifyLap(lap, best) { if (lap._inPit) return 'pit'; 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.type === 'green' && navigator.vibrate) { try { navigator.vibrate([40, 30, 80, 30, 120]); } catch (e) {} } } if (lap.maxSpeed > sess.maxSpeed) sess.maxSpeed = lap.maxSpeed; if (lap.latG > sess.maxLatG) sess.maxLatG = lap.latG; if (lap.lonG > (sess.maxLonG || 0)) sess.maxLonG = lap.lonG; if (lap.brakeG > (sess.maxBrakeG || 0)) sess.maxBrakeG = lap.brakeG; 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 _plHandleSfCross(now, spd) { var pl = _pocketLogger; if (!pl.sfArmed) { pl.curLap.start = now; pl.lapPeakG = { lat: 0, lon: 0, brake: 0, accel: 0 }; pl.sfArmed = true; toast('S/F armed — lap 1 timing starts now'); pl.lastSfCross = now; pl.awayFromSf = false; return; } if (pl.curLap && pl.curLap.speedCnt >= 2 && spd >= 8) { _plFinishLap(); } else if (pl.curLap) { pl.curLap.start = now; pl.lapPeakG = { lat: 0, lon: 0, brake: 0, accel: 0 }; toast('Lap timer reset at S/F — need more speed for a counted lap'); } pl.lastSfCross = now; pl.awayFromSf = false; } function _plOnPosition(pos) { var pl = _pocketLogger; if (!pl.active || !pl.session) return; var lat = pos.coords.latitude; var lon = pos.coords.longitude; var now = Date.now(); var spd = pos.coords.speed != null ? _plMpsToMph(pos.coords.speed) : _plDeriveSpeedMph(lat, lon, now); if (spd < 0) spd = 0; pl.prevPos = pl.lastPos; 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() && spd >= 6) { _plHandleSfCross(now, spd); } var lap = pl.curLap; if (!lap) return; if (spd > lap.maxSpeed) lap.maxSpeed = spd; if (spd > 0) { lap.speedSum += spd; lap.speedCnt++; } if (!lap.gps) lap.gps = []; if (lap.gps.length < 72) { lap.gps.push({ t: now - lap.start, s: Math.round(spd), d: Math.round(dist) }); } var gpsDt = pl.prevPos && pl.prevPos.ts ? (now - pl.prevPos.ts) / 1000 : 1; if (gpsDt > 0 && gpsDt < 10) { if (spd < 15) lap.lowSpeedSec += gpsDt; else lap.lowSpeedSec = Math.max(0, lap.lowSpeedSec - gpsDt * 0.25); } 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.accelerationIncludingGravity || 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; if (pl.motionSamples && pl.motionSamples.length) { pl.session.motionSamples = pl.motionSamples.slice(-2400); } 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 > 2400) 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); // Live readout for budget racers try { var ro = document.getElementById('pl-live-readout'); if (ro && pl.session && pl.curLap) { var lt = ((Date.now() - pl.curLap.start) / 1000).toFixed(1); var cg = (pl.peakG && pl.peakG.lat) ? pl.peakG.lat.toFixed(2) : '0'; ro.textContent = 'Lap: ' + lt + 's | LatG: ' + cg + 'g'; } } catch(e){} }, 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.sf = _plLoadSf(); pl.pit = _plLoadPit(); 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.sfArmed = pl.session.laps.length > 0; pl.lastSfCross = 0; pl.awayFromSf = false; pl.prevPos = null; pl.motionSamples = pl.session.motionSamples || pl.motionSamples || []; pl.watchId = navigator.geolocation.watchPosition( function(pos) { if (!pl.awayFromSf && pl.sf) _plSeedAwayFromSf(pos.coords.latitude, pos.coords.longitude); _plOnPosition(pos); }, function(err) { toast('GPS: ' + (err.message || 'error')); }, { enableHighAccuracy: true, maximumAge: 400, timeout: 12000 } ); navigator.geolocation.getCurrentPosition(function(pos) { _plSeedAwayFromSf(pos.coords.latitude, pos.coords.longitude); _plOnPosition(pos); }, function() {}, { enableHighAccuracy: true, timeout: 8000 }); 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.session = null; 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(); var resumed = pl.session && !pl.session.ended && pl.session.laps; if (!resumed) { pl.session = _plNewSession(); pl.session.phase = phase; pl.curLap = _plNewLap(1); pl.sfArmed = false; } else { pl.curLap = _plNewLap((pl.session.laps.length || 0) + 1); pl.sfArmed = pl.session.laps.length > 0; pl.motionSamples = pl.session.motionSamples || pl.motionSamples || []; toast('Resumed session — ' + pl.session.laps.length + ' laps logged'); } pl._nextPhase = phase; if (!resumed) pl.session.phase = phase; pl.active = true; pl.awayFromSf = false; pl.lastSfCross = 0; pl.prevPos = null; 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) { if (!pl.awayFromSf && pl.sf) _plSeedAwayFromSf(pos.coords.latitude, pos.coords.longitude); _plOnPosition(pos); }, function(err) { toast('GPS: ' + (err.message || 'error')); }, { enableHighAccuracy: true, maximumAge: 400, timeout: 12000 } ); navigator.geolocation.getCurrentPosition(function(pos) { _plSeedAwayFromSf(pos.coords.latitude, pos.coords.longitude); _plOnPosition(pos); }, function() {}, { enableHighAccuracy: true, timeout: 8000 }); 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(resumed ? 'Pocket logger resumed' : 'Pocket logger ON — drive away from S/F, then cross to arm lap 1'); _plRenderUI(); _phoneLoggerUIOn(); } function _plStop() { var pl = _pocketLogger; if (!pl.active) return; if (typeof _plStopHardware === 'function') _plStopHardware(); else { _plClearMotionInterval(); if (pl.sfArmed && pl.curLap && pl.curLap.speedCnt >= 2) _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 — drive ~150 ft from S/F, cross once to arm lap 1, then cross again to log.
'; 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); // Phase 3 start: real-time memory fusion hint in logger if (typeof _getTrackMemorySummary === 'function' && S.curTrack) { var mm = _getTrackMemorySummary(S.curTrack.name); if (mm && mm.avgStagger) { var mh = document.createElement('div'); mh.style.cssText = 'font-size:8px;color:#c8960a;margin-bottom:4px'; mh.textContent = 'Your history at track: avg ' + mm.avgStagger + '" stagger'; card.appendChild(mh); } } if (on && pl.curLap) { var curMs = Date.now() - pl.curLap.start; var curS = (curMs / 1000).toFixed(1); var cN = pl.curLap.n || (sess && sess.laps ? sess.laps.length + 1 : 1); var lg = pl.lapPeakG || {}; var curBox = document.createElement('div'); curBox.style.cssText = 'background:#0a0a0a;border:1px solid rgba(208,25,14,.45);padding:8px 10px;margin-bottom:8px;font-family:var(--mono);font-size:11px;line-height:1.1'; curBox.innerHTML = 'CUR LAP L' + cN + '' + '' + curS + 's' + 'LAT ' + (lg.lat ? lg.lat.toFixed(2) : '--') + 'g · LON ' + (lg.lon ? lg.lon.toFixed(2) : '--') + 'g · BRK ' + (lg.brake ? lg.brake.toFixed(1) : '--') + ''; card.appendChild(curBox); } 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); } if (sess && sess.laps && sess.laps.length && !on && typeof _trackMemoryPlayUrl === 'function') { var g = document.createElement('div'); var t = (typeof S !== 'undefined' && S.curTrack) ? S.curTrack : null; var tid = t ? (t.id || '') : ''; var tnm = t ? t.name : (sess.track || ''); var gurl = _trackMemoryPlayUrl(tid, tnm); g.style.cssText = 'margin:4px 0 8px;padding:6px 8px;background:rgba(255,45,26,.08);border:1px solid rgba(255,45,26,.3);font-family:var(--head);font-size:11px;text-align:center'; g.innerHTML = 'PRACTICE THE LINE IN TRACK MEMORY →'; card.appendChild(g); } 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 window.location.href = '/engine-ear.html'; }; btns.appendChild(ear); var standalones = document.createElement('div'); standalones.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;padding-top:8px;border-top:1px solid rgba(255,255,255,.06)'; [['OPEN BEATSEAT','/beatseat-log.html'],['PIT GAUGE','/gauge-widget.html'],['ALIGN','/align'],['TIRE PSI','/tire-psi.html'],['ALL TOOLS','/tools.html']].forEach(function(pair) { var a = document.createElement('a'); a.href = pair[1]; a.textContent = pair[0]; a.style.cssText = 'font-family:var(--mono);font-size:7px;letter-spacing:1px;padding:4px 8px;border:1px solid rgba(200,150,10,.25);color:var(--amber);text-decoration:none'; standalones.appendChild(a); }); card.appendChild(standalones); 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; _pocketLogger.sfArmed = (saved.laps && saved.laps.length > 0) || false; if (saved.motionSamples && saved.motionSamples.length) { _pocketLogger.motionSamples = saved.motionSamples; } } } 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 hunterFetch(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 _updateCarTrackMemory(entry) { try { if (!entry || !S.cur) return; var carId = String(S.cur.id || 'local'); var tname = entry.track || (S.curTrack && S.curTrack.name) || ''; if (!tname) return; var key = 'bb_mem_' + carId + '_' + tname.toLowerCase().replace(/[^a-z0-9]/g,'-'); var mem = JSON.parse(localStorage.getItem(key) || '{}'); mem.visits = (mem.visits || 0) + 1; if (entry.finish != null) { mem.finishes = (mem.finishes || []).concat(parseInt(entry.finish)).slice(-8); } if (entry.stagger != null) { mem.staggers = (mem.staggers || []).concat(parseFloat(entry.stagger)).slice(-8); } var curStag = parseFloat((S.cur && S.cur.setup && S.cur.setup.stagger) || _su.stagger_r || 0); if (curStag) mem.staggers = (mem.staggers || []).concat(curStag).slice(-8); if (S.cur.fuel) mem.fuel = S.cur.fuel; if (S.cur.carb_type) mem.carb = S.cur.carb_type; // ear data if present if (window._engineEarLive && window._engineEarLive.rpm) { mem.earPeaks = (mem.earPeaks || []).concat(Math.round(window._engineEarLive.rpm)).slice(-5); } // walk data from this visit if (S && S._walkSummary) { var ws2 = S._walkSummary; if (ws2.e1 && ws2.e1.grip != null) { mem.walkGrips = (mem.walkGrips || []).concat(ws2.e1.grip).slice(-3); } if (ws2.banking) { var bavg = Object.keys(ws2.banking).map(function(k){return ws2.banking[k];}).filter(function(v){return typeof v==='number';}); if (bavg.length) mem.lastWalkBank = (bavg.reduce(function(a,b){return a+b;},0)/bavg.length).toFixed(1); } } // compute avgs if (mem.staggers && mem.staggers.length) { mem.avgStagger = (mem.staggers.reduce((a,b)=>a+b,0) / mem.staggers.length).toFixed(2); } if (mem.finishes && mem.finishes.length) { mem.avgFinish = (mem.finishes.reduce((a,b)=>a+b,0) / mem.finishes.length).toFixed(1); mem.bestFinish = Math.min.apply(null, mem.finishes); } mem.last = Date.now(); localStorage.setItem(key, JSON.stringify(mem)); // cloud if (typeof _cloudSave === 'function') _cloudSave('track_memory', carId, {track: tname, data: mem}); } 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)'; } // Phase 1 memory amplification helper — uses existing rec history + lastTrackSetup function _getTrackMemorySummary(trackName) { try { if (!trackName) return null; var carId = S.cur && S.cur.id ? String(S.cur.id) : 'local'; var key = 'bb_mem_' + carId + '_' + trackName.toLowerCase().replace(/[^a-z0-9]/g,'-'); var mem = JSON.parse(localStorage.getItem(key) || '{}'); // also pull recs for extra var hist = JSON.parse(localStorage.getItem('bb_rec_history_' + carId) || '[]'); var trackHits = hist.filter(function(r){ return (r.track || '').toLowerCase() === trackName.toLowerCase(); }).slice(0, 6); var recNote = ''; if (trackHits.length) { var types = {weight:0, balance:0, refinement:0, phone:0}; trackHits.forEach(function(r){ if (r.recType) types[r.recType] = (types[r.recType]||0) + 1; }); var topType = Object.keys(types).sort(function(a,b){return types[b]-types[a];})[0]; recNote = ' ' + trackHits.length + ' recs (mostly ' + topType + ')'; } var note = 'Your data at ' + trackName + ': ' + (mem.visits||0) + ' visits' + recNote + '.'; if (mem.avgStagger) note += ' Avg stagger ' + mem.avgStagger + '"'; if (mem.avgFinish) note += ' · avg finish ' + mem.avgFinish; if (mem.bestFinish) note += ' (best ' + mem.bestFinish + ')'; if (mem.fuel) note += ' · fuel ' + mem.fuel; if (mem.lastWalkBank) note += ' · last walk bank ' + mem.lastWalkBank + '°'; if (mem.walkGrips && mem.walkGrips.length) note += ' walk grips logged'; if (mem.carb) note += ' ' + mem.carb; if (mem.earPeaks && mem.earPeaks.length) note += ' · ear peaks ~' + Math.round(mem.earPeaks.reduce((a,b)=>a+b,0)/mem.earPeaks.length); return {visits: mem.visits||0, avgStagger: mem.avgStagger, avgFinish: mem.avgFinish, bestFinish: mem.bestFinish, fuel: mem.fuel, carb: mem.carb, note: note, raw: mem}; } catch(e){ return null; } } 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' || lastSession.beatseat || lastSession.source === 'beatseat_log' || lastSession.source_detail === 'beatseat_log' || (lastSession.brand && String(lastSession.brand).indexOf('BeatSeat') >= 0); 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' }; var _bsLab = (sessionLog && (sessionLog.beatseat || sessionLog.source_detail === 'beatseat_log')) ? 'BEATSEAT REC' : 'PHONE LOGGER REC'; showActiveRecommendationInGarage(recPayload, _bsLab, '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' + '(budget in-pocket: GPS laps + phone G)' + '
    ' + 'Auto lap times (GPS) + G-force peaks. No $500 logger needed. Feeds Hunter & your setups.' + '
    ' + '
    ' + statusText + '
    ' + '
    Lap: -- | G: --
    ' + '
    ' + '' + '' + '
    ' + '' + '
    '; } 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 { if(typeof _updateCarTrackMemory==='function') _updateCarTrackMemory({track: archived.track, fuel: S.cur.fuel, carb_type: S.cur.carb_type}); 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, maxLonG: sess.maxLonG || 0, maxBrakeG: sess.maxBrakeG || 0, maxSpeed: sess.maxSpeed || 0, maxRpm: sess.maxRpm || 0, motionSamples: (sess.motionSamples && sess.motionSamples.length) || (_pocketLogger.motionSamples && _pocketLogger.motionSamples.length) || 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.sfArmed && pl.curLap && pl.curLap.speedCnt >= 2) _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, motionSamples: (_pocketLogger.motionSamples && _pocketLogger.motionSamples.length) ? _pocketLogger.motionSamples.slice(-2400) : (sess.motionSamples || []), 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; // Prefer explicit onboarding data for fuel/carb (micros etc) var fuel = (S.cur && (S.cur.fuel || S.cur.fuel_system || '')).toLowerCase(); var isMethanol = /methanol|meth|alc/i.test(fuel); var isGas = /gas|pump|87|93/i.test(fuel); var isInj = /inject|injection|efi|fi/i.test(fuel); var isCarb = /carb/i.test(fuel); for (i = 0; i < ENGINE_EAR_PROFILES.length; i++) { p = ENGINE_EAR_PROFILES[i]; if (p.match.test(lc)) { var prof = _earProfileFrom(p); if (isMethanol && prof.mode !== 'methanol') prof.mode = 'methanol'; if (isGas && /micro|sprint|midget/.test(lc)) prof.mode = 'carb'; // gas micro rare but allow if (isInj && prof.mode === 'carb') prof.mode = 'efi'; // fallback if (isCarb && prof.mode === 'efi') prof.mode = 'carb'; return prof; } } 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(typeof _updateCarTrackMemory==='function') _updateCarTrackMemory(entry); 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(){ window.open('/align','_blank'); } 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();if(S.curTrack && typeof _renderTrackMemoryPanel === 'function') _renderTrackMemoryPanel(); } function _renderTrackMemoryPanel() { var mount = $('garage-tonight-panel') || $('t-garage'); if (!mount || !S.curTrack || typeof _getTrackMemorySummary !== 'function') return; var mem = _getTrackMemorySummary(S.curTrack.name); if (!mem || !mem.visits) return; var id = 'track-memory-panel'; var el = $(id); if (el) el.remove(); el = document.createElement('div'); el.id = id; el.style.cssText = 'background:var(--dark2);border:1px solid rgba(200,150,10,.2);padding:8px 10px;margin:6px 0;font-size:10px;line-height:1.3'; el.innerHTML = '
    YOUR TRACK MEMORY
    ' + (mem.note || '') + '
    '; if (mount.firstChild) mount.insertBefore(el, mount.firstChild); else mount.appendChild(el); } 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; } function _computeScanVariance(scans) { if (!scans || scans.length < 2) return null; var vals = scans.map(function(s){return s.val;}); var mean = vals.reduce(function(a,b){return a+b;},0) / vals.length; var sqDiffs = vals.map(function(v){return Math.pow(v-mean,2);}); var variance = sqDiffs.reduce(function(a,b){return a+b;},0) / sqDiffs.length; return Math.round(variance * 10) / 10; } function _computeBankingConsistency(data) { if (!data || !data.points) return null; var vars = []; Object.keys(data.points).forEach(function(k){ var p = data.points[k]; if (p && p.banking_var != null) vars.push(p.banking_var); }); if (!vars.length) return null; return Math.round( (vars.reduce(function(a,b){return a+b;},0) / vars.length) * 10 ) / 10; } // ── 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 _walkRenderSummaryCard(modal, data, key) { try { var existing = modal.querySelector('#walk-summary-card'); if (existing) existing.remove(); var zones = typeof _walkZoneSummary === 'function' ? _walkZoneSummary(data) : null; if (!zones || !(zones.total_points > 0 || zones.avg_banking != null)) return; var scroll = modal.querySelector('[style*="overflow-y"]') || modal; var card = document.createElement('div'); card.id = 'walk-summary-card'; card.style.cssText = 'margin:10px;padding:14px;background:rgba(200,150,10,.1);border:2px solid rgba(200,150,10,.45);'; var bits = []; if (zones.avg_banking != null) bits.push('Avg bank ' + zones.avg_banking + '°'); if (zones.bank_t1 != null) bits.push('T1 ' + zones.bank_t1 + '°'); if (zones.bank_t3 != null) bits.push('T3 ' + zones.bank_t3 + '°'); if (zones.surface_summary) bits.push(zones.surface_summary); if (zones.e1 && zones.e1.grip != null) bits.push('E1 grip ' + zones.e1.grip); if (zones.e2 && zones.e2.grip != null) bits.push('E2 grip ' + zones.e2.grip); card.innerHTML = '
    WALK SUMMARY · SHAREABLE
    ' + '
    ' + (S.curTrack ? S.curTrack.name : 'Track') + '
    ' + '
    ' + bits.join(' · ') + '
    ' + '
    ' + (zones.total_points || 0) + ' points · ' + (zones.total_points >= 12 ? 'full-depth' : 'quick/partial') + '
    '; var hdr = modal.firstChild; if (hdr && hdr.nextSibling) modal.insertBefore(card, hdr.nextSibling); else modal.appendChild(card); } catch (e) {} } window._buildTrackWalkQuick = function() { window._walkQuickMode = true; _buildTrackWalk(); window._walkQuickMode = false; }; function _buildTrackWalkQuick_legacy() { window._walkQuickMode = true; _buildTrackWalk(); window._walkQuickMode = false; } 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); } var QUICK_IDS = {e1_t1_en:1,e1_t1_ap:1,e1_t2_ap:1,e2_t3_en:1,e2_t3_ap:1,e2_t4_ap:1}; if (window._walkQuickMode) { var qHint = document.createElement('div'); qHint.style.cssText = 'padding:10px 12px;margin-bottom:10px;background:rgba(200,150,10,.1);border:1px solid rgba(200,150,10,.35);font-family:Share Tech Mono;font-size:10px;color:var(--gold);line-height:1.5'; qHint.textContent = 'QUICK BANKING READ \u2014 T1/T3 entry + apexes. Log banking + surface. Then USE FOR TONIGHT.'; scroll.insertBefore(qHint, scroll.firstChild); if (typeof hTitle !== 'undefined' && hTitle) hTitle.textContent = 'QUICK BANKING READ'; } zones.forEach(function(zone) { var pts = zone.points; if (window._walkQuickMode) pts = pts.filter(function(pt){ return QUICK_IDS[pt.id]; }); if (!pts.length) return; 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); pts.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); }; var useNightBtn = document.createElement('button'); useNightBtn.id = 'walk-use-night-btn'; useNightBtn.type = 'button'; useNightBtn.style.cssText = 'width:100%;background:var(--gold);border:none;color:var(--dark);font-family:Barlow Condensed;font-size:16px;font-weight:900;letter-spacing:1px;padding:16px;cursor:pointer;min-height:52px;'; useNightBtn.textContent = 'USE THIS WALK FOR TONIGHT\'S RACE NIGHT \u2192'; useNightBtn.onclick = function() { var d2 = _walkLoad(key); var count = _walkCount(d2); if (count < 1) { toast('Log at least one point first'); return; } var zones = _walkZoneSummary(d2); S._walkSummary = zones; if (typeof window._bbAttachWalkToRaceSession === 'function') { window._bbAttachWalkToRaceSession({ track_name: S.curTrack && S.curTrack.name, track_slug: S.curTrack ? _trackSlug(S.curTrack.name) : '', count: count, is_full: count >= 12, zones: zones, mode: count >= 12 ? 'full' : 'quick', source: 'walk_use_tonight', avg_banking: zones.avg_banking, bank_t1: zones.bank_t1, bank_t3: zones.bank_t3, surface_summary: zones.surface_summary }); } toast('Walk attached to tonight \u2014 open Race tab'); modal.remove(); if (typeof switchTab === 'function') switchTab('race'); setTimeout(function(){ if (typeof buildRaceTab === 'function') buildRaceTab(); }, 200); }; footer.appendChild(useNightBtn); footer.appendChild(hunterBtn); footer.appendChild(submitBtn); modal.appendChild(footer); document.body.appendChild(modal); _walkRefreshProgress(data, key); if (typeof _walkRenderSummaryCard === 'function') _walkRenderSummaryCard(modal, 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 { if (typeof window._bbParseVisionJson === 'function') soil = window._bbParseVisionJson(reply); else { var m = reply.match(/\{[\s\S]*\}/); 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 { if (typeof window._bbParseVisionJson === 'function') soil = window._bbParseVisionJson(reply); else { var m = reply.match(/\{[\s\S]*\}/); 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 (typeof window._bbAlgoWalkPredict === 'function') { try { window._bbAlgoWalkPredict(zones, track_name); } catch (_ap) {} } 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); } try { if (typeof S !== 'undefined') { S._walkSummary = S._walkSummary || {}; S._walkSummary.total_points = count; S._walkSummary.zones = zones; S._walkSummary.id = slug; S._walkSummary.ts = Date.now(); } if (typeof window._bbAttachWalkToRaceSession === 'function') { window._bbAttachWalkToRaceSession({ slug: slug, track_slug: track_slug, track_name: track_name, count: count, is_full: is_full, zones: zones }); } if (typeof toast === 'function') toast('Walk on tonight\'s session \u2713 \u2014 open Race tab'); } catch (_we) {} 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; }); var bankVals = []; Object.keys(apexBanking).forEach(function(k){ var v=Number(apexBanking[k]); if(!isNaN(v)&&v>0) bankVals.push(v); }); if (data.points) { Object.keys(data.points).forEach(function(id){ var p = data.points[id]; if (p && p.banking != null && !isNaN(Number(p.banking))) bankVals.push(Number(p.banking)); }); } var avgBank = bankVals.length ? Math.round((bankVals.reduce(function(a,b){return a+b;},0)/bankVals.length)*10)/10 : null; var e1z = zoneStats(e1pts), e2z = zoneStats(e2pts), fsz = zoneStats(fsPts), bsz = zoneStats(bsPts); var allTags = []; [e1z, e2z, fsz, bsz].forEach(function(z){ if(z.dominant_tag) allTags.push(z.dominant_tag); }); var surfaceBits = []; if (e1z.dominant_tag) surfaceBits.push('E1 '+e1z.dominant_tag); if (e2z.dominant_tag) surfaceBits.push('E2 '+e2z.dominant_tag); if (avgBank != null) surfaceBits.push('avg bank '+avgBank+'°'); return { e1: e1z, e2: e2z, fs: fsz, bs: bsz, banking: apexBanking, avg_banking: avgBank, bank_t1: apexBanking.e1_t1_ap != null ? Number(apexBanking.e1_t1_ap) : null, bank_t3: apexBanking.e2_t3_ap != null ? Number(apexBanking.e2_t3_ap) : null, surface_summary: surfaceBits.join(' · ') || null, surface_tags: allTags, track: data.track || null, car: data.car || null, banking_consistency: _computeBankingConsistency(data), 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); // Enhance: store scan stats for variance/consistency power try { var d2 = _walkLoad(key); if (!d2.points[ptId]) d2.points[ptId] = {}; var scans = d2.points[ptId].banking_scans || []; scans.push({val:val, ts:Date.now(), samples:readings.length}); if (scans.length > 5) scans.shift(); d2.points[ptId].banking_scans = scans; d2.points[ptId].banking_var = _computeScanVariance(scans); _walkSave(key, d2); data.points = d2.points; } catch(e){} }; 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); }
    ACTIONS
    Race night
    Car profile
    Data