My Progress: Custom lanes option — track any Boost Planner mix (e.g. L4+L4+L2)
Phase dropdown gains 'Custom lanes'; a lanes input derives profit/cycle from LEVELS, maps the ladder rung to the highest lane, estimates cycles-to-next accelerate-basis (labeled est.), persists with the rest of the ROI state. Engine untouched; test_scenarios ALL PASS.
This commit is contained in:
+52
-14
@@ -1440,7 +1440,10 @@
|
|||||||
<option value="l6l3">L6 + L3 Hybrid</option>
|
<option value="l6l3">L6 + L3 Hybrid</option>
|
||||||
<option value="l7solo">L7 Solo</option>
|
<option value="l7solo">L7 Solo</option>
|
||||||
<option value="l7x3">3× L7 Endgame</option>
|
<option value="l7x3">3× L7 Endgame</option>
|
||||||
|
<option value="custom">🛠 Custom lanes… (any mix)</option>
|
||||||
</select>
|
</select>
|
||||||
|
<input type="text" id="roiCustomLanes" placeholder="e.g. L4+L4+L2" style="display:none;margin-top:6px;">
|
||||||
|
<small id="roiCustomHint" style="display:none;color:var(--text-muted);font-size:0.72rem;margin-top:3px;">The campaigns you're actually running, up to 3 lanes — Boost Planner mixes welcome (e.g. L4+L4+L2)</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="roi-field">
|
<div class="roi-field">
|
||||||
<label>Cycles Completed This Phase</label>
|
<label>Cycles Completed This Phase</label>
|
||||||
@@ -2746,30 +2749,64 @@ function updateROI() {
|
|||||||
// Update label to reflect profit mode
|
// Update label to reflect profit mode
|
||||||
document.getElementById('roiStatRemaining').querySelector('.roi-stat-label').textContent = remaining > 0 ? 'To Breakeven' : 'Pure Profit';
|
document.getElementById('roiStatRemaining').querySelector('.roi-stat-label').textContent = remaining > 0 ? 'To Breakeven' : 'Pure Profit';
|
||||||
|
|
||||||
// Phase progress
|
// Phase progress. "custom" = the Boost Planner reality: any mix of up to
|
||||||
const pd = PHASE_DATA[phase];
|
// 3 campaign slots (e.g. L4+L4+L2). Economics derived from LEVELS; the
|
||||||
|
// cycles-to-next estimate is accelerate-basis (bank everything) — pocket
|
||||||
|
// modes stretch it, so it's labeled est.
|
||||||
|
const customEl = document.getElementById('roiCustomLanes');
|
||||||
|
const customHintEl = document.getElementById('roiCustomHint');
|
||||||
|
const isCustom = phase === 'custom';
|
||||||
|
customEl.style.display = isCustom ? '' : 'none';
|
||||||
|
customHintEl.style.display = isCustom ? '' : 'none';
|
||||||
|
let pd, phaseKeyForLadder = phase, customBadge = null, customHi = 0;
|
||||||
|
if (isCustom) {
|
||||||
|
const lanes = (customEl.value.match(/[1-7]/g) || []).slice(0, 3).map(Number);
|
||||||
|
if (lanes.length) {
|
||||||
|
const profitPer = lanes.reduce((s, l) => s + LEVELS[l - 1].netProfit, 0);
|
||||||
|
customHi = Math.max(...lanes);
|
||||||
|
const nextLvl = customHi < 7 ? LEVELS[customHi] : null; // LEVELS[hi] = the (hi+1) record
|
||||||
|
pd = {
|
||||||
|
cycles: nextLvl ? Math.max(1, Math.ceil(nextLvl.total / profitPer)) : 999,
|
||||||
|
profitPer,
|
||||||
|
nextLabel: nextLvl ? `L${customHi + 1}` : null,
|
||||||
|
needsCapital: nextLvl ? nextLvl.total : 0,
|
||||||
|
est: true,
|
||||||
|
};
|
||||||
|
const equiv = { 1: 'l1', 2: 'l2l1', 3: 'l3l1', 4: 'l4', 5: 'l5', 6: 'l6', 7: 'l7solo' };
|
||||||
|
phaseKeyForLadder = equiv[customHi];
|
||||||
|
customBadge = 'Custom: ' + lanes.map(l => 'L' + l).join('+') + ` · $${profitPer.toFixed(2)}/cycle`;
|
||||||
|
} else {
|
||||||
|
pd = { cycles: 0, profitPer: 0, nextLabel: null, needsCapital: 0 };
|
||||||
|
customBadge = 'Type your lanes above (e.g. L4+L4+L2)';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pd = PHASE_DATA[phase] || { cycles: 0, profitPer: 0, nextLabel: null, needsCapital: 0 };
|
||||||
|
}
|
||||||
let phasePct = 0;
|
let phasePct = 0;
|
||||||
let phaseLabel = '';
|
let phaseLabel = '';
|
||||||
if (phase === 'l7x3') {
|
if (phase === 'l7x3') {
|
||||||
phasePct = 100;
|
phasePct = 100;
|
||||||
phaseLabel = '🏆 Endgame Reached';
|
phaseLabel = '🏆 Endgame Reached';
|
||||||
} else {
|
} else if (pd.cycles > 0) {
|
||||||
phasePct = Math.min((cyclesDone / pd.cycles) * 100, 100);
|
phasePct = Math.min((cyclesDone / pd.cycles) * 100, 100);
|
||||||
phaseLabel = `Cycle ${cyclesDone} of ${pd.cycles}`;
|
phaseLabel = pd.est ? `Cycle ${cyclesDone} of ~${pd.cycles} (est.)` : `Cycle ${cyclesDone} of ${pd.cycles}`;
|
||||||
|
} else {
|
||||||
|
phaseLabel = '—';
|
||||||
}
|
}
|
||||||
document.getElementById('roiStatPhase').querySelector('.roi-stat-value').textContent = phaseLabel;
|
document.getElementById('roiStatPhase').querySelector('.roi-stat-value').textContent = phaseLabel;
|
||||||
document.getElementById('roiBarPhase').style.width = phasePct + '%';
|
document.getElementById('roiBarPhase').style.width = phasePct + '%';
|
||||||
|
|
||||||
// Ladder visualization
|
// Ladder visualization (custom lanes map to their highest level's rung)
|
||||||
const ladderOrder = ['l1','l2l1','l3l1','l4','l4l3','l5','l5l3','l6','l6l3','l7solo','l7x3'];
|
const ladderOrder = ['l1','l2l1','l3l1','l4','l4l3','l5','l5l3','l6','l6l3','l7solo','l7x3'];
|
||||||
const phaseIndex = ladderOrder.indexOf(phase);
|
const phaseIndex = ladderOrder.indexOf(phaseKeyForLadder);
|
||||||
// Location badge
|
// Location badge
|
||||||
const phaseNames = {
|
const phaseNames = {
|
||||||
l1:'L1 Only', l2l1:'L2 + L1', l3l1:'L3 + L1', l4:'L4 Only',
|
l1:'L1 Only', l2l1:'L2 + L1', l3l1:'L3 + L1', l4:'L4 Only',
|
||||||
l4l3:'L4 + L3 Hybrid', l5:'L5 Only', l5l3:'L5 + L3 Hybrid',
|
l4l3:'L4 + L3 Hybrid', l5:'L5 Only', l5l3:'L5 + L3 Hybrid',
|
||||||
l6:'L6 Only', l6l3:'L6 + L3 Hybrid', l7solo:'L7 Solo', l7x3:'3× L7 Endgame'
|
l6:'L6 Only', l6l3:'L6 + L3 Hybrid', l7solo:'L7 Solo', l7x3:'3× L7 Endgame'
|
||||||
};
|
};
|
||||||
document.getElementById('roiHerePhase').textContent = phase && phaseNames[phase] ? phaseNames[phase] : '—';
|
document.getElementById('roiHerePhase').textContent =
|
||||||
|
customBadge || (phase && phaseNames[phase] ? phaseNames[phase] : "—");
|
||||||
|
|
||||||
document.querySelectorAll('.roi-ladder-step').forEach(el => {
|
document.querySelectorAll('.roi-ladder-step').forEach(el => {
|
||||||
el.classList.remove('active', 'passed');
|
el.classList.remove('active', 'passed');
|
||||||
@@ -2785,11 +2822,11 @@ function updateROI() {
|
|||||||
if (earned >= 337.50) milestones.push({ icon: '✅', label: '75% ROI Recouped', sub: 'Three quarters there' });
|
if (earned >= 337.50) milestones.push({ icon: '✅', label: '75% ROI Recouped', sub: 'Three quarters there' });
|
||||||
if (earned >= deposits) milestones.push({ icon: '💰', label: '100% ROI — Breakeven!', sub: 'Everything from here is pure profit' });
|
if (earned >= deposits) milestones.push({ icon: '💰', label: '100% ROI — Breakeven!', sub: 'Everything from here is pure profit' });
|
||||||
|
|
||||||
if (phase === 'l5l3' || phaseIndex > ladderOrder.indexOf('l5l3'))
|
if (phase === 'l5l3' || phaseIndex > ladderOrder.indexOf('l5l3') || customHi >= 5)
|
||||||
milestones.push({ icon: '🏅', label: 'L5 Active', sub: 'You reached the serious scaling tier' });
|
milestones.push({ icon: '🏅', label: 'L5 Active', sub: 'You reached the serious scaling tier' });
|
||||||
if (phase === 'l6l3' || phaseIndex > ladderOrder.indexOf('l6l3'))
|
if (phase === 'l6l3' || phaseIndex > ladderOrder.indexOf('l6l3') || customHi >= 6)
|
||||||
milestones.push({ icon: '🏅', label: 'L6 Active', sub: 'Double-digit clicks/day territory' });
|
milestones.push({ icon: '🏅', label: 'L6 Active', sub: 'Double-digit clicks/day territory' });
|
||||||
if (phase === 'l7solo' || phaseIndex > ladderOrder.indexOf('l7solo'))
|
if (phase === 'l7solo' || phaseIndex > ladderOrder.indexOf('l7solo') || customHi >= 7)
|
||||||
milestones.push({ icon: '🌟', label: 'L7 — The Big One', sub: '$840/cycle profit, $270/day clicks' });
|
milestones.push({ icon: '🌟', label: 'L7 — The Big One', sub: '$840/cycle profit, $270/day clicks' });
|
||||||
|
|
||||||
if (phase === 'l7x3')
|
if (phase === 'l7x3')
|
||||||
@@ -2833,7 +2870,7 @@ function updateROI() {
|
|||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
// Save to localStorage
|
// Save to localStorage
|
||||||
saveROIData(deposits, earned, phase, cyclesDone);
|
saveROIData(deposits, earned, phase, cyclesDone, customEl.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── localStorage persistence ──────────────────────────
|
// ─── localStorage persistence ──────────────────────────
|
||||||
@@ -2842,9 +2879,9 @@ function getROIStorageKey() {
|
|||||||
return 'cbp_roi_' + ref;
|
return 'cbp_roi_' + ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveROIData(deposits, earned, phase, cyclesDone) {
|
function saveROIData(deposits, earned, phase, cyclesDone, customLanes) {
|
||||||
const key = getROIStorageKey();
|
const key = getROIStorageKey();
|
||||||
const data = { deposits, earned, phase, cyclesDone };
|
const data = { deposits, earned, phase, cyclesDone, customLanes: customLanes || '' };
|
||||||
try { localStorage.setItem(key, JSON.stringify(data)); } catch(e) {}
|
try { localStorage.setItem(key, JSON.stringify(data)); } catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2858,7 +2895,7 @@ function loadROIData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wire up ROI inputs
|
// Wire up ROI inputs
|
||||||
['roiDeposits','roiTotalEarned','roiPhase','roiCyclesDone'].forEach(id => {
|
['roiDeposits','roiTotalEarned','roiPhase','roiCyclesDone','roiCustomLanes'].forEach(id => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
el.addEventListener('input', updateROI);
|
el.addEventListener('input', updateROI);
|
||||||
el.addEventListener('change', updateROI);
|
el.addEventListener('change', updateROI);
|
||||||
@@ -2871,6 +2908,7 @@ if (saved) {
|
|||||||
document.getElementById('roiTotalEarned').value = saved.earned || '';
|
document.getElementById('roiTotalEarned').value = saved.earned || '';
|
||||||
if (saved.phase) document.getElementById('roiPhase').value = saved.phase;
|
if (saved.phase) document.getElementById('roiPhase').value = saved.phase;
|
||||||
document.getElementById('roiCyclesDone').value = saved.cyclesDone !== undefined ? saved.cyclesDone : '';
|
document.getElementById('roiCyclesDone').value = saved.cyclesDone !== undefined ? saved.cyclesDone : '';
|
||||||
|
document.getElementById('roiCustomLanes').value = saved.customLanes || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial render on page load
|
// Initial render on page load
|
||||||
|
|||||||
Reference in New Issue
Block a user