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 = {