Closes #14.
shared/economy.ts adds pure filterLedger + ledgerSummary. Text matches player,
reason AND the actor - 'what did this admin change' is the question the ledger
exists to answer, and it is useless if you can only search by player.
Desktop Store view: the ledger is now collapsed by default (it is a long audit
list, not something you need in front of you to run the store) with the totals
in the header so it still says something while closed. Expanded, it gets a
search box and a kind filter, plus a shown/total count.
Web panel: same collapse + search + kind filter. The panel is plain browser JS
in a template literal and cannot import the module, so its filter is a hand-kept
mirror - verified identical against the shared function across 7 filter
combinations, and the summary line matches character for character.
Purchases count as spend only, never as a removal, so the header totals cannot
double-count the same entry.
What
Closes #14. The balance ledger is now hidden by default and, when expanded, searchable and filterable — on both surfaces.
shared/economy.ts(new): purefilterLedger(entries, { text, kind })andledgerSummary(entries).12 entries · +600 granted · −40 removed · 25 spent), so it still tells you something while closed. Expanded it gains a search box, a kind filter (grants / removals / set / purchases) and a shown-of-total count./store/admin/ledgerendpoint.Design notes
Search matches the actor, not just the player. That is the whole point of the ledger per #15 — "which admin changed this balance" — and a search that only covers player names cannot answer it. Text matches
mcName,reasonandby.Purchases are spend, never removal. Both are negative deltas, so the naive summary counts a purchase twice.
ledgerSummarybranches onkindfirst.removedandspentare reported positive, because "−450 spent" next to a minus sign reads as a double negative.Filter order is preserved. The ledger is stored newest-first and both UIs rely on that; the filter never re-sorts.
Verification
npm run typecheck+npm run build— clean.MSMS_SMOKE_WEB— exit 0, with new units: empty filter keeps all; player search is case-insensitive; actor search returns exactly that actor's entries; reason is searchable; kind filter;'all'does not filter; text AND kind both apply (not either/or); order preserved; and a summary assertion that pinsgranted=600 / removed=40 / spent=25— i.e. the purchase is not double-counted.MSMS_SMOKE— exit 0.ledgerFiltered()was compared tofilterLedger()across 7 filter combinations — identical in all 7 — and the rendered summary line matched the shared one character for character.toggleLedger()was driven both ways (▸/▾, display none/block).Disclosed gaps
shared/economy.tswill not automatically propagate. Commented as such at both sites.