TOGTools

ToGTools is a convenient place to aggregate one off tools that don't make sense to have it's own addon.

File Details

TOGTools-v0.7.1

  • R
  • Jul 18, 2026
  • 2.19 MB
  • 18
  • 12.0.7+9
  • Retail + 3

File Name

TOGTools-TOGTools-v0.7.1.zip

Supported Versions

  • 12.0.7
  • 12.0.5
  • 12.0.1
  • 12.0.0
  • 11.2.7
  • 5.5.3
  • 4.4.2
  • 3.4.5
  • 2.5.6
  • 1.15.8

Changelog

[v0.7.1] (2026-07-17) - Battle.net whisper attribution fix

Bug Fixes

  • Battle.net whispers logged against the wrong person — In the Whisper Log, a Battle.net whisper could show under a different friend's name than the one who actually sent it, especially after a relog. Root cause: CHAT_MSG_BN_WHISPER / _INFORM deliver the partner in arg2 as a Kstring (e.g. |Kq2|k) — a session-scoped index into the client's presence table, not a stable name (confirmed against the Blizzard docs mirror ChatInfoDocumentation.lua and Warcraft Wiki). The engine persisted that token verbatim, so on a later session the client re-resolved the same |Kq2|k to whichever presence now held that slot, relabelling old messages. In-game whispers (real name + GUID) were never affected. Fix: a new resolveBNName(bnSenderID, rawName) resolves the sender to a stable accountName (fallback battleTag) from bnSenderID (arg13) via C_BattleNet.GetAccountInfoByID at capture time — the same path Blizzard's own chat UI uses (ChatFrameOverrides.lua) — and stores that plain string, never the Kstring. Secret-guarded (bnSenderID isn't NeverSecret; can be secret in Retail chat lockdown), pcall-wrapped, and feature-detected (C_BattleNet.GetAccountInfoByID exists Classic Era 1.15 → Retail, so all six flavors resolve; anything lacking it falls back to the label "Battle.net friend"). Display-side, fmtOther now masks any already-stored Kstring on BN rows with "Battle.net friend", so pre-fix entries stop rendering a mislabelled name — non-destructive, saved data is left intact. New global C_BattleNet in .luarc.json. Location: Modules/WhisperLog/WhisperLog.lua, GUI/WhisperLogSubTab.lua.

[v0.7.0] (2026-06-14) - Logs overhaul (Diagnostics, filter/sort/search, Gathering source), Graphs, Name Prefix/GRM dedup, the Smack tool, and Retail/Midnight secret-value handling

New Features

  • Name Prefix: hide a duplicate main name shown by GRM (opt-in) — When a guildmate's addon self-prefixes their main into a message ((Bob) …) and you run Guild Roster Manager with "show main name" on, GRM injects its own (Bob): into the same message body — so you see the main twice. A new off-by-default toggle in the Name Prefix tab adds a receive-side chat filter (over GRM's decorated channels — guild/officer/whisper/party/raid/instance) that collapses the redundant copy, keeping GRM's canonical class-coloured tag. It consumes GRM's own data (GRM.GetPlayer / GetFormattedMainName / S().showMainName) per our data-scope rule — never a home-grown roster. Safety: it only removes a (main) token that appears a second time near the front and matches the sender's GRM main, so it can't eat legitimately-typed text like (brb) …. Zero footprint when off — no filter registered, no GRM call — so there's no integration or cost unless ticked; registration is deferred at login so it runs after GRM's filter. New SV field namePrefix.dedupeGRMMain; new global ChatFrame_RemoveMessageEventFilter. Location: Modules/NamePrefix/NamePrefix.lua, GUI/NamePrefixTab.lua.

  • Smack: per-filter hotkeys + "Manual (hotkey only)" trigger — Any filter can carry a key combo that fires it on demand, and a new manual trigger makes a filter that only fires from its hotkey (never automatically). This is the key answer to the Retail/Midnight restrictions: a keypress is a hardware event, so the resulting SendChatMessage is user-initiated and sidesteps both the secret system and the encounter public-chat block — hotkey smack works everywhere, every flavour, including /say mid-boss, where the automatic triggers can't. Bound via SetOverrideBindingClick on a private owner frame, so bindings are reversible (ClearOverrideBindings restores the original; they auto-clear on logout) and the user's saved keybinds are never overwritten — no clobbering other addons. The capture UI is an AceGUI Keybinding widget; on assignment we look up GetBindingAction(key) and print a non-blocking heads-up if the key is already taken (filtering out our own override buttons so reusing a Smack key doesn't false-positive). Binding changes are combat-protected, so an apply requested in combat defers to PLAYER_REGEN_ENABLED. Hotkey fires skip the per-filter minute cooldown (deliberate press) but keep a 1-second anti-double-tap floor. New filter field hotkey; a Key column in the list. New globals SetOverrideBindingClick / ClearOverrideBindings / GetBindingAction / C_KeyBindings in .luarc.json. Location: Modules/Smack/Smack.lua, GUI/SmackTab.lua.

  • Smack module (user request) — A new tab that fires a randomly-chosen chat line when a combat trigger hits — DDO-style "trash-talk the boss while you heal". Built on the Gratz / FGI-Filters pattern: a top form (Name + Trigger + threshold + Channels + Lines + Cooldown + Save) and a RowList below where each row is a saved filter with an On checkbox for active/passive. Click a row to load it into the form; Save overwrites by name, a new name appends. Location: Modules/Smack/Smack.lua (engine) + GUI/SmackTab.lua (UI); SV schema db.global.smack = { enabled, filters = {} } (keyed by name, like gratz). Classic-only: shipped in the five Classic TOCs but removed from the Retail (Mainline) TOC — on Midnight 12.0 the platform blocks essentially everything Smack needs (enemy health is secret, COMBAT_LOG_EVENT_UNFILTERED registration is refused, and automated public-chat sends are ADDON_ACTION_BLOCKED), all confirmed in-game, so rather than ship a half-working tab there it simply doesn't load on Retail.

  • Thirteen triggers, one per filter — Six percent-threshold triggers (show a % field, fire on a crossing): self-hp-below, self-hp-above (recover), target-hp-below (re-arms on a new target), self-mana-below, group-hp-below (tracked per member), and self-bighit (a single hit ≥ pct% of max health). Seven event triggers: killing-blow (CLEU PARTY_KILL, source = you), combat-start (PLAYER_REGEN_DISABLED), combat-end (PLAYER_REGEN_ENABLED, only if alive), group-death (CLEU UNIT_DIED with party/raid+player affiliation flags, not you), self-cc (LOSS_OF_CONTROL_ADDED), self-death (PLAYER_DEAD), and bloodlust (CLEU SPELL_AURA_APPLIED of a Bloodlust/Heroism/Time Warp/etc. spell on you).

  • Crossing + re-arm, combat-only — Threshold filters arm at PLAYER_REGEN_DISABLED from the current value (so an "above" filter doesn't fire just because you pulled at full health), fire once on the crossing, and re-arm only when the value returns to the waiting side; target filters also re-arm on PLAYER_TARGET_CHANGED (without firing on an already-low new target). Group filters arm per member GUID. Everything is gated on _inCombat; the high-frequency UNIT_HEALTH / UNIT_POWER_UPDATE / CLEU handlers early-out when out of combat, and CLEU also bails before parsing unless an active filter needs it (_clActive, refreshed at combat start).

  • Lines, channels, cooldown — Each filter holds a pool of lines (multi-line editor, one per row); one is picked at random per fire, with placeholders [me] / [target] / [hp] / [targethp] / [member]. Channels reuse the Gratz set plus Emote (Guild / Officer / Party / Raid / Say / Yell / Instance / Emote), dispatched immediately (no Gratz-style combat queue — the point is to talk mid-fight). Per-filter cooldown is entered in minutes (minimum 1) and caps repeats.

  • Public-chat restriction documented (not a protected call, but restricted) — Per Blizzard's ChatInfoDocumentation (SendChatMessage HasRestrictions / RestrictedForMacroChatMessages, FailureMode = "ReturnNothing"), the chat types observable by external players — Say / Yell / Emote and public channels — are silently dropped when sent from automated/addon code during an instance boss encounter. So a boss-fight smack set to Say/Yell/Emote won't appear inside a dungeon/raid encounter (open-world combat is unaffected); Party / Raid / Guild / Officer / Instance are not restricted and work during encounters. SendChatMessage is not a protected function (no taint / blocked-action error), so the send just no-ops — surfaced in the tab help and the Say/Yell/Emote channel tooltips rather than worked around. Mirrors why DBM/BigWigs announce to Raid, not /say, during pulls.

  • Version safety — Every trigger maps to an event present Era→Retail; ones that can't apply degrade to silence (Bloodlust never lands on Classic Era; mana filters no-op for a no-mana class/form). Percent helpers return nil for a secret value (issecretvalue, feature-detected) so HP/damage comparisons never throw during Retail tainted combat. New globals in .luarc.json: UnitHealth / UnitHealthMax / UnitPower / UnitPowerMax / UnitPowerType / UnitExists / UnitIsUnit / UnitIsDeadOrGhost / UnitClass / CombatLogGetCurrentEventInfo / bit.

Improvements

  • Smack: Retail/Midnight-aware trigger gating — Six triggers can't work on Retail/Midnight and are now gated to Classic only, for two confirmed reasons. (1) Target HP below % and Group member below % read another unit's health, which is a secret value to addons there (UnitHealth is SecretReturns = true; only the player's own health reads back plainly — DBM reads player health ungated but guards every other unit). (2) Killing blow, Big hit, Group death, and Bloodlust depend on COMBAT_LOG_EVENT_UNFILTERED, whose registration is blocked for addons on Midnight 12.0 — verified in-game (a diagnostic showed issecure() is false in our PLAYER_ENTERING_WORLD handler, so the registration is never attempted and CLEU never fires) and independently by Recount (which pivoted to C_DamageMeter) and DBM (stub Midnight mods). A central isTriggerAvailable() check in eachActiveFilter keeps all six dormant on Retail — no fire, no notice — so a Classic-made filter synced via the account-wide SV just does nothing there. The trigger dropdown hides them on Retail (a Classic-made filter opened on Retail still shows its trigger tagged (Classic only)); tab help + tooltips state the limit. Retail keeps every self/event trigger (My HP/mana, Combat start/end, CC'd, I die) plus the Manual hotkey (a hardware event — works everywhere). Detection-flag branching in the shared engine, not a forked Retail file, so the paths can't drift. CLEU GUID compares were also hardened with a secret guard. Location: Modules/Smack/Smack.lua, GUI/SmackTab.lua.

  • Smack list columns align left like the other tabs — The Smack filter list right-packed all its columns because every column carried a width (the Name column was autoFit, so RowList had no zero-width "stretch" column and right-anchored everything). Made Name the auto-width stretch column (no width/autoFit), matching the Addon Load tab's Addon Name column, so the table spans from the left edge. Location: GUI/SmackTab.lua.

  • Graphs: filter dropdowns now cascade — On the Logs → Graphs page, each per-log filter's options are now drawn from the entries passing the OTHER active filters, so choosing Mining trims the Item / Zone / Source lists to mining gathers only (previously every dropdown listed everything regardless of the other selections). A selection that's no longer offered resets to "All". Implemented via a getOptions(scopedEntries) signature and a rebuildFilterDropdowns() pass run inside refresh(). Location: GUI/LogGraph.lua, Modules/LogGraphs/LogGraphs.lua.

  • Graphs: distinguish same-name items (e.g. the two Refulgent Copper Ore tiers) — The Gathering chart keyed series by itemID but labelled them by name, so two items sharing a name rendered as identical legend/tooltip entries. Series labels and the Item filter now use the captured item link (quality colour + the reagent-tier "pentagon" icon) and the Item filter is keyed by itemID, matching the Gathering log's Item column — so the tiers are visually distinct and individually selectable. Location: Modules/LogGraphs/LogGraphs.lua.

Bug Fixes

  • Smack: automatic public-chat sends blocked on Retail/Midnight (ADDON_ACTION_BLOCKED) — Confirmed in-game: an auto-trigger (combat-end) firing to Say threw ADDON_ACTION_BLOCKED on C_ChatInfo.SendChatMessage — even in the open world, not just encounters. So Blizzard requires a hardware event to send to public channels (Say/Yell/Emote) on Midnight; automated addon sends are blocked. Fix: sendToChannels now takes an isHardware flag — for automatic fires on 12.0+ it skips the public channels (no blocked-action error) and warns the user once per filter to use Party/Raid/Guild or bind a hotkey; hotkey fires (a real keypress) go through to every channel, and Classic is unaffected. Non-public channels (Party/Raid/Guild/Officer/Instance) are still attempted for auto-fires. The isHardware flag also subsumes the hotkey's cooldown bypass. Location: Modules/Smack/Smack.lua, GUI/SmackTab.lua.

  • Smack enemy-health triggers explained instead of silently dead on Retail/MidnightTarget HP below % (and big hit, which reads combat-log damage) never fired on Midnight 12.0 and looked broken. Confirmed via the API docs (UnitHealth is annotated SecretReturns = true; its unit arg is a UnitTokenPvPRestrictedForAddOns) and an in-game trace (UNIT_HEALTH fires for target but hpPct reads nil every tick): on Retail/Midnight a unit's health is a secret value to addons, so it can't be compared — and there's no read-around (a secret may be handed to a display sink but never compared/sorted/summed; verified against Recount's Tracker_Midnight.lua pass-through approach). Smack now warns once per filter when a trigger's value is unreadable (instead of silently doing nothing or spamming chat every health tick), and the Target HP / big hit trigger tooltips note the limit. These triggers work normally on Classic (no secret system); own-health, mana, and event triggers are unaffected everywhere. Location: Modules/Smack/Smack.lua, GUI/SmackTab.lua.

  • Gathering Log threw on a secret tooltip/GUID while gathering in tainted combat — In the UNIT_SPELLCAST_SENT handler, the object-source path read the node name via GameTooltipTextLeft1:GetText() and tested if t and t ~= "" and not issecretvalue(t) — but on Retail the tooltip text is a secret value when gathering during tainted combat, and the t ~= "" comparison runs before the issecretvalue test and throws on a secret ("attempt to compare local 't' (a secret string value…)"). Reordered so issecretvalue(t) is tested before any comparison (truthiness is safe; comparison isn't). Two adjacent GUID reads in the same mid-combat gathering path had the same latent hole — UnitGUID("target") (UNIT_SPELLCAST_SENT) and GetLootSourceInfo(1) (LOOT_OPENED) were strsplit/compared with no secret guard even though the name beside the first was guarded — so both now carry an issecretvalue check before strsplit. No-op on Classic Era. Location: Modules/GatheringLog/GatheringLog.lua.

  • Secret values in name placeholders could throw on Retail — Smack's [target] / [member] / [me] placeholders read UnitName (and the combat-log destName), which on Retail can be secret values during tainted combat. Feeding one into an outgoing line would make the message text secret, and Ace3's ChatThrottleLib SendChatMessage post-hook (tostring(text) on every send) throws "attempt to perform string conversion on a secret string value" on it. The earlier audit guarded the numeric reads (UnitHealth/power) but not the name reads. Fix: all placeholder name values now go through a safeStr/safeName helper that collapses a secret (or nil) to "", plus a final issecretvalue(msg) guard in fireFilter that skips the send entirely rather than ever passing a secret to SendChatMessage. No-op on Classic Era (no secret system). Location: Modules/Smack/Smack.lua.

  • ADDON_ACTION_FORBIDDEN registering COMBAT_LOG_EVENT_UNFILTERED — The engine registered all events in the file-load main chunk. COMBAT_LOG_EVENT_UNFILTERED is a secure-registration event, and TOGTools' load chain is tainted (the !!TOGT / Diagnostics seterrorhandler wrap runs earlier in the load order), so registering it from that context tripped ADDON_ACTION_FORBIDDEN — the insecure events (PLAYER_REGEN_*, UNIT_HEALTH, …) registered fine, only the secure combat-log event was blocked. Fix: the insecure events still register at load (they're unaffected by taint); the secure combat-log event is registered from the PLAYER_ENTERING_WORLD handler and gated on issecure() — the documented test for whether the running execution path is untainted — so the secure call is only attempted when it's guaranteed not to be blocked, rather than assuming a C_Timer callback happens to be clean. PEW is a clean Blizzard dispatch and fires on login / /reload / zone change, so a one-off tainted dispatch just retries next time; once wired, PEW is unregistered. The handler also picks up InCombatLockdown() so a mid-combat /reload isn't dormant until the next fight. New global issecure in .luarc.json. Location: Modules/Smack/Smack.lua.

New Features (Logs / Diagnostics)

  • Diagnostics sub-tab (developer tool) — A dev-gated sub-tab under the Logs nexus (Modules/Diagnostics/Diagnostics.lua + GUI/DiagnosticsTab.lua, registered as addon.logCategories["diagnostics"]) that logs the Lua errors and Lua warnings (LUA_WARNING) the client raises in memory. Blocked/forbidden addon-action events (ADDON_ACTION_BLOCKED / _FORBIDDEN + the MACRO_ variants) are folded in as errors, tagged blocked:/forbidden: in Detail — these are NOT "taint" (true taint propagation goes only to the unreadable on-disk taint.log); they're the addon-action errors the scriptErrors CVar surfaces. Errors keep ONLY the parsed message summary (addon, file:line, reason via splitMessage) — never the stack/locals — and identical messages de-duplicate into one row with a count + last-seen time (Blizzard's ScriptErrorsFrame approach). One RowList (Type / Source / Detail / # / Last-seen), a search box, a Clear button, a Test button (throws real errors on command — see below), and a right-click context menu (Copy); filtering to just Errors or Warnings is the Type column-header filter. SV schema db.global.diagnostics (entries, capped at 500 distinct, oldest-by-last pruned; alert flag). Added to all six TOCs; new globals C_CVar / GetCVar / SetCVar / GetCVarBool / issecretvalue / CopyToClipboard / MenuUtil / EasyMenu in .luarc.json.

  • Early capture via the !!TOGT companion (requires !!TOGT v0.2.0+) — Capture lives in !!TOGT (renamed from !TOGT so it loads before EVERY other addon, including !BugGrabber), not in TOGTools, because most addon load errors fire during the loading screen — before TOGTools, a lettered addon, has initialized. !!TOGT saves the real seterrorhandler at file-load, installs a wrapping handler that captures then TAIL-CALLs the previous one (return prev(msg)), and re-asserts itself on top whenever something (e.g. BugGrabber, which neuters seterrorhandler without chaining) replaces it; it also runs a persistent warning / blocked-action event frame. Records buffer into TOGToolsEarlyData.diagnostics (ring-capped at 250). The tail-call wrap (Lua 5.1 drops our frame) means TOG Tools and BugSack both capture every error, with neither broken. TOG Tools' six TOCs now declare ## Dependencies: !!TOGT. On OnEnable, the Diagnostics module's _attachCapture sets TOGToolsEarlyData.diagnostics.cb (so later captures arrive live) and drains the buffer through _ingestRaw_record; the callback is attached BEFORE the drain iterates, so a capture firing mid-drain is never lost or double-counted. When the companion provides the buffer there is exactly one !!TOGT error handler in our pair (BugGrabber, if present, is wrapped beneath it). Fallback: if TOGToolsEarlyData.diagnostics is absent (an older companion without capture, or it failed to load), _attachCapture calls _installSelfCapture to install TOGTools' own chained handler + event frame, so live capture still works (only the pre-load window is missed). _record self-gates on the Developer Tools switch, so end users store nothing.

  • New-error alert toast — BugSack-style on-screen toast (TOGToolsDiagAlert, a backdrop Button anchored TOP of UIParent) shown when a NEW distinct error is recorded. Left-click opens Logs > Diagnostics; right-click or a 5s auto-fade (Alpha AnimationGroup) dismisses; hover pauses the fade. Gated by db.global.diagnostics.alert (default on) via the "Alert on new errors" checkbox. Only new distinct errors alert — repeats of an already-seen error just bump its count.

  • Titan Panel / LibDataBroker integration — A LibDataBroker-1.1 "data source" object (TOGToolsDiagnostics, label "TOG Tools Errors") shows live error / warning counts; left-click opens Logs > Diagnostics, right-click clears, tooltip breaks down the counts. type = "data source" (not "launcher") so Titan Panel auto-adds it as a plugin — pattern lifted from Grouper's GrouperCore.lua LDB object. Created only when Developer Tools is on (no broker clutter for end users); Titan picks it up live via LibDataBroker_DataObjectCreated, and the Settings devTools toggle calls _ensureBroker so it appears immediately. Text refreshes on every _record / ClearAll via _updateBroker.

  • Right-click a row for a context menu — Right-click pops a dropdown menu (cross-version: MenuUtil.CreateContextMenu on Retail 11.0+, EasyMenu/UIDropDownMenu on Classic) with a Copy option — built to be extensible so more row actions can be added later. Copy opens a focused, pre-selected StaticPopup EditBox (TOGTOOLS_DIAG_COPY) for a manual Ctrl+C (addons can't write the OS clipboard); the copied line is plain-text and reconstructs the original source: detail message with a [kind xN] … (last …) tag. RowList now passes the mouse button and the row frame to onRowClick (backward-compatible extra args) so the tab can split right-click → menu from other clicks and anchor the menu to the row. Location: GUI/DiagnosticsTab.lua, GUI/RowList.lua.

  • Sortable, multi-select-filterable column headers (every table) — Clicking any RowList column header opens a cross-version dropdown menu (new global addon.UI.OpenColumnMenu; MenuUtil.CreateContextMenu on Retail 11.0+, EasyMenu/UIDropDownMenu on Classic). It offers sort (auto-labelled by type — text A > Z/Z > A, number Low > High/High > Low, date Oldest > Newest/Newest > Oldest, via col.sortType or inferred from the first value) and, on opted-in columns (col.filterable = true), a multi-select filter with Select all / Clear all and per-value checkboxes that keep the menu open (MenuResponse.Refresh on Retail, keepShownOnClick + checked-as-function on Classic). RowList owns the filter state (colExcluded[colKey], in-memory), applies it in a rewritten _getSortedData (filter → sort, cached in _displayData), marks a filtered header with a gold *, and Refresh/scrollbar size to the filtered count. Cascading: a column's checkbox list is built from the rows passing the OTHER columns' filters (_rowsForFilterMenu), so choosing Mining in Type narrows the Item / Source / Zone lists. Nested groups: a column with col.filterGroup(entry) renders its values as submenus (Gathering's Zone groups by continent, _distinctGroups). Value lists cap at 200 with an "N more (not shown)" note. Filtering is opt-in — only short categorical columns enable it (Gathering Type/Source/Zone, Guild & Guild Bank Type, Guild Bank Tab, Diagnostics Type); name/item/free-text/numeric/timestamp columns stay sort-only. Labels are colour/link/atlas-stripped (stripEscapes) so quality variants group. Location: GUI/RowList.lua, GUI/UI.lua.

  • Per-table free-text searchRowList:SetSearch(text) filters rows by case-insensitive substring across EVERY column's rendered text (escapes stripped), composing with the column filters and the date range. Each log sub-tab gets a magnifying-glass search box via addon.UI.StyleSearchBox — ported verbatim from TOGProfessionMaster (Interface\Common\UI-Searchbox-Icon + text inset, OnRelease cleanup). Live (OnTextChanged, no button), persisted per sub-tab in self._search. Location: GUI/RowList.lua, GUI/UI.lua, all GUI/*LogSubTab.lua, GUI/DiagnosticsTab.lua.

  • Gathering Log: source tracking (what each item came from) — Each gather entry now records source (the node / object / creature it came from), sourceID (a stable per-type id), and continent. The capture method was established empirically because the API exposes no node-name function and UNIT_SPELLCAST_SENT.target is a Secret value on Retail: a SOURCE_KIND map splits object gathers (mining / herbalism / fishing / gas — name from the node's tooltip first line at UNIT_SPELLCAST_SENT, stable id from GetLootSourceInfo's GameObject template id on the first LOOT_OPENED inside the pre-loot window, so a treasure chest opened mid-window can't steal the id) from unit gathers (skinning / pickpocketing — name + creature id from the target unit). A new Source column shows it, and the Zone filter nests continent → zone (continent resolved by walking the C_Map tree to the Continent-type map at gather time). Location: Modules/GatheringLog/GatheringLog.lua, GUI/GatheringLogSubTab.lua; new globals UnitGUID / C_Map / Enum / GetNumLootItems / GetLootSourceInfo in .luarc.json.

  • Gathering Log: gas extraction + pickpocketing — Two gather types beyond mining/herb/skin/fish: Gas Extraction (the Extract Gas cast — a world/object source like a node) and Pickpocketing (Pick Pocket — a unit source like skinning). Wired via the same SOURCE_KIND object/unit split (which also gives skinning proper creature attribution), detected by ability-name keyword so they self-confirm against the existing unlogged-loot debug line. Location: Modules/GatheringLog/GatheringLog.lua.

  • Graphs: per-log filter dropdowns — The Graphs sub-tab replaced its per-item series-checkbox wall with filter dropdowns that change with the selected log: Gathering = Character / Profession / Zone / Source / Item / Expansion, Mail = Character / Direction, Trade = Character / Partner. The spec's single subFilter became a filters list ({ key, label, getOptions, match }); LogGraph renders one single-select "All …" dropdown per filter (wrapping by width), scopes the entries by the active selections, and getSeries(filteredEntries) charts one line per item (top 12) of that scoped set. The coloured totals line under the chart is now the legend. Location: Modules/LogGraphs/LogGraphs.lua, GUI/LogGraph.lua, GUI/LogGraphsSubTab.lua.

Bug Fixes (Logs / Diagnostics)

  • Trades still never logged on Retail after the v0.6.4 attempt — v0.6.4 tracked "both sides accepted" as a live boolean updated on every TRADE_ACCEPT_UPDATE and re-snapshotted the slots each time, committing on TRADE_CLOSED if the flag was set. On Retail a completing trade fires a final TRADE_ACCEPT_UPDATE(0,0) teardown and empties the trade slots (TRADE_PLAYER_ITEM_CHANGED / TRADE_TARGET_ITEM_CHANGED) in the instant before TRADE_CLOSED — so the live flag was cleared (→ no commit) and the re-snapshot wiped the captured items anyway. Reworked to a latch + frozen copy: on the (1,1) "both accepted" edge we snapshot the slots and freeze an independent copy of the items/money/enchant/partner into _completed (allocated via slotsToList, immune to later slot teardown). The latch is never cleared by a subsequent reset; an edit-then-re-accept just produces a new (1,1) that re-freezes. TRADE_CLOSED commits the frozen copy; a _committed flag dedupes against the Classic ERR_TRADE_COMPLETE path (which fires before teardown, so it freezes the still-valid live staging). Confirmed against the Retail flow in f:\blizzard api docs (TradeFrame.xml accept → C_SecureTransfer.AcceptTrade; TradeInfoDocumentation.lua payload playerAccepted/targetAccepted). Added [trade] addon:Debug traces of the accept/close sequence so the live event order can be captured if anything is still off. Location: Modules/TradeLog/TradeLog.lua.

  • Whisper / Gathering logging errored on Retail in combat ("secret string value") — On Retail, chat-event payloads can arrive as secret values while execution is tainted during combat (the restricted-data / "Secret" system). WhisperLog's append compared the whisper text (text == "") and GatheringLog's parseLoot ran text:match(...) / tostring(text) on the loot line — any compare, match, or tostring on a secret value throws (attempt to compare local 'text' (a secret string value, while execution tainted by 'TOGTools')). The error was previously invisible because scriptErrors was off; the new Diagnostics "Display Lua errors" toggle (which enables scriptErrors) surfaced it — the tool catching a real latent combat crash. Fix: feature-detect issecretvalue (GlobalAPI.lua; absent on Classic Era) and skip any whisper whose text/sender is secret, and any loot line that is secret — we can't read, compare, or persist a secret value, and storing one would taint SavedVariables. Classic Era is unaffected (no Secret system). Location: Modules/WhisperLog/WhisperLog.lua, Modules/GatheringLog/GatheringLog.lua; new global issecretvalue in .luarc.json.

  • Diagnostics tab controls overlapped — The filter dropdown (which carries its own "Show" label, making it taller than a checkbox) shared one AceGUI Flow row with the capture checkboxes, so the differing widget heights overlapped. Split into a dedicated filter row plus a uniform-height toggle row (the ItemDB layout). Location: GUI/DiagnosticsTab.lua.

  • Alert toast text overlapped — The toast's hint line was anchored to the icon's bottom while the message was anchored below the title, so in the short (54px) frame the message and hint drew on top of each other. Reworked to stack title / message / hint top-down from one left edge in a taller (66px) frame. Location: Modules/Diagnostics/Diagnostics.lua (ensureAlert).

  • Titan/LDB counter stayed stale after ClearClearAll reset the entries and refreshed the tab but never refreshed the LibDataBroker text, so Titan kept the pre-clear counts; it now calls _updateBroker. The broker shows DISTINCT issue counts (matching the tab summary). Location: Modules/Diagnostics/Diagnostics.lua (ClearAll).

Improvements (Logs / Diagnostics)

  • CVar capture toggles — Errors need no CVar (always captured via the handler). "Capture warnings" toggles scriptWarnings (the LUA_WARNING gate). "Display Lua errors" toggles scriptErrors — Blizzard's built-in error popup, which is also the gate that lets the blocked/forbidden addon-action errors reach addons (so those rows populate only while it's on). taintLog is intentionally NOT touched — it only writes the unreadable on-disk taint.log (an early build set it and so captured nothing in-game despite the file filling up; and an early build mislabeled this category "taint" — it's just scriptErrors output). The tooltip is explicit that the toggle is scriptErrors, re-enables the default popup, and can fight BugSack. All SetCVar writes are pcall-wrapped with the live state read back and a /console fallback shown.

  • SavedVariables-too-large captureSAVED_VARIABLES_TOO_LARGE (payload = culprit addon) is recorded as an error ("SavedVariables too large — not saved (data lost)"). It's the usual cause of silent settings/data loss, and — per a full sweep of the event list — the only generic addon-troubleshooting signal beyond errors / warnings / blocked-actions (everything else error-ish is feature-specific gameplay noise; CPU/memory is deliberately left to dedicated profilers). Deprecation warnings are already covered — the deprecation system routes through LUA_WARNING. Location: !!TOGT/!TOGT.lua, Modules/Diagnostics/Diagnostics.lua.

  • Test button throws REAL errors — Instead of injecting synthetic rows, the Test button now generates two genuine, harmless errors on command (FireTestError), each deferred to the next frame so it fires OUTSIDE the click's AceGUI pcall and reaches the global handler/events: (1) a real Lua error() → exercises the seterrorhandler path and the !!TOGT tail-call wrap (it also lands in BugSack, proving coexistence); (2) a forbidden protected-function call (CopyToClipboard) → an ADDON_ACTION_FORBIDDEN event exercising the event-frame path. An honest end-to-end test of both live capture paths rather than a fabricated display. Location: Modules/Diagnostics/Diagnostics.lua (FireTestError), GUI/DiagnosticsTab.lua.

  • Diagnostics moved under the Logs nexus as a sub-tab — It was originally a top-level devOnly tab, but structurally it's just another log (a de-duplicated RowList over time), so it now registers as addon.logCategories["diagnostics"] and renders inside the Logs tab's sub-tab strip — reusing the nexus's per-sub help dispatch, last-viewed-sub-tab memory, and deep-linking instead of reimplementing them. Gating is deliberately decoupled: rather than the standard Logs-master-AND-per-category check (IsLogCategoryEnabled), the category declares a gate() that tracks ONLY the Developer Tools switch — so a dev who disables gameplay logging (or any individual log type) still sees Diagnostics, and it never gets a player-facing toggle. LogsTab.BuildInnerTabDefs now honours cat.gate in place of IsLogCategoryEnabled (a gated category is governed solely by its gate, with no fallback); the Logs module gained showWhenDev = true so MainWindow.BuildTabDefs keeps the Logs tab present for developers even when the Logs module itself is toggled off — otherwise disabling Logs would also hide Diagnostics. Since it left addon.modules, whose OnEnable loop no longer reaches it, Ace:OnEnable gained a second loop over addon.logCategories to call OnEnable on any log engine that exposes it (only Diagnostics does; the rest self-wire). The toast and the Titan/LDB broker now deep-link to Logs > Diagnostics via pendingSubTab; new dev-gated /togt diag (alias /togt diagnostics) does the same. order = 900 sorts it last in the strip. Location: Modules/Diagnostics/Diagnostics.lua, GUI/DiagnosticsTab.lua, GUI/LogsTab.lua, GUI/MainWindow.lua, GUI/Settings.lua, SlashCommands.lua, TOGTools.lua.

  • Log sub-tab filter rows decluttered — Now that the column headers filter, the external filter-dropdown rows were removed wherever they duplicated a column: Character on every log, Profession/Zone on Gathering, Type on Guild, Type/Tab on Guild Bank, and the All/Errors/Warnings "Show" dropdown on Diagnostics. Non-column filters stay as dropdowns — Whisper's Direction / Type / GM, Mail's & Trade's Direction, and the Guild & Guild Bank Guild selector (which chooses which guild's stored bucket loads — a data source, not a display filter). Whisper's two substring boxes (Partner + Message) collapsed into the single all-column search box. Every tab keeps its Date range picker. Stale "use the … dropdown above" column tooltips were updated to point at the column-header filter / search box. The four gameplay tabs (Mail / Trade / Guild / Guild Bank) were done in parallel against the Whisper/Gathering template. Location: all GUI/*LogSubTab.lua, GUI/DiagnosticsTab.lua.

  • Gathering stats text summary removed — The multi-line "By profession / Top items / By zone / By source" block above the Gathering table was dropped — sortable/filterable headers cover lookup and the Graphs tab covers trends, so the cramped prose was redundant. The Gathering data tab is now Date range + search + table. Location: GUI/GatheringLogSubTab.lua.

  • Gathering Log: quantity shown as its own sortable column — Item pickups show the count in a dedicated Qty column (between Type and Item) rather than a "Nx" prefix, so it sorts independently. Location: GUI/GatheringLogSubTab.lua.

  • Removed the Mail/Trade log load-verification diagnostic globals — Dropped _G.TOGTOOLS_MAILLOG_VERSION and _G.TOGTOOLS_TRADELOG_VERSION (and their comment blocks). These existed only during the v0.6.4 development cycle so a /run print(TOGTOOLS_MAILLOG_VERSION) / print(TOGTOOLS_TRADELOG_VERSION) could confirm a /reload had pulled in the latest source; the MailLog one was marked "remove before release". No runtime or user-facing behaviour change — they only assigned a string to a global. Location: Modules/MailLog/MailLog.lua, Modules/TradeLog/TradeLog.lua.


[v0.6.4] (2026-06-04) - Mail + Trade logs: Retail capture fixes

Bug Fixes

  • Sent mail logged with no money or attachments on Retail — The Mail log's send path re-read attachments (GetSendMailItemLink) and money (GetSendMailMoney) at commit time, inside the send-confirmation handler. On Retail the send-completion event is MAIL_SEND_SUCCESS, which runs Blizzard's SendMailFrame_Reset() (clearing the compose money/attachment slots), and by then the mail has already left the client — so both reads returned 0/nil. Only the recipient + subject survived (captured earlier in the SendMail hook), which is why a row appeared but money/items were empty. Fix: snapshot money + staged items inside the SendMail hook — the one moment they're guaranteed present on every flavour, since nothing can be added after SendMail is invoked — and commit those captured values rather than re-reading. Commit now fires from a shared commitSend() driven by whichever send-confirmation event arrives first: MAIL_SEND_SUCCESS (Retail, registered only when addon.gameVersion.isRetail) or UI_INFO_MESSAGE/ERR_MAIL_SENT (Classic); the second event is a no-op via the existing _pendingSend = nil dedupe. Removed the MAIL_SEND_INFO_UPDATE rebuild, which fired during the post-send compose reset on empty slots and would clobber the captured items. Classic keeps its working ERR_MAIL_SENT path and also gains the more-robust send-time capture (no regression). Location: Modules/MailLog/MailLog.lua.

  • Trades never logged on Retail (incl. M+ / dungeon loot redistribution) — The Trade log committed only on UI_INFO_MESSAGE with ERR_TRADE_COMPLETE. Retail's TradeFrame no longer registers UI_INFO_MESSAGE and signals trade completion purely through TRADE_CLOSED (which fires for both completion and cancel), so no trade — items, money, or enchant — was ever recorded on Retail; the dungeon/M+ context the reports came from is simply where players trade most. Fix: track whether both sides have accepted from each TRADE_ACCEPT_UPDATE(playerAccepted, targetAccepted), and on TRADE_CLOSED treat "both accepted at close time" as a completed trade and commit. The both-accepted flag is driven purely by the accept-update args (adding/removing an item resets both accept states, so it self-clears on cancel), and is never cleared on item/money changes — a successful trade may fire no further events between the both-accepted update and TRADE_CLOSED. Commit logic extracted into a shared commitTrade(); the Classic ERR_TRADE_COMPLETE path is retained and deduped against TRADE_CLOSED via _staging = nil (whichever fires first logs, the other is a no-op). The partner name is now captured via UnitName("npc") during the slot snapshot (TRADE_SHOW + each accept update) and read from staging at commit time — by TRADE_CLOSED, the Retail commit point, the npc unit can already be gone, which would otherwise log every Retail trade's partner as ?. Location: Modules/TradeLog/TradeLog.lua.

Developer Tooling

  • Dev-sync watcher failed to auto-start and could spawn duplicate instances — Two faults kept wow-version-replication.ps1 from mirroring edits into the other installed WoW clients. (1) Its single-instance guard created a Global\ named mutex, which requires SeCreateGlobalPrivilege (absent for a standard, non-elevated user) — New-Object threw, the guard's catch failed open, and duplicate watchers piled up. Changed the mutex scope to Local\ (per-session; no special privilege), so the guard actually dedupes; the watcher and its relaunchers always share one interactive session. Location: wow-version-replication.ps1. (2) Companion fix outside the repo, in the global ~/.claude/settings.json SessionStart hook that auto-launches the watcher: it located the script with $PWD — not guaranteed to be the repo root, so nothing launched under the VS Code extension — and used Start-Process … -ArgumentList '-File',$s (array form), which left the space-containing script path unquoted so powershell saw -File c:\Program and exited. Now resolves the directory via ${CLAUDE_PROJECT_DIR} → hook stdin cwd$PWD, and launches with a single quoted argument string. Net effect: the watcher reliably starts on session open and back-fills every client.

[v0.6.3] (2026-05-31) - Gathering Log, Log Graphs, Item DB load fix

New Features

  • Gathering Log — automatic mining / herbalism / skinning / fishing log — New gathering log sub-category (Modules/GatheringLog/GatheringLog.lua + GUI/GatheringLogSubTab.lua) that records every node you gather: item, quantity, profession, zone, and time. Capture is spell-cast-gated: UNIT_SPELLCAST_SUCCEEDED (unit player) on a gather spell arms a short window, and the CHAT_MSG_LOOT that follows is attributed to it — so mob drops, vendor buys, and mail are never logged because nothing gathered them. The gather cast is identified by resolving the Classic rank-1 anchor spell IDs (Mining 2575, Herb Gathering 2366, Skinning 8613, Fishing 7620) to localized names, plus a profession-keyword word-boundary match so Retail's per-expansion casts also resolve (verified: Midnight mining is spell 471013 "Midnight Mining" — ends in "Mining"; "Examining" and similar lookalikes are rejected). Fishing gets a longer pre-loot window (30s vs 3s) for the bobber bite; multi-drop nodes group under one action id (a persisted monotonic counter) so per-node yield stats are exact. Loot is parsed locale-safely — itemID/name from the embedded |Hitem: link, stack count from LOOT_ITEM_SELF_MULTIPLE turned into a pattern. The sub-tab has Character / Profession / Zone / Date-range filters, a stats panel (total items, nodes worked + per-node yield, per-profession, top items, by-zone, items/hour), and a sortable table with an absolute MM/DD/YY HH:MM:SS time column. Each item's expansionID (15th GetItemInfo return) is captured for the graph's expansion filter. Settings toggle + "Clear Gathering Log" wired in GUI/Settings.lua; schema in TOGTools.lua; new globals C_Spell / GetSubZoneText / LOOT_ITEM_SELF_MULTIPLE in .luarc.json. Added to all six TOCs.

  • Log Graphs — LibGraph-2.0 line charts for log data — New Graphs sub-tab, forced first in the Logs strip via a new order field on log categories (GUI/LogsTab.lua now sorts by order then label; capture logs default to 100 and stay alphabetical). A reusable addon.LogGraph component (GUI/LogGraph.lua), generalized from FastGuildInvite's Statistics tab, takes a per-log "graph spec" (named series with colour + match/value functions, an entry provider, and an optional sub-filter), buckets the log's entries by time over a selectable period (24h hourly / 7-14-30d daily / all-time), and draws one LibGraph line per visible series with per-series checkboxes and a totals line. It uses a single persistent raw-frame set reparented onto the AceGUI host per render (avoids orphaning frames on pooled containers, the RowList approach); period / series / sub-filter changes repopulate in place. Modules/LogGraphs/LogGraphs.lua registers the category and owns the specs: Gathering = one line per gathered item with an Expansion sub-filter (resolved from each item's expansionIDEXPANSION_NAME<id>, top-12 items charted, defaults to the newest expansion present); Mail = Received vs Sent; Trade = items received vs given. Per-log prefs (period / series visibility / expansion) persist in db.global.logGraphs. Hovering the chart pops a GameTooltip for the time bucket under the cursor, listing each visible series' value for that bucket — a transparent mouse overlay maps the cursor's X to a bucket by inverting LibGraph's linear plot transform (pixel = Width·(x−XMin)/(XMax−XMin)), so it needed no LibGraph change (FGI's shared copy is untouched). The X axis renders our own time anchors (oldest → "now", e.g. "24h ago / 12h ago / now", or dates on the multi-day views) instead of LibGraph's bare 1..N numbers, which read backwards on a past→now axis and collided with the axis label (XLabelsEnabled is left off). Bundled LibGraph-2.0 into libs/ (self-locates its textures via debugstack) and registered it in all six TOCs; new UIDropDownMenu_SetSelectedValue global in .luarc.json.

Bug Fixes

  • Item DB SavedVariables failed to load once large (constant table overflow) — The Item DB stored each item as its own classes[classID][idStr] = packed entry. At ~240k items that put ~480k string constants in the saved file's single Lua chunk, past Lua's ~262,144-constant-per-function loader limit, so the entire TOGTools_DB failed to compile on load — taking every log and setting with it (and on next logout WoW would overwrite the file with empty defaults). Storage is now one concatenated string blob per item class (per-item records joined by \30, fields by \31) — a single string constant per class (~13 total), which loads at any size. The engine works against an in-memory index parsed from the blobs on first use and re-serializes on pause / complete / PLAYER_LOGOUT; a legacy per-item classes table that still loaded is migrated into the blob form automatically. Location: Modules/ItemDB/ItemDB.lua (ensureIndex, flushBlobs, _ingest, Search / GetClasses / GetSubClasses, Purge, PLAYER_LOGOUT), TOGTools.lua (itemDB schema). Note: a DB already saved in the old format must be rebuilt (the old file can't be read back).

  • Gratz guild level-ups never fired (all flavours)refreshGuildTracker read the roster with local name, _, _, level = GetGuildRosterInfo and GetGuildRosterInfo(i) or nil, nil, nil, 0. Lua operator precedence parses the right-hand side as four expressions; wrapping GetGuildRosterInfo(i) in and/or truncated its multi-return to the first value (the name), so level was assigned the literal 0 every iteration. The level > 0 guard then rejected every member, so _guildTracker never populated and fireGuildLevelUp never ran. Fixed with a plain destructure (local name, _, _, level = GetGuildRosterInfo(i) — level is the documented 4th return) and the existence guard moved to if not (GetNumGuildMembers and GetGuildRosterInfo). The party path (UNIT_LEVEL) was unaffected. Location: Modules/Gratz/Gratz.lua.


[v0.6.2] (2026-05-30) - Item DB: item level + stats capture

Improvements

  • Item DB walk now records item level + full stats_ingest packs each item as name\31quality\31subClassID\31equipLoc\31itemLevel\31stats (was four fields, name/quality/sub/equip). The stat blob is the client's own GetItemStats serialized as KEY=val,… over its ITEM_MOD_* / RESISTANCE* keys (primary stats, spell power, mp5, resistances, weapon DPS, …) — the only fully-aggregated source, since the raw DB2 export omits effect-granted stats. New EncodeStats(link) helper; Rescan upgrades older four-field entries to the six-field form in place (the bucket-merge dedups on field count, so a partial DB converges without a wipe). This is the data the standalone LibItemDB library ships (stats are locale-independent — captured once per game version; names come per-locale). Location: Modules/ItemDB/ItemDB.lua (EncodeStats, _ingest), GUI/ItemDBTab.lua, TOGTools.lua (itemDB schema).

[v0.6.1] (2026-05-30) - Classic whisper-menu crash fix

Bug Fixes

  • Right-clicking a player name in the Whisper Log and choosing Whisper errored on Classic — The shared RowList player-link right-click handler passed the RowList row frame as the 5th argument (the chat frame) to FriendsFrame_ShowDropdown. On Classic the UnitPopup Whisper action routes through ChatFrame_SendTell(name, chatFrame)ChatFrame_SendTellWithMessage, which reads chatFrame.editBox; our row isn't a real ChatFrame, so it has no editBox and threw attempt to index local 'editBox' (a nil value) at Blizzard_ChatFrameBase/Classic/ChatFrame.lua:1688. Retail tolerates the row frame, so that path was left exactly as-is; on Classic the call now passes DEFAULT_CHAT_FRAME, so the Whisper edit box resolves and the menu's Whisper/Invite/Inspect/Ignore/Report actions all work. Only the Whisper Log emits |Hplayer:|h links today, so that's the only affected surface. Location: GUI/RowList.lua.

[v0.6.0] (2026-05-29) - Item DB builder (developer tool)

New Features

  • Item DB module — runtime-built, searchable item catalog (dev tool) — First piece of a planned shared item database for the TOG suite. WoW exposes no item-name search API and ships no searchable item table, so the only way to resolve a name→link offline is to ask the server about every item ID once and persist the answers — the same technique the "Get Link" / Ludwig addons use. New ItemDB module (Modules/ItemDB/ItemDB.lua) walks the item-ID space (db.global.itemDB.cursortopID, default 240000) on a throttled C_Timer.NewTicker: each ID is gated by GetItemInfoInstant — a local, synchronous call that returns the itemID plus classID / subClassID / equipLoc for real items and nil for non-existent IDs (no server traffic) — then GetItemInfo supplies name / quality. Uncached items return nil from GetItemInfo (which issues the async request); GET_ITEM_INFO_RECEIVED re-ingests on success == true, and on success == false (a phantom item the server has no data for — removed / test / cross-version stubs the client carries static data for, so GetItemInfoInstant reports them real) drops the ID from pending. Some phantoms are worse: after the server answers "no data" once, the client negative-caches it, so a later GetItemInfo returns nil and fires no event at all — they can't be cleared reactively. So the gap-fill phase is patient (GAPFILL_MAX_STALL = 300 ticks ≈ 15s of zero progress; any real-but-slow item resolving resets the window) and, when that window elapses with nothing resolving, drops the remaining unresolvable IDs outright — a nameless item can never live in a name-search DB — so a build / Fill gaps converges to 0 gaps instead of parking on a few hundred permanently-stuck IDs. (The existence gate was originally C_Item.DoesItemExistByID, but on Classic Era that returns true for essentially every ID in range — it flooded pending with ~216k non-existent IDs and fired a request for each. GetItemInfoInstant is the reliable gate; _ingest also prunes any non-real ID it encounters out of pending, so a legacy over-stuffed gap ledger self-cleans on the next Fill gaps / Rescan.) Throttle constants: WALK_PER_TICK = 500 IDs/tick but REQ_PER_TICK = 25 new server requests/tick at TICK_INTERVAL = 0.05 — the request budget (not the walk count) is the disconnect-safety governor, deliberately conservative (~500 req/s peak). All API access is feature-detected (C_Item.* with bare-global fallback) so the same file runs on every flavour; if DoesItemExistByID is absent the builder reports unsupported and no-ops. Storage is bucketed by classID so type/subtype filtering iterates one class instead of the whole catalog: classes[classID][idStr] = "name\31quality\31subClassID\31equipLoc" (US-separator pack, \31 never appears in a name). Stop/resume across sessions is a two-part state, both persisted in SV: a forward cursor (sequential walk progress) AND a pending set ([idStr]=true) of IDs requested-but-not-yet-stored. The walk runs in two phases — phase 1 walks cursortopID; phase 2 (gap-fill) drains pending by re-poking a REQ_PER_TICK batch each tick until it empties or stalls (GAPFILL_MAX_STALL = 60 ticks of no shrink → finalise). The pending set is what makes a pause/logout mid-walk lossless: items left in-flight when you stop (the cursor already passed them) are re-requested on resume instead of skipped. GetProgress exposes a phase (idle/walking/gapfill/complete), cursor, and pending so the tab shows the exact resume point. Start() resumes from cursor (no-op if already complete with no pending); Pause() just stops the ticker (state survives in SV); locale/build are stamped so the tab can flag a rebuild after a language or client-version change. Read API: :GetProgress(), :GetClasses(), :GetSubClasses(classID), :Search({query, classID, subClassID, quality, max}) (returns reconstructed interactive links), plus :Start()/:Pause()/:Purge() (:ClearAll alias). Location: Modules/ItemDB/ItemDB.lua.

  • Item DB tab + Developer Tools gateGUI/ItemDBTab.lua provides Build / Resume / Fill gaps / Rescan / Pause / Rebuild / Purge controls (Rebuild + Purge two-step confirm via StaticPopupDialogs). The DB already tracks both halves of "have vs don't-have": classes is the have-set (and _ingest's existing-bucket short-circuit skips them with no server request), while pending is the persisted gap ledger — IDs confirmed to exist via DoesItemExistByID whose name reply never arrived. Fill gaps (:FillGaps()) is the cheap "fetch only the ones we don't have" pass: it jumps the cursor past topID straight into the gap-fill phase so it re-requests just the pending IDs, skipping the ~240k forward re-walk entirely (no-op when there are no gaps). Rescan (:Rescan()) is the thorough version — resets cursor/complete but preserves classes/count/pending and re-walks from id 1, catching existing items that were never walked at all. The progress line shows the have-count and a N gaps figure so the split is always visible. a live progress line (item count, %, id cursor, locale-change warning) driven by an ItemDB.onProgress hook the tab sets in :Draw and clears on the body OnRelease, and a search row: Type dropdown (item classes present in the DB, via GetItemClassInfo), Subtype dropdown (repopulated per class via GetItemSubClassInfo), and a name search box (2+ chars, or any length when a Type filter is active). Results render in a RowList (ID · Item · Type · Subtype) where the Item column is the stretchy auto-width column carrying the reconstructed |Hitem:ID|h link — hover for the tooltip, shift-click into chat, for free via RowList's existing hyperlink handling. The controls (progress line, button row, filter row, result-info) are collected into a single auto-height List SimpleGroup, with the table body as its one full-height sibling — AddonLoad's two-child header+fill shape. Earlier attempts (loose relative-width children, then per-row SimpleGroups) dropped into the outer Flow let the full-height body draw on top of the row directly above it; collapsing everything into one solidly-measured List group leaves only that group above the body. Every button and filter widget carries a hover tooltip via addon.UI.AttachTooltip (which handles the Dropdown/EditBox label-area hover too). The module is flagged devOnly: MainWindow.BuildTabDefs now skips devOnly modules unless db.global.devTools is set, and the Settings auto-generated Modules list skips them (gated solely by the new General > Developer Tools toggle, which calls MainWindow:Rebuild() so the tab appears/disappears immediately). Hidden slash command /togt itemdb (/togt db) opens it when dev tools are on; intentionally absent from the help list. Schema in TOGTools.lua, gate in GUI/MainWindow.lua, toggle + Modules-skip in GUI/Settings.lua, slash in SlashCommands.lua, .luarc.json gained GetItemClassInfo / GetItemSubClassInfo / strsplit, and the module pair was added to all six *.toc. Curseforge description intentionally NOT updated — this is a hidden developer tool, not a player-facing feature.


[v0.5.4] (2026-05-28) - Whisper Log: right-click partner menu

New Features

  • Whisper Log: left/right-click partner names — User-requested follow-up to v0.5.3. The Other column's partner name (in-game whispers) is now a |Hplayer:NAME|h[NAME]|h hyperlink — left-click opens a whisper edit box, right-click pops the standard chat-name context menu (whisper / invite / inspect / ignore / report) — exactly the same dropdown you get from clicking a name in the chat frame. Implementation routes through SetItemRef(link, text, button, frame) because it has version-specific dispatch baked in: on Mainline the LinkUtil.RegisterLinkHandler(LinkTypes.Player, HandlePlayerLink) path runs FriendsFrame_ShowDropdown; on Classic Era the same SetItemRef entry resolves through the older inline player-link branch — both terminate at the same UI. Confirmed against the Blizzard API docs mirror (Blizzard_UIPanels_Game/Mainline/ItemRefHandlers.lua:1-51). Battle.net partners (isBN == true) skip the player-link wrapping in v1 — the canonical link type is |HBNplayer:|h with bnetIDAccount in the link options, and cross-version BN routing is fragile enough to defer; BN rows still render correctly as plain text with the [BN] tag. Unknown senders (other == "?") also skip the wrap. Location: GUI/WhisperLogSubTab.lua, Modules/WhisperLog/WhisperLog.lua (help blurb).

Improvements

  • RowList: player links dispatch left vs right click separately; main window strata dropped so menus float above — Touched the global RowList hyperlink handler so any consumer that emits |Hplayer:|h links gets the canonical chat-frame behavior automatically. OnHyperlinkClick captures (self, link, text, button) instead of just link and splits on button: right-click extracts the name via link:match("player:([^:]+)") and calls FriendsFrame_ShowDropdown(name, 1, nil, nil, row) DIRECTLY rather than routing through SetItemRef. This is the pattern Blizzard's own Blizzard_Communities/CommunitiesInvitationFrame.lua:103-109 uses, because SetItemRef's right-click dispatch doesn't reliably pop the UnitPopup menu for non-chat-frame contexts on Retail (initial attempt did this and the menu never appeared in testing). FriendsFrame_ShowDropdown lands at UnitPopup_OpenMenu("FRIEND", contextData) which produces the same dropdown as clicking a name in chat (Whisper / Invite / Inspect / Ignore / Report Player / Copy Character Name / etc.). Left-click stays on SetItemRef(link, text, button, row) since its left-click branch does the right thing (opens a whisper edit box on Retail and Classic alike). Strata root cause + fix: AceGUI's Frame widget hard-codes itself to FULLSCREEN_DIALOG strata with frame level 100 (Ace3/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua:81-82); Blizzard's UnitPopup context menu also defaults to FULLSCREEN_DIALOG (Blizzard_Menu/Menu.lua:2084 — only escalating to TOOLTIP when its ownerRegion is on TOOLTIP). Same strata, AceGUI's frame level 100 wins z-order, so the menu opens BEHIND the window. Two attempts to bump the menu's strata up via Menu.GetManager():GetOpenMenu():SetFrameStrata("TOOLTIP") didn't take — likely either timing or the proxy's __index forwarding doesn't propagate SetFrameStrata to the rendered frame. A third version added an ugly window-strata-lowering fallback with C_Timer poll-and-restore which worked but was tangled. Final solution is a one-liner in GUI/MainWindow.lua right after AceGUI:Create("Frame"): f.frame:SetFrameStrata("DIALOG"). The window drops one strata below the menu, so the menu's FULLSCREEN_DIALOG naturally floats above without any per-click gymnastics. No noticeable side effects — the main window is a content tool, not a modal that needs to sit above other UI; DIALOG is the strata most addon windows of this kind use. All other link types fall through to the existing HandleModifiedItemClick / ChatEdit_InsertLink chain unchanged. OnHyperlinkEnter early-returns for player links — no GameTooltip:SetHyperlink representation, and the chat frame itself doesn't pop a tooltip on hover for them. .luarc.json gained SetItemRef + FriendsFrame_ShowDropdown to the known-globals list. Location: GUI/RowList.lua, GUI/MainWindow.lua, .luarc.json.

[v0.5.3] (2026-05-27) - Whisper Log

New Features

  • Whisper Log sub-tab — New whispers sub-category under the Logs nexus. Captures every whisper sent and received — both in-game whispers (CHAT_MSG_WHISPER / CHAT_MSG_WHISPER_INFORM) and Battle.net whispers (CHAT_MSG_BN_WHISPER / CHAT_MSG_BN_WHISPER_INFORM) — into a flat list under db.global.whisperLog.entries. Entry shape: { ts, dir = "in"|"out", player, other, text, isGM, isBN, guid, bnSenderID, lineID, zone }. Event payload positions (arg1 text, arg2 playerName, arg6 specialFlags, arg11 lineID, arg12 guid, arg13 bnSenderID) were confirmed against the Blizzard API docs mirror at Blizzard_APIDocumentationGenerated/ChatInfoDocumentation.lua and are stable across every supported version (Classic Era through Retail). isGM uses the specialFlags == "GM" test that Blizzard's own Blizzard_GMChatUI.lua uses for the same purpose. Dedup: a 5-second recent-lineID set drops repeats when chat events re-fire through filter chains, without losing real back-to-back whispers (distinct lineIDs). Filters: Character (per-alt), Direction (Received / Sent / Both), Type (In-game / Battle.net / Both), GM (All / Hide GM / GM only), Date range, plus a Partner substring search and a Text body substring search — both case-insensitive, both apply on Enter or the green check button. Table: Time · Character · Other · Message, with auto-fit on the fixed columns and Message as the rightmost stretchy column. The Other column folds direction (<- / ->) + [BN] / [GM] tags into one cell to keep the table compact. Wiring: per-category sub-toggle in Settings > Modules > Log categories > Whisper Log; clear button at Settings > Clear Data > Clear Whisper Log (two-step confirm via AceConfig's native popup); slash command aliases /togt wl and /togt logs whispers; deep-link via Tab.pendingSubTab. Location: Modules/WhisperLog/WhisperLog.lua, GUI/WhisperLogSubTab.lua, plus surface-level wiring in TOGTools.lua (DB defaults), GUI/Settings.lua (sub-toggle + Clear button), SlashCommands.lua (slash aliases), GUI/LogsTab.lua (outer-tab help text), and every *.toc.

[v0.5.2] (2026-05-25) - AceGUI widget-pool tooltip leak

New Features

  • Guild Bank Log — Bank Tab filter — User-requested. New "Tab" dropdown in the Guild Bank Log filter row, between Type and Date range. Options: |cffffd700All tabs|r (default), |cffffd700Tab 1|r through |cffffd700Tab N|r (where N = MAX_GUILDBANK_TABS - 1, normally 7), and |cffffd700Money log|r. Numeric filters match entries whose tab1 OR tab2 equals the selected tab — Move transactions span two tabs and surface under either tab's filter. The Money log filter restricts to kind == "money" entries (gold deposit/withdraw). Engine signature gained a fifth optional parameter: GuildBankLog:GetEntries(guildKey, filterType, fromTs, toTs, filterTab). Filter state is session-scoped (GuildBankLog._filterTab) — picks reset on /reload, same as the existing Guild / Type / Date filters. Tab column header tooltip updated to point users at the dropdown; help blurb mentions the new filter. Location: GUI/GuildBankLogSubTab.lua, Modules/GuildBankLog/GuildBankLog.lua.

Bug Fixes

  • Mail Log capture under TradeSkillMaster + consecutive-mail merge collapse — Two-part fix landing in one architecture. (1) Coexistence with TSM. User reported on TBC: taking mail produced a MailFrame.lua:357 InboxFrameItem_OnEnter taint trace (SetTooltipMoney → MoneyFrame_Update on a TSM-supplied "secret number value") and the Mail Log captured nothing even though mail was successfully removed from the inbox. TSM disabled → Mail Log captured every mail; TSM enabled → nothing. Root cause (from reading TSM's Core/Service/Accounting/Mail.lua): TSM does a full global function replacement on TakeInboxItem / AutoLootMailItem / TakeInboxMoney at its Mail.OnInitialize, then runs up to five deferred 0.2-second retry passes through its own ScanCollectedMail before finally calling back to the original via private.hooks[oFunc]. By the time our hooksecurefunc callback fires through that chain, the Blizzard client-side inbox cache is already cleared and GetInboxItemLink / GetInboxHeaderInfo return nil. (2) Consecutive-mail merge collapse. Independent bug exposed by the TSM work: each Take shifts the inbox so all consecutive takes arrive at MailIndex=1, and the v0.4.0 merge-window guard only checked cached.other == sender. Five "Auction expired: X" / "Auction expired: Y" mails all have sender="Auction House" so the guard accepted the merge and collapsed five distinct mails into one row. Fix — two paths, deterministic dedupe. Hook path stays the primary capture for the 99% case (no TSM). New snapshot-diff fallback path listens to MAIL_SHOW / MAIL_INBOX_UPDATE / MAIL_CLOSED — plain events with no global-replacement vulnerability — snapshots inbox state at MAIL_SHOW, and on each subsequent update compares sender|subject bucket counts to the prior snapshot; when a bucket loses an entry the mail vanished and gets logged with its captured pre-take metadata. Snapshot events are gated on TSM detection: probes four signals (IsAddOnLoaded("TradeSkillMaster"), C_AddOns.IsAddOnLoaded, _G.TradeSkillMaster, and our saved-reference vs _G.<mail-api> to catch the globals being swapped after our hook ran) lazily on first MAIL_SHOW (PLAYER_LOGIN was too early — TSM's init hadn't finished on Classic Era). Merge predicate now requires strict equality on sender AND subject AND firstAttachmentLink AND daysLeft (fractional-days mail expiration, 6-decimal float — unique per arrival moment) AND a 5-second window — covers same-mail multi-take (all four match) while distinguishing all distinct-mail patterns including same-sender / same-subject / same-day batches. appendReceive bails early if sender is empty or subject is nil (TSM-tainted post-take cache reads) so degraded entries never enter SV. Dedupe between paths uses a reference-counted _takenSenders[sender|subject] table: appendReceive increments only when it creates a brand-new entry (multi-item-mail merges don't double-stamp); appendReceiveFromSnapshot decrements one stamp per vanishing — present = take-hook captured cleanly, skip the snapshot log; absent = take-hook couldn't read, snapshot logs it. Counter (not boolean) so batch-identical mails decrement in lockstep. Keyed by sender|subject not MailIndex because the snapshot stores the index a mail was at when snapshotted while the take-hook fires at the LIVE post-shift index — those drift apart and broke earlier mailIndex-based dedupe. AutoLootMailItem now also reads money from GetInboxHeaderInfo because that API takes both items AND money in one server call without dispatching through TakeInboxItem / TakeInboxMoney. _takenSenders wiped on MAIL_CLOSED. Take-hook + snapshot traces gated behind addon.DB.global.debug for future TSM-style breakage diagnosis. Location: Modules/MailLog/MailLog.lua.

  • Tooltip text rendered twice in Settings > Modules — User-reported on TBC: hovering a Settings panel toggle (e.g. Login Digest) showed the option's desc body text rendered twice at slightly offset positions, giving a doubled / blurry appearance. Root cause: addon.UI.AttachTooltip uses HookScript("OnEnter", ...) on the passed frame, and HookScript cannot be unhooked — once attached, the handler persists for the entire session. Several consumers (Gratz, Guild Log Clear button, Guild Bank Log Clear / dropdowns / Pick-range button) passed widget.frame from AceGUI widgets to this function. AceGUI pools widget frames across the entire UI: when AceConfigDialog later acquired the same pooled CheckBox/Dropdown/Button frame for the Settings panel's option toggles, our residual HookScript fired alongside AceConfig's own tooltip handler. Both rendered tooltip content for the same hover, producing the doubled body text the user saw. Fix: migrate every call site from addon.UI.AttachTooltip(widget.frame, ...) to addon.GUI.AttachTooltip(widget, ...) — the latter (defined in Compat.lua) uses widget:SetCallback("OnEnter", ...) which AceGUI clears on widget Release, so the handler doesn't survive into the next pool acquirer. addon.UI.AttachTooltip kept available for raw CreateFrame frames (which aren't pooled) but the docstring now loudly warns against using it on AceGUI widget frames. Files touched: GUI/GratzTab.lua, GUI/GuildBankLogSubTab.lua, GUI/UI.lua (docstring), Compat.lua (no change — already had the safe version).

Improvements

  • Guild Bank Log 5-minute background ticker removed — Was gated on GuildBankFrame:IsShown() so it only fired while the user was parked at the bank, and even then was redundant with GUILDBANKLOG_UPDATE events that already fire when transactions land while the bank is open. Blizzard's own Blizzard_GuildBankUI.lua has no equivalent periodic re-query — they rely entirely on the event stream. Confirmed factually against the Blizzard UI source mirror rather than guessing. Removing the ticker simplifies the engine and removes ~7 RPCs every 5 minutes for parked users; the bank-open event handler still triggers requestAllTabs on every visit with a 2-second deferred ingest for the TBC / Anniversary / Era flavour where GUILDBANKLOG_UPDATE doesn't fire reliably. Location: Modules/GuildBankLog/GuildBankLog.lua.

  • Every tooltip site routes through the global anchor helpers — Consolidated raw GameTooltip:SetOwner(...) fallbacks in GUI/RowList.lua (column header tooltips, row hyperlink tooltips) and removed Compat.lua's duplicate-and-stale addon.Tooltip.Owner definition. Now every tooltip in the addon goes through either addon.Tooltip.Owner(frame, [budget]) (attachment-time SetOwner) or addon.Tooltip.AnchorTo(tooltipFrame, ownerFrame, [budget]) (post-population SetPoint re-anchor), both of which share an internal _computePlacement so the flip threshold (250 px below the owner) is identical across the codebase. The earlier RowList fallbacks defaulted to fixed ANCHOR_TOPLEFT / ANCHOR_RIGHT anchors with no flip; the old Compat.lua definition used a simpler top-vs-bottom-half heuristic that diverged from UI.lua's budget-based version once UI.lua landed. Both anomalies are gone.

  • Clear Data moved from the sub-tabs into a new Settings > Clear Data tab — Each log sub-tab (Mail / Trade / Guild / Guild Bank) used to have its own "Clear data" button in the filter row, each with its own copy of the two-step "Really? (5s)" countdown helper (~30 lines of identical Lua duplicated four times). Consolidated into a single clearData group in GUI/Settings.lua's OPTIONS, exposed as a third tab in the Settings panel alongside General and Modules. Each clear button is an AceConfig type = "execute" with confirm = true + a confirmText line — that surfaces Blizzard's native StaticPopup confirm gate, which is consistent with how every other destructive Bliz UI action works and is more discoverable than an inline "Really?" countdown. Removed all four clearBtn blocks plus the four duplicate attachClearConfirm helpers from GUI/MailLogSubTab.lua, GUI/TradeLogSubTab.lua, GUI/GuildLogSubTab.lua, GUI/GuildBankLogSubTab.lua. Engine :ClearAll() methods on each module unchanged; the Settings buttons just call them via addon.logCategories[<key>]:ClearAll(). Location: GUI/Settings.lua, GUI/MailLogSubTab.lua, GUI/TradeLogSubTab.lua, GUI/GuildLogSubTab.lua, GUI/GuildBankLogSubTab.lua.

  • Global tooltip-placement helper: fixed inverted room-below check + narrow-owner label offset — User-confirmed the flip logic wasn't working. Two bugs in _computePlacement and addon.Tooltip.AnchorTo: (1) the room-below check used GetScreenHeight() - frame:GetTop(), which measures distance from the frame to the TOP of the screen — not space BELOW the frame. WoW's coordinate origin is at the BOTTOM of the screen, so the correct check is frame:GetBottom() > budget. The old check flipped the anchor exactly opposite of intent (frames near the top of the screen got the "rise up" anchor; frames near the bottom got the "drop down" anchor). (2) For narrow owners (checkbox ~22 px, icon ~20 px) the tooltip's left edge was anchored flush with the owner's left edge — landing UNDER the icon rather than next to the label text the user was hovering. New _xOffsetForLabel helper detects owners thinner than 40 px and adds width + 8 px of horizontal offset so the tooltip aligns with where the LABEL begins. Wider owners (header buttons, dropdowns) keep their existing left-aligned drop-down behaviour. Both fixes live in the global helper at GUI/UI.lua, so every caller benefits — including the Settings>Modules tooltip re-anchor hook. Location: GUI/UI.lua.

  • Settings>Modules toggle row hit-rect extension reverted — Earlier in this version I tried to make the entire toggle row (checkbox + label) hover-sensitive by walking the panel's children recursively and calling SetHitRectInsets(0, -500, -2, -2) on every frame with an OnEnter script. User reported they could no longer select the Modules tab — the General tab button's OnEnter-bearing frame got its hit rect extended 500 px rightward too, overlapping the Modules tab button's click target and stealing every click meant for it. Reverted the extension; relying on the global narrow-owner offset in addon.Tooltip.AnchorTo to place the tooltip next to the label even when only the checkbox itself triggers OnEnter. If we re-add row-hover later, the filter must exclude tab-strip buttons specifically — SetHitRectInsets on horizontally-laid-out siblings will always steal clicks from the neighbour.

  • Tooltip re-anchor scoped back to the Settings panel only — Briefly in v0.5.2 the hooksecurefunc(GameTooltip, "SetOwner", ...) was promoted to global scope (lived in GUI/UI.lua, no gate) so every tooltip in the addon routed through addon.Tooltip.AnchorTo. User reported that broke Blizzard's native tooltips — hovering a corpse / item / unit produced visibly wonky tooltip placement because the global hook was re-anchoring those too. Reverted: the SetOwner hook lives back in GUI/Settings.lua gated by a _blizPanel:OnShow/OnHide flag so it only fires while the TOG Tools settings panel is the active page. Settings>Modules tooltips still get the smart re-anchor (which was the original goal); native + other-addon tooltips are untouched again. Lesson logged: global hooksecurefunc on shared Blizzard frames affects every consumer — always gate on a frame-specific flag.

  • Scoped tooltip re-anchor for the Settings>Modules panel + global addon.Tooltip.AnchorTo helper — User reported that AceConfigDialog's tooltips for our addon's Settings options were positioned "way out there" — anchored hard-right of the option widget, often clipping near the screen edge on TBC at typical UI scales. AceConfigDialog uses GameTooltip:SetOwner(widgetFrame, "ANCHOR_TOPRIGHT") internally with no per-addon override. Two-part fix: (1) added a new global addon.Tooltip.AnchorTo(tooltipFrame, ownerFrame, [budget]) helper in GUI/UI.lua — uses ClearAllPoints + SetPoint so it can re-position a tooltip without clearing AceConfigDialog's already-populated content. Shares an internal _computePlacement function with addon.Tooltip.Owner so the flip threshold (250 px below the owner frame's top edge) is identical between attachment-time SetOwner calls and post-population SetPoint re-anchors. (2) _blizPanel:HookScript("OnShow"/"OnHide") in GUI/Settings.lua flips a ourPanelShown flag; GameTooltip:HookScript("OnShow", ...) delegates to addon.Tooltip.AnchorTo when the flag is set. Cross-addon impact is bounded: the hook is no-op except while the TOG Tools panel is the active Settings page. Location: GUI/UI.lua, GUI/Settings.lua.

  • Single canonical tooltip-attach helper — Previously two helpers existed for the same purpose: addon.UI.AttachTooltip (HookScript-based, in GUI/UI.lua) and addon.GUI.AttachTooltip (SetCallback-based, in Compat.lua). Different callers picked one or the other, with different leak characteristics. Consolidated into a single smart helper at addon.UI.AttachTooltip that auto-detects the target type: AceGUI widget (table with :SetCallback) → uses widget:SetCallback("OnEnter"/"OnLeave", ...) (AceGUI clears callbacks on Release, so the handler is properly released when the widget returns to the pool); raw frame (no :SetCallback) → uses HookScript (safe because raw CreateFrame frames aren't pool-shared, so the never-unhookable handler doesn't leak). For Dropdown / EditBox the label area is outside the interactive body — both paths fall through to addon.AceGUIFrameScripts which saves the prior script and restores it on Release so the recycled widget's next owner gets a clean frame. addon.GUI.AttachTooltip is now an alias for addon.UI.AttachTooltip at the bottom of GUI/UI.lua, so call sites using either namespace get identical leak-safe behaviour. The standalone Compat.lua definition was removed to keep a single source of truth. Location: GUI/UI.lua, Compat.lua (removed duplicate).


[v0.5.1] (2026-05-25) - Bug fix roll-up

Improvements

  • RowList: interactive hyperlinks on every row — Cell text containing |Hitem:...|h[Name]|h (or any other |H... markup — spell / achievement / quest) now hover-shows the GameTooltip with the linked subject, plain-click defers to Blizzard's HandleModifiedItemClick (which handles shift-into-chat, ctrl-dressup, etc.), and non-item links fall back to ChatEdit_InsertLink for the shift-into-chat case. Implementation: row:SetHyperlinksEnabled(true) + OnHyperlinkEnter / OnHyperlinkLeave / OnHyperlinkClick scripts in _buildRow. Guarded on SetHyperlinksEnabled presence so Vanilla (where the API doesn't exist) silently degrades to inert rendered markup as before. Applies to every RowList consumer at once — Guild Bank Log item column was the trigger for the feature (item links rendered as colored bracketed text but weren't hoverable), but Mail Log / Trade Log / Gratz also benefit. .luarc.json gained HandleModifiedItemClick, IsModifiedClick, ChatEdit_InsertLink globals. Location: GUI/RowList.lua, .luarc.json.

  • RowList: per-column autoFit measures the widest visible cell and grows the column to fit — New optional column descriptor flags autoFit = true + minWidth = N. On every SetData(), RowList walks the data (and the column header label) measuring each cell's rendered width via a cached hidden UIParent:CreateFontString per font face. Each autoFit column gets col.width = max(col.minWidth, measured + 8 px padding). After measurement, _repositionColumns rewrites every header button and every row cell's SetWidth + SetPoint in place — same two-pass logic as the existing _buildHeader (right-anchored cols right of the auto col, left-anchored cols left of it, stretchy auto col between). :New() seeds col.width = col.minWidth on autoFit cols before the first _buildHeader so the initial render has something to anchor against, then SetData's auto-fit pass grows widths to real content. Result: columns are exactly as wide as their longest visible entry, with no dead horizontal space inside fixed-width columns. The stretchy column (the one without a width) absorbs whatever space remains — making it the only column that can be visually truncated when the window is too narrow, since every other column was sized to fit content exactly. Backward compatible: columns without autoFit keep their static width (Mail / Trade / Addon Load / Gratz / Guild Bank tabs unchanged). Location: GUI/RowList.lua.

  • AutoFit applied to every RowList consumer (Mail Log / Trade Log / Guild Bank Log / Addon Load / Gratz) — User confirmed Guild Log's autoFit behaviour was working well and asked to apply globally. Each consumer's fixed-width columns were converted from width = N to autoFit = true, minWidth = M with column-appropriate floors (60-80 for name/character cols, 50-60 for type/time/money cols, 35-45 for narrow numeric cols, ~45-55 for memory/load-time cols). Stretchy columns (the one without a width — Subject on Mail, Given on Trade, Item/Amount on Guild Bank, Addon Name on Addon Load) are unchanged and remain the sole truncation target per the established convention. Gratz's checkbox column kept its fixed width = 30 (no autoFit — boxSize dictates the slot). The outdated "auto-width column always goes leftmost" comment was dropped from Gratz now that RowList supports mid-array auto columns. _repositionColumns updated to special-case checkbox cells when sibling autoFit columns trigger a column-layout pass — the box is re-centred within its slot instead of being SetWidth'd to the column width (which would stretch the CheckButton). Location: GUI/MailLogSubTab.lua, GUI/TradeLogSubTab.lua, GUI/GuildBankLogSubTab.lua, GUI/AddonLoadTab.lua, GUI/GratzTab.lua, GUI/RowList.lua.

  • Guild Log sub-tab — every column except Detail opted into autoFit; Detail is the rightmost stretchy truncatable reservoir — Wired the new RowList autoFit flag on Time / Guild / Type / Player / Rank / Actor with reasonable minWidth floors (60–80 px). Detail dropped its fixed width and became the stretchy column, deliberately positioned rightmost so it's the only column subject to truncation. User constraint: non-rightmost columns must never truncate — autoFit honours this by always growing to fit content. Trial-gated to Guild Log only; once validated will be lifted into a shared library alongside RowList / DateRangePicker / addon.UI helpers (target: TOGUI-1.0 LibStub-versioned standalone addon used across the TOG suite). Location: GUI/GuildLogSubTab.lua.

  • RowList: auto-width column can now live anywhere in the array — Previous behaviour positioned the auto-width column (the one with width = nil) at the visual leftmost regardless of its array position, because _buildHeader / _buildRow used a single right-to-left pass that anchored the auto column at LEFT_PAD + total fixed-width offset. Caller convention was "put the stretchy column at array index 1"; Guild Log's Detail column (last in array, no width) was therefore rendering leftmost — the opposite of where users expect a "details" column to live. Fix: two-pass placement in both _buildHeader and _buildRow. Pass 1 walks right-to-left from #columns down to autoColIndex + 1 and right-anchors those cols. Pass 2 walks left-to-right from 1 up to autoColIndex - 1 and left-anchors those cols. Pass 3 stretches the auto column between the captured autoColRightOffset and autoColLeftOffset. _makeHeaderColumn signature renamed autoLeftPadanchorLeft and grew a third case: LEFT-only anchored with explicit width (for the new pass 2). _buildRow factored the fixed-cell construction into a placeFixedCell closure so the checkbox + fontstring paths both branch on anchorLeft vs anchorRight. Net effect: array order now equals visual order; sub-tabs can put the stretchy column wherever they want. Backward compatible — Mail / Trade / Addon Load / Gratz / Guild Bank tabs all keep their auto column at array index 1, which still produces the same visual layout. Location: GUI/RowList.lua.

  • Guild Log Rank column populates default rank for join events — Previously blank for join events because e.rank is empty from the API. User feedback: new members ARE assigned the guild's default rank on join, so the column should reflect that. New GuildLog:GetDefaultRankFor(guildKey) method walks GetGuildRosterInfo for the current guild and returns the rank name at the highest rankIndex (= lowest privilege = the default join rank). Result cached per session, keyed by guild key, so the roster scan only happens once per guild switch. Sub-tab Rank column format gained a join branch that calls into this. Invite events stay deliberately blank — the invitee may decline the invite and never actually get a rank, so attributing the default rank to an invite would be misleading; the matching join event (if/when it lands) carries the actual outcome. Limitation: only resolves for the character's currently-active guild — Blizzard's roster API only exposes one guild at a time, so entries from an alt's guild still fall back to an empty cell. Header tooltip rewritten to document the per-type rule. Location: Modules/GuildLog/GuildLog.lua, GUI/GuildLogSubTab.lua.

  • Guild Log sub-tab — Detail moved to rightmost, Player becomes the stretchy column — Now that RowList supports an auto-width column in any array position, Guild Log's columns render in the order they appear in the array: Time | Guild | Type | Player | Rank | Actor | Detail. Detail gained an explicit width = 90 (fixed-width rightmost), and Player (target) dropped its width to become the stretchy column — natural fit since player names vary most. Location: GUI/GuildLogSubTab.lua.

  • Guild Log sub-tab — new dedicated Rank column between Player and Actor — User request: surface the player's guild rank as its own column rather than burying it inside Detail. Added { key = "rank", header = "Rank", width = 110 } immediately after Player and before Actor in GUI/GuildLogSubTab.lua. Format function applies a per-event-type rule because Blizzard's rank field carries different semantics by event: for promote / demote it's the target's NEW rank; for quit / leave / remove it's the rank the target held at the time of the action; for join it's blank (the API doesn't expose the default-rank assignment in the log entry); for invite it's the INVITER's rank (NOT the invitee's), so we suppress it in this column — showing it next to the invitee's name would mislead readers into thinking the invitee already has Guild Master rank. Cell renders the rank in brand-gold (|cffffd700) when present, blank when not (no em-dash placeholder per the existing convention). Column tooltip documents the per-type rule. fmtDetail simplified to return "" since Detail's only previous job (showing the rank for promote / demote) is now the Rank column's responsibility; Detail kept as the rightmost auto-width column, reserved for future event-specific annotations. Confirmed via the /togt gldump user provided that the rank data is already in SV for every applicable entry — no engine / capture changes needed. Location: GUI/GuildLogSubTab.lua.

  • Guild Log sub-tab — Actor column falls back to player name for self-events instead of rendering empty — Confirmed via /togt gldump that out of 96 persisted entries on the user's TBC guild, 56 (~58%) were self-events (type=join/quit, occasionally leave) where Blizzard's GetGuildEventInfo returns nil for player2 because no officer is involved. The Actor column rendered those as |cff888888—|r, producing the "wall of em-dashes" the user wanted gone. Fix: format function on the Actor column now falls back to e.target when e.actor is empty — so a quit row for Hikorakara renders Player=Hikorakara, Actor=Hikorakara (visually saying "they did this to themselves") instead of Player=Hikorakara, Actor=—. Actor-driven events (invite/remove/promote/demote) are unchanged because e.actor is already populated for those. Column structure / width / sort key unchanged. Header tooltip rewritten to explain the fallback rule. Location: GUI/GuildLogSubTab.lua.

  • Guild Log: empty-string validation + ghost-row cleanup + debug trace — TBC reproduction showed blank-everywhere rows landing in the SV. Root cause: readEvent's if not etype then return nil end guard was falsy-only, so empty-string etype (or any blank field permutation) slipped past. The (type, target, actor, rank) dedupe key collapsed to "|||" for blank rows, so multiple distinct blank events folded into one persistent ghost row that survived every re-query. Fix: tighten readEvent to reject when etype == "" OR when player1 is empty (a self-event with no player name is meaningless data), with addon:Debug traces that say why each rejection happened. Existing SV ghost rows are handled by a new one-shot cleanupBlankEntries(bucket) pass that runs once per session inside ingest, walks every bucket entry, and drops rows where the type is blank OR both actor and target are blank — marked on the bucket via _blanksCleaned = true so it doesn't repeat in the same session. The full ingest path is now traced through addon:Debug (gated by db.global.debug): skip-reasons, n from GetNumGuildEvents, bucket-before count, per-iteration new / deduped / readNil tallies, bucket-after count. /togt glcheck grew a "Blank/ghost entries in SV" audit that counts the existing blank rows and prints up to 3 samples so the user can confirm before letting cleanupBlankEntries drop them on the next ingest. Location: Modules/GuildLog/GuildLog.lua.

  • /togt gbcheck diagnostic command + verbose debug trace through the Guild Bank Log capture path — Mirrors the existing /togt glcheck shape for Guild Log. /togt gbcheck prints API availability (QueryGuildBankLog, GetNumGuildBankTransactions, GetGuildBankTransaction, GetNumGuildBankMoneyTransactions, GetGuildBankMoneyTransaction, GuildBankFrame), in-guild status, GuildBankFrame:IsShown(), persisted entry count for the current guild, then — if the bank frame is shown — forces a per-tab QueryGuildBankLog(1..7) + QueryGuildBankLog(8) and 2 s later dumps the first row of each tab's buffer plus the money log, then runs ingestTab for every tab and reports the final persisted count. Empty-bank-frame and empty-API cases each get a specific failure-mode hint instead of a silent return. Engine paths gained addon:Debug instrumentation gated on db.global.debug (toggle via /togt settings > Verbose debug output): GUILDBANKFRAME_OPENED logs the scheduled requestAllTabs; requestAllTabs logs its enabled / API-present / in-guild gates and the tab range it queries; GUILDBANKLOG_UPDATE logs the arg1 it received; ingestTab logs per-tab n, bucket-before, new / deduped / readNil counts, bucket-after; readItemTxn / readMoneyTxn log when Blizzard returns a nil etype. Reason: empty Guild Bank Log on TBC reproduced by user — adding both the on-demand chat-dump diagnostic and the always-available debug trace so the failure mode (event-not-firing vs. buffer-empty vs. ingest-rejecting) can be pinned down on the next test pass without a code change. Location: Modules/GuildBankLog/GuildBankLog.lua, SlashCommands.lua.

Bug Fixes

  • Guild Bank Log — move-row Tab cell rendered an unsupported glyph instead of an arrow on TBC / AnniversaryfmtTab formatted move-type transactions as "%d → %d" using U+2192 RIGHTWARDS ARROW; WoW's default chat font on TBC and Anniversary clients doesn't ship that code point, so the cell showed a missing-glyph box between the two tab numbers. Swapped to ASCII > so the cell always renders. Same swap propagated to the Tab column header tooltip text. Location: GUI/GuildBankLogSubTab.lua.

  • Guild Bank Log — bank-open ingest never ran on TBC / Anniversary because GUILDBANKLOG_UPDATE doesn't reliably fire there — Follow-up after the previous bank-open fix still didn't repopulate. User confirmed /togt gbcheck populated INSTANTLY when run manually post-open — gbcheck fires QueryGuildBankLog per tab and then ingests 2 s later. That timing difference was the diagnostic: the on-open flow was firing queries and the server WAS responding and populating the per-tab buffers, but GUILDBANKLOG_UPDATE (the event our ingest waits for) wasn't firing on TBC / Anniversary — identical failure mode to GUILD_EVENT_LOG_UPDATE getting silenced on Retail Midnight (legacy notification event dropped while the legacy read API was kept). Fix: requestAllTabs now schedules an explicit C_Timer.After(2, ingest all tabs) directly after firing the queries, exactly like requestQuery does in Modules/GuildLog/GuildLog.lua. The GUILDBANKLOG_UPDATE handler stays registered (it does fire on some flavours) and ingestTab is idempotent via the dedupe map, so the double-fire is harmless. Net effect: open bank → 1 s deferral for Blizzard's UI queries to land → requestAllTabs fires → 2 s deferred ingest reads the now-populated buffers → table populates. No more 5-minute-ticker wait. Location: Modules/GuildBankLog/GuildBankLog.lua.

  • Guild Bank Log — Clear button silently re-populated when a deferred ingest was in flight — User reported "Clear data button isn't clearing" after the previous bank-open ingest fix. Root cause: the new requestAllTabs deferred-ingest fallback was a fire-and-forget C_Timer.After(2, ...) with no cancellation handle. Timeline that triggered the bug: user opens bank → requestAllTabs fires queries + schedules ingest in 2 s → user clicks Clear (1st + 2nd confirm) within those 2 s → SV wiped → deferred ingest fires moments later → reads Blizzard's per-tab buffers (still populated client-side) → re-fills the SV → UI shows the data back. Fix: switch the deferred ingest to C_Timer.NewTimer and track the handle in a module-scope _deferredIngestTimer. New cancelDeferredIngest() helper kills any pending pass. requestAllTabs cancels any prior pending pass before scheduling a new one (so rapid consecutive calls don't queue overlapping ingests). ClearAll calls cancelDeferredIngest() before wiping the SV, so the queued ingest can't revert the clear. Same protection added implicitly to /togt gbcheck since it goes through requestAllTabs. Location: Modules/GuildBankLog/GuildBankLog.lua.

  • Guild Bank Log — first-open ingest delayed by 8 server round-trips even when buffers were already cached client-sideGUILDBANKFRAME_OPENED deferred all ingest by 1 second + 8 sequential server round-trips for the 7 item tabs and the money log to refresh, so users saw blank tables for several seconds even when data was already cached client-side from a previous bank visit in the same session. Fix: GUILDBANKFRAME_OPENED now runs an immediate local-buffer ingest BEFORE scheduling the 1-second-deferred fresh requestAllTabs. ingestTab is idempotent via the dedupe map, so the post-response re-ingest is harmless. Net effect: opening the bank with cached data → instant table; bank with no cached data → falls back to the server-query-and-wait flow. Note: ClearAll deliberately does NOT re-ingest — that would defeat the user's clear intent. To get fresh data after Clear, close + reopen the bank or run /togt gbcheck. Location: Modules/GuildBankLog/GuildBankLog.lua.

  • Guild Bank Log — Player column blank for many transactions, including newest — User report on TBC: lots of rows showed in the Player column despite being recent deposits / withdrawals that obviously had a player attached. /togt gbdump revealed the actual problem: SV had total=239 entries when the bank only had ~120 real transactions, and the per-type breakdown showed roughly half of every type's entries missing a name (deposit 128/74-hasName, withdraw 61/36, move 35/19). The dump's per-entry list made the duplication pattern obvious — same transaction appearing TWICE with identical type / itemSig / count / tab1 / tab2 / amount / ymdh, once with name populated and once with name=''. Root cause: Blizzard's GetGuildBankTransaction returns the SAME transaction on consecutive QueryGuildBankLog calls with inconsistent name field — populated on one query, nil on the next (likely a server-side race between the bank-log buffer and the player-name resolver on TBC / classic-flavour servers). Our dedupeKey included name, so the two payloads hashed to different keys and BOTH landed in the SV. Fix: (1) drop name from dedupeKey and rowId — Blizzard returns it unstably so it can't be part of identity; the remaining tuple (type, itemSig, count, tab1, tab2, amount) uniquely identifies the transaction. (2) Switch ingestTab's seen set to a dedupeKey → existing-entry MAP so we can detect duplicates AND backfill name and itemLink onto an existing entry when a later query gives populated values where the earlier gave nil. (3) New cleanupDuplicateEntries(bucket, guildKey) one-shot pass runs on first ingestTab per session per bucket, rehashes existing SV entries under the new name-less key, collapses matches keeping the populated-name copy, and regenerates id fields from the new (name-less) formula. Marker bucket._dupesCleaned = true persists in SV so subsequent sessions don't redo the work. Trace gained a backfilled count in the ingest debug print. Same dedupe-collapse caveat as before: two truly-distinct transactions in the same hour with identical (type, item, count, tabs, amount) collapse to one. Location: Modules/GuildBankLog/GuildBankLog.lua.

  • Main window's gear/help icons tainted other addons that use AceGUI Frame — Cleanup of the bottom-row icon strip was wired to the AceGUI Frame's OnClose callback, but OnClose only fires when the user clicks the AceGUI X-button. Every other close path — ESC via _escProxy's OnHide, /togt toggle, minimap LMB toggle, MainWindow:Rebuild collapsing the strip empty, and even the just-added Settings-flag suppression refactor — calls MainWindow:Close() directly, which calls AceGUI:Release(self.frame). Confirmed by reading Ace3/AceGUI-3.0/AceGUI-3.0.lua:172-198: AceGUI:Release fires "OnRelease" (line 177), wipes widget.events (line 188-190), reparents the frame back to UIParent, and pushes it into the shared widget pool — but it never fires "OnClose". Net effect: every close path except the X-button left our _gearIcon and _helpIcon still parented to the released f.frame. Next addon to call AceGUI:Create("Frame") (FGI's main window, ChatCopyPaste, any AceConfigDialog Bliz-options group, etc.) got our frame back from the pool with our gear icon's OnClick = function() addon:OpenSettings() end still attached and visible. User-visible symptom: clicking what looked like FGI's gear opened TOG Tools' settings page, because the click was actually landing on our orphaned gear that AceGUI had reparented into FGI's frame. Fix: move the addon.UI.DetachChildren(self, { "_helpIcon", "_gearIcon" }) call out of the f:SetCallback("OnClose", ...) handler and into MainWindow:Close() itself, immediately before AceGUI:Release(self.frame). The X-button path now delegates: f:SetCallback("OnClose", function() MainWindow:Close() end) so all five close paths converge on the same cleanup. Re-checking the proxy lifecycle: _escProxy:Hide() was already in MainWindow:Close() and the _settingsOpen flag suppresses the recursive OnHide so the consolidation is safe. Comment added at MainWindow:Close() documenting WHY the cleanup must live there and not in OnClose, so future contributors don't move it back. Location: GUI/MainWindow.lua.


[v0.5.0] (2026-05-24) - Gratz Module

Bug Fixes (pre-release)

  • Gratz skipped cross-realm guildmate achievementsCHAT_MSG_GUILD_ACHIEVEMENT for a cross-realm guildmate (arg2 = "Xiomaara-MoonGuard") was correctly identified as a non-self event, but the guild-scope filter then rejected it with skip: sender 'Xiomaara' not found in guild roster. Root cause: fireAchievement's defensive roster-membership check walks GetGuildRosterInfo for every entry and compares shortName(memberName) == short. Roster representation for cross-realm members varies (some flavours return "Name", some "Name-Realm", recently invited entries can be briefly absent during async load), so the local lookup is fragile. The check was redundant in the first place: CHAT_MSG_GUILD_ACHIEVEMENT is server-gated by Blizzard to deliver only to guild members about guild members, so trust the channel. Fix: thread a fromGuildChannel flag into fireAchievement (true when the source event is CHAT_MSG_GUILD_ACHIEVEMENT, false for CHAT_MSG_ACHIEVEMENT), and skip the roster-lookup branch when the flag is true. CHAT_MSG_ACHIEVEMENT still runs the lookup because that event fires for nearby non-guildies on Retail and genuinely needs the filter. Same-realm guildies (Spedo-Stormrage) were unaffected because their roster representation happened to match shortName-stripped. Location: Modules/Gratz/Gratz.lua.

  • Main window closed when the user dismissed the Settings panel — Opening Settings (via the gear icon, /togt settings, or right-clicking the minimap button) correctly left the main window open, but closing Settings via ESC or its X button caused the main window to close alongside it. Root cause: _escProxy is registered in UISpecialFrames so ESC closes the main window — and Blizzard's ESC handler iterates UISpecialFrames via CloseSpecialWindows, hiding every visible entry in the list. While Settings was up, our proxy was Shown (re-armed on the next frame after the gear click). When the user pressed ESC to dismiss Settings, the cascade hid both the Settings panel AND our proxy, and the proxy's OnHide handler closed the main window. The X button on the Settings panel hits the same path because Blizzard's Settings close logic also calls CloseSpecialWindows internally. Fix: introduce MainWindow._settingsOpen inhibit flag. addon:OpenSettings sets the flag to true before calling Settings.OpenToCategory so the synchronous CloseSpecialWindows cascade triggered by the open call sees it set (the gear click previously needed an ad-hoc _escProxy:SetScript("OnHide", nil) swap for the open-time cascade; with the flag in place that dance is no longer needed and was removed). New MainWindow:_HookSettingsClose installs a one-shot HookScript("OnHide") on the Settings panel (Retail SettingsPanel, falling back to older Classic InterfaceOptionsFrame) the first time OpenSettings runs; the hook defers _settingsOpen = false and _escProxy:Show() by one frame via C_Timer.After(0, ...) so any same-frame UISpecialFrames cascade is still suppressed by the still-set flag. Result: gear click leaves main window open (as before), ESC or X on Settings closes only Settings, ESC after Settings closes the main window normally. Location: GUI/MainWindow.lua, GUI/Settings.lua.

New Features

  • Guild Bank Log (phase 4) — Fourth Logs sub-tab snapshotting Blizzard's in-game guild bank transaction log every time the bank frame opens, and persisting entries beyond Blizzard's small rolling buffer. Per-guild buckets keyed by GuildName-PlayerRealm (same partition scheme as Guild Log) so an account with alts in multiple guilds keeps independent histories. Engine recipe: GUILDBANKFRAME_OPENED schedules a C_Timer.After(1, requestAllTabs) (1-s delay so Blizzard's own queries land first and our reads aren't racing partial buffers); requestAllTabs issues QueryGuildBankLog(tab) for each of the 7 item tabs and the money log (tab 8); GUILDBANKLOG_UPDATE fires per-tab and triggers ingestTab(tab) which walks GetNumGuildBankTransactions(tab) + GetGuildBankTransaction(tab, i) (or the GetGuildBankMoneyTransactions pair for the money log), dedupes against persisted entries, and appends new ones. Safety re-query ticker every 5 min that only fires when GuildBankFrame:IsShown() (covers users parked at the bank watching new guildmate transactions). Entry shape: { kind, type, name, itemLink, itemSig, count, tab1, tab2, y, m, d, h, ts, amount, id }kind = "item" / "money", type is Blizzard's transaction type (deposit / withdraw / move / repair / withdrawForTab / depositSummary / buyTab), tab2 populated for move (destination tab), amount populated for money entries. Same ymdh-is-relative caveat as Guild Log: those fields are "X ago" offsets, not absolute calendar values, so they're stored verbatim and a synthesised ts is derived via now - y*365*86400 - m*30*86400 - d*86400 - h*3600. Sync identity — each entry carries a stable cross-viewer id field computed at ingest as "gblog|<guildKey>|<type>|<name>|<itemSig>|<count>|<tab1>|<tab2>|<amount>" with NO timestamp component (the ymdh tuple drifts every query). itemSig is the bare itemID parsed from |Hitem:N: to keep dedupe stable across player-context payloads (crafter names, suffix rolls). Two viewers ingesting the same transaction compute the same id so future cross-addon sync with TOGBankClassic / cross-account merge can dedupe deterministically. The "gblog|" prefix distinguishes from sibling log types ("glog|", "mlog|", "tlog|"). Public InjectEntry(guildKey, entry) API exposed for future TOGBankClassic integration to feed Vanilla bank snapshots directly into the same SV bucket (Classic Era predates guild banks; the engine no-ops gracefully when the bank API is absent and the sub-tab stays present as a target for injected entries). Sub-tab UI: filter row (Guild / Type / Date range / Clear) over a 7-column RowList — Time · Guild · Type · Player · Item/Amount · Qty · Tab — with type-coloured Type cells (green = deposit, yellow = withdraw, blue = move, orange = repair, purple = tab purchase), GetCoinTextureString rendering for money entries, and a from → to Tab-cell format for moves. Empty-state copy adapts: "open your guild bank" message on TBC+, "no guild banks on this version" message on Vanilla with mention of the TOGBankClassic injection path. Permission-gated like Guild Log — players without View Tab on a tab see nothing for that tab, money log requires Withdraw Gold privilege. Sub-tab Clear button uses addon.UI.AttachTooltip for hover help and the standard two-step confirm. Settings > Modules > Log categories grows a |cffffd700Guild Bank Log|r toggle at order 40; inline group still greys out when the Logs master toggle is off. Slash deep-links: /togt logs guildbank (aliases: /togt logs bank, /togt logs gbank) and the shorthand /togt gb. SV partition: db.global.guildBankLog.guilds[GuildKey].entries. .luarc.json gains globals for QueryGuildBankLog, GetGuildBankTransaction, GetGuildBankMoneyTransaction, GetNumGuildBankTransactions, GetNumGuildBankMoneyTransactions, GuildBankFrame, GetCoinTextureString, MAX_GUILDBANK_TABS. Location: Modules/GuildBankLog/GuildBankLog.lua, GUI/GuildBankLogSubTab.lua, TOGTools.lua (DB defaults), GUI/Settings.lua, SlashCommands.lua, GUI/LogsTab.lua (help blurb), .luarc.json, all six TOGTools*.toc.

  • RowList gains interactive checkbox columns — ported from FGI's RowList. Column descriptors now accept checkbox = true + optional boxSize (default 16) + onToggle(entry, val, index, rl) to render a real UICheckButtonTemplate cell instead of a text fontstring. Click toggles the row's entry[col.key] boolean, then fires onToggle. Cell render path detects col.checkbox and calls SetChecked instead of SetText. Used by the Gratz tab's "On" column so users can enable/disable profiles directly from the list without round-tripping through the form. Location: GUI/RowList.lua.

  • Gratz module — auto-congratulations on triggered events — New top-level tab + engine for sending congratulatory chat messages when something gratz-worthy happens. Profile-based: each profile names a trigger type, its criteria, the destination channels, a templated message, and a cooldown. Profiles persist under db.global.gratz.profiles[name] and are managed FGI-Filters-tab-style — top form with Name / Trigger / criteria / Channels / Message / Cooldown / Save, RowList below showing every profile with On / Name / Trigger / Channels / Count / delete. Click a row to load it into the form; Save with an existing name updates in place. Phase-1 trigger types: |cffffd700achievement|r (Retail — hooks CHAT_MSG_ACHIEVEMENT for guild + visible achievements, ACHIEVEMENT_EARNED for the player's own IDs to cross-reference points / category / link via GetAchievementInfo); |cffffd700level-up (party)|r (UNIT_LEVEL + GROUP_ROSTER_UPDATE bootstrap); |cffffd700level-up (guild)|r (GUILD_ROSTER_UPDATE event + 60-second polling ticker with a level-diff cache). The achievement-trigger profile sits dormant on Classic Era / TBC (no achievement system); level-up triggers work on every flavour. Template placeholders: [player], [achievement], [link], [points], [category], [level]. Per-(profile, target, eventKey) cooldown (default 300 s; eventKey = achievement ID for achievement triggers, "lvl:N" for level-ups) prevents spam from duplicate events around loading screens. Skip-self is hardcoded — the engine never congratulates the player on their own events. Combat-safe send: messages queue and flush on PLAYER_REGEN_ENABLED. Level-up tracking pattern adapted from AutoGrats (MIT, Copyright 2024 elromanov) with attribution in the file header; achievement triggering is original. Picked up automatically by the per-module toggle framework via Tab.configKey = "gratz" — Settings > Modules > Gratz toggles the entire module on/off. New slash commands: /togt gratz and /togt gz. Location: Modules/Gratz/Gratz.lua, GUI/GratzTab.lua, TOGTools.lua (DB defaults), SlashCommands.lua, all six TOGTools*.toc.


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