From f7f4fbb4c61c3395e9c5ab0efccb2466aa8be7e3 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Thu, 17 Sep 2026 14:43:30 -0400 Subject: [PATCH] Have the ordering sync fetch its own inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The instrumentation finally said it plainly: inputs {versionId: '29', versionFound: false, bucket_order: null, logMetaCount: 0, seqs: []} It read the `versions` prop and the `logMeta` state, both populated asynchronously, while running from initViewer — which finishes well before them on a large load. So it was called with nothing every time, could never build an expression, and the effect meant to re-run it once the data landed never fired. Two small queries beat depending on that timing, the same correction the master-data effect needed for the same reason. Also explains why the ordering half-worked: `existing: Array(1)` with Segment already applied. An earlier session had built it and it has been riding in the saved layout since, so segment ordering appeared to work while Bucket never existed. Co-Authored-By: Claude Opus 5 (1M context) --- ui/src/views/Forecast.jsx | 44 ++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 1530b5c..0d2c1ab 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -644,7 +644,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio useEffect(() => { refreshLogMeta(versionId) }, [versionId]) // Reordering on the Baseline page takes effect here without a reload: the // expressions are rebuilt and the pivot re-renders. - useEffect(() => { if (tableRef.current) syncOrderExpressions() }, [logMeta, versions, versionId]) + // Re-run when the ordering could have changed on the Baseline page. logMeta is + // the signal, not the source: the function reads the ordering itself. + useEffect(() => { if (tableRef.current) syncOrderExpressions() }, [logMeta, versionId]) useEffect(() => { refreshTags(sourceId) }, [sourceId]) // Stream an Arrow IPC endpoint into one buffer, reporting download progress. @@ -932,20 +934,38 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio console.log(`[pf-order] ${msg}`, extra) } + // Fetches its own inputs rather than reading the `versions` prop and the + // `logMeta` state. + // + // Those are populated asynchronously, and this runs from initViewer — which + // finishes well before them on a large load. It was therefore called with + // versionFound: false and logMetaCount: 0 every time, could never build + // anything, and the effect meant to re-run it once the data arrived never + // fired. Two small queries are worth more than the right timing. async function syncOrderExpressions() { - dbgOrder('enter') const viewer = viewerRef.current - if (!viewer) { dbgOrder('exit: no viewer'); return } - const version = versions.find(v => String(v.id) === String(versionId)) - const wanted = buildOrderExpressions(version?.bucket_order, logMeta) + if (!viewer) return + + let bucketOrder = null + let entries = [] + try { + const [vers, log] = await Promise.all([ + fetch(`/api/sources/${sourceId}/versions`).then(r => r.ok ? r.json() : []), + fetch(`/api/versions/${versionId}/log`).then(r => r.ok ? r.json() : []), + ]) + bucketOrder = (Array.isArray(vers) ? vers : []) + .find(v => String(v.id) === String(versionId))?.bucket_order ?? null + entries = Array.isArray(log) ? log : [] + } catch (err) { + console.error('[pf-order] could not read the ordering', err) + return + } + + const byId = Object.fromEntries(entries.map(e => [e.id, e])) + const wanted = buildOrderExpressions(bucketOrder, byId) dbgOrder('inputs', { - versionId, - versionFound: !!version, - bucket_order: version?.bucket_order ?? null, - logMetaCount: Object.keys(logMeta || {}).length, - seqs: Object.entries(logMeta || {}) - .filter(([, m]) => m.seq != null) - .map(([id, m]) => `${id}=${m.seq}`), + bucket_order: bucketOrder, + seqs: entries.filter(e => e.seq != null).map(e => `${e.tag || e.note}=${e.seq}`), wanted: Object.keys(wanted), })