From c566aab014fb943fbf89845ccfb214b0e99b0742 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Wed, 16 Sep 2026 23:36:57 -0400 Subject: [PATCH] Make the change log's slice column readable A multi-slice operation logs an array of slices, and Object.entries over an array yields its indices -- so the column read "0 = [object Object], 1 = [object Object], ...", which says nothing about what was adjusted. Each slice now gets a line. And since a dragged region is one dimension walked across a fixed set of others, the shared part is pulled out: log 118 is five slices differing only in omon, which now reads as one line of the four fixed fields and one listing Dec through Apr, rather than the same four fields five times over. Only collapsed when exactly one key varies. Two independent axes would lose their pairing when flattened, so those stay listed slice by slice. Dates arrive as epoch millis and are shown as dates; the raw payload is on the cell's title for anything the rendering has flattened. Co-Authored-By: Claude Opus 5 (1M context) --- ui/src/views/Forecast.jsx | 64 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index b15fd28..df581e7 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -1424,7 +1424,24 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio {entry.operation} - {fmtSlice(entry.slice)} + + {(() => { + const lines = fmtSliceLines(entry.slice) + if (lines.length === 0) return 'โ€”' + // the raw payload is on the title, for the case where the + // rendering has flattened something worth seeing + return ( +
+ {lines.map((l, i) => ( +
+ {lines.length > 1 && {i + 1}.} + {l} +
+ ))} +
+ ) + })()} + `${k} = ${v}`).join(', ') +// A multi-slice operation logs an array of slices, and Object.entries over an +// array yields its indices -- which is where "0 = [object Object]" came from. +// Each slice gets its own line; a date arrives as epoch millis and is useless +// as a number. +function fmtSliceLines(slice) { + if (!slice) return [] + const one = (sl) => Object.entries(sl) + .map(([k, v]) => `${k} = ${fmtSliceValue(v)}`) + .join(' ยท ') + if (!Array.isArray(slice)) return Object.keys(slice).length ? [one(slice)] : [] + + const list = slice.filter(sl => sl && Object.keys(sl).length) + if (list.length <= 1) return list.map(one) + + // A dragged region is one dimension walked across a fixed set of others -- the + // five slices of log 118 differ only in omon. Printing the other four fields + // five times buries the one that actually varies, so pull the shared part out + // and list the varying values on their own line. + const keys = [...new Set(list.flatMap(sl => Object.keys(sl)))] + const varying = keys.filter(k => new Set(list.map(sl => JSON.stringify(sl[k]))).size > 1) + + // Two or more independent axes would lose their pairing if flattened this way, + // so only collapse when a single dimension is doing the varying. + if (varying.length !== 1) return list.map(one) + + const [vk] = varying + const fixed = Object.fromEntries(keys.filter(k => k !== vk).map(k => [k, list[0][k]])) + const values = [...new Set(list.map(sl => fmtSliceValue(sl[vk])))] + return [ + ...(Object.keys(fixed).length ? [one(fixed)] : []), + `${vk} = ${values.join(', ')}`, + ] +} + +function fmtSliceValue(v) { + if (v === null || v === undefined || v === '') return 'โˆ…' + if (typeof v === 'number' && v > 1e11) { + const d = new Date(v) + if (!isNaN(d)) return d.toISOString().slice(0, 10) + } + if (typeof v === 'object') return JSON.stringify(v) + return String(v) } const OP_BADGE = {