promotional bannermobile promotional banner

TOG Bank Classic

The Old Gods' version of GBankClassic-Revived - with in-game item requests.
Back to Files

TOGBankClassic-v1.3.2

File nameTOGBankClassic-TOGBankClassic-v1.3.2.zip
Uploader
EY3G0R3EY3G0R3
Uploaded
Aug 3, 2026
Downloads
1.0K
Size
826.7 KB
Flavors
Classic TBCClassic
File ID
8570161
Type
R
Release
Supported game versions
  • 2.5.6
  • 1.15.9

What's new

TOGBankClassic Changelog

[v1.3.2] (2026-08-03) - ElvUI & Baganator Item Highlighting, Offline Test Suite

Bug Fixes

  • BAGANATOR-001: "Highlight needed items" did nothing when running Baganator — Same failure as ELVUI-001 below and from the same cause: Baganator replaces the bag UI, so the Blizzard-frame fallback found only hidden buttons and ApplyOverlay bailed on its visibility guard. This is the path that affects the TBC install, where Baganator is the bag addon in use.

    Unlike ElvUI there is no public way to drive Baganator's search or its per-slot dimming — Baganator.API exposes no search setter, and the per-button SetItemFiltered/SetMatchesSearch methods live on internal mixins. The one sanctioned integration is the corner-widget API, so the visual differs by design here: needed items get a marker in the icon's top-left corner instead of everything else going grey. Registered via Baganator.API.RegisterCornerWidget, following the pattern of Baganator's own equipment_set_icon and CanIMogIt widgets in API/ItemButton.lua (lines 316-355), and refreshed through Baganator.API.RequestItemButtonsRefresh({Baganator.Constants.RefreshReason.ItemWidgets}).

    Three details worth recording. The onUpdate contract is tri-state — true shows, false hides, and nil means "item data isn't available yet" — so a cold item cache returns nil rather than false, which would otherwise latch the marker off until the next full refresh; ID-keyed requests skip that entirely since they need no cache. The marker uses SetColorTexture rather than a texture path because the Era client is missing many icon assets and a missing file renders as a blank square with no error. And RegisterCornerWidget asserts on a duplicate id, so registration is latched and wrapped in pcall — a Baganator API change degrades to the previous behaviour instead of breaking highlighting for everyone. Not yet exercised in-game. Locations: Modules/ItemHighlight.lua, .luarc.json.

  • BAGANATOR-001/ELVUI-001: integration latches were read out of scopeSetEnabled's disable path sits above the ElvUI and Baganator implementations in the file, so its reference to the baganatorRegistered latch resolved to a nil global rather than the local declared further down. The guard would never have fired, leaving markers on screen after unticking the box. Both latches now sit at the top of the file with the module's other state. Caught by the language server, not by luac -p — a bare global read is valid Lua. Location: Modules/ItemHighlight.lua.

  • ELVUI-001: "Highlight needed items" did nothing when running ElvUI — The checkbox ticked, saved its state, and had no visible effect. ItemHighlight only ever supported two bag UIs: Bagnon (driven through its search string) and Blizzard's default ContainerFrameNItemN buttons. ElvUI replaces the bag UI wholesale, so the Bagnon branch found no Bagnon/BagBrother global, fell through to the Blizzard branch, and every button lookup landed on a frame ElvUI keeps hidden — where ApplyOverlay's if not button:IsVisible() then return end guard bailed silently. No error, no message, nothing dimmed.

    Added a dedicated ElvUI path that reuses ElvUI's own per-slot searchOverlay texture — the dark overlay it already shows over items filtered out by its search box — rather than writing icon vertex colours that ElvUI overwrites on its next rebuild. Both writers of that overlay are hooked with hooksecurefunc (B:UpdateSlot, which sets it during a per-slot rebuild, and B:InventorySearchUpdate, which re-asserts it on search events) so our pass runs after ElvUI's and wins. We only ever turn the overlay on for unneeded items and never off, so a slot ElvUI is already hiding for its own search stays hidden and its search keeps working. Disabling the checkbox calls B:UpdateAllBagSlots() with enabled already false, making the hook a no-op so ElvUI reasserts its own state. ElvUI's bank is covered for free, since UpdateSlot is invoked with B.BankFrame too.

    Verified against ElvUI/Game/Shared/Modules/Bags/Bags.lua in tukui-org/ElvUI: B:UpdateSlot(frame, bagID, slotID) (line 678), slot.searchOverlay:SetShown(info.isFiltered) (lines 734, 742), SetColorTexture(0, 0, 0, 0.6) (line 2734), B:InventorySearchUpdate (line 824), frame.Bags[bagID][slotID] (lines 679-680), B.BagFrame/B.BankFrame (lines 3757-3758), B:UpdateAllBagSlots() (line 507), and E:NewModule('Bags', ...) in Game/Shared/General/Initialize.lua. That module is shared across flavours and branches internally on E.Classic/E.Retail, so one path serves Classic Era and TBC with no flavour-specific code on our side. Not yet exercised in-game on either flavour — ElvUI is not installed on the development machine. Every lookup fails safe: if any assumption is wrong the path returns nil and falls through to the previous behaviour rather than erroring. Locations: Modules/ItemHighlight.lua, .luarc.json.

  • ELVUI-001: ElvUI is only claimed when it is actually drawing the bags — The Bags module object exists even when the user has switched ElvUI's bag replacement off (running Bagnon underneath it, for instance). Detecting the module alone would have let the ElvUI path claim a UI it wasn't driving and skip the Bagnon integration entirely. The check now also requires B.BagFrame, which is only assigned in B:Initialize(), so a disabled bag module falls through to the Bagnon and Blizzard paths as before. Location: Modules/ItemHighlight.lua.

Internal

  • TEST-001: offline test suite — The addon now carries a unit-test suite that runs against a fake WoW client with no game and no LuaRocks, matching the layout used by the other addons in the tree. Tests/wowapi is the shared WoWAPITesting harness as a git submodule; Tests/env_togbank.lua adds the environment this addon needs on top of it (a controllable clock and timer queue, the container/bag API, the guild roster API, the item and ItemMixin APIs, and a .toc-ordered module loader); Tests/coverage.lua is the zero-dependency line-coverage tool, copied from GuildRoster. Run with lua Tests/wowapi/run.lua from the addon root — Lua 5.1 is the only requirement. Tests is in .pkgmeta's ignore list so none of it ships. New: .busted, .luacheckrc, Tests/README.md, Tests/HARNESS_CONTRACT.md.

    211 specs across 8 files: 189 pass, 22 fail. The 22 failures are deliberate — each one asserts correct behaviour against a defect recorded in docs/AUDIT_2026-08-03.md and names the audit ID in its failure message. They are the audit made executable and go green as the fixes land in v1.4.0; none should be made to pass by weakening an assertion.

    One environment decision is worth recording because it is load-bearing: C_Timer.After returns nothing in the harness, exactly like the real API. Only NewTimer/NewTicker return a cancellable handle. The convenient stub — returning a handle from After too — would have made all eight TIMER-001 sites pass and hidden the entire bug class, since every one of those cancels sits behind an if timer then guard that makes a broken cancel indistinguishable from a working one.

  • AUDIT-001: full-codebase audit recordeddocs/AUDIT_2026-08-03.md is the working document for the v1.4.0 overhaul: 2 critical, 4 high, 12 medium and 9 low findings, each with a greppable ticket ID, a location, a stated fix and a checkbox. It also records what was verified healthy (TOC lockstep, .pkgmeta correctness, comm-prefix registration, the three-way debug-category consistency) so a later pass does not re-litigate settled ground, and it is explicit about which modules were line-read and which were only mechanically scanned. Roughly half the codebase — Chat, RequestLog, DeltaComms, Mail, Options and the UI modules — still needs a second read pass, tracked as AUDIT-PASS2.

    Nothing in this release fixes any of those findings. The suite and the document exist so v1.4.0 can fix them against a safety net rather than by inspection.

  • DEBUG-001: five further mis-categorised debug calls found by the suiteTests/constants_spec.lua scans every Debug() call site in every file and validates the category/tag pair against DEBUG_CATEGORY and DEBUG_TAGS. It caught five Debug("FULFILL", …) calls in Modules/RequestLog.lua (lines 2070, 2075, 2077, 2153, 2158) that the manual read missed — RequestLog.lua is one of the modules not yet line-read. An unregistered category is not cosmetic: Output:Debug falls through to the category-only branch, the string becomes the format string, every argument shifts, and the line renders with a raw %d in it. Recorded in the audit, not yet fixed.

[v1.3.1] (2026-08-02) - Banker Inventory Never Scanned

Bug Fixes

  • SCAN-001: The per-character "enable scanning" flag could read as unset, silently disabling all scanning — Found while investigating a report of a banker's Inventory tab staying empty. This is a real latent defect but it was not the cause of that report, and it is not TBC-specific.

    Bank:Scan() and Mail:Scan() both gate on Options:GetBankEnabled(), which reads db.char.bank.enabled. That key was missing from the AceDB char defaults in Options:Init() (the table declared only donations = true), so on any profile where it had not been explicitly written it read nil — falsy — and every scan returned early. The single place that ever wrote it was Options:InitGuild(), which was reachable only from inside if TOGBankClassic_Guild:Init(guild) then in the GUILD_RANKS_UPDATE handler. Guild:Init returns false as soon as Info.name matches the current guild, so InitGuild got exactly one attempt per session — and that attempt fires before the guild roster carries public/officer notes. With memberRoster still empty (it is built by RefreshOnlineCache behind a C_Timer.After(0.5)), IsBank() fell through to GetBanks(), which found no gbank notes, returned nil, and made InitGuild bail at its own IsBank guard. Nothing retried it, so enabled would stay nil in SavedVariables for the life of that character. InitGuild is also what registers the Bank options panel, so losing that race additionally left the "Enable for <character>" tick box absent from the options tree.

    Hardened in three parts: (1) enabled = true is now a declared default in the char scope, so the flag can never read nil — safe for non-bankers because both scan paths already gate on IsBank() independently; (2) Options:InitGuild() now latches on success via self.guildInitialized instead of relying on Guild:Init's once-per-guild return, so it is safe to call repeatedly and retries until banker status is actually known — AddToBlizOptions still runs exactly once, so no duplicate Bank panels; (3) it is now called unconditionally on GUILD_RANKS_UPDATE and from the deferred block in GUILD_ROSTER_UPDATE immediately after RebuildBankerRoster(), which is the first moment IsBank() can answer correctly — GUILD_RANKS_UPDATE alone is not a reliable retry hook because it may not fire again after the roster loads. Locations: Modules/Options.lua, Modules/Events.lua.

  • SCAN-001: An empty Inventory tab showed "Loading items..." forever — When a character's aggregated item list was empty, OnGroupSelected added the loading label and then skipped the entire if items and #items > 0 block, so the scroll:ReleaseChildren() that clears the label — which lives inside the Item:GetItems callback — never ran. An empty record was therefore indistinguishable from a stalled load, which is what disguised the scan bug above as a hang. Empty tabs now clear the label and state the real situation, with different wording for your own character (which tells you to open the bank or run /togbank share) versus another banker's (which is waiting on them to share). Location: Modules/UI/Inventory.lua.

Improvements

  • SCAN-001: Bank:Scan() now logs why it declined to scan — All five early returns (nothing marked dirty, Guild.Info not loaded, no bankers found in guild notes, this character not in the banker list, scanning disabled for this character) previously returned in silence, and the function's first debug line sat well past all of them. Diagnosing a non-scanning banker meant reading the source and guessing which precondition was unmet. Each gate now emits a BANK.GATE line naming the precondition and, where useful, the remedy; a matching BANK.SCAN line on the success path reports the item and slot totals and whether the bank vault was included (it is skipped away from a bank NPC, which is expected and previously invisible). Enable with the BANK category in the debug options. Location: Modules/Bank.lua.

Internal

  • Wired up the BANK debug category, which was declared but unreachableDEBUG_CATEGORY.BANK had existed in Modules/Constants.lua since the category system was introduced, but it had no CATEGORY_META row (so no toggle appeared in the debug options), no entry in Database:Init()'s debugCategories defaults, and no DEBUG_TAGS block — and not one line of Modules/Bank.lua ever wrote to it. Added all three, with GATE and SCAN tags. ITEM was likewise missing from the debugCategories defaults (it did have an options row) and has been added alongside, restoring the invariant in CLAUDE.md that the category list, the defaults table, and CATEGORY_META stay in sync. Locations: Modules/Constants.lua, Modules/Database.lua, Modules/Options.lua.

[v1.3.0] (2026-08-02) - TBC Client Support

New Features

  • TBC-001: Added a TBC TOC so the addon loads on Burning Crusade clients — TOGBankClassic previously shipped a single TOGBankClassic.toc at Interface 11508, so a TBC client (2.5.x) treated it as out of date and the CurseForge listing offered no TBC build. Added TOGBankClassic_BCC.toc at Interface 20506, matching the file list of the Era TOC exactly (same libraries, same module load order, same SavedVariables, same Ace3, VersionCheck-1.0 dependencies) and differing only in the ## Interface value. The BigWigs packager reads every *.toc in the tree to decide which game versions to publish for, so the same source tree now produces both the Era and the TBC build. .pkgmeta keeps enable-toc-creation: no — both TOCs are checked in and maintained by hand. Location: TOGBankClassic_BCC.toc.

Improvements

  • Bumped the Classic Era interface to 11509TOGBankClassic.toc still declared 11508 while the live Era client is 1.15.9, so the addon showed as out of date in the character-select AddOns list until "Load out of date AddOns" was ticked. Location: TOGBankClassic.toc.

Internal

  • The two TOC files must be kept in lockstep. Any new module, vendored library, SavedVariable, or metadata line has to be added to both TOGBankClassic.toc and TOGBankClassic_BCC.toc — a module added to only one silently fails to load on that flavour. Recorded in CLAUDE.md.

  • Added the dev-sync watcher so both flavours can be tested from one working tree — ported wow-version-replication.ps1 from the FastGuildInvite repo and retargeted it at this addon. It mirrors the _classic_era_ source tree into _anniversary_\Interface\AddOns\TOGBankClassic (the TBC install) on a 2-second poll. Two independent launchers start it: the developer's global Claude Code SessionStart hook (which scans the project dir for the script) and, for editor-only sessions, a folderOpen task in .vscode/tasks.json. A per-repo named mutex plus the hook's own process scan mean whichever fires second exits cleanly instead of racing the first — confirmed in practice, the first live run logged one LAUNCH followed a second later by one SKIP already-running. $WowVersions deliberately lists only _classic_era_ and _anniversary__classic_ (MoP) and _retail_ are excluded because there is no TOC for them and a copy there would sit permanently "out of date". The skip list is built by parsing .pkgmeta's ignore: block, so the synced install mirrors the shipped zip; the repo's flavour-specific .git pointer file is hard-skipped so it can never resolve the _anniversary_ copy back at the Era git dir. Verified end to end on the first real run: 50 files landed in the TBC install, no .git, no dot-prefixed entries anywhere in the tree, no docs/ or tools/ — matching the -DryRun projection exactly. Locations: wow-version-replication.ps1, .vscode/tasks.json, .pkgmeta.

  • .pkgmeta cleaned up to the documented ignore-syntax rules — removed seven dot-prefixed entries (.git, .github, .gitattributes, .gitignore, .vscode, .luarc.json, .markdownlint.json) and the "*.DS_Store" glob. All eight matched nothing: the packager's copy_directory_tree() prunes dot-prefixed paths unconditionally, so listing them implied coverage the entries weren't providing. Rather than couple the packager config to the dev-sync script (the script had been reading those entries to build its skip list), wow-version-replication.ps1 now mirrors the packager's prune directly in its own $AlwaysSkip — one dot-segment pattern replacing the .gitignore/.gitattributes/.gitmodules/.pkgmeta special cases. Verified equivalent with -DryRun: the same 50 files copy and the same 31 skip as before the change. Also documented the full ignore-syntax ruleset in a comment block (bare folder names, single-star quoted globs, no dot-prefixed entries, and the trailing-comment trap that ships empty zips), and corrected the stale enable-toc-creation comment which read "Enable …" above a no. Locations: .pkgmeta, wow-version-replication.ps1.

[v1.2.1] (2026-07-30) - Settings Panel Open Fix

Bug Fixes

  • SETTINGS-002: Opening the settings panel errored (bad argument #1 to 'OpenSettingsPanel') — Clicking the gear icon in the Inventory window or choosing Settings from the minimap button threw Blizzard_Settings.lua:144: bad argument #1 to 'OpenSettingsPanel' (outside of expected range ...) and the panel never opened. Options:Open called Settings.OpenToCategory("TOGBankClassic") — a name, which only ever worked because AceConfigDialog-3.0 overwrote the registered category's ID field with the category name. The current Ace3 build stops doing that override on any client exposing C_SettingsUtil.OpenSettingsPanel (which Classic Era now does), because OpenSettingsPanel requires a numeric category ID, so our name string reached it and was rejected. Fixed by capturing the real category ID from AddToBlizOptions' second return value at registration time and passing that to Settings.OpenToCategory. Added Options:GetBlizCategoryID(), which falls back to AceConfigDialog's BlizOptionsIDMap and finally to the bare name so older clients (where the ID is the name) keep working, plus a guard that reports an error instead of throwing if Settings.OpenToCategory is missing entirely. Also removed a stale comment claiming the call had to be made twice. Location: Modules/Options.lua.

[v1.2.0] (2026-07-22) - API Compatibility Sweep, Manual-Fill & UI Fixes

Bug Fixes

  • STATICPOPUP-001: Manual "Mark Filled" quantity was never recorded (order stayed open) — Reported by multiple users in Classic: opening the Complete (hand-off) prompt, entering a quantity, and clicking Mark Filled did nothing — the amount never reached the Sent column and the order was never marked filled. Root cause is unrelated to the global-removal wave: the Era client's earlier StaticPopup refactor (the Blizzard_StaticPopup mixin rewrite) replaced the dialog's .editBox / .button1 fields with :GetEditBox() / :GetButton1() accessor methods. The prompt read self.editBox, which is now nil, so the typed quantity read as nil and the code silently bailed (no error). Fixed with small popupEditBox() / popupButton1() helpers that read through the methods (falling back to the legacy fields), used in the prompt's OnShow, OnAccept, and EditBoxOnEnterPressed. The quantity is now captured and recorded via FulfillRequestById as designed. Location: Modules/UI/Requests.lua.

  • WRAP-001: Re-open button wrapped on top of the fulfill/mail button on smaller windows — The per-row Actions column holds a 5-button group (fulfill + complete + cancel + delete + re-open) whose Flow layout reserves all 120px of buttons + 20px of spacers = exactly 140px even when some buttons are hidden. With the column pinned at 140px, the last button (re-open) sat exactly on the content-equals-column boundary and tipped into a second row — rendering on top of the fulfill (mail) button — whenever sub-pixel rounding shrank the usable width on a smaller window. Fixed by widening the Actions column to 152px so the button row always has headroom and can't wrap. Location: Modules/UI/Requests.lua.

  • ROSTER-001: Login/reload error attempt to call a nil value 'GuildRoster' — A recent Classic Era client update removed the global GuildRoster() function, migrating it to C_GuildInfo.GuildRoster(). The addon called the old global from five places, so it threw on PLAYER_ENTERING_WORLD (login/reload), on guild join/leave system messages, on banker-roster fallback scans, and on the delta online-banker fallback — leaving the guild roster refresh dead. Fixed centrally by the new compatibility shim (see COMPAT-001); call sites keep calling GuildRoster().

  • METADATA-001: Opening a window errored with attempt to call a nil value 'GetAddOnMetadata' — Same removed-global class of bug: the client moved the global GetAddOnMetadata() to C_AddOns.GetAddOnMetadata(). The addon called the old global from seven places (the Inventory window title, /togbank version, version handshakes, and roster/request version stamps), so opening the Inventory window from the minimap icon threw at Inventory.lua:85. Fixed centrally by COMPAT-001.

  • OFFNOTE-001: CanViewOfficerNote() was removed — officer features silently disabled — The client removed the global CanViewOfficerNote() (moved to C_GuildInfo.CanViewOfficerNote()), and unlike the item APIs below it has no deprecation fallback, so the bare global is already nil. One unguarded call (Modules/Guild.lua:3229) threw attempt to call a nil value; the other eight sites used the defensive CanViewOfficerNote and CanViewOfficerNote() idiom and so didn't crash but always evaluated to false — quietly turning off every officer-gated feature (the Requests Settings tab, cancel-reason editor, help-note editing, officer options). Fixed centrally by COMPAT-001, which restores the global so all nine sites — guarded and unguarded — work again. Found via a full audit of the addon's global usage against the current Era source (v1.15.9).

  • ITEMAPI-001 / COINAPI-001: Proactively migrated shim-gated item & currency globalsGetItemInfo, GetItemInfoInstant, GetItemQualityColor, PickupItem (→ C_Item.*) and GetCoinTextureString (→ C_CurrencyInfo.*) still work today, but only through Blizzard's Blizzard_DeprecatedItemScript / Blizzard_DeprecatedCurrencyScript addons, which are gated behind the loadDeprecationFallbacks CVar and explicitly slated for removal. GetItemInfo alone has ~20 call sites across the addon. These would break the moment that CVar is flipped (the same removal wave that already took the three globals above), so they are pre-emptively covered by COMPAT-001. No user-visible change today; this is insurance against the next client update.

  • FONT-001: SetFont error on opening a window (bad argument #3 ... ITALIC) — The status bar's centre text called SetFont(font, size, "ITALIC"), but "ITALIC" was never a valid SetFont flag (only OUTLINE/THICKOUTLINE/MONOCHROME/etc. are). Older clients silently ignored the bad flag; the current client validates strictly and errors, which broke opening the Inventory/Requests windows at StatusBar.lua:325. The text had always rendered non-italic anyway (the flag was ignored), so the fix re-applies the font with no flags — same appearance, no error. Location: Modules/UI/StatusBar.lua.

Internal

  • PKGMETA-001: Fixed the packager ignore list so dev files stop shipping — Several ignore entries used forms the BigWigs CurseForge packager silently doesn't match against files: trailing-slash directory entries (docs/, tools/, .vscode/) become docs//* after the packager appends its own /*, matching nothing, so the whole docs/ tree and tools/ were bleeding into the released zip; and **/*.ps1 / **/.DS_Store miss root-level files. Rewrote the list in canonical packager syntax — directory entries with no trailing slash (the packager appends /* itself) and plain *.ext globs (verified that * matches / in the packager's case-based matcher, so a single * covers all depths) — and dropped the now-redundant inline comments. Location: .pkgmeta.

  • COMPAT-001: Central API compatibility shim (Modules/Compat.lua) — Added a single, load-first module that re-establishes every bare global the client removed by aliasing it to its C_* namespace equivalent (if _G[name] == nil and C_Foo and C_Foo[fn] then _G[name] = C_Foo[fn] end), exactly as Blizzard's own deprecation addons do but unconditionally (independent of the loadDeprecationFallbacks CVar), so the addon works whether or not the fallback shim is loaded. This keeps ~35 call sites clean (no C_*. rewrites, guarded idioms keep working) and gives one authoritative map of "APIs Blizzard removed and where they went." Currently covers GuildRoster, CanViewOfficerNote (→ C_GuildInfo), GetAddOnMetadata (→ C_AddOns), GetItemInfo/GetItemInfoInstant/GetItemQualityColor/PickupItem (→ C_Item), and GetCoinTextureString (→ C_CurrencyInfo). Added to TOGBankClassic.toc as the first module; C_GuildInfo and C_CurrencyInfo added to .luarc.json. A full cross-reference of the addon's ~50 API globals against the Era v1.15.9 source confirmed all others (guild roster reads, mail APIs, chat/FCF helpers, memory profiling) are still called bare by Blizzard and remain safe.

[v1.1.4] (2026-05-30) - Re-open Orders, Multi-Order Mail & Fulfillment Fixes

New Features

  • REOPEN-001: Re-open a finished order — A banker, officer, or GM can now re-open a completed order (filled, manually-completed, or cancelled) from the Requests window, in case it was marked done by mistake. A re-open icon appears on finished rows for those roles; confirming resets the order to open, clears its Sent count, and drops any cancel reason. New Guild:ReopenRequest(requestId, actor) (gated by CanManageRequests = banker/officer/GM), a reopen mutation type, and an optional reopenedAt field appended to the request record and the togbank-rd2 wire format (slot 14, append-only). The request log normally ratchets terminal statuses — it refuses to un-cancel/un-complete an order during sync so a stale fulfillment can't revert a finished order — so the re-open carries a reopenedAt timestamp and a narrow exception in mergeRequest lets a re-open stamped after the terminal defeat the ratchet, so it survives sync instead of snapping back to done. Older clients (pre-REOPEN-001) keep the order terminal until they update. Locations: Modules/RequestLog.lua (ReopenRequest, mergeRequest ratchet exception, ApplyRequestMutation reopen auth, wire serialize/deserialize/sanitize), Modules/UI/Requests.lua (re-open button + confirm dialog).

Bug Fixes

  • COMPLETEQTY-002: Manual "Mark Filled" hand-off didn't complete the order — The row check-mark button (manual completion, for items handed over in person or mailed yourself) opened a quantity prompt whose confirm button read "Mark Sent", but entering the amount and confirming often left the order unchanged. The prompt routed through the by-name FulfillRequest(bank, requester, item, …) path, which re-matches the request on bank + requester + item and silently no-ops if any field doesn't compare equal — so nothing was recorded and the order stayed open (only a quiet "Unable to record that quantity." status line). It now completes the request by its id via the new Guild:FulfillRequestById(requestId, count, actor): the amount is recorded into the Sent column and, once Sent reaches the requested quantity, the order is closed outright (status complete, broadcast as a full snapshot so peers replicate the Sent total and terminal status); a partial amount records the Sent total and leaves the order open. The confirm button is relabelled "Mark Filled" (covers both an in-person hand-off and mail you sent yourself) and the prompt wording generalised to match. Locations: Modules/RequestLog.lua (FulfillRequestById), Modules/UI/Requests.lua (prompt text, button label, OnAccept).

  • HITBOX-002: Bottom-row icons still dead in their bottom half (HITBOX-001 follow-up) — The HITBOX-001 lift (v1.1.2) raised each bottom-row icon to window.frame:GetFrameLevel() + 10 once, at construction time. But the Inventory/Search/Requests windows are FULLSCREEN_DIALOG AceGUI frames whose frame level jumps to a much higher value when they are shown; AceGUI's sizer_s resize strip tracks the parent up to parentLevel + 1, while the icons stayed pinned at the stale construction-time level — so once shown, the sizer sat back above the icons and swallowed clicks/hover across the bottom ~half of every bottom-row control (and AceGUI's own Close button, which HITBOX-001 never lifted at all). Fixed with a shared helper TOGBankClassic_UI:KeepAboveResizeSizers(window, buttons) that re-asserts the lift against the live parent level on every OnShow (plus a next-frame pass, since the final level lands just after OnShow), and that also locates and lifts AceGUI's Close button. The button set is stored on the frame and the OnShow hook is attached once per frame, so the Requests window's release/reacquire (banker-status change) and AceGUI's frame pooling neither stack hooks nor lift recycled buttons. Locations: Modules/UI.lua, Modules/UI/Inventory.lua, Modules/UI/Search.lua, Modules/UI/Requests.lua.

Improvements

  • MULTIORDER-001: One mail can close several of a person's orders — When a banker mails items to a guild member, the addon now credits every matching open order that member has with that banker, instead of only one. A fully manual mail already spread across multiple orders (matched by item name); this extends the same behaviour to addon-generated mails: if the banker uses the Fulfill button and then hand-attaches extra items to "save a mail," OnSendMail diffs the actual attachments against what the addon attached and records the surplus as pending.extraItems, and ApplyPendingSend credits the button's targeted order via the request's own stored item name (locale-safe, by requestId), then spills the hand-added extras across the recipient's other open orders by name. The addon's pending.items is deliberately not overwritten with GetSendMailItem names (the banker's client locale), which would break the targeted match in a mixed-locale guild. Orders assigned to a different banker are left untouched. Locations: Modules/Mail.lua (OnSendMail, ApplyPendingSend).

[v1.1.3] (2026-05-30) - Cancel-Stale Broom Icon Hotfix

Bug Fixes

  • BROOM-001: Cancel-Stale button was invisible — The broom icon shipped in v1.1.2 used Interface\Icons\INV_Broom_01, which does not exist in the Classic Era client (it rendered as the blue missing-texture box, so the bulk-cancel button appeared blank). INV_Misc_Broom_01 and INV_Pet_Broom are likewise absent from the Era texture set — Classic Era ships no broom icon at all. Fixed by bundling a custom broom texture with the addon (Textures/broom.tga, a 64×64 32-bit TGA with alpha) and pointing the Cancel-Stale button at it via addon path (Interface\AddOns\TOGBankClassic\Textures\broom), so the icon renders regardless of which icons the client happens to include. The new Textures/ folder ships in the build; its spec note (Textures/README.md) is excluded via .pkgmeta. Location: Modules/UI/Requests.lua, Textures/broom.tga, .pkgmeta.

[v1.1.2] (2026-05-30) - Requests Tabs, Custom Cancel Reasons & Armor Slot Filter

New Features

  • REQUI-001: Officer-only Settings tab in the Requests window — Added a third tab, Settings, to the Requests window, visible only to the GM and officers (gated on CanViewOfficerNote()). It renders as an opaque overlay panel over the request list with three editable numeric fields — Archive threshold (days), Auto-cancel stale (days), and Maximum request amount (%) — mirroring the three controls previously reachable only via the Blizzard options panel. Each field commits on Enter or focus-loss and only acts when the value actually changed, so unchanged focus-loss no longer re-broadcasts. The two guild-synced settings reuse the existing TOGBankClassic_Guild:BroadcastSettings("ALERT") path (SETTINGS-001), so changes propagate guild-wide exactly as the options panel does. New methods BuildSettingsPanel, PopulateSettings, ShowSettings. Location: Modules/UI/Requests.lua.

  • CANCELREASON-001: Custom guild cancel reasons (officer-authored, guild-synced) — The officer Settings tab now includes a cancel-reason editor styled after the FastGuildInvite Filters tab: a [Member] [Banker] [reason text] [Save] strip over a banded, scrolling list. Officers add custom reasons and tick Member and/or Banker to choose whether each appears in the member self-cancel dropdown, the banker-cancel dropdown, or both. The built-in flavor presets also appear in the list, greyed/read-only (no edit, no delete), each with a single native-role tick officers can clear to stop offering that preset. Custom rows are click-to-edit and have a delete X. The whole config lives in Info.settings.cancelReasons ({ custom = { {text, member, banker} }, presetDisabled = { banker = {key=true}, member = {key=true} } }) and rides the existing BroadcastSettings path, so every member's cancel dialog offers the same reasons. Non-officers never see the editor (Settings tab is officer-only) but consume the synced reasons. The cancel dialog now builds its list from buildPresetReasons(role) minus presetDisabled, plus enabled customs for that role, and always offers at least one option. New methods BuildReasonsEditor, RefreshReasonsList, _BuildReasonRow, _ConfigureReasonRow, _OnReasonToggle, _OnReasonDelete, _OnReasonEdit, _EnsureReasonConfig. Locations: Modules/UI/Requests.lua, Modules/Guild.lua, Modules/Database.lua.

  • FILLALL-001: "Fulfill Oldest Order" stepped button (spam-to-fill) — A new envelope icon in the Requests window's bottom-right cluster (bankers only) walks the oldest order you can fully fill from your bags through one action per click: select (sets the recipient, switches to the Send Mail tab) → split (only if a stack split is needed) → attachsend, then the next click picks the next-oldest. One WoW action per frame deliberately — the earlier single-click version raced the send ahead of the async split; stepping it lets the cursor/bag state settle between actions. The split commits into a free bag slot as its own stack (like the manual split), and ATTACH waits for it to land before grabbing it. Oldest-first (FIFO by date, with a stable request-id tiebreak so same-second orders pick deterministically instead of appearing to jump around the list) so item contention favours whoever asked first; only orders assigned to your own character are eligible (that's the constraint for fulfillment credit). Mail collect: if the oldest serviceable order's items are sitting in your mail inbox (not bags), each click first pulls one matching item into your bags (TakeOneInboxItemFor, gated on free bag space) until enough is collected, then it selects + fulfills — so the flow now spans bags and mail, only matching your own open orders. Orders you can't cover from bags + mail are skipped. After a send, a batchInFlight guard blocks re-selecting that order until the send confirms (cleared on MAIL_SEND_SUCCESS/ApplyPendingSend, mail error/UI_ERROR_MESSAGE, or a 5s safety timer); the step state resets on MAIL_CLOSED. The status bar shows the next step at each click. New TOGBankClassic_Mail:FulfillStep / FindOldestServiceableOrder / TakeOneInboxItemFor / ResetFulfillStep (+ inbox-match helpers), reusing CalculateFulfillmentPlan + the existing pendingSendFulfillRequest path. SendMail added to .luarc.json. Locations: Modules/Mail.lua, Modules/UI/Requests.lua, Modules/Events.lua.

  • COMPLETEQTY-001: "Complete" now asks how much was handed over — The row's check-mark button (for items given directly, not mailed) used to silently mark the whole request complete. It now opens a quantity prompt; the number you enter is recorded in the Sent column via Guild:FulfillRequest(request.bank, …), and the order only flips to fulfilled once Sent reaches the amount requested — so partial hand-offs are tracked correctly. New TOGBankClassic_CompleteQty static popup (hasEditBox, numeric) + showCompleteQtyPrompt/ensureCompleteQtyDialog; applied against the request's own bank so it works whoever clicks (button visibility still gated by CanCompleteRequest). Location: Modules/UI/Requests.lua.

  • HELPNOTE-001: Officer help notes on the help (?) tooltips — GM/officers can now add a custom note that appends to the bottom of the help "?" tooltip on each of the three windows (Inventory, Search, Requests) — e.g. how to submit a request and expected turnaround time. Edited in the Blizzard options panel (Esc → Options → AddOns → TOGBankClassic → Requests → "Guild Help Notes"), per window, as multi-line inputs gated to CanViewOfficerNote(). Stored in Info.settings.helpNotes = { inventory, search, requests }, synced guild-wide over the existing BroadcastSettings path (sanitized on receive by SanitizeHelpNotes, clamped to 400 chars/window). The tooltips read the note at hover time via a shared TOGBankClassic_UI:AppendGuildHelpNote(windowKey) (Inventory/Requests call it directly; Search passes a note key to AttachTooltip). New TOGBankClassic_Guild:GetHelpNote, TOGBankClassic_Options:SetHelpNote. Locations: Modules/Guild.lua, Modules/Database.lua, Modules/Options.lua, Modules/UI.lua, Modules/UI/Inventory.lua, Modules/UI/Search.lua, Modules/UI/Requests.lua.

  • VIEWBANK-001: View-only bank toons (visible but not requestable) — A bank character can now be flagged "view only" so its stock stays visible everywhere (inventory, search, item tooltips) while guild members are blocked from sending requests for it — e.g. a raid bank. Officers flag it by adding a view-only marker to the toon's guild note alongside the usual gbank tag: gbank viewonly (also accepted: view-only, readonly, read-only, or the compact gbankro). New TOGBankClassic_Guild:IsViewOnlyBank(name) (O(1) via a viewOnly flag stored on memberRoster, computed from notes in RefreshOnlineCache/RebuildBankerRoster, with a roster-scan fallback). Enforced in three places: Guild:AddRequest hard-rejects any request whose target bank is view-only; the Search request dialog (ShowRequestDialog) refuses to open for a view-only banker and prints a reason; and Search result rows tag view-only banks with a (view only) marker. Items on both a normal and a view-only banker stay requestable from the normal one (requests are per-banker). Locations: Modules/Guild.lua, Modules/RequestLog.lua, Modules/UI/Search.lua.

Bug Fixes

  • HITBOX-001: Bottom-row icons only clickable in a center sliver (clicks and tooltips) — The gear, help ?, < / > page arrows, broom, and fulfill envelope icons that sit along the bottom edge of the Inventory, Search, and Requests windows responded to clicks and hover only in a tiny center spot. Cause: AceGUI's Frame widget lays an invisible, mouse-enabled resize strip (sizer_s, full bottom width, 25px tall) plus a corner sizer across that whole row for the drag-to-resize handle. The parent frame is at frame level 100, so the sizers — and any icon added as a child of the same frame — all default to level 101; two mouse-enabled frames overlapping at the same level produce ambiguous hit-testing, so the sizer swallowed most of each icon's input. Fixed by lifting every bottom-row icon to window.frame:GetFrameLevel() + 10 (level 110) so it sits above the sizers and the full icon is live for both clicking and mouseover. Not a texture/SetSize/SetHitRectInsets issue (the gear's hit-rect was already expanded and still failed). Locations: Modules/UI/Inventory.lua, Modules/UI/Search.lua, Modules/UI/Requests.lua.
  • FILLALL-002: Mail collect over-pulled stackable items — The "Fulfill Oldest Order" mail-collect step counted attachments pulled rather than items, so for a stackable item (where one mail attachment can be a whole stack) it kept pulling past the amount needed and filled your bags. TakeOneInboxItemFor now returns the quantity taken (via GetInboxItem) and the collector tracks items pulled against the deficit, stopping once enough is in your bags — correct for both single items (1 = 1) and stacks. Location: Modules/Mail.lua.
  • REQUI-005: Pagination "snapped back" to the first page — Clicking the < / > page arrows would jump back to page 1 a split second later. A background request sync (RefreshRequestsUIDrawContent) was unconditionally resetting currentPage = 1 on every redraw. DrawContent now resets the page only when the active tab actually changed (tracked via _lastDrawnTab), and DrawRows clamps currentPage to the valid range so a shrinking data set can't strand the view on an empty page. Location: Modules/UI/Requests.lua.

Improvements

  • OFFICERTAB-001: Options "Requests" group renamed to "Officer" and gated to officers only — The Blizzard options group (Esc → Options → AddOns → TOGBankClassic) holding the request thresholds + help notes is now titled Officer and its hidden function is not CanViewOfficerNote(), so only the GM and officers can see or change those settings (previously bankers could too). Location: Modules/Options.lua.
  • REQUI-002: Requests window top strip decluttered + real tab widget — The top strip now holds only the tabs (Requests | Archive | Settings), and they are now an AceGUI TabGroup (the proper WoW tab-shaped tabs, matching FastGuildInvite) instead of red UIPanelButtons. The widget is used purely as a tab bar — its content box backdrop is removed so only the tab row shows; the request list and Settings panel still render below as separate window children. Tab selection drives currentTab via OnGroupSelected; the old UpdateTabButtons text-prefix highlighting was removed (the tab widget shows the active tab itself). Per-tab hover tooltips use OnTabEnter/OnTabLeave. The Cancel Stale button and the full-width < Prev / Next > pagination buttons no longer crowd the top. Location: Modules/UI/Requests.lua.
  • REQUI-003: Pagination and Cancel Stale moved to compact status-bar icons — The < Prev / Next > buttons are now compact page-turn arrow icons (UI-SpellbookIcon-PrevPage/NextPage) next to the bottom-right help ? icon, dimming automatically at the first/last page. The Cancel Stale action is now a small broom icon (Interface\Icons\INV_Broom_01, the Hallow's End Magic Broom texture) in the same cluster, shown only to officers/bankers. The status bar's right edge auto-shrinks to clear the icon cluster (wider when the broom is present). On the Settings tab these icons are hidden since they don't apply. Location: Modules/UI/Requests.lua.
  • REQUI-004: Settings panel compacted + tooltip cleanup — The three numeric settings (Archive, Auto-cancel, Max request %) now sit on a single compact row instead of three stacked label+description blocks, freeing ~110px the cancel-reason list now uses. The redundant "Request Settings" title was removed (the tab already says Settings). Field descriptions now live on the label's hover tooltip rather than the edit box, so the tooltip no longer covers the field while typing. The cancel-reason Mbr / Bnk / Reason column headers and the "Custom Cancel Reasons" heading now have hover tooltips (the heading's how-to text was moved off-screen into its tooltip), via a new attachLabelTooltip helper that overlays a hit frame on a FontString. Location: Modules/UI/Requests.lua.
  • REQUI-006: Bottom status-bar row tidied — The status bar now extends right to meet the icon cluster instead of stopping ~22px short, and every bottom-row icon (help ?, < / > page arrows, broom, fulfill envelope) is the same size (22px) with equal 8px gaps. The cluster sits in the gap left of the AceGUI Close button (which occupies x -127..-27), with the help icon at -133 so it never overlaps Close. Rather than a hardcoded right-edge inset, the status bar's BOTTOMRIGHT is anchored 6px to the left of whichever icon is actually leftmost (self.FulfillOldestBtn or self.CancelStaleBtn or prevPageBtn), so the bar always meets the cluster with even spacing regardless of which icons a given user has — fixing the large gap that appeared when the old fixed offsets didn't match the real cluster width. Location: Modules/UI/Requests.lua.
  • REQUI-007: Clickable text column headers (no more red buttons) — The request-table column headers were red UIPanelButtons whose centered text didn't line up with the data cells below. They are now plain InteractiveLabel sort headers — gold text with a hover glow, click to sort, sort arrow appended — each justified to match its column's data so headers and rows align. Mirrors the FastGuildInvite RowList header style. A per-column headerAlign override centers the Item header, and a headerSuffix (trailing space) nudges the right-justified # header in by one character so it sits over the first digit of the quantity rather than the x. EnsureHeaderRows builds InteractiveLabels instead of Buttons. Location: Modules/UI/Requests.lua.
  • REQUI-008: Tightened vertical spacing above the request list — The tab strip's TabGroup height was trimmed (34 → 30) to close the gap between the tabs and the filter dropdowns, and the column-header row gained ~3px of breathing room above it (header group content y offset 0 → -3). Location: Modules/UI/Requests.lua.
  • SEARCH-001: Armor equip-slot filter in the Search window — When the Filter is set to Type → Armor, a new Slot dropdown (between the subtype and Sort dropdowns) lets you narrow results to a specific equip slot — Head, Shoulder, Chest, Wrist, Hands, Waist, Legs, Feet, Back, Neck, Finger, Trinket, Shield, Held In Off-hand, or Relic. It combines with the existing armor subclass (Cloth/Leather/Mail/Plate) filter, so e.g. Plate + Legs works. The dropdown is disabled for non-armor types. Items' equip slot is resolved on demand from GetItemInfo (#9) and cached on the item's Info table (equipSlot), so no change to the synced data schema; INVTYPE_CHEST/INVTYPE_ROBE collapse to one Chest entry, etc. New SLOT_LIST/SLOT_ORDER/INVTYPE_TO_SLOT tables, a resolveSlotKey helper, a subSlotDropdown widget + resetSlot cascade, self.SubFilterSlot matching in SubFilterMatches. Location: Modules/UI/Search.lua.

Internal

  • Pagination buttons are now raw Button frames (with normal/pushed/disabled/highlight textures) rather than AceGUI buttons; a file-local setBtnEnabled helper replaces the old SetDisabled calls at the two page-state update sites. The Settings overlay is a BackdropTemplate frame rebuilt per window; its references (SettingsOverlay, the three editboxes, SettingsTabBtn, and the cancel-reason editor widgets/row pool) are cleared in the window's reset block, and CancelStaleBtn is cleared before its conditional creation so a lost-banker-status window recreation doesn't read a stale reference. Location: Modules/UI/Requests.lua.
  • CANCELREASON-001 sync/storage: cancelReasons is added to the guild-settings broadcast payload and validated on receive by a new TOGBankClassic_Guild.SanitizeCancelReasons helper (clamps to 20 custom reasons × 160 chars, coerces booleans, ignores a missing field so old clients don't wipe local state). Defaults and a migration block were added to both Info.settings init sites in Modules/Database.lua. The built-in flavor presets were extracted from the cancel dialog into a shared buildPresetReasons(role) builder (keyed so they can be individually disabled). A strtrim global was added to .luarc.json. Locations: Modules/Guild.lua, Modules/Database.lua, Modules/UI/Requests.lua, Modules/Constants.lua (SETTINGS tag description).

[v1.1.1] (2026-05-29) - Sorting Fixes & Random-Suffix Request Variants

Bug Fixes

  • SORT-001: "By Type" split same-material gear across equip slots — The inventory "By Type" sort ordered items by item class → equip slot → subclass, so a player's cloth pieces were broken up by slot and interleaved with leather/mail (e.g. 6 cloth, 3 leather, then 1 more cloth) instead of grouping all cloth together. Reordered the comparator to class → subclass/material → required level (see SORT-003) → equip slot → rarity → name, so all cloth groups, then all leather, then all mail. Location: Modules/Item.lua Sort (type mode). The Search window had no type sort case at all (selecting "By Type" left results in scan order); added a matching comparator there. Location: Modules/UI/Search.lua DrawContent sort block.

  • SORT-002: "Level" sort and the Min/Max level filters used item level, not required levelInfo.level was populated from GetItemInfo's item-level return (#4) but was treated everywhere as the required-to-use level — in the "Level (High/Low)" sort, the Search window's "Minimum/Maximum Required Level" filters, and the "usable by my level" filter. Because item level and required level diverge non-monotonically, the "High to Low" sort looked like it descended, jumped back up, and repeated rather than producing a clean ordering. Now captures the required-level return (#5) into a new Info.reqLevel field and uses it for the level sort and all three level filters. Required level is resolved from the live item cache (GetItemInfo), which is warm by the time items are on screen. Items that arrive with a pre-existing Info table — item data synced from other players or loaded from saved data predates the field — are resolved at sort time, retrying whenever reqLevel is unresolved (nil or 0) and only writing a positive result. This avoids the bug where a 0 written during a cold-cache window stuck permanently and broke the ordering. Locations: Modules/Item.lua (Info captures, Sort prep + level comparators, GetItems backfill, GetInfo), Modules/UI/Search.lua (sort-prep resolution, level comparators, SubFilterMatches).

  • REQ-003: Requests for random-suffix items matched the wrong variant — Random-property gear such as "Spiked Club of the Tiger" and "Spiked Club of the Monkey" share a single base item ID and differ only by their random-suffix ID. Because a request stored only the numeric item ID, the requests screen tooltip and the mail fulfillment/availability checks matched the first item sharing that base ID — so a request for the Tiger variant showed (and would be fulfilled by) the Monkey variant. Requests now also capture the random-suffix ID and match on it: the tooltip resolves to the requested variant, and bag scanning / fulfillment only count the matching suffix. New optional suffixID field appended to the request record and the togbank-rd2 wire format (slot 13, append-only); older clients and pre-existing requests have no suffix data and fall back to the previous item-ID matching, so there is no regression. New helper TOGBankClassic_Item:GetSuffixID(link). Locations: Modules/Item.lua, Modules/RequestLog.lua, Modules/UI/Search.lua (request creation), Modules/UI/Requests.lua (tooltip), Modules/Bank.lua (FindItemsByName/CountItemInBags), Modules/Mail.lua (CanFulfillRequest/PrepareFulfillMail).

Improvements

  • SORT-003: "By Type" now orders each material by level — Within each material/subclass group, items are ordered by required-to-use level high→low (then equip slot, rarity, name as tie-breakers), so a type-sorted list reads cleanly within each group (all plate: 50, 49, 48…) instead of relying on slot/name alone. Applies to both the inventory and Search windows. Locations: Modules/Item.lua Sort (type mode), Modules/UI/Search.lua DrawContent sort block.

Internal

  • .luarc.json — Added strsplit to diagnostics.globals (used by the new GetSuffixID helper).

[v1.1.0] (2026-05-23) - Data Corruption Fix: Linkless Gear Ghosts & Inflated Counts

Bug Fixes

  • ITEM-004: EnsureLegacyFields was poisoning alt.bank.items with mail-item references — When peer-relayed alt data arrived carrying only alt.items (the aggregated bank+bags+mail view) without the separate bank/bags/mail fields, EnsureLegacyFields "reconstructed" alt.bank.items by copying every entry from alt.items — including mail items. Subsequent re-aggregation in ApplyDelta then ran Aggregate(bank, bags) followed by Aggregate(result, mail), summing mail items twice per delta application. Across many peer-relay cycles, gear item counts inflated monotonically — in real SavedVariables, "Battlefell Sabre of Power" (ID=15220) reached Count=6237 and base "Battlefell Sabre" reached Count=21 (both physically impossible for non-stacking weapons). Fix removes the copy loop entirely; the next direct delta from the actual banker repopulates bank.items cleanly. Location: Modules/Guild.lua EnsureLegacyFields (~line 2282). Root cause documented in docs/DELTA_BUGS.md ITEM-004.

  • ITEM-003 guard holes on ApplyItemDelta update/fallback paths — The receive-side guard against linkless weapons/armor only fired on the new-insert paths. The ID-only fallback paths in both STEP 2 (modified) at line 904 and STEP 3 (added) at line 1010 silently mutated linkless gear ghost entries into suffixed entries via for field, value in pairs(changes) do existingItem[field] = value end and existingItem.Count = newItem.Count. This propagated whatever Count the inbound delta carried into a ghost that should never have existed, causing count divergence across replicas. Fix detects ID-only-fallback matches against linkless gear and DROPS the ghost before falling through to the clean-add path, where the existing ITEM-003 new-insert guard catches subsequent linkless gear payloads. Location: Modules/DeltaComms.lua ApplyItemDelta STEP 2 and STEP 3.

  • NeedsLink / ItemClassNeedsLink could strip gear links during cold-cache windows — The fallback path in NeedsLink consulted the item's hyperlink suffix field when GetItemInfo's class lookup returned nil (uncached). For base/no-suffix gear items the suffix is 0, so the fallback returned false and stripped the link, producing the linkless gear ghosts that ITEM-003 / ITEM-004 then propagated. Replaced both functions with a default-deny strip policy: a link is stripped ONLY when class can be positively confirmed as non-gear (class != 2 AND != 4). Uncached, unparseable, or unknown items now preserve the link. The "Weapons (class 2) and Armor (class 4) ALWAYS keep their Link" rule documented in the file finally actually holds. Location: Modules/Item.lua NeedsLink, ItemClassNeedsLink, plus new Item:GetClass(itemID) tiered-lookup helper.

New Features

  • Generic tooltip helper TOGBankClassic_UI:AttachTooltip(target, anchor, title, lines) — Single one-call API for non-item tooltips. Auto-detects AceGUI widget vs raw frame and wires OnEnter/OnLeave via the right API. Replaces the 5-line GameTooltip:SetOwner / ClearLines / AddLine / Show scriptlet pattern that was sprinkled across UI modules. New Search-window tooltips use it; existing call-sites kept as-is for now (gradual migration). Location: Modules/UI.lua (~line 230).

  • Search window: info "i" icon + Prev/Next at bottom-right — Mirrors the inventory window's bottom-right layout. The "?" help icon explains how the Search window works (input field, filters, pagination). Pagination buttons moved from a full-width "< Previous / Next >" row to compact < / > icon-sized buttons next to the close button — saves ~30px of vertical space, freeing the result list. Status bar shrunk by ~210px to leave room. Both pagination buttons keep the existing :SetDisabled(bool) API so DrawContent's page-state logic works unchanged. Location: Modules/UI/Search.lua bottom-right control row.

  • Search window: tooltips on Min lvl / Max lvl / Usable — All three filter controls now have explanatory hover tooltips wired via the new AttachTooltip helper. Min/Max explain that empty/0 means "no constraint" and that items without a level are hidden when a min is set. Usable explains the gating (disabled until a Type/Quality is picked).

  • Search window: Sort tooltip moved from dropdown control to label — Previously the Sort tooltip fired when hovering the dropdown itself, which competed with the click-to-open-dropdown gesture (popped up while the user was trying to click). Now it lives on a hit frame over the "Sort" label, matching the Filter dropdown's pattern.

  • Search window: filter row reordered (Min lvl, Max lvl, then the rest) — The numeric inputs now lead the row so the small controls cluster densely in the top-left and don't get orphaned on their own row when the window is narrow.

  • Search window: Min/Max EditBox labels repositioned — Labels shifted 5px right (+5, -2) so they no longer overhang the EditBox's left edge.

  • Min/Max level filter in the Search window — Two new compact numeric inputs (Min lvl and Max lvl, 60px each) let players filter results by the item's required level. Empty or non-numeric input is treated as "no constraint" so partial ranges work (just a min, just a max, or both). Cheap to compute → no gating on other filters being set first. Combines with the existing Type/Quality/Usable filters. Location: Modules/UI/Search.lua SubFilterMatches and filter section.

  • Compact, auto-wrapping filter row in the Search window — The filters used to be four full-width-stacked dropdowns plus an inline checkbox glued to the Filter dropdown's right edge — five tall rows that ate half the window before the results even started. They're now a single Flow-laid-out row with each control sized to its content (Filter 110px, Subtype 130, Sub-subtype 130, Sort 150, Min lvl 60, Max lvl 60, Usable 80 — total ~720px). On a wide search window everything fits on one row; resize the window narrower and they wrap onto multiple rows automatically. The "Usable by my level" checkbox is now a standalone AceGUI CheckBox (previously a raw CheckButton anchored to the Filter dropdown's frame), so it participates in the wrap layout instead of forcing the Filter dropdown to stay 165px wider than it needs to be. Location: Modules/UI/Search.lua filter section.

  • Settings gear icon on the main inventory window — A new ⚙ button sits next to the existing help "?" icon at the bottom-right of the inventory window. Clicking it opens the TOGBankClassic options panel directly (equivalent to Escape → Options → AddOns → TOGBankClassic), so players don't have to navigate through the game menu to change banker/scan configuration, minimap button, debug settings, etc. Hover for a tooltip. Location: Modules/UI/Inventory.lua (~line 144).

  • One-shot ghost-purge migration on Database:Init — Scheduled 30 seconds after addon init (gives WoW's item cache time to warm). Walks every alt's items, bank.items, bags.items, and mail.items arrays; drops entries that have no Link field AND are confirmed by ItemClassNeedsLink to be class 2/4 gear. Recovers existing corruption in SavedVariables without requiring /togbank wipe. Always prints a result line so users know it ran (purged count + skipped-suspect count, even when zero). Can be manually re-run via /togbank dev purgeghosts. Location: Modules/Database.lua PurgeLinklessGearGhosts.

  • Static item DB populated from wago.tools (Modules/Static/ItemDB.lua + SuffixDB.lua) — Ships with ~24,000 item entries (every item in Classic Era 1.15.8) and ~2,000 random-suffix fragments. NeedsLink / ItemClassNeedsLink consult TOGBankClassic_ItemDB first via a tiered lookup (static DB → GetItemInfo → default-deny), so strip decisions no longer depend on the volatile WoW client cache. Regenerated by tools/build-itemdb.py which pulls Blizzard's actual DB2 dumps (ItemSparse + Item + ItemRandomProperties + ItemRandomSuffix). Wire schema unchanged in this release; bandwidth-reduction changes (Phase 3) will land in a follow-up release.

  • tools/build-itemdb.py — wago.tools fetch + Lua generator — Python script (no third-party deps, Python 3.9+) that fetches DB2 tables from wago.tools, joins, filters suffix junk (rejects fragments not starting with "of "), and emits Modules/Static/{ItemDB,SuffixDB}.lua. Caches downloaded CSVs under tools/wago_cache/ (gitignored). Re-run when a new Classic patch ships new items. Pattern modelled on TOGProfessionMaster's tools/wago_probe.py. Excluded from packaged builds (.pkgmeta tools/ entry).

  • Developer-only command namespace /togbank dev <subcommand> — Twenty-two dev/debug commands previously listed in /togbank help (clearhistory, clearsnapshots, deltaerrors, deltahistory, deltastats, forcedelta, forcefull, perfstats, persistcheck, protocol, resetmetrics, test, versioncheck, hashupdate, hashdebug, hashdump, netq, reqscan, debugdump, debuglogsave, clear-delta-errors, plus the new purgeghosts) are now hidden from the user-facing help output and dispatched only via the dev namespace. /togbank dev help lists them for developers. Reduces the top-level command list from ~30 to ~11 entries. New DEV_COMMAND_NAMES lookup in Modules/Chat.lua controls which commands route through the dev dispatcher — flipping a command between user-facing and dev-only is a one-line change. Full catalogue and developer workflows documented in docs/DEV_COMMANDS.md (not packaged to users — docs/ is ignored in .pkgmeta).

  • /togbank dev purgeghosts — manual ghost-purge trigger — Re-runs the linkless-gear-ghost migration on demand. With the populated static TOGBankClassic_ItemDB shipping in this release, the purge can confidently classify almost any item without relying on the WoW client's session cache. Location: Modules/Chat.lua COMMAND_REGISTRY.

Internal

  • Removed obsolete local-build pipeline — Deleted package.bat (referenced a no-longer-existing embeds.xml and would have errored on run) and the stale dist/ directory (contained one orphan TOGBankClassic.TOGBankClassic-v1.3.2.zip from before the move to the BigWigs CurseForge packager). All release builds now flow exclusively through .pkgmeta + the CurseForge auto-builder. Defensive **/*.bat ignore pattern kept in .pkgmeta to catch any future leftover scripts.

  • Removed dead function s(a) at Guild.lua:3030 — Generic table-entry counter, defined as a global (lowercase), never called from anywhere in the codebase. Eliminating it removes one lowercase-global warning and two unused-local hints (c, d loop variables).

  • CLAUDE.md updated — Replaced the references to package.bat/dist/ (now gone) with notes on the current packaging pipeline. The "scratch files use tmpclaude- prefix" rule is preserved but no longer mentions the obsolete robocopy exclusion.

  • Documentation cleanup — Removed duplicate docs/CHANGELOG.md (the canonical changelog has always been the repo-root CHANGELOG.md). Updated .pkgmeta ignore list: docs/ directory now fully excluded from packaged builds (was: *.md only), CLAUDE.md explicitly excluded, *.md no longer blanket-ignored so root-level CHANGELOG.md ships to CurseForge as intended.

  • README.txt cleanup — Removed dev commands from the EXPERT COMMANDS section. Rewrote MONITORING DELTA SYNC and two TROUBLESHOOTING entries to direct players at debug logging instead of dev-only counters. Kept genuinely user-facing expert commands: compact, debuglog/debuglogclear/debuglogstats/debugtab/debugtabremove, roster, wipe, wipeall, wipeframes, debug.

  • Tests.lua lint fixes — Suppressed two duplicate-set-field warnings on the mocked Database.GetGuildDeltaSupport reassignments using the established ---@diagnostic disable-next-line pattern.

  • Minor lint cleanup in Chat.lua — Removed an unused vararg and an unused loop-variable name in ProcessQueue and PrintDeltaHistory respectively (encountered while editing the dispatcher).

  • .luarc.json global registration — Added TOGBankClassic_ItemDB, TOGBankClassic_SuffixDB to diagnostics.globals.

  • TOC additions — new Modules/Static/ItemDB.lua and Modules/Static/SuffixDB.lua load entries (loaded early so anything that queries item class has them available).

Developer / Sync architecture follow-ups (planned, not in this release)

  • Bump PROTOCOL.VERSION to 3 once the static DB is populated and committed.
  • Peer-aware StripDeltaLinks: emit minimal {ID, Count, suffixID?, randomProperty?} payload to peers known to support the static DB; continue sending legacy {ID, Count, Link/ItemString} to old peers. Backwards-compatible per Option A in the design discussion.
  • Update ApplyItemDelta and ReceiveAltData to reconstruct items from minimal payloads using TOGBankClassic_ItemDB and TOGBankClassic_SuffixDB.
  • Expected wire bandwidth reduction: 4-5x for non-gear items, 5-7x for random-suffix gear, 8-10x for fixed-roll gear once everyone is on the new protocol.

[v1.0.0] (2026-04-11) - First Stable Release: Fulfill Location Awareness & Polish

New Features

  • Fulfill button location awareness — The fulfill button now shows distinct icons and contextual tooltips for three new states, making it clear why an item cannot be mailed immediately and where to find it:
    • Item in mail inbox — wax letter icon (INV_Letter_06); tooltip: "Item is in your mail inbox. Retrieve it first, then fulfill the order."
    • Item split across bank and mail — paired bag + letter icons; tooltip: "Item is split between your mail inbox and bank. Retrieve mail items first, then pick up the rest from the bank."
    • Shortage — more available in bank/mail — contextual icon matching the location; tooltip shows exact current bag count and target quantity (e.g. "Have 125 in bags. More available in your bank and mail inbox — pick up or retrieve the rest to reach 150."). Three sub-states: bank only, mail only, or both.

Bug Fixes

  • TOOLTIP-001: Item link tooltips showed banker data for ex-guild members — The OnTooltipSetItem hook in TooltipBankerInfo.lua iterated all database entries with no guild membership check, surfacing data from characters who had left the guild. Fixed by adding an IsInCurrentGuildRoster() check as a combined guard — only alts currently in memberRoster (O(1) lookup) are shown. Location: Modules/TooltipBankerInfo.lua.

  • FULFILL-001: Fulfill button icon stuck on shovel after a bag split — After splitting a stack to fulfill an order, the bag-update event called DrawRows(), which skips non-dirty rows. The row was already drawn so DrawRows() was a no-op and the split icon never transitioned. Fixed by replacing the DrawRows() call in OnBagUpdate with _RefreshFulfillButtons(), which re-evaluates all visible rows regardless of dirty state. Location: Modules/UI/Requests.lua.

  • FULFILL-003: "Item in bank and mail" showed a blank red button — The combined icon used INV_Misc_Chest_01, which does not exist in Classic Era; the engine renders a blank red placeholder for any missing texture. Fixed by replacing it with two confirmed-working icons rendered side-by-side at 14px: INV_Misc_Bag_07 (bag) and INV_Letter_06 (wax letter). Location: Modules/UI/Requests.lua.

  • HIGHLIGHT-001: Bagnon bag highlighting broken for recipe and pattern itemsUpdateBagnonHighlighting inserted raw item names into the Bagnon search string. Tradeskill items whose names include a colon prefix (Pattern: Ironfeather Breastplate, Formula: Enchant Weapon, etc.) caused Bagnon's search parser to silently discard the entire term. Fixed with a shared stripRecipePrefix helper that strips all known Blizzard craft prefixes before appending to the search string. Location: Modules/ItemHighlight.lua.



Older releases (v0.10.10 and earlier) are archived in CHANGELOG_ARCHIVE.md.