Closes #13.
An economy runs on grants, refunds, event payouts and penalties that have no
product behind them, so labelling a balance change with a store product would
force fake products into the shop just to name a payout. Categories are now a
separate, editable list per server, seeded with sensible defaults.
- shared/web.ts: EconomyCategory + an optional category on LedgerEntry.
- shared/economy.ts: DEFAULT_CATEGORIES, categoryName(), and filterLedger gains
a category dimension including a 'none' bucket - purchases and every entry
recorded before categories existed carry none, and 'show me what was never
labelled' is how you find them.
- store/economy.ts: category CRUD; addBalance/setBalance take one. Only a
category that actually exists is recorded - a ledger entry is an audit record
and must not claim a label nothing on the server defines. Deleting a category
does NOT rewrite past entries; the UI falls back to the raw id.
- Desktop Store view splits into Economy | Store tabs, so balances, categories
and the ledger stop living inside the shop. Panel gets the same picker and
ledger filter.
Old store files migrate on load (categories seeded once); an operator who
deletes them all keeps an empty list rather than having them reappear.
What
Closes #13. The economy gets its own categories, defined independently of the store catalogue, and the desktop view splits into Economy | Store tabs so balances stop living inside the shop.
shared/web.ts—EconomyCategory+ an optionalcategoryonLedgerEntry.shared/economy.ts—DEFAULT_CATEGORIES(Reward, Event payout, Refund, Penalty, Correction),categoryName(), andfilterLedgergains a category dimension.store/economy.ts— category CRUD;addBalance/setBalanceaccept one.Interpretation, stated explicitly
"Distinct economy categories, not tied to store products" is read here as categories for balance movements — an economy runs on grants, refunds, event payouts and penalties that have no product behind them, and labelling those with a store product would force fake products into the shop just to name a payout.
The other possible reading is multiple currencies/wallets (Coins + Gems + Tokens). That is a much larger change — every balance becomes a map, and purchase/public-store/API all change shape — so I did not assume it. If wallets were the intent, say so and I will open it as its own issue rather than bending this one.
Decisions worth reviewing
A category is validated on write.
addBalance(..., 'not-a-real-category')records the entry with no category rather than storing the string. A ledger entry is an audit record; letting a caller write a free-string label means the log can claim a category nothing on the server defines.Deleting a category does not rewrite history. Past entries keep the id they were recorded with, and the UI falls back to showing that raw id. Editing an audit trail to tidy up a dropdown is exactly what an audit trail must never do.
'none'is a real filter value, not the absence of one. Purchases never get a category, and neither does anything recorded before this PR — "what was never labelled" is a question an operator will actually ask.Verification
MSMS_SMOKE_WEBexit 0;MSMS_SMOKEexit 0.'all'and an absent category do not filter;'none'returns exactly the uncategorised entries; category AND text both apply; a real category is recorded and an invented one is not; deleting a category leaves the past entry's category intact;categoryNamefalls back to the raw id.filterLedgeracross 7 combinations; the picker and filter render the right options; a category namedReward <b>comes outReward <b>(escaped); the filter defaults toall.storeblock parity: 53/53.Disclosed gaps
store.jsonfiles have nocategorieskey; the loader seeds defaults when the field is missing but keeps an empty array if an operator deleted them all. I did not replay an actual v1 file from disk.