diff --git a/ui/src/components/BridgeView.jsx b/ui/src/components/BridgeView.jsx index f61b71d..14f0820 100644 --- a/ui/src/components/BridgeView.jsx +++ b/ui/src/components/BridgeView.jsx @@ -71,11 +71,28 @@ export function buildSteps(rows, { valueCol, unitsCol, logMeta = {}, excludeIters = ['reference'], // kept for callers with no bucket data basis = null, // a pf_bucket name, or null for composition - forecastBucket = 'Forecast', + forecastBucket, }) { const hasBuckets = rows.some(r => r.pf_bucket != null) const excl = new Set(excludeIters) + // Which bucket is the forecast. It used to be the literal 'Forecast', and the + // moment the buckets were renamed to carry their sort prefix -- '04 - Forecast' + // -- no row matched it: every row counted as a comparison, the walk had no + // steps, and the bridge showed the basis cancelling itself to zero. + // + // The caller passes the version's adjustment_bucket, which is the same value + // an unbucketed adjustment is labelled with, so the two agree by construction. + // Falling back to whichever bucket actually holds the adjustments keeps a + // renamed or unconfigured version working rather than silently empty. + const fcBucket = (() => { + if (forecastBucket && rows.some(r => r.pf_bucket === forecastBucket)) return forecastBucket + const adjusted = rows.find(r => r.pf_bucket && !['baseline', 'reference'].includes(r.pf_iter)) + if (adjusted) return adjusted.pf_bucket + const baseline = rows.find(r => r.pf_bucket && r.pf_iter === 'baseline') + return baseline ? baseline.pf_bucket : (forecastBucket || 'Forecast') + })() + const num = (r, col) => (col ? (parseFloat(r[col]) || 0) : 0) const blank = () => ({ value: 0, units: 0, rows: 0 }) @@ -96,7 +113,7 @@ export function buildSteps(rows, { } // Anything outside the forecast is a comparison, never a step. - if (hasBuckets && bucket !== forecastBucket) { + if (hasBuckets && bucket !== fcBucket) { if (basis && bucket === basis) { basisT.value += v; basisT.units += u; basisT.rows += 1 } continue } @@ -137,7 +154,7 @@ export function buildSteps(rows, { }) } else { out.push({ - key: 'baseline', label: hasBuckets ? forecastBucket + ' loads' : 'Baseline', kind: 'anchor', + key: 'baseline', label: hasBuckets ? fcBucket + ' loads' : 'Baseline', kind: 'anchor', delta: loads.value, start: 0, end: loads.value, units: loads.units, rows: loads.rows, entries: 1, }) @@ -151,7 +168,7 @@ export function buildSteps(rows, { } out.push({ - key: 'current', label: hasBuckets ? forecastBucket : 'Current', kind: 'anchor', + key: 'current', label: hasBuckets ? fcBucket : 'Current', kind: 'anchor', delta: running, start: 0, end: running, units: loads.units + mid.reduce((a, g) => a + (g.units || 0), 0), rows: loads.rows + mid.reduce((a, g) => a + g.rows, 0), @@ -194,7 +211,7 @@ export function layoutSteps(steps, width, H = 340, PAD = { t: 24, r: 16, b: 64, export default function BridgeView({ open, onClose, tableRef, viewerRef, logMeta = {}, valueCol, unitsCol, colMeta = [], slices = [], - excludeIters = ['reference'], versionName, + excludeIters = ['reference'], versionName, forecastBucket, }) { const hasSelection = slices.length > 0 // 'selection' | 'filtered' | 'all' @@ -260,14 +277,16 @@ export default function BridgeView({ await view.delete() } - setSteps(buildSteps(rows, { valueCol, unitsCol, logMeta, excludeIters, basis: basis || null })) + setSteps(buildSteps(rows, { + valueCol, unitsCol, logMeta, excludeIters, basis: basis || null, forecastBucket, + })) } catch (err) { setError(err.message || String(err)) setSteps(null) } finally { setLoading(false) } - }, [tableRef, viewerRef, scope, logMeta, valueCol, unitsCol, excludeIters, slices, colMeta, basis]) + }, [tableRef, viewerRef, scope, logMeta, valueCol, unitsCol, excludeIters, slices, colMeta, basis, forecastBucket]) useEffect(() => { if (!hasSelection && scope === 'selection') setScope('filtered') diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 4de19c6..8f9fa72 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -2006,6 +2006,11 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio slices={slices} excludeIters={versions.find(v => String(v.id) === String(versionId))?.exclude_iters || ['reference']} versionName={versions.find(v => String(v.id) === String(versionId))?.name} + // Which bucket *is* the forecast. Not a constant: it is the version's + // adjustment_bucket, which is how an adjustment with no bucket of its + // own gets labelled, so the two always agree by construction. + forecastBucket={versions.find(v => String(v.id) === String(versionId))?.adjustment_bucket + || '04 - Forecast'} /> {/* Main area — the panel lives in one of three shells, chosen by `dock` */}