Ka0s Bank Ledger

Records every item and gold movement between your bags and your banks, tags each one with the store, direction and place, and lets you browse and analyze your banking history — account-wide.
Back to Files

1.0.0-release

File nameBankLedger-1.0.0-release.zip
Uploaded
Jul 28, 2026
Downloads
3
Size
3.9 MB
Flavors
Retail
File ID
8527290
Type
R
Release
Supported game versions
  • 12.0.7

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