1.0.0-release
What's new
commit ad8ded71aca910b18d7fa37818226a7497143512
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Jul 28 14:22:53 2026 +0000
chore(release): bump to 1.0.0 and declare the CurseForge project id
0.1.0 was never tagged or published -- the 2026-07-27 audit records it as
unreleased -- so 1.0.0 is the first release anyone actually installs. The
README presents it that way: a "What's new in 1.0.0" section covering the
whole feature set, and a Version History that starts here rather than
carrying a 0.1.0 row no player ever saw.
X-Curse-Project-ID is now in the TOC. The standard makes it mandatory
once the addon is listed, and the README's CurseForge badge went live
with project 1629058, so it stopped being optional.
Two mentions needed rewriting rather than a version swap:
- ARCHITECTURE justified removing the retired stores with "v0.1.0 is
unpublished, with no tags and no CurseForge listing". That is no
longer true. The argument still holds for a different reason -- both
were removed before the first published release, so no shipped build
ever wrote a row against them -- and now says so.
- Database's [Init] example line also carried a stale "schema v1"
alongside the version; the shipped schema is v2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaSE26YqHAhBWdpa7CGAYt
commit 9bf3653cef1c62c5607742a1ef20efd80f5de225
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Jul 28 14:14:33 2026 +0000
chore(media): add the project screenshots, kept out of the package
Seven screenshots behind the README's new gallery. .pkgmeta was ignoring
docs/ and tests/ but not media/, so these 3.9 MB would have shipped
inside every player's download for art that is only ever served from the
CurseForge CDN. Ignore media/screenshots so the packaged zip carries
only what the client actually loads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaSE26YqHAhBWdpa7CGAYt
commit 91c9c1ca4caefaef3148491b9ee8394d69e3d107
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Jul 28 14:14:24 2026 +0000
feat(browser): save, reset and clear the filter view
The filter bar's single Clear button becomes a Save / Reset / Clear
cluster, matching Loot History. Clear now returns to your SAVED view
rather than always to stock, so a layout you use every session is one
click away instead of a rebuild.
The saved view lives account-wide in db.global.savedView -- absent until
you press Save, because "no key" is what "nothing saved" means and an
empty table has to stay available for a deliberately all-cleared save.
It is another architecture-5 storage carve-out, like `window` and the
id-lists: a captured view has no Schema widget to drive it.
Character scope is deliberately NOT captured. The window always opens
scoped to whoever is logged in, so a stale save can never pin it to one
alt. Test mode keeps its existing carve-out and opens unscoped, since
the sample ledger is synthetic characters.
Slash:CliResetAll discards the saved view alongside the filter lists --
it is a user setting with no widget, exactly like they are -- so the
Panel's Defaults button and /bl resetall stay total.
Also drops soloRow from state.debugConsole so Master Controls pairs its
four switches two to a line (enable / minimap, session / debug console)
instead of trailing a ragged single column.
Docs resynced: the savedView carve-out and its semantics in
ARCHITECTURE, five new S-8 smoke steps, the [tests] badge at 603/603,
and test-cases.md regenerated. README also picks up the published
CurseForge project id, CDN-hosted logo and the screenshot gallery.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaSE26YqHAhBWdpa7CGAYt
commit a26252c911393e01717d3fc163629e158b0e3390
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Jul 28 07:35:39 2026 +0000
feat(export): add a wowhead link column to the History CSV
The ledger CSV gains a trailing `wowhead` column -- a plain URL carrying
the item's bonus IDs, e.g. item=151300?bonus=12801:13440:6652 -- so a row
resolves to the variant that actually moved rather than to the base item.
Appended after `zone`, so every existing column keeps its index. A plain
URL rather than an =HYPERLINK() formula: a formula breaks plain-CSV
consumers and is a CSV-injection vector, and a bare URL is auto-linkified
on paste anyway.
The column needed a capture fix to mean anything. Snapshots key inventory
by itemID, and BuildEntry re-derived the link with GetItemInfo(itemID),
which can only ever answer with the BASE item -- so every stored itemLink
had zero bonus IDs and the column would have been base-item URLs forever.
The scan now keeps a side table, snapshot.links = { [itemID] = hyperlink }
from the live per-slot link Compat already read, the diff passes it
through as move.link, and BuildEntry prefers it over the derived link.
The counts map keeps its plain { [itemID] = count } shape, so the diff and
the settle check are untouched, and SnapshotsDiffer never sees it. One id
resolves to one link per snapshot: exact for gear, which does not stack,
and irrelevant for stackables, which carry no bonuses.
Only data recorded from here on benefits. Rows already in the ledger
stored the base link and cannot be migrated -- the bonus IDs were never
captured.
Docs resynced alongside. The event table credited GUILDBANKFRAME_OPENED
with arming the differ and filed GUILDBANKBAGSLOTS_CHANGED under generic
change events, contradicting the prose 20 lines below it; the guild data
event now gets its own row saying it both arms and reconciles, and
PLAYER_LOGOUT is no longer missing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaS8K6RzUqoMB4MMYWoazE
commit f697a95ab29c6ebdc26a570b4703b75851ec9546
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 15:53:15 2026 +0000
refactor: drop the retired stores; wire the Settings defaults path
Midnight removed the reagent bank and void storage, so REAGENT_BANK and
VOID_STORAGE are gone from C.Store rather than kept as dead members: the
addon is Retail-only at 120007, REAGENT_BANK's container group already
resolved empty so StoresFor dropped it before a movement could be
attributed, and v0.1.0 is unpublished. Zero reachable rows, so the export
contract's "never rename a member" rule has nothing to protect here --
ARCHITECTURE records the reasoning and the opposite answer for a store
retired after rows exist. PLAYERREAGENTBANKSLOTS_CHANGED goes with them;
the client rejected it on every login.
The empty-group guard in StoresFor stays -- it is what makes the next
retired container degrade quietly -- and test_ledger now drives it
directly by emptying a live store's group. The retired-event regression
switches to a name the addon still registers, so it keeps proving the
isolation it was written for.
Also closes the 2026-07-27 standards audit:
- BL-06: every registered canvas frame now carries OnCommit/OnDefault/
OnRefresh, so Blizzard's own Settings defaults control reaches the
addon. OnDefault shares one closure with the header Defaults button via
setDefaultsAction, so the two paths cannot drift. On General that
action is non-destructive; wiping the ledger stays behind the
confirm-gated popup.
- BL-10: /bl reset echoes through FormatKV against the stored value, so
it matches get/set and reflects clamping.
- BL-07: accepted as a documented deviation -- account-wide by design, so
defaults live in Global.lua and there is no Profile.lua.
- BL-08: agent-context re-dropped at context pack v2.7.0, with a note
that the standards-conformance bullet is a repo-local addition a
verbatim re-drop deletes.
- BL-09: the mono-font glyph is no longer listed as a deviation --
debug-logging-2 now sanctions it.
574 passed, 0 failed; luacheck 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit d74f2e8f3ef7e15e106ec6e4f3da11d93bdd20fc
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 14:46:37 2026 +0000
docs: resync the docs with the code after the review cycle
Twelve commits of review fixes left the docs describing the addon as it
was. Every item below is drift the code had already moved past.
- docs/test-cases.md regenerated (lua tests/run.lua --list). It is a
generated file, never hand-edited: total 531 -> 567, eight suite counts
corrected, 36 new cases enumerated. The README tests badge moves in the
same change, as docs/testing.md requires.
- docs/ARCHITECTURE.md cited LT.testMode, removed this cycle in favour of
LT:IsTestMode, and described /bl debug scan as dumping the container
model only — it now also reports the money-balance readers.
- docs/smoke-tests.md had no coverage for three behaviours this cycle
introduced: that spending gold at a bank writes no row (S-5), that
opening a guild bank after doing so writes no row either — the client
reports a closed guild bank's balance as 0 rather than unknown (S-6),
and that the row menu is read-only in test mode (S-15). S-7's window
scale step checked one window; it must now rescale both.
Also gives Browser:ApplyFilterNow real callers. It was added in the
C-010 commit purely so a test had a named entry point while the seven
dropdown handlers and ClearFilters still called the file-local
ApplyFilter — API surface existing only for a test. Those eight sites now
call it, so the immediate and debounced paths are a named, symmetric
pair. Behaviour is unchanged.
Flagged, not actioned: seven exports with no production callers
(Compat.QualityFromLink has no callers at all, the rest are
tests-only), and docs/agent-context.md is pinned at standard v2.6.0
while the review resolved v2.11.0 — its header says to re-drop the file
rather than hand-edit it.
Tests: lua tests/run.lua 567 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 940dca97fb769d9412b58307d3cf28e3611e519b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 14:34:25 2026 +0000
docs: correct the smoke tests for what actually shipped
The smoke plan was written before implementation and three sections no longer
matched the code:
- C-001 still offered an "if C-001b was implemented, no row is correct" branch.
C-001 landed in full, so that branch is dead and step 4 producing no row is
now a FAIL, with a diagnostic note pointing at the settle window rather than
the rule.
- C-005 now names the actual 0.20s debounce and that only the search box is
debounced.
- C-008 now distinguishes the "uncached" reason (which proves the fix) from
"quality" (which does not), and notes that the second movement of a skipped id
legitimately reports the other reason.
- C-012 drops a step that contradicted C-002, and checks the show/hide/toggle
verbs since B:Unavailable was deleted.
Adds C-001-B for the failure mode the live probe exposed: GetGuildBankMoney()
returns 0 rather than nil with the frame closed, so opening a guild bank could
have fabricated a very large gold row. It cannot be reproduced headlessly at the
client's real timing, which is exactly why it needs a smoke test.
Adds a priority order so the capture-engine tests — the only ones that can lose
or invent permanent history — are run first.
Review docs/reviews/2026-07-27
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 739983e444de732b463ee373a6e4f6fa08a44619
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:29:18 2026 +0000
docs: sync README and ARCHITECTURE for the capture changes
F-016: the README command table listed test/session in the opposite order to
NS.COMMANDS and did not document `debug scan` / `debug panel` at all — the two
sub-verbs most useful in a bug report. Both now match NS.COMMANDS exactly, and
the read-only behaviour of the sample ledger is documented where a user meets
it.
ARCHITECTURE gains the money corroboration rule and, specifically, the reason
Compat.GetStoreMoney returns nil rather than a number: GetGuildBankMoney answers
0 when the guild frame is closed, which is the kind of client behaviour this
file exists to record. The SettingsChanged row now enumerates its reason
strings, and Known limitations gains the two the capture changes create — no
retroactive cleanup of pre-fix gold rows, and no name backfill for an uncached
item.
05_FINAL_SUMMARY.md is filled in from what actually landed, including what the
M0 probe returned, that C-001b was not needed, and an explicit split between
what the headless gate proves and what still needs a client.
Findings: F-016 · Changes: C-012, doc ripple from C-001/C-008
Tests: lua tests/run.lua 567 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit a6d566d5b25adf483511271a86c70aed02b6455b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:25:48 2026 +0000
chore: single source for schema version and addon version
F-008: the shipped schemaVersion default was 1 while NS:RunMigrations targeted
2, so every fresh install replayed the v1->v2 pass over an empty ledger. The
two were separately maintained and had already drifted apart by one, so both
now read NS.SCHEMA_VERSION and a test asserts they still agree.
Existing profiles are unaffected — they already carry an explicit value, and
AceDB only applies a default where the key is absent. No user data is touched
and no reset is required.
F-017: the help header read NS.version directly while /bl version preferred the
TOC's ## Version, so a TOC bump without the constant would have made the two
commands report different numbers. Both now resolve through Sl:Version().
Findings: F-008, F-017 · Change: C-012 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 567 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit f3d3af3289cce8354cec6cc0c0cab8843ebdc533
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:24:23 2026 +0000
fix(slash): correct the /bl list group order; add the missing tooltip
F-007: LIST_GROUP_ORDER was { "Capture", "Window" }. "Window" is a group that
no longer exists and "Master Controls" was missing, so every name in the
constant was inert and the listing silently fell through to first-seen order —
a "declared page order" that was not the order in force (slash-commands-§5).
The existing test agreed with the bug: it asserted "Capture" first against a
constant that also said "Capture", so it passed while the ordering mechanism
did nothing. It now asserts against the schema's own first group, and a second
test asserts every name in the constant resolves to a real group — a name that
matches nothing otherwise fails invisibly.
F-019: settings.excludedStores was the one row with no tooltip, and the one
that most needed a sentence — it is stored as the MUTED set and rendered
inverted, so a ticked box means "record this store". A guard test now requires
a tooltip on every row.
Findings: F-007, F-019 · Change: C-004 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 563 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit fb6f94328d8e03e55dfebb8ddcefadfab9c61c65
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:22:50 2026 +0000
refactor(core): name the open-frame token C.Context
NS.State.openContext held C.Store values, but C.Store's own comment promises
those are the STORED export keys — and BANK_FRAME never was one (F-012). The
field's real domain is "which frame is open", which is a separate axis from
"which store": the retail bank frame reaches three stores at once, and that is
the entire reason the two must not share an enum.
C.Context now declares that axis beside its neighbours, runtime-only and never
persisted. Values are byte-identical, so nothing stored, exported or messaged
changes. GUILD_BANK stays the one token that is legitimately both, and a test
pins the two to the same value so the guild-bank close path cannot drift apart
from it.
Findings: F-012 · Change: C-009 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 561 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit d85ae1f9339d86864435f6840b44a213af51707e
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:21:32 2026 +0000
fix(options): surface refresher/rebuilder failures on the debug console
Three pcall sites discarded their error, so a settings widget that started
failing stopped updating forever with nothing in chat, nothing on the console
and nothing in a test to say so (F-011). The pcall itself is right — one bad
widget must not abort the loop — so all three now route through one safeRun
helper that keeps the isolation and reports the reason under a [Panel] tag,
tagged with which refresher or rebuilder failed.
Debug output goes to the on-screen console, never chat (anti-pattern #18), and
only while debug logging is on.
Findings: F-011 · Change: C-011 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 559 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 9aadde203ea2807185dc9010351dfc5edd9dc14e
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:20:48 2026 +0000
refactor(browser): copy the filter to the table; drop the shared-bus fallback
F-013: the table was handed a live reference to B.activeFilter, so a later
dropdown toggle mutated the filter it was already painting under — the table
could show results for criteria that had never been applied. It now gets the
same copy Insights always got.
F-010: `B.__ev = NS.NewBusTarget() or NS.bus` was unreachable, and had it ever
run it would have registered these handlers on the shared bus — precisely the
receiver-clobber the factory exists to prevent, taking out the Ledger's
SettingsChanged handler. The fallback would have caused the failure it was
written to avoid. Browser now returns early like SessionWindow and Insights,
with the window, tabs and minimap button set up first so only live refresh goes
inert.
F-014: deletes B:Unavailable — zero callers, and the soft fallback its comment
describes was never wired into the show/hide/toggle verbs. It was Browser's
only chat emission, so the file-local print alias goes with it.
Findings: F-010, F-013, F-014 · Changes: C-010, C-011 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 559 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 0cc50613a656b1053741888a7e7130288f49dcc4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:18:47 2026 +0000
perf(options): skip the storage recompute while the panel is hidden
P:Refresh ran its refreshers regardless of whether the settings panel was on
screen, and one of them walks the whole ledger to estimate the SavedVariables
size (F-018). That cost was paid on every settings change and every /bl debug
toggle, for widgets nobody was looking at.
A hidden panel is re-synced by its own OnShow, so this is correct as well as
cheaper — and it is the same guard the bus path already uses.
Findings: F-018 · Change: C-007 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 558 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit c8535486a15fc3b2e3ec0a8a6bf3f2c460b3acbb
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:18:07 2026 +0000
perf(browser): debounce filter application and coalesce entry refreshes
Two paths did O(n) work far more often than the data changed, both worst at the
moment the user is interacting.
F-004: the search box fires OnTextChanged per keystroke, and each application
is a full Database:Query on History plus a full Database:Stats (~20 sorts) on
Insights — so typing "linen" paid for five, four of them for a prefix already
passed. Search input is now debounced at 0.20s. Dropdown selections keep
calling ApplyFilter directly: they are single deliberate acts and must feel
instant, and a debounce applied that broadly is a worse experience than the
cost it saves.
F-005: EntryAdded is one message per moved stack, so emptying a 20-slot bag
into the bank fanned out 20 messages and 20 full repaints of a window showing
the same data. Browser and Insights now collapse the burst into one pass. The
ledger is fully written by the time it runs. LedgerChanged stays synchronous —
it is already one message per bulk operation.
Both schedulers fall back to running inline when no timer library is present,
so a headless run or an unexpected load order pays the cost rather than
silently dropping the update.
Findings: F-004, F-005 · Changes: C-005, C-006 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 558 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 6e1a723dd98ffa1469b1ad2f7df7241d46a1347f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:16:14 2026 +0000
fix(ledger): don't let an uncached item bypass the quality gate
An id the client has not cached has no quality, so the threshold comparison was
skipped and the item was recorded whatever minimum the user had set (F-006).
"Cannot be judged" is not "passes": the gate now asks the client to cache the
id and reports the skip as "uncached" on the debug console, joining the
existing disabled/kind/store/blacklist/quality vocabulary.
The asymmetry that keeps this safe: at the default qualityThreshold of 0 the
branch is unreachable, because every quality clears 0. Only a user who
explicitly asked for a threshold sees a behaviour change — which is the case
the old code violated.
Also corrects the comment above BuildEntry, which promised that "a later
session fills in the name". There is no backfill and never was; the stored row
keeps only its id. That comment is why recording an unjudgeable row looked
harmless.
Findings: F-006 · Change: C-008 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 555 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 7d3e281fce1c9c3a8438653f6d21baa777b1d02a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:14:56 2026 +0000
fix(settings): broadcast windowScale on the bus so both windows rescale
The Window scale slider called Browser:SetScale directly and sent nothing on
the bus, so SW:OnSettingsChanged never ran and the session window kept its old
scale until a /reload (F-003). SessionWindow already subscribes to
SettingsChanged and already reads windowScale — it was simply never told, so no
consumer changed.
The direct call stays for the no-latency repaint of the window the user is
looking at. A second direct call to SessionWindow would have been one line and
anti-pattern #19, and would leave the same hole for the next window added.
Findings: F-003 · Change: C-003 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 551 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 99f48b006577005b45184cc10bb9d8ae3bf302f1
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:13:43 2026 +0000
fix(table): make test mode read-only for delete and filter actions
The /bl test sample dataset publishes synthetic item ids (190001+) through the
real render path, but the WRITE paths were never told about it: blacklisting a
sample row put a fake id into the real, persisted capture filter, and Delete
matched nothing in db.global.ledger while still firing LedgerChanged, so the
row visibly stayed (F-002). A user exploring the demo could silently corrupt
their own configuration.
The row menu now refuses. Refusing at the menu keeps Database ignorant of which
dataset is live, rather than spreading test-mode knowledge into the persistence
layer. "Link to chat" mutates nothing and stays available.
Test mode is also now DERIVED from the one stored fact (State.testRecords)
instead of tracked in a second LT.testMode flag that could disagree with it
(F-009) — the old Browser test could set the flag without a dataset, which is a
state the addon can no longer be in.
Menu entries are extracted to LT:RowMenuItems as plain data so the enable rules
are unit-testable without frames. LedgerTable also picks up the file-local
print alias the other five emitting files use (F-015, part of C-012).
Findings: F-002, F-009, F-015 · Change: C-002 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 550 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 8c8e18ed1f469d4b4fb2a7ca606a6c239e35b507
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 13:10:48 2026 +0000
fix(ledger): corroborate money movements against the store's own balance
A money row now requires the same "both sides must change, in opposite
directions" evidence an item row has always needed. The purse alone proves
nothing: BANK_FRAME reaches the warband bank, so buying a bank tab, repairing,
or a mail COD landing mid-visit was written to permanent history as a warband
deposit (F-001).
Compat.GetStoreMoney reads the store's own coin and returns nil when it cannot,
which Diff treats as "no movement provable here". Ground truth from a live
12.0.7 /bl debug scan drove two details the design could not have guessed:
- GetGuildBankMoney() returns 0, not nil, when the guild bank frame is closed.
Passing that through would read as a 6,553g deposit on the next open, so the
guild read is gated on IsGuildBankVisible().
- C_Bank.FetchDepositedMoney(Enum.BankType.Account) exists and is not
frame-bound, so warband gold capture survives; the C-001b narrowing fallback
was not needed. BankType ids are read by name (Account=2).
The settle machinery carries the purse half across to the pass where the store
balance lands, so corroboration costs no real rows.
Also adds the /bl debug scan money-API probe (M0-T1) that produced the above,
and the read_globals entries it needs.
Findings: F-001 · Change: C-001 · Review docs/reviews/2026-07-27
Tests: lua tests/run.lua 546 passed 0 failed, luacheck . 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 8d577221a499ac2d1381e2799c652f984cb160e3
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 12:33:31 2026 +0000
docs: correct chart counts and the back-to-back rationale
The Insights panel lost Net Flow By Store, so "eighteen charts" is now
seventeen in both the README and the module map, and the design bullet
justifying diverging bars is replaced by the one that explains why the
direction companions are drawn back to back.
Repairs a sentence the In/Out terminology sweep broke: removing "flow
per store," left the word "net" dangling at the end of the README's
Insights bullet. Also notes the ranked lists now split by store, and
bumps the tests badge to 531.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 284ae8390bf124645369ecb745a4e8a51171a3f7
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 11:41:04 2026 +0000
feat(insights): drop Net Flow, per-store triptychs, Deposit/Withdraw wording
Four changes to the Insights panel.
Net Flow By Store is removed. It was the last consumer of the diverging
bar, so W.DivergingFill / MakeDivergingBar / PlaceDivergingBar and their
tests go with it. Stats keeps netByStore -- the Insights CSV still
reports it.
BY STORE becomes a sub-section per store, each with its own All /
Deposits / Withdrawals row, instead of one combined panel per store. The
per-store item index now carries a direction split, so each store gets
the same three rankings every other metric has.
Every back-to-back half's tooltip now carries its share of that row --
"Deposit: 58 (47%)" -- because the percentage is the one thing the eye
cannot read off two bar lengths. Every other chart element's tip gains
the value it is showing too: bars, stacked bars and ranked-list rows all
print a truncated label, so the tip is the only place the full name and
its number appear together.
In/Out is renamed to Deposits/Withdrawals throughout the user-facing
surface: the ratio-bar captions, every companion chart title, the KPI
cards (items deposited / items withdrawn / gold deposited / gold
withdrawn), and the History table's Direction column and its two filter
labels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 377757f925b430a5904bda46513fc7ccdadc8b98
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 10:57:33 2026 +0000
feat(insights): draw every x In/Out chart back to back
The companions were left-aligned stacked bars, so the boundary between
deposits and withdrawals landed somewhere different on every row and
"which way does this lean" took arithmetic to answer.
Withdrawals now grow LEFT of a fixed centre axis and deposits grow
RIGHT, both sides sharing one scale -- the largest single-direction
magnitude in the list. The split sits on the same vertical line in every
row, so the lean of a whole chart reads straight down that line. Each
half keeps its own hover tip with the exact count, and the legend is
ordered withdraw-then-deposit to match the chart left to right.
Movements By Character x In/Out moves onto the same path, so every
in/out chart in the panel shares one reading. Movements By Character x
Store stays left-aligned -- it splits by store, not by direction.
Companion titles now mirror their parent in full: "Movements By Item
Type x In/Out" rather than "Item Type x In/Out", so a companion scrolled
past on its own still says what it companions.
The shared key -> display lookups move above the render helpers; a local
declared after them resolved as a nil global at call time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 18e95f956f44b4b54e5b9dc0d78fdeb4123e59a9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 10:39:56 2026 +0000
fix(insights): render net flow as a count, and make two tests bite
Net Flow By Store still formatted netByStore with SignedMoney after it
was rebased from copper to a signed movement count, so three net
deposits rendered as a copper coin amount. Export was updated with the
rebase; the panel was the missed consumer.
Two regression guards could not fail: the icon-truncation test asserted
against a row it built itself rather than the one the layout produces,
and the card-key test hardcoded its key list. Both now exercise the real
path.
Also drops the dead storeLeg pool key and I.FormatNet, caps the x In/Out
companions to BAR_ROWS like their parents, pins the migration's
edge cases, and corrects the Stats-key list in ARCHITECTURE.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit 337272ce3a792f4cbdc38e81c3ee0bc3075c3797
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 10:21:53 2026 +0000
docs: sync for the Insights overhaul and schema v2
Records the accepted CSV export-contract deviation, the new and removed
Stats keys, the reorganized Insights section order, and the /bl preview
-> /bl test rename. Regenerates the test-cases inventory.
commit a098ae9fb333a0da3dc5c9ada86c00c01990c8cb
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 10:11:18 2026 +0000
feat(test-mode): rename /bl preview to /bl test and enrich the dataset
Adopts the Ka0s house vocabulary set by LootHistory's /lh test: the
command, the flag, the builders and the badge all move from 'preview' to
'test'. SessionWindow's own preview session (/bl session) is a separate
feature and keeps its name.
The old dataset was near-uniform across 8 items, 3 stores, 3 characters
and 1 zone, which made every chart a flat comb. The generator now
weights stores, directions, classes, zones, types and qualities, runs a
hot-item head over a long tail, and follows an evening-leaning hour
curve. A coverage seed pass still guarantees every store, both
directions, every quality and every character appear over a >14 day
span, and the fixed-seed Park-Miller LCG keeps it byte-identical so the
headless tests can assert on it.
commit 29c992ecef4d8d6974229feac7cb6bb5cf9e9353
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 10:04:31 2026 +0000
test(insights): make the panel-pool test catch a shared row pool
The pooling test asserted only that a panel's row pool was non-nil,
which a single module-level pool shared by every panel would also
satisfy -- the exact corruption per-panel pools exist to prevent. It
also never checked that ReleasePanels releases each panel's rows.
Now asserts the two pools are distinct objects and that a checked-out
row is returned when its panel is released.
commit e37f326c921215cfaa92c6880f69c84979a2e393
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:59:37 2026 +0000
feat(insights): reorganize Top Of The List into an All/In/Out grid
Three columns instead of two, grouped under ITEMS / CATEGORIES / WHERE /
BY STORE sub-headings. Each metric becomes one row of three panels, so
the All / Deposits / Withdrawals triptych is itself the organization.
Adds the rankings behind it, all inside the existing single Stats pass:
item records gain movesIn/movesOut/qtyIn/qtyOut so six item rankings are
six sorts over the SAME record tables; topZones records gain the
direction split; byTypeSub pivots type and sub-type as one pair; and
itemsByStore drives a ranked list per store.
byZone deliberately keeps its plain count-map shape -- Export feeds it
straight to rankedRows, and reshaping it would break the stats CSV.
Panels are now pooled with per-pass titles, each carrying its own row
pool, because the per-store lists vary in count.
commit 2f2962b88049234c1f30c4f2009b48d5cc55a32c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:51:49 2026 +0000
test(stats): drop the duplicated storeByDirection case
The direction-split companion tests added a storeByDirection assertion
that already existed higher in the file, under the same test name. Two
identical names also duplicate the entry in the generated test-case
inventory. Keeps the original; the new section points at it.
commit 858b493759c56e1c856e7dcfb8a44a965cb360aa
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:50:14 2026 +0000
feat(insights): add Store/Quality/Type/Sub-type x In/Out companions
Four stacked-bar companions, each sitting immediately under its parent
chart in the direction colours. Store x In/Out finally consumes
storeByDirection, which Stats has computed and nothing has read; the
other three matrices are new accumulators in the same single pass, so
the panel got richer without the aggregation getting slower.
Each row keeps its parent chart's colour -- store colour, quality
colour, palette colour by rank -- so a category stays recognisable
across the pair.
commit 47d9e02074fcb7dc9aa1589d9539c019debe8ae5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:44:58 2026 +0000
test(insights): make the card-font test able to fail
The MakeCard headline test compared card.baseSize across two cards, but
the mock's CreateFontString returns the frame itself, so GetFont() never
yields a size and baseSize is nil for every card under test. The
assertion was nil == nil and would have passed with the two-template
bug reintroduced.
The mock now records font templates in creation order, and the test
asserts the headline template directly.
commit 2c18bf144838c357c054df8b08d74bab7a1375d1
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:40:03 2026 +0000
fix(insights): move the split labels below the bar, unify the card font
The Deposits vs Withdrawals captions were centred inside their own share
and dropped entirely when a share fell under 56px, so a lopsided split
lost its numbers. They now sit in a caption row beneath the bar, pinned
to its two ends and coloured by direction, readable at any split.
Cards lose the smallValue option: date range and busiest day were the
only two on a smaller template, which made them read as a different kind
of card. Every headline now shares one base font and relies on the
existing shrink-to-fit for the long strings.
commit bafc139f380e856fab2d2d52d1844f9d45f2dd16
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:35:21 2026 +0000
fix(insights): commit the W.Truncate markup warning
The doc note belongs with the icon-truncation fix in ffe78e5 but was
left unstaged. It warns that the cut is byte-based, so markup must be
kept out of band and prepended after truncating.
commit ffe78e5f41d7708c099feb462afebe3fd9d87275
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:32:26 2026 +0000
fix(insights): stop truncating the class icon into a raw texture path
The character bars concatenated the |T...|t class-icon escape into the
label before W.Truncate cut it at 17 bytes, so the cut landed inside the
escape, the |T never closed, and WoW rendered the texture path as text.
Rows now carry the icon out of band and only the name is truncated, with
a 14-char budget so icon plus name still fit the 118px label column.
commit 0a932ec344ca2ff2c2de1ff46480e44464186cc8
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:25:32 2026 +0000
style(insights): wrap the itemsMoved tooltip under 100 columns
Fix-round follow-up: the new itemsMoved card tooltip line ran 104
columns. Wrap it with the same concatenation style the deleted value
card's tooltip used, matching the file's line-length convention without
touching the pre-existing over-length lines elsewhere in the file.
commit a0e1753f24915216de6203883944e40e3178b81f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:21:45 2026 +0000
feat(insights): retire the value charts, add top store and items moved
Removes the value moved and biggest move cards, the Value Moved By Store
bars, the Value Moved Over Time strip and the Top Items By Value panel.
Dropping two cards left 14 column-units in a 4-wide grid, so two derived
cards fill it back to four clean rows: top store (from byStore) and
items moved (items in + items out). Both come from figures the single
Stats pass already computes.
commit 1144bc165f8a93009545ce8a230e5550645fbe8f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:14:42 2026 +0000
feat(schema)!: drop the vendor-value dimension, bump to schema v2
Value is no longer derived, captured or persisted. Util.EntryValue and
Util.SignedValue are deleted, Compat.GetItemDetails stops reading vendor
price, and a v1->v2 migration strips vendorPrice from every stored entry
so the bytes actually leave the SavedVariables file.
Stats loses totalValue, valueByStore, valueByDay, topItemsByValue,
biggestMove and the per-item/per-character value fields. Net Flow By
Store re-bases on movement count, the only signed non-gold quantity
left. Two new totals replace the removed cards: itemsMoved and topStore.
Gold is untouched -- a MONEY row's amount lives in quantity and was
never vendor-priced.
Folds in Task 3 (remove value from both CSV exports): Export.lua's
COLUMNS and InsightsCSV called the now-deleted value helpers directly,
so there was no green gate without it. The ledger CSV loses
vendorPrice/value/valueRaw/net; the insights CSV loses the Value moved
summary row and the Top Items By Value section, and writes Net by Store
as a movement count. Accepted deviation from the stable-column contract,
tied to this same schema v2 bump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit e8be9a40786033fbb2c2e79d87c54e36662cda78
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 09:01:27 2026 +0000
docs(plan): implementation plan for the Insights panel overhaul
Ten tasks over the spec, each ending on a green gate and its own commit
-- except Tasks 1 and 2, which share one because deleting Util.EntryValue
breaks Database:Stats immediately and there is no honest intermediate
state between them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit c878e553b0b305557514931fff1f3e20c0367d84
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 08:50:49 2026 +0000
docs(spec): design the Insights panel overhaul
Seven changes to the Insights tab, specced together because six of them
fall out of the first: removing the value dimension rewrites
Database:Stats, and every other item edits a chart that pass feeds or
adds a new one to it.
- Value is no longer derived, captured or persisted; schema bumps to v2
with a migration that strips vendorPrice from stored entries. Net Flow
By Store re-bases on movement count. Gold is untouched.
- Records the CSV export-contract break as an accepted deviation.
- Diagnoses the character bar labels rendering as raw texture paths:
W.Truncate cuts a 17-byte label mid-|T...|t escape.
- Ratio-bar labels move below the bar; span/busiest cards drop
smallValue so every headline shares one base font.
- /bl preview becomes /bl test, with a weighted generator replacing the
near-uniform dataset.
- Adds Store/Quality/Item Type/Sub-type x In/Out companions.
- Reorganizes Top Of The List into a 3-column All/In/Out grid over
pooled panels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KMC9SCpEMtxj8HUtRfuynK
commit a43c00f16c3d63d0a0c3153b9aab13abbab0fe10
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 06:32:48 2026 +0000
fix(media): ship the logo TGA, and sharpen the 256 avatar
The settings landing page was rendering NO logo at all. C.LOGO_PATH points at
media/logos/bankledger.logo.tga and that file had never been generated — only
the PNG master and the two JPEGs were in the repo. A missing texture draws
nothing and raises no error, so the page just came up blank and nothing failed.
Added bankledger.logo.tga: 512x512, 24-bit RLE, Lanczos from the 1254x1254
master, byte-format identical to the sibling addon's. 512 because the master is
1254 square, which is NOT a power of two — the client rescales anything else,
which is its own source of softness — and 512 is the smallest power of two
comfortably above the 300px the panel draws it at.
The 256 avatar was not badly resampled as it first appeared (it measured as a
competent Lanczos, RMS 4.5 against one). It is simply dense artwork — a ledger
page of tiny text, coin stacks, filigree — losing detail at a sixth of the
master's linear size. Regenerated with a light unsharp mask afterwards and no
chroma subsampling: edge energy 40.4 -> 47.2, now ahead of the sibling addon's
45.1 that prompted the comparison.
ARCHITECTURE gains a Logo art section: what each of the four files is for, the
three rules the derivatives must follow (each of which has now gone wrong once)
and the snippet that regenerates them. The smoke test for the landing page now
actually checks the logo is present and crisp, since blank was passing before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit 0d11b9269b81ad544bca7ec22ff719810d166704
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 06:10:11 2026 +0000
fix(browser): give the ledger window the same guaranteed geometry saves
The main window had the identical exposure the session window just had: its
position and size were written only by the drag-stop and resize-grip handlers,
both of which fire at the end of an interaction, and the resize one is easy to
miss entirely because releasing the grip outside a 16x16 button never delivers
its OnMouseUp. Miss it and the geometry lives only in the frame, which outlives
every Hide/Show — so it looks persistent all session and is gone on /reload.
SaveWindow/RestoreWindow become B:SaveGeometry()/B:ApplyGeometry(), matching
SessionWindow method-for-method, and now also run on every OnHide (closing the
window is guaranteed) and on PLAYER_LOGOUT (a /reload with the window on screen
never runs OnHide). Same two hardenings as the session window: no point-less
table is ever written, and a restored size is clamped to the window floor on the
way in as well as out.
Two test-quality fixes fell out of it. test_browser now imports assertFalse,
and the Insights render cases state the filter they expect instead of inheriting
it — opening the ledger window scopes the shared filter to the current
character, which silently emptied their fixtures once these cases started
building the real window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit 0f9a4ea2b3b0724c58d0c7109fd0067cfb0b99af
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 05:46:23 2026 +0000
fix(session): persist the window's geometry across game sessions
Position and size survived every banking session and were lost on /reload.
Root cause: WHEN the save ran, not what it wrote. Geometry was only written by
the drag-stop and resize-grip handlers — both fire at the end of an
interaction, and the resize one is easy to miss entirely, because releasing the
grip a pixel outside a 16x16 button never delivers its OnMouseUp. Miss it and
the geometry lives only in the frame object, which outlives every Hide/Show.
That is exactly why it looked persistent between bank visits: nothing was ever
being read back from SavedVariables, so the save path was never exercised.
Fix: anchor the save to moments that are guaranteed instead of to the end of an
interaction. SaveGeometry() now also runs on every OnHide (every bank close) and
on PLAYER_LOGOUT (a /reload with the window on screen never runs OnHide). The
interaction handlers stay — they cost nothing and keep the value current
mid-session. SaveGeometry also refuses to write a point-less table, which would
have made the restore fall through to the default and silently discard a real
position; and ApplyGeometry clamps a restored size to the column minimum on the
way in as well as on the way out.
saveWindow/restoreWindow become the public SW:SaveGeometry()/SW:ApplyGeometry()
seams so the round trip is testable without a test-only backdoor.
The frame mock now models position and size, since a blanket no-op made this
whole class of bug invisible: GetPoint() returned the frame itself, so "we saved
the position" and "we saved a garbage table" looked identical. It also runs
OnHide hooks on a real shown-to-hidden transition. Six new cases cover the save,
the restore, the clamp, the hide-save, the logout-save and the full round trip.
Note: modules/Browser.lua has the same interaction-only save and the same
latent exposure; left alone as it was not in scope.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit 2b76ca619456d6e38b4fc2cde8d1f1f505769804
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Jul 27 05:38:15 2026 +0000
fix(session): end a guild-bank session when the frame closes
Closing the guild bank left the session window open until some later,
unrelated bag update happened along.
Root cause: the guild bank has no usable CLOSE event either. GUILDBANKFRAME_CLOSED
registers without complaint and never fires on 12.0.7, exactly like its _OPENED
sibling that the arming path already works around. The only disarm was the
IsGuildBankVisible() check inside Reconcile — and closing the window changes no
container and moves no money, so no event fires, no reconcile pass runs, and the
context stays armed. That check is the backstop for a frame that vanished
without hiding; it was never a close path. The character and warband banks were
unaffected because BANKFRAME_CLOSED genuinely fires.
This was a latent bug in the capture engine — a guild context already lingered
this way — that the session window made visible for the first time.
Fix: hook GuildBankFrame's own OnHide, the one notice the client actually gives.
It ends only the guild bank's own context (the frame also hides whenever it is
simply not the panel on screen, and closing the character bank's session on that
basis would discard a baseline still in use). Blizzard_GuildBankUI loads on
demand, so the hook goes in the first time the guild bank is in play, once only;
hooking OnHide on a plain non-secure frame taints nothing. The visibility
backstop stays for a frame that goes away without hiding.
/bl debug scan now reports whether the hook is installed, since NOT INSTALLED is
the whole explanation for a guild session that will not end.
The frame mock grew an OnHide hook and a __closeGuildBank() that closes it the
way the client does, so the regression is covered headlessly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit ff4f9ddc832a59505893a1ebfed630308140cc7e
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Sun Jul 26 22:02:59 2026 +0000
fix: three defects found reviewing the two new features
* The session window opened at a bank even with capture switched off, where
no movement can ever be recorded — a live window guaranteed to stay empty
for the whole visit reads as broken, not as disabled. Enabled() now gates on
the master switch too.
* SessionWindow recomputed the column list and the column layout once per row
per bind: ~13 identical passes and 13 throwaway tables per repaint. The
specs are cached (only once fully resolved, so a partial pre-load resolution
can't stick) and the layout is computed once per bind in BuildHeaderCells.
* Insights pre-normalized the quality and weekday bar fractions before handing
them to RenderBars, which normalizes against the true peak itself. Redundant,
and the comment explaining it was wrong about why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit 09b9014815217516e45a0785f41988c793f2a6df
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Sun Jul 26 22:01:08 2026 +0000
docs: describe the session window and the rebuilt Insights panel
ARCHITECTURE gains a Windows section (why the session window is not a second
LedgerTable instance, and what the two share instead), an Insights section
naming the three choices a future change is most likely to undo, the fourth
bus message, the new schema row and geometry carve-out, and the slash verb.
smoke-tests gains S-17 for the session window — including the guild bank,
which has no close event — and rewrites S-9 to walk all sixteen Insights
sections.
README is rolled forward in place rather than bumped: 0.1.0 is dated today and
unreleased, so the session window and the expanded Insights belong in its own
"What's new" and Version History row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit 180df60810bc88701d380d8cfea25c2b2ef0c5a8
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Sun Jul 26 21:58:00 2026 +0000
feat(insights): rebuild the Insights tab around flow
The old tab was eight bare numbers and four flat blue bar lists. A bank
ledger measures FLOW, so the panel is now organised around direction, store
and net rather than around a flat count-by-category list.
Fourteen KPI cards on backdrop panels with shrink-to-fit headlines and a
tooltip each (half of these are derived figures; a bare number under a
two-word caption is a quiz, not an insight), then sixteen sections under
ITEMS & FLOW / GOLD / TOP OF THE LIST dividers:
* deposits vs withdrawals as one proportional ratio bar — the question is
"what share", which two bars cannot answer
* movements, net flow and value moved by store, net on DIVERGING bars: the
only honest form for a signed quantity, and both directions share one
scale so +100 is visibly twice -50
* movements by character, plus Character x Store and Character x In/Out
stacked companions — who fills the bank and who empties it
* quality (ascending, in the game's own colours), item type and sub-type
(categorical palette by rank, so adjacent bars are never lookalikes)
* movements, value and gold over time as per-day strips with rotated,
auto-thinned axis labels, plus hour-of-day and weekday
* four ranked list panels in two columns: top items by value, by movements
and by quantity, and top banking spots
The GOLD block hides entirely when the slice holds no coin movement: two
blank charts under a banner read as a broken addon, not as "you moved no
gold". Quiet days keep a ghost bar, because a gap of nothing cannot be told
apart from a day outside the range.
Everything still computes off ONE Database:Stats pass over the browser's
shared filter, so the charts and the History table cannot disagree.
Stats gains fourteen keys — sub-type, quality, zone, hour, weekday, coin per
day and per store, character/store x direction matrices, three top-item
rankings, top zones, net items, gross gold — all as extra accumulators in the
same single pass, and all additive: no existing key changed name or meaning.
Export:InsightsCSV gains a matching section per new chart, appended, so a
sheet keyed on the old headers keeps working.
The visual vocabulary is peeled into modules/InsightsWidgets.lua so Insights
stays about WHAT is shown and that file about HOW; nothing in it knows what a
ledger entry is, which is also what makes the geometry unit-testable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit 2ae676ddc61209a7736bede8a31e3aebacfad0ca
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Sun Jul 26 21:47:08 2026 +0000
feat(session): live "Current Banking Session" window
Opens automatically whenever a character, warband or guild bank frame is
open and lists, in real time, exactly what moved during that one visit.
Closes with the frame.
A session is the span the capture engine already arms openContext for, so
the window rides a new Ka0s_BankLedger_SessionChanged message (one sender,
Ledger) rather than re-deriving the span from the open/close events — which
would have missed the guild bank, armed by tab data rather than by an event.
Session rows are references to the entries Database:Add already stored, held
in NS.State.sessionEntries and dropped when the frame closes: no second copy
of the ledger and no new SavedVariables field. A delete or purge prunes them,
so the view cannot outlive its data.
The view is the History table minus what it has no use for — no Insights tab,
no filter bar, no search/Clear/Export, no footer, inert headers, and no Date,
Time or Character columns (every row happened moments ago, and capture only
ever records your own movements). Width is derived from the seven remaining
columns.
It is not a second NS.LedgerTable instance: that module is a stateful
singleton and this table has no sort, group, filter or collapse. What is NOT
duplicated is the definition of a column — LedgerTable gains public Column()
and PaintCell() seams, so a cell's text and colour have one definition and
cannot read differently in the two windows.
Settings > General > "Session window" (on by default) gates the window only;
capture is untouched. /bl session opens it on a synthetic visit so it can be
positioned without standing at a bank (preview-mode).
The frame stub now models visibility: a blanket no-op made IsShown() return
the frame — permanently truthy — so "the window closed" was untestable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit d6312c3a71384f70d09aa2145442f5297c1a6278
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Sun Jul 26 21:39:37 2026 +0000
docs: design spec for the banking-session window and expanded Insights
Two independent features on one branch. Feature 1 adds a live, minimal
"Current Banking Session" window driven by the existing openContext span.
Feature 2 rebuilds the Insights tab on the widget vocabulary the sibling
LootHistory Analytics panel established, over an additively extended
Database:Stats.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit b4b60ebb80b7c8d65d94ffc60a36f4dfa699a786
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Sun Jul 26 21:16:17 2026 +0000
feat(browser): colour-coded table, sub-type/quality filters, class icons
History table
- Withdraw turns red (the ff5555 the destructive row action already uses) and
each row gains a direction glyph: a real ▲/▼ text glyph in the vendored mono
font, so it sits on the label's own baseline and takes the direction's colour
from the same SetTextColor call. Blizzard's arrow textures carry uneven
padding and misalign against the row.
- Stores are colour-coded from a shared palette: character bank gold, warband
bank heirloom cyan, guild bank magenta, bags/reagent muted.
- New Sub-type column; the Value column is gone and a gold row now shows its
amount as money in Qty, sorted on what the cell displays.
- Gold rows read Type/Sub-type "Gold", a dash for Quality, a pale gold item
name, and hovering one builds a tooltip by hand (there is no item link).
- Characters show their class icon beside the class-coloured name.
Filters and grouping
- New Sub-type and Quality dropdowns; Quality runs Poor→Legendary rather than
alphabetically and carries each quality's colour.
- Type/Sub-type now match the EFFECTIVE type (Util.EntryType), so "Gold"
filters gold movements — the column, the dropdown, the query and the grouping
all read one definition.
- In/Out and Store menus paint their options in the table's colours, with the
direction glyph, from the palette in Constants.
- Group by gained Type, Sub-type and Quality; the kind group's prefix is now
"Item/Gold", which no longer collides with the Type column's own grouping.
- The window opens scoped to Character: Current — one definition of "default",
used by the first build, a dataset swap and Clear alike. Preview mode stays
unscoped, since its data is synthetic alts.
Settings
- General is restructured to the collection's shape: Master Controls (enable,
minimap, debug console, window scale + Reset all), then Capture (quality,
retention, item/gold toggles, per-store grid), then Storage.
- Retention default drops from 90 to 30 days.
- The header's Defaults button is built lazily on first OnShow instead of at
registration, so it isn't left on Blizzard's stock red art by a load-order
race with UI skins that hook AceGUI's RegisterAsWidget (standard v2.11.0,
options-ui-§5). New `/bl debug panel` dump verb, which is what found it.
Media
- Ship the logo: a 512×512 24-bit RLE TGA (the runtime asset) plus 1024 and 256
JPEGs, all LANCZOS-resampled from the PNG master with 4:4:4 chroma.
Window widens to 1036px to fit the new columns; min and default widths still
derive from the column model. 366 tests green, luacheck clean.
commit 47e62db3f7387773002a07da23736e7ab6e179a5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Sun Jul 26 19:35:20 2026 +0000
fix: record guild bank movements, and stop polling the settle wait
The guild bank has no usable open event. GUILDBANKFRAME_OPENED is a valid
name that registers without complaint and never fires, so the context was
never armed and every GUILDBANKBAGSLOTS_CHANGED was discarded. It now arms
on that data arriving instead, which is the signal the server actually
sends: the first lands as the window opens, before anything can be moved,
which is exactly when the baseline wants taking. It never steals the
context from an open bank frame, and disarms once the window is gone so it
stops rescanning six 98-slot tabs on every bag update. That check is
three-valued -- nil means this build cannot tell, and must not disarm.
Guild tabs also hold no data until queried, so only the tab being looked
at was readable. Arming now queries every tab; replies arrive async and
reconcile like any other change. Both getters are guarded independently so
a build that retires one degrades to no data rather than raising mid-scan.
The settle wait no longer polls. Waiting out a one-sided change that will
never balance -- an item deleted with the bank open -- burned a dozen full
rescans and two dozen log lines over six seconds to learn nothing. The
other half of a real movement always arrives with an event, so the timer
is only a give-up deadline, re-armed on the remaining window so event
traffic cannot push it out.
Compat reads frames as bare globals, never through _G: an explicit _G
lookup steps around the headless loader's mock environment into the real
global table, so it is untestable and silently always nil.
Tests 319 -> 335, luacheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit afc8e5101b0eaf7b01d4488b572563350554c685
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Sun Jul 26 19:13:28 2026 +0000
fix: make capture actually work on live 12.0.7
Four independent faults meant nothing was ever recorded. Each was found by
instrumenting the client rather than guessing, and each is pinned by a test
reproducing it.
Frames, not stores. The bank window hosts the character bank and the warband
tabs behind one BANKFRAME_OPENED, and switching tabs fires nothing, so no
event can say which store is open. Every store a frame reaches is now diffed
each pass and the both-sides-changed rule picks the one you used.
Container ids by name. Numeric fallbacks for enum members this build lacks
landed on unrelated containers: BANK swallowed warband tab 1 (AccountBankTab_1
is 12) and listed ids 6-11 twice, double-counting every stack. Groups are now
derived from Enum.BagIndex by anchored name match.
Isolated event registration. RegisterEvent raises on a retired name, so one
stale entry aborted the loop and left the addon deaf to every change event.
Five names are retired on this build. Each registration is now independent and
the rejects are reported by /bl debug scan.
Settle before advancing the baseline. The two halves of one movement can be
seconds apart when a warband tab waits on a server round-trip. A pass that saw
one side used to advance the baseline and destroy it. The baseline is now held
until the change balances, re-anchoring after a timeout so an unmatched delta
cannot later pair with something unrelated.
Void storage is removed: retired in 12.0.7, with neither events nor a
container, so the addon no longer advertises a store it cannot observe.
The mock gains an enum copied verbatim from a live client, a RegisterEvent
that rejects names, and a fireable timer queue. All three gaps are why a green
suite shipped a broken addon.
Tests 277 -> 315, luacheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
commit e5e69083b679b47597a87e0ed2b7488e3441108f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Sun Jul 26 18:29:35 2026 +0000
feat: scaffold Ka0s Bank Ledger v0.1.0
A passbook of item and gold movements between the player's bags and the
character, reagent, warband and guild banks plus void storage. WoW fires
no deposit event, so capture is a snapshot differ: an item only becomes a
ledger row when it falls in the bags and rises in the open store, which
keeps loot, vendoring and questing out of the book.
Ships the standalone browser (History table + Insights tab over one shared
filter), CSV export for both tabs, an item blacklist/whitelist, the
on-screen debug console, and the schema-driven settings panel.
Born compliant with the Ka0s WoW Addon Standard v2.10.0: modular layout,
vendored libs, eager settings category with lazy bodies, secret-safe
printer, session-only debug state, and a headless harness at 277/277 with
luacheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzsdh9Hp4oCSzbwXycX8tn
This mod has no additional files