From af9e6de88e7424d9cf1dbef12d37615725cae7de Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Thu, 17 Sep 2026 22:15:25 -0400 Subject: [PATCH] Give a segment an editable label and bucket, at creation and after Two gaps you hit. There was nowhere to set "counts toward" while defining a segment -- only in the list afterwards -- and the Edit buttons disappear entirely once any adjustment exists. That guard is right in principle and too broad in practice. Editing a segment's filters or date offset after a scale would silently recalibrate a distribution that was sized against the old rows, so it stays gated. But the label and the bucket are presentation: they change what the pivot shows and what the segment counts toward, never which rows were loaded. Those are now editable in the list at any time, and settable on the create form. pf.log.label is new: the segment's display name, falling back to tag then note. Separate from both because those have jobs already -- tag groups adjustments into initiatives for the bridge, note is commentary -- and because the label is where sort order lives. Perspective orders column groups by the value string, so a leading "01 - " is how ordering gets expressed, and putting that in the note would put it in every note. The load routes do not yet carry bucket and label through: they return only rows_affected, with no log id to attach them to. That comes with the switch away from computed prefixes. Co-Authored-By: Claude Opus 5 (1M context) --- routes/log.js | 13 +++++--- setup_sql/01_schema.sql | 9 ++++++ ui/src/views/Baseline.jsx | 68 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/routes/log.js b/routes/log.js index 130dd41..917f4ff 100644 --- a/routes/log.js +++ b/routes/log.js @@ -131,9 +131,12 @@ module.exports = function(pool) { // a closed version, where relabelling history is still legitimate. router.patch('/log/:logid', async (req, res) => { const logId = parseInt(req.params.logid); - const { note, tag, bucket, seq } = req.body; - if (note === undefined && tag === undefined && bucket === undefined && seq === undefined) { - return res.status(400).json({ error: 'Nothing to update — send note, tag, bucket and/or seq' }); + const { note, tag, bucket, seq, label } = req.body; + if (note === undefined && tag === undefined && bucket === undefined + && seq === undefined && label === undefined) { + return res.status(400).json({ + error: 'Nothing to update — send note, tag, bucket, label and/or seq' + }); } try { // COALESCE on the flag, not the value: an explicit null or '' must be @@ -143,7 +146,8 @@ module.exports = function(pool) { note = CASE WHEN $2::bool THEN $3::text ELSE note END, tag = CASE WHEN $4::bool THEN $5::text ELSE tag END, bucket = CASE WHEN $6::bool THEN $7::text ELSE bucket END, - seq = CASE WHEN $8::bool THEN $9::int ELSE seq END + seq = CASE WHEN $8::bool THEN $9::int ELSE seq END, + label = CASE WHEN $10::bool THEN $11::text ELSE label END WHERE id = $1 RETURNING *`, [ logId, @@ -152,6 +156,7 @@ module.exports = function(pool) { bucket !== undefined, bucket === undefined ? null : (String(bucket).trim() || null), seq !== undefined, (seq === undefined || seq === null || seq === '') ? null : parseInt(seq), + label !== undefined, label === undefined ? null : (String(label).trim() || null), ] ); if (!result.rows.length) return res.status(404).json({ error: 'Log entry not found' }); diff --git a/setup_sql/01_schema.sql b/setup_sql/01_schema.sql index a2e7b9c..cd66384 100644 --- a/setup_sql/01_schema.sql +++ b/setup_sql/01_schema.sql @@ -92,6 +92,15 @@ WHERE TRUE -- bucket_order lives on the version rather than the source because the Baseline -- page, where it is maintained, is version-scoped. log.seq orders the segments -- within that. +-- The segment's display name in the pivot, falling back to tag then note. +-- +-- Separate from both because those have jobs already -- tag groups adjustments +-- into initiatives for the bridge, note is free commentary -- and because the +-- label carries the sort order. Perspective orders column groups by the value +-- string, so a leading "01 - " is how ordering is expressed; putting that in the +-- note would put it in every note. +ALTER TABLE pf.log ADD COLUMN IF NOT EXISTS label text; + ALTER TABLE pf.version ADD COLUMN IF NOT EXISTS bucket_order jsonb; ALTER TABLE pf.log ADD COLUMN IF NOT EXISTS seq integer; diff --git a/ui/src/views/Baseline.jsx b/ui/src/views/Baseline.jsx index a41b18e..d193697 100644 --- a/ui/src/views/Baseline.jsx +++ b/ui/src/views/Baseline.jsx @@ -103,6 +103,10 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio const [rawSql, setRawSql] = useState('') const [offset, setOffset] = useState('0 days') const [segNote, setSegNote] = useState('') + // Presentation, not definition: what the segment counts toward and how it is + // labelled in the pivot. Safe to set at any time, unlike its filters. + const [segBucket, setSegBucket] = useState('') + const [segLabel, setSegLabel] = useState('') const [submitting, setSubmitting] = useState(false) const [editingLogId, setEditingLogId] = useState(null) const [showAddForm, setShowAddForm] = useState(false) @@ -140,6 +144,25 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio const [seqs, setSeqs] = useState({}) const [bucketOrder, setBucketOrder] = useState([]) + // Label and bucket are presentation, not definition: they change what the pivot + // shows and what the segment counts toward, never which rows were loaded. So + // they stay editable after adjustments exist, unlike the filters and offset, + // where an edit would silently recalibrate scales sized against the old rows. + async function saveLogField(entry, field, value) { + const next = value.trim() + if (next === (entry[field] || '')) return + try { + const res = await fetch(`/api/log/${entry.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ [field]: next }), + }) + if (!res.ok) { const d = await res.json(); flash(d.error, 'error'); return } + loadLog() + flash('Saved') + } catch (err) { flash(err.message, 'error') } + } + async function saveBucket(entry, value) { const next = value.trim() if (next === (entry.bucket || '')) return @@ -236,6 +259,8 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio where_clause: clause, note: description || segNote, date_offset: offsetStr, + ...(segBucket.trim() ? { bucket: segBucket.trim() } : {}), + ...(segLabel.trim() ? { label: segLabel.trim() } : {}), ...(useRaw ? { raw_where: clause } : { filters }), } setSubmitting(true) @@ -462,6 +487,7 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio # seq + label note counts toward rows @@ -473,11 +499,11 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio {log.length === 0 && ( - No segments loaded yet + No segments loaded yet )} {!showAddForm && !editingLogId && ( - +