From 8bc7d65fce67b13623db1bbe7921a8a96ece554e Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Tue, 22 Sep 2026 14:30:03 -0400 Subject: [PATCH] Say which change-log entries are yours The version this was built against carries 127 adjustments from five accounts, one of them the reader's, so finding your own meant reading the By column down the whole list. Own entries are now marked in place -- a blue rule down the left of the row, the By cell in blue -- and the modal header carries an Everyone / Mine control with a count on each. Marked as well as filterable on purpose: the mark tells you without hiding anyone, the filter is for when you want only yours. The filter resets to Everyone on every open and is not persisted. A filter that hides other people's work is fine as a thing you asked for and a trap as a thing you inherited from last week. `isMine()` is strict authorship, deliberately not `canUndo()` -- an admin can undo everything, so marking every row as theirs would say nothing. The fetch stays unfiltered, since the counts on the control need the whole set. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 16 ++++++++++++ ui/src/views/Forecast.jsx | 53 +++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f8f1f0f..46beb80 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -755,6 +755,22 @@ Enforced at: Moving a row between territories is reassignment, not forecasting, and it would vanish from the view that would have shown what happened. +**Whose entries the change log lists.** Everyone's, as before, with an +**Everyone / Mine** control in the modal header and each account's own entries +marked in place — a blue rule down the left of the row and its `By` cell in +blue. On the version this was built against that is 127 adjustments from five +accounts, one of them the reader's, so finding your own meant reading the `By` +column down the whole list. + +Marked *and* filterable on purpose: the mark is how you tell without hiding +anybody, the filter is for when you want only yours. It resets to Everyone on +every open and is not persisted — a filter that hides other people's work is +fine as a thing you asked for and a trap as a thing you inherited from last +week. `isMine()` is strict authorship, deliberately not `canUndo()`: an admin +can undo everything, and marking every row as theirs would say nothing. + +The fetch stays unfiltered, since the counts on the control need the whole set. + **The change log shows an entry's full impact**, not the reader's share. The totals stamped on `pf.log` are company-wide, so an admin's version-wide scale reads the same in every account — deliberate, and labelled, rather than diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index eb96dc5..02298b3 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -76,6 +76,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // account that made it, or an admin. Mirrored here to say so before the click // rather than after the 403. const canUndo = (entry) => !!user && (user.is_admin || entry.pf_user === user.username) + // Strictly authorship, which is not the same test as canUndo: an admin can + // undo everything, and marking every row as theirs would say nothing. + const isMine = (entry) => !!user && entry.pf_user === user.username const [loading, setLoading] = useState(false) const [largeDataset, setLargeDataset] = useState(false) // The pivot's own filter, refreshed whenever the ledger recomputes. A ref @@ -252,6 +255,10 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio const [showBridge, setShowBridge] = useState(false) const [logEntries, setLogEntries] = useState([]) const [logLoading, setLogLoading] = useState(false) + // Whose entries the log lists. Not persisted, and it resets to everyone on + // every open: a filter that hides other people's work is fine as a thing you + // asked for and a trap as a thing you inherited from last week. + const [logWho, setLogWho] = useState('all') // 'all' | 'mine' const [editingCell, setEditingCell] = useState(null) // { id, field: 'note'|'tag', text } const [undoingId, setUndoingId] = useState(null) @@ -1725,6 +1732,10 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio return out } + // What the history modal lists. The fetch is unfiltered -- the counts on the + // Everyone/Mine control need the whole set -- so the narrowing happens here. + const visibleLog = logWho === 'mine' ? logEntries.filter(isMine) : logEntries + // The scope is read from the ref rather than passed in: the request preview in // the panel calls this too, and when it was a parameter the preview defaulted // it away -- showing a payload with no scope for a write that had one. @@ -1817,6 +1828,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio async function openLog() { setShowLog(true) setExpandedLog(null) + setLogWho('all') setLogLoading(true) try { // Baseline and reference loads are segment construction, not forecasting. @@ -2043,15 +2055,38 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
setShowLog(false)}>
e.stopPropagation()}>
- Change History +
+ Change History + {/* Whose work this is. A version under active use carries + everyone's adjustments, and finding your own in it meant + reading the By column down a list that is mostly other + people. Own entries are marked in place as well, so the + filter is a way to narrow rather than the only way to tell. */} + {!!user && logEntries.length > 0 && ( +
+ {[['all', `Everyone`, logEntries.length], + ['mine', `Mine`, logEntries.filter(isMine).length]].map(([val, label, n]) => ( + + ))} +
+ )} +
{logLoading ? (
Loading…
- ) : logEntries.length === 0 ? ( -
No log entries yet.
+ ) : visibleLog.length === 0 ? ( +
+ {logEntries.length === 0 + ? 'No log entries yet.' + : `None of the ${logEntries.length} entries here are yours.`} +
) : ( @@ -2068,9 +2103,12 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio - {logEntries.map(entry => ( + {visibleLog.map(entry => ( - + )).flatMap((row, i) => { - const entry = logEntries[i] + const entry = visibleLog[i] if (expandedLog !== entry.id) return [row] return [row, (
{fmtStamp(entry.stamp)} + {fmtStamp(entry.stamp)} + {entry.operation} @@ -2095,7 +2133,8 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio )} /> - + {entry.pf_user || '—'}