From 1a0a9db8d00488c46b349ed3d2975d74ebd61752 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Fri, 18 Sep 2026 08:41:24 -0400 Subject: [PATCH] Show the row count while the load is still waiting on it X-Row-Count is exact but travels with the response headers, and in grain mode the server aggregates the whole table before sending any -- so the number appeared just as the fifteen-second wait ended, which is no use to anyone watching it. The forecast table's own count goes up first instead, from the same table-info the status bar already reads, and the exact figure replaces it when the headers arrive. The count query is deliberately not awaited: it scans the whole table, and the load must not wait on a progress message. Co-Authored-By: Claude Opus 5 (1M context) --- ui/src/views/Forecast.jsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 243b339..88c97fc 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -57,10 +57,15 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio const [loading, setLoading] = useState(false) const [largeDataset, setLargeDataset] = useState(false) const [loadProgress, setLoadProgress] = useState(null) // { received, total } - // Rows the server says it is sending, from X-Row-Count. Known as soon as the - // headers land, well before the body has been read, so the wait can say what - // it is waiting for -- on this data the row count is the wait (see CLAUDE.md, - // "Load time is dominated by row count"). + // Rows the load is waiting on, so the wait can say what it is waiting for -- + // on this data the row count is the wait (see CLAUDE.md, "Load time is + // dominated by row count"). + // + // Filled twice. X-Row-Count is exact but arrives with the response headers, + // and in grain mode the server aggregates before sending any: the number + // turned up just as the wait ended. So the forecast table's own count goes in + // first, from the same table-info the status bar reads, and the exact figure + // replaces it when the headers land. const [loadRows, setLoadRows] = useState(null) const [msg, setMsg] = useState(null) @@ -646,7 +651,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio if (!r.ok) { const { error } = await r.json(); throw new Error(error || 'Failed to load data') } const rowCount = parseInt(r.headers.get('X-Row-Count') || '0') const total = parseInt(r.headers.get('Content-Length') || '0') || null - setLoadRows(rowCount || null) + if (rowCount) setLoadRows(rowCount) const reader = r.body.getReader() const chunks = [] let received = 0 @@ -684,6 +689,12 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio setLargeDataset(false) setLoadProgress(null) setLoadRows(null) + // deliberately not awaited: it is a count over the whole forecast table and + // the load must not wait on it + fetch(`/api/versions/${vid}/table-info`) + .then(r => r.ok ? r.json() : null) + .then(info => { if (info?.rows && initIdRef.current === myId) setLoadRows(n => n ?? info.rows) }) + .catch(() => {}) setSlices([]) setExpandDepth(null) adoptSplit([], 0)