File Details
TOGTools-v0.5.4
- R
- May 29, 2026
- 2.07 MB
- 18
- 12.0.7+9
- Retail + 3
File Name
TOGTools-TOGTools-v0.5.4.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.5
- 1.15.8
Changelog
[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]|hhyperlink — 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 throughSetItemRef(link, text, button, frame)because it has version-specific dispatch baked in: on Mainline theLinkUtil.RegisterLinkHandler(LinkTypes.Player, HandlePlayerLink)path runsFriendsFrame_ShowDropdown; on Classic Era the sameSetItemRefentry 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:|hwith 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:|hlinks gets the canonical chat-frame behavior automatically.OnHyperlinkClickcaptures(self, link, text, button)instead of justlinkand splits on button: right-click extracts the name vialink:match("player:([^:]+)")and callsFriendsFrame_ShowDropdown(name, 1, nil, nil, row)DIRECTLY rather than routing throughSetItemRef. This is the pattern Blizzard's ownBlizzard_Communities/CommunitiesInvitationFrame.lua:103-109uses, becauseSetItemRef'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_ShowDropdownlands atUnitPopup_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 onSetItemRef(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 toFULLSCREEN_DIALOGstrata with frame level 100 (Ace3/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua:81-82); Blizzard's UnitPopup context menu also defaults toFULLSCREEN_DIALOG(Blizzard_Menu/Menu.lua:2084— only escalating toTOOLTIPwhen 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 viaMenu.GetManager():GetOpenMenu():SetFrameStrata("TOOLTIP")didn't take — likely either timing or the proxy's__indexforwarding doesn't propagateSetFrameStratato the rendered frame. A third version added an ugly window-strata-lowering fallback withC_Timerpoll-and-restore which worked but was tangled. Final solution is a one-liner inGUI/MainWindow.luaright afterAceGUI:Create("Frame"):f.frame:SetFrameStrata("DIALOG"). The window drops one strata below the menu, so the menu'sFULLSCREEN_DIALOGnaturally 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 existingHandleModifiedItemClick/ChatEdit_InsertLinkchain unchanged.OnHyperlinkEnterearly-returns for player links — noGameTooltip:SetHyperlinkrepresentation, and the chat frame itself doesn't pop a tooltip on hover for them..luarc.jsongainedSetItemRef+FriendsFrame_ShowDropdownto 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
whisperssub-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 underdb.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 atBlizzard_APIDocumentationGenerated/ChatInfoDocumentation.luaand are stable across every supported version (Classic Era through Retail).isGMuses thespecialFlags == "GM"test that Blizzard's ownBlizzard_GMChatUI.luauses for the same purpose. Dedup: a 5-second recent-lineIDset 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 inSettings > Modules > Log categories > Whisper Log; clear button atSettings > Clear Data > Clear Whisper Log(two-step confirm via AceConfig's native popup); slash command aliases/togt wland/togt logs whispers; deep-link viaTab.pendingSubTab. Location:Modules/WhisperLog/WhisperLog.lua,GUI/WhisperLogSubTab.lua, plus surface-level wiring inTOGTools.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 whosetab1ORtab2equals the selected tab — Move transactions span two tabs and surface under either tab's filter. The Money log filter restricts tokind == "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_OnEntertaint trace (SetTooltipMoney → MoneyFrame_Updateon 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'sCore/Service/Accounting/Mail.lua): TSM does a full global function replacement onTakeInboxItem/AutoLootMailItem/TakeInboxMoneyat itsMail.OnInitialize, then runs up to five deferred 0.2-second retry passes through its ownScanCollectedMailbefore finally calling back to the original viaprivate.hooks[oFunc]. By the time ourhooksecurefunccallback fires through that chain, the Blizzard client-side inbox cache is already cleared andGetInboxItemLink/GetInboxHeaderInforeturn nil. (2) Consecutive-mail merge collapse. Independent bug exposed by the TSM work: each Take shifts the inbox so all consecutive takes arrive atMailIndex=1, and the v0.4.0 merge-window guard only checkedcached.other == sender. Five "Auction expired: X" / "Auction expired: Y" mails all havesender="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 toMAIL_SHOW/MAIL_INBOX_UPDATE/MAIL_CLOSED— plain events with no global-replacement vulnerability — snapshots inbox state at MAIL_SHOW, and on each subsequent update comparessender|subjectbucket 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 onsenderANDsubjectANDfirstAttachmentLinkANDdaysLeft(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.appendReceivebails early ifsenderis empty orsubjectis nil (TSM-tainted post-take cache reads) so degraded entries never enter SV. Dedupe between paths uses a reference-counted_takenSenders[sender|subject]table:appendReceiveincrements only when it creates a brand-new entry (multi-item-mail merges don't double-stamp);appendReceiveFromSnapshotdecrements 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 bysender|subjectnotMailIndexbecause 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.AutoLootMailItemnow also readsmoneyfromGetInboxHeaderInfobecause that API takes both items AND money in one server call without dispatching throughTakeInboxItem/TakeInboxMoney._takenSenderswiped onMAIL_CLOSED. Take-hook + snapshot traces gated behindaddon.DB.global.debugfor 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
descbody text rendered twice at slightly offset positions, giving a doubled / blurry appearance. Root cause:addon.UI.AttachTooltipusesHookScript("OnEnter", ...)on the passed frame, andHookScriptcannot 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) passedwidget.framefrom AceGUI widgets to this function. AceGUI pools widget frames across the entire UI: whenAceConfigDialoglater 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 fromaddon.UI.AttachTooltip(widget.frame, ...)toaddon.GUI.AttachTooltip(widget, ...)— the latter (defined inCompat.lua) useswidget:SetCallback("OnEnter", ...)which AceGUI clears on widget Release, so the handler doesn't survive into the next pool acquirer.addon.UI.AttachTooltipkept available for rawCreateFrameframes (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 withGUILDBANKLOG_UPDATEevents that already fire when transactions land while the bank is open. Blizzard's ownBlizzard_GuildBankUI.luahas 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 triggersrequestAllTabson every visit with a 2-second deferred ingest for the TBC / Anniversary / Era flavour whereGUILDBANKLOG_UPDATEdoesn't fire reliably. Location:Modules/GuildBankLog/GuildBankLog.lua.Every tooltip site routes through the global anchor helpers — Consolidated raw
GameTooltip:SetOwner(...)fallbacks inGUI/RowList.lua(column header tooltips, row hyperlink tooltips) and removed Compat.lua's duplicate-and-staleaddon.Tooltip.Ownerdefinition. Now every tooltip in the addon goes through eitheraddon.Tooltip.Owner(frame, [budget])(attachment-timeSetOwner) oraddon.Tooltip.AnchorTo(tooltipFrame, ownerFrame, [budget])(post-populationSetPointre-anchor), both of which share an internal_computePlacementso the flip threshold (250 px below the owner) is identical across the codebase. The earlier RowList fallbacks defaulted to fixedANCHOR_TOPLEFT/ANCHOR_RIGHTanchors 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
clearDatagroup inGUI/Settings.lua's OPTIONS, exposed as a third tab in the Settings panel alongside General and Modules. Each clear button is an AceConfigtype = "execute"withconfirm = true+ aconfirmTextline — 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 fourclearBtnblocks plus the four duplicateattachClearConfirmhelpers fromGUI/MailLogSubTab.lua,GUI/TradeLogSubTab.lua,GUI/GuildLogSubTab.lua,GUI/GuildBankLogSubTab.lua. Engine:ClearAll()methods on each module unchanged; the Settings buttons just call them viaaddon.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
_computePlacementandaddon.Tooltip.AnchorTo: (1) the room-below check usedGetScreenHeight() - 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 isframe: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_xOffsetForLabelhelper detects owners thinner than 40 px and addswidth + 8 pxof 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 atGUI/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 anOnEnterscript. User reported they could no longer select the Modules tab — the General tab button'sOnEnter-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 inaddon.Tooltip.AnchorToto place the tooltip next to the label even when only the checkbox itself triggersOnEnter. If we re-add row-hover later, the filter must exclude tab-strip buttons specifically —SetHitRectInsetson 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 inGUI/UI.lua, no gate) so every tooltip in the addon routed throughaddon.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 inGUI/Settings.luagated by a_blizPanel:OnShow/OnHideflag 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.AnchorTohelper — 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 usesGameTooltip:SetOwner(widgetFrame, "ANCHOR_TOPRIGHT")internally with no per-addon override. Two-part fix: (1) added a new globaladdon.Tooltip.AnchorTo(tooltipFrame, ownerFrame, [budget])helper inGUI/UI.lua— usesClearAllPoints+SetPointso it can re-position a tooltip without clearing AceConfigDialog's already-populated content. Shares an internal_computePlacementfunction withaddon.Tooltip.Ownerso the flip threshold (250 px below the owner frame's top edge) is identical between attachment-timeSetOwnercalls and post-populationSetPointre-anchors. (2)_blizPanel:HookScript("OnShow"/"OnHide")inGUI/Settings.luaflips aourPanelShownflag;GameTooltip:HookScript("OnShow", ...)delegates toaddon.Tooltip.AnchorTowhen 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, inGUI/UI.lua) andaddon.GUI.AttachTooltip(SetCallback-based, inCompat.lua). Different callers picked one or the other, with different leak characteristics. Consolidated into a single smart helper ataddon.UI.AttachTooltipthat auto-detects the target type: AceGUI widget (table with:SetCallback) → useswidget: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) → usesHookScript(safe because rawCreateFrameframes 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 toaddon.AceGUIFrameScriptswhich saves the prior script and restores it on Release so the recycled widget's next owner gets a clean frame.addon.GUI.AttachTooltipis now an alias foraddon.UI.AttachTooltipat the bottom ofGUI/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 theGameTooltipwith the linked subject, plain-click defers to Blizzard'sHandleModifiedItemClick(which handles shift-into-chat, ctrl-dressup, etc.), and non-item links fall back toChatEdit_InsertLinkfor the shift-into-chat case. Implementation:row:SetHyperlinksEnabled(true)+OnHyperlinkEnter/OnHyperlinkLeave/OnHyperlinkClickscripts in_buildRow. Guarded onSetHyperlinksEnabledpresence 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.jsongainedHandleModifiedItemClick,IsModifiedClick,ChatEdit_InsertLinkglobals. Location:GUI/RowList.lua,.luarc.json.RowList: per-column
autoFitmeasures the widest visible cell and grows the column to fit — New optional column descriptor flagsautoFit = true+minWidth = N. On everySetData(), RowList walks the data (and the column header label) measuring each cell's rendered width via a cached hiddenUIParent:CreateFontStringper font face. Each autoFit column getscol.width = max(col.minWidth, measured + 8 px padding). After measurement,_repositionColumnsrewrites every header button and every row cell'sSetWidth+SetPointin 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()seedscol.width = col.minWidthon autoFit cols before the first_buildHeaderso 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 withoutautoFitkeep their staticwidth(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 = NtoautoFit = true, minWidth = Mwith 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 fixedwidth = 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._repositionColumnsupdated 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
autoFitflag on Time / Guild / Type / Player / Rank / Actor with reasonableminWidthfloors (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.0LibStub-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/_buildRowused a single right-to-left pass that anchored the auto column atLEFT_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_buildHeaderand_buildRow. Pass 1 walks right-to-left from#columnsdown toautoColIndex + 1and right-anchors those cols. Pass 2 walks left-to-right from1up toautoColIndex - 1and left-anchors those cols. Pass 3 stretches the auto column between the capturedautoColRightOffsetandautoColLeftOffset._makeHeaderColumnsignature renamedautoLeftPad→anchorLeftand grew a third case: LEFT-only anchored with explicit width (for the new pass 2)._buildRowfactored the fixed-cell construction into aplaceFixedCellclosure so the checkbox + fontstring paths both branch onanchorLeftvsanchorRight. 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.rankis empty from the API. User feedback: new members ARE assigned the guild's default rank on join, so the column should reflect that. NewGuildLog:GetDefaultRankFor(guildKey)method walksGetGuildRosterInfofor the current guild and returns the rank name at the highestrankIndex(= 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 inGUI/GuildLogSubTab.lua. Format function applies a per-event-type rule because Blizzard'srankfield 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.fmtDetailsimplified 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 gldumpuser 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 gldumpthat out of 96 persisted entries on the user's TBC guild, 56 (~58%) were self-events (type=join/quit, occasionallyleave) where Blizzard'sGetGuildEventInforeturns 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 toe.targetwhene.actoris empty — so aquitrow 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 becausee.actoris 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'sif not etype then return nil endguard was falsy-only, so empty-stringetype(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: tightenreadEventto reject whenetype == ""OR whenplayer1is empty (a self-event with no player name is meaningless data), withaddon:Debugtraces that say why each rejection happened. Existing SV ghost rows are handled by a new one-shotcleanupBlankEntries(bucket)pass that runs once per session insideingest, 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 = trueso it doesn't repeat in the same session. The full ingest path is now traced throughaddon:Debug(gated bydb.global.debug): skip-reasons,nfromGetNumGuildEvents, bucket-before count, per-iteration new / deduped / readNil tallies, bucket-after count./togt glcheckgrew 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 gbcheckdiagnostic command + verbose debug trace through the Guild Bank Log capture path — Mirrors the existing/togt glcheckshape for Guild Log./togt gbcheckprints 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-tabQueryGuildBankLog(1..7) + QueryGuildBankLog(8)and 2 s later dumps the first row of each tab's buffer plus the money log, then runsingestTabfor 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 gainedaddon:Debuginstrumentation gated ondb.global.debug(toggle via/togt settings> Verbose debug output):GUILDBANKFRAME_OPENEDlogs the scheduledrequestAllTabs;requestAllTabslogs its enabled / API-present / in-guild gates and the tab range it queries;GUILDBANKLOG_UPDATElogs the arg1 it received;ingestTablogs per-tabn, bucket-before, new / deduped / readNil counts, bucket-after;readItemTxn/readMoneyTxnlog when Blizzard returns a niletype. 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 / Anniversary —
fmtTabformatted 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_UPDATEdoesn't reliably fire there — Follow-up after the previous bank-open fix still didn't repopulate. User confirmed/togt gbcheckpopulated INSTANTLY when run manually post-open — gbcheck firesQueryGuildBankLogper 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, butGUILDBANKLOG_UPDATE(the event our ingest waits for) wasn't firing on TBC / Anniversary — identical failure mode toGUILD_EVENT_LOG_UPDATEgetting silenced on Retail Midnight (legacy notification event dropped while the legacy read API was kept). Fix:requestAllTabsnow schedules an explicitC_Timer.After(2, ingest all tabs)directly after firing the queries, exactly likerequestQuerydoes inModules/GuildLog/GuildLog.lua. TheGUILDBANKLOG_UPDATEhandler stays registered (it does fire on some flavours) andingestTabis 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 →requestAllTabsfires → 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
requestAllTabsdeferred-ingest fallback was a fire-and-forgetC_Timer.After(2, ...)with no cancellation handle. Timeline that triggered the bug: user opens bank →requestAllTabsfires 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 toC_Timer.NewTimerand track the handle in a module-scope_deferredIngestTimer. NewcancelDeferredIngest()helper kills any pending pass.requestAllTabscancels any prior pending pass before scheduling a new one (so rapid consecutive calls don't queue overlapping ingests).ClearAllcallscancelDeferredIngest()before wiping the SV, so the queued ingest can't revert the clear. Same protection added implicitly to/togt gbchecksince it goes throughrequestAllTabs. Location:Modules/GuildBankLog/GuildBankLog.lua.Guild Bank Log — first-open ingest delayed by 8 server round-trips even when buffers were already cached client-side —
GUILDBANKFRAME_OPENEDdeferred 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_OPENEDnow runs an immediate local-buffer ingest BEFORE scheduling the 1-second-deferred freshrequestAllTabs.ingestTabis 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:ClearAlldeliberately 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 gbdumprevealed the actual problem: SV hadtotal=239entries 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'sGetGuildBankTransactionreturns the SAME transaction on consecutiveQueryGuildBankLogcalls with inconsistentnamefield — 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). OurdedupeKeyincludedname, so the two payloads hashed to different keys and BOTH landed in the SV. Fix: (1) dropnamefromdedupeKeyandrowId— 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) SwitchingestTab'sseenset to a dedupeKey → existing-entry MAP so we can detect duplicates AND backfillnameanditemLinkonto an existing entry when a later query gives populated values where the earlier gave nil. (3) NewcleanupDuplicateEntries(bucket, guildKey)one-shot pass runs on firstingestTabper session per bucket, rehashes existing SV entries under the new name-less key, collapses matches keeping the populated-name copy, and regeneratesidfields from the new (name-less) formula. Markerbucket._dupesCleaned = truepersists in SV so subsequent sessions don't redo the work. Trace gained abackfilledcount 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
OnClosecallback, butOnCloseonly fires when the user clicks the AceGUI X-button. Every other close path — ESC via_escProxy'sOnHide,/togttoggle, minimap LMB toggle,MainWindow:Rebuildcollapsing the strip empty, and even the just-added Settings-flag suppression refactor — callsMainWindow:Close()directly, which callsAceGUI:Release(self.frame). Confirmed by readingAce3/AceGUI-3.0/AceGUI-3.0.lua:172-198:AceGUI:Releasefires"OnRelease"(line 177), wipeswidget.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_gearIconand_helpIconstill parented to the releasedf.frame. Next addon to callAceGUI:Create("Frame")(FGI's main window, ChatCopyPaste, any AceConfigDialog Bliz-options group, etc.) got our frame back from the pool with our gear icon'sOnClick = function() addon:OpenSettings() endstill 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 theaddon.UI.DetachChildren(self, { "_helpIcon", "_gearIcon" })call out of thef:SetCallback("OnClose", ...)handler and intoMainWindow:Close()itself, immediately beforeAceGUI: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 inMainWindow:Close()and the_settingsOpenflag suppresses the recursive OnHide so the consolidation is safe. Comment added atMainWindow:Close()documenting WHY the cleanup must live there and not inOnClose, 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 achievements —
CHAT_MSG_GUILD_ACHIEVEMENTfor a cross-realm guildmate (arg2 ="Xiomaara-MoonGuard") was correctly identified as a non-self event, but the guild-scope filter then rejected it withskip: sender 'Xiomaara' not found in guild roster. Root cause:fireAchievement's defensive roster-membership check walksGetGuildRosterInfofor every entry and comparesshortName(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_ACHIEVEMENTis server-gated by Blizzard to deliver only to guild members about guild members, so trust the channel. Fix: thread afromGuildChannelflag intofireAchievement(true when the source event isCHAT_MSG_GUILD_ACHIEVEMENT, false forCHAT_MSG_ACHIEVEMENT), and skip the roster-lookup branch when the flag is true.CHAT_MSG_ACHIEVEMENTstill 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:_escProxyis registered inUISpecialFramesso ESC closes the main window — and Blizzard's ESC handler iteratesUISpecialFramesviaCloseSpecialWindows, 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'sOnHidehandler closed the main window. The X button on the Settings panel hits the same path because Blizzard's Settings close logic also callsCloseSpecialWindowsinternally. Fix: introduceMainWindow._settingsOpeninhibit flag.addon:OpenSettingssets the flag to true before callingSettings.OpenToCategoryso the synchronousCloseSpecialWindowscascade 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). NewMainWindow:_HookSettingsCloseinstalls a one-shotHookScript("OnHide")on the Settings panel (RetailSettingsPanel, falling back to older ClassicInterfaceOptionsFrame) the first timeOpenSettingsruns; the hook defers_settingsOpen = falseand_escProxy:Show()by one frame viaC_Timer.After(0, ...)so any same-frameUISpecialFramescascade 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_OPENEDschedules aC_Timer.After(1, requestAllTabs)(1-s delay so Blizzard's own queries land first and our reads aren't racing partial buffers);requestAllTabsissuesQueryGuildBankLog(tab)for each of the 7 item tabs and the money log (tab 8);GUILDBANKLOG_UPDATEfires per-tab and triggersingestTab(tab)which walksGetNumGuildBankTransactions(tab)+GetGuildBankTransaction(tab, i)(or theGetGuildBankMoneyTransactionspair for the money log), dedupes against persisted entries, and appends new ones. Safety re-query ticker every 5 min that only fires whenGuildBankFrame: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",typeis Blizzard's transaction type (deposit/withdraw/move/repair/withdrawForTab/depositSummary/buyTab),tab2populated formove(destination tab),amountpopulated 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 synthesisedtsis derived vianow - y*365*86400 - m*30*86400 - d*86400 - h*3600. Sync identity — each entry carries a stable cross-vieweridfield computed at ingest as"gblog|<guildKey>|<type>|<name>|<itemSig>|<count>|<tab1>|<tab2>|<amount>"with NO timestamp component (the ymdh tuple drifts every query).itemSigis 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 sameidso future cross-addon sync with TOGBankClassic / cross-account merge can dedupe deterministically. The"gblog|"prefix distinguishes from sibling log types ("glog|","mlog|","tlog|"). PublicInjectEntry(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),GetCoinTextureStringrendering for money entries, and afrom → toTab-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 usesaddon.UI.AttachTooltipfor 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.jsongains globals forQueryGuildBankLog,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 sixTOGTools*.toc.RowList gains interactive checkbox columns — ported from FGI's RowList. Column descriptors now accept
checkbox = true+ optionalboxSize(default 16) +onToggle(entry, val, index, rl)to render a realUICheckButtonTemplatecell instead of a text fontstring. Click toggles the row'sentry[col.key]boolean, then firesonToggle. Cell render path detectscol.checkboxand callsSetCheckedinstead ofSetText. 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 — hooksCHAT_MSG_ACHIEVEMENTfor guild + visible achievements,ACHIEVEMENT_EARNEDfor the player's own IDs to cross-reference points / category / link viaGetAchievementInfo); |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 onPLAYER_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 viaTab.configKey = "gratz"— Settings > Modules > Gratz toggles the entire module on/off. New slash commands:/togt gratzand/togt gz. Location:Modules/Gratz/Gratz.lua,GUI/GratzTab.lua,TOGTools.lua(DB defaults),SlashCommands.lua, all sixTOGTools*.toc.
[v0.4.0] (2026-05-24) - Logs Nexus & Retail Compatibility
Bug Fixes (pre-release)
Mail Log collapsed N consecutive mails from the same sender into one row —
appendReceivemerged into the most-recent entry when(player + sender)matched within a 5-second window. An addon like Postal opening 12 mails from the Auction House in quick succession landed them all in the same entry, with same-itemLinkitems stacking — the user saw one row containing the aggregate, looking like "only the first mail was logged." Fix: switch the merge key from(last-entry sender + time window)to a per-MailIndexcache. Items, money, and COD updates for the sameMailIndexstill merge into one entry (correct for a single Take-All on a multi-item mail); takes on a differentMailIndexalways create a new entry even if the sender matches. The cache is sender-guarded too — if the mailbox got refreshed and indices shifted, a cached entry at the same slot with a different sender no longer matches, so we create a fresh entry rather than mis-merging. Cache is wiped onMAIL_CLOSED. Location:Modules/MailLog/MailLog.lua.Calendar SimpleGroup height collapsed to 0, empty-state body label overlapped the day grid — Custom-day calendar widget had
SetHeight(230)but the empty-state body rendered on top of the buttons. Root cause: AceGUI'sSimpleGrouphas aLayoutFinishedhook (per Ace3 source line 27-30) that auto-overwrites the SimpleGroup's height with the measured height of its AceGUI children every time the parent runsDoLayout. The calendar has no AceGUI children (only rawCreateFramebuttons + dropdowns parented to.content), soLayoutFinished(0)reset height to 0 right after my explicitSetHeight(230). The parent's Flow layout then placedbodyat Y=0 thinking the calendar was zero-height — body overlapped the buttons. Diagnosed via temporaryaddon:Debugprints hookingSetHeightandLayoutFinished. Fix: setgroup.noAutoHeight = truebefore any layout pass (the escape-hatch flag Ace3 itself reads at line 28:if self.noAutoHeight then return end). AlsoSetLayout("Fill")so subsequentDoLayoutcalls don't recurse through the (unused) "List" default. Diagnostic prints removed after confirmingframe.h=230survives end-to-end. Location:GUI/DateRangePicker.lua.Calendar greyed out days without entries, making it look like a "days with data" indicator rather than a date picker — initial design gated each cell's
Enable()on an availability set computed from current entries (mirrored MailLogger's UX). User feedback: they want to pick ANY date and see entries-or-empty, not be limited to days that already have data. Fix: every in-month day enables unconditionally; the availability set is no longer consulted for cell enablement (still consulted for the year-dropdown list so the year selector shows years with data). Removed the unusedavailMonthlocal. Location:GUI/DateRangePicker.lua."Mail Log captures every mail..." onboarding hint cluttered the Custom-mode no-date state — when the user picked Custom day but hadn't selected a date yet, the empty-state body still rendered below the calendar with the generic "no entries match" message. In that state the calendar IS the prompt, and the onboarding hint is noise. Fix: all three sub-tabs (Mail / Trade / Guild) early-return from
Drawafter adding the calendar when_filterRange == "custom"and_filterCustomDateis nil, skipping the spacer + body. Once the user picks a day, the table renders normally. Location:GUI/MailLogSubTab.lua,GUI/TradeLogSubTab.lua,GUI/GuildLogSubTab.lua.Guild Log auto-capture never populated on Retail — even after fixing the function name (
QueryGuildEventLoginstead of the legacyGuildEventLog_QueryGuildUI-helper) the log stayed empty on login. Diagnostic/togt glcheckshowed Blizzard's buffer correctly held 100 events post-query and ouringest()happily appended all 100 when invoked manually — but the wiredGUILD_EVENT_LOG_UPDATEevent handler never fired in the auto-capture path. Root cause: on Retail Midnight the legacy notification event has been dropped even though the legacy read API was kept. The Communities UI uses its own signaling mechanism andGUILD_EVENT_LOG_UPDATEno longer fires afterQueryGuildEventLog(). Fix:requestQuerynow schedules an explicitC_Timer.After(1.5, ingest)immediately after callingQueryGuildEventLog(), so the buffer gets ingested whether or not the event fires. The event handler is still registered for Classic flavours where the event does fire;ingestis idempotent via dedupe so double-firing on Classic is harmless. Location:Modules/GuildLog/GuildLog.lua.Guild Log timestamps were treated as absolute dates instead of "X ago" offsets —
GetGuildEventInforeturns(year, month, day, hour)but on Retail these are RELATIVE offsets (years-ago, months-ago, days-ago, hours-ago), not absolute calendar values. Confirmed empirically by re-running the diagnostic an hour apart and watching the same three events shift fromymdh=0/0/0/3to0/0/0/4.tupleToTswas treating them as absolute and callingtime({year=0, month=0, day=0, hour=4, ...})which gave nonsense timestamps (Lua'stime()normalises month=0 to December-of-previous-year etc.). Fix: subtract fromGetServerTime()—now - y*365*86400 - m*30*86400 - d*86400 - h*3600. Calendar-accurate month arithmetic isn't worth it given the underlying precision is hour-coarse. Location:Modules/GuildLog/GuildLog.lua.Guild Log dedupe key included the drifting "X ago" fields, would have re-added every event on every query — the dedupe tuple was
(y, m, d, h, type, target, actor)but ymdh values drift with every re-query (3-hours-ago becomes 4-hours-ago an hour later). Even if the auto-capture had worked, every 2-minute ticker tick would have seen "new" entries that were really the same events with updated relative-age fields, exploding the SV. Fix: drop ymdh from the dedupe key entirely —(type, target, actor, rank)is stable across queries. Edge case: same-person-repeatedly-joining-the-same-rank collapses to one entry; acceptable because Blizzard's 100-entry buffer cap means by the time a re-join happens, the original event has rolled off and the new join appears fresh. Location:Modules/GuildLog/GuildLog.lua.Guild Log captured nothing — wrong function name — The engine called
GuildEventLog_QueryGuild(), which was a UI-helper from Blizzard's oldBlizzard_GuildUIstandalone addon. That UI was retired when the Communities frame replaced it on Retail (8.0 / Battle for Azeroth, Aug 2018), so the helper is no longer present in the live Retail UI — ourif not GuildEventLog_QueryGuild then return endguard silently no-op'd and never queried Blizzard's buffer. The actual underlying API global isQueryGuildEventLog()(one word, capital Q) — that's the function the legacy UI-helper wrapped, and it's still present on every supported flavour including Retail. Verified by searching Guild Roster Manager's source (GRM_ScanRoster.lua callsQueryGuildEventLog()from its retail-shipping codebase). Fix: rename the call site, drop the (now mistaken)isRetailUnsupportedshort-circuit added during the misdiagnosis, swap.luarc.jsonglobals entry toQueryGuildEventLog.GetNumGuildEventsandGetGuildEventInfowere correct already. Location:Modules/GuildLog/GuildLog.lua,.luarc.json.Mail Log table cells rendered past the left edge of the window — RowList anchors columns right-to-left, computing the auto-width column's width as
parent_width - sum_of_fixed_widths. Mail Log had 7 columns (Time 80, Dir 60, Character 140, Other 140, Subject auto, Items 160, Money 90) for ~670 px of fixed widths + ~54 px of gaps/padding/scrollbar gutter ≈ 724 px total — but the default 760 px Logs window has only ~710 px of effective inner content area after AceGUI Frame chrome and the inner TabGroup padding. The auto-width Subject column came out roughly -14 px wide, which inverted its anchors and caused every right-of-Subject cell (Items, Money) to render at negative positions, spilling past the parent's left edge. Fix has two parts: (1) drop the dedicated Dir column entirely and fold direction into the Other column via a coloured arrow prefix (|cff66ff66<-|r Senderfor received,|cffffaa55->|r Recipientfor sent) — saves ~64 px without losing information; (2) shrink Character (140→110), Other (140→145, slight widen to fit the arrow prefix), Items (160→130), Money (90→80). New fixed total ≈ 545 px, leaves ~165 px for Subject at default width. Same shrink applied prophylactically to Trade Log (Character 130→110, Partner 130→110, Received 175→150, Enchant 110→90 — fixed total 540 px) and Guild Log (Guild 130→110, Player 140→120, Actor 140→120 — fixed total 525 px), both of which had the same latent overflow at the old default. Also bumpedLogs.WINDOW_SIZEwidth 760→820 and minWidth 600→740 so the columns stay positive even at the new minimum. Location:GUI/MailLogSubTab.lua,GUI/TradeLogSubTab.lua,GUI/GuildLogSubTab.lua,GUI/LogsTab.lua.
Improvements (pre-release)
Custom range picker — two-click range selection — "Custom day..." renamed to "Custom..." and reworked to support multi-day ranges via a two-click pattern (universal: Google Calendar / Outlook / GitHub all use it). First click stages the range start with a visual highlight; second click later than start stages the end and every day between gets highlighted; second click earlier than start treats as a new start (per user preference — simpler than auto-swapping endpoints); third click anywhere resets to a fresh single-day range. Apply commits the staged range. Single-click + Apply still works as before (committed as a single-day window). State tracked as
startY/M/DandendY/M/Don the calendar widget so navigation between months preserves the picked range — endpoints in months you're not currently viewing stay staged and re-highlight when you scroll back. Apply button moved from bottom-center to top-right (same row as the year / month dropdowns) per user feedback that the bottom placement overlapped sibling UI; calendar SimpleGroup height trimmed 260→210 since the bottom slot is no longer needed. New API:CreateCalendaroptsdefaultFrom/defaultTo(replacedefault),onApply(fromTs, toTs)(toTs is nil for single-day picks).Resolve(key, customFrom, customTo)produces a midnight-to-end-of-day-inclusive window from the (from, to) pair; nilcustomTois treated as a single-day range. Sub-tab state split:_filterCustomDate→_filterCustomFrom+_filterCustomTo. "Pick date..." button renamed "Pick range..." to match. Location:GUI/DateRangePicker.lua,GUI/MailLogSubTab.lua,GUI/TradeLogSubTab.lua,GUI/GuildLogSubTab.lua.Custom-day calendar: explicit Apply button + collapse-on-commit — Previously, clicking a day in the calendar immediately committed the selection and rebuilt the sub-tab, but the calendar stayed visible alongside the now-filtered results — cluttering the view. New flow: clicking a day STAGES the selection (visual highlight only, no rebuild), an Apply button below the grid (disabled until a day is picked) commits the date and collapses the calendar. State tracked as
pickedY / pickedM / pickedDon the calendar widget so navigation between months preserves the picked day's highlight when you return to its month, and Apply commits the actually-chosen date even after the user browses other months. When the calendar is collapsed and a date is set, a "Pick date..." button appears in the filter row to re-open the calendar (date dropdown won't fire OnValueChanged when re-clicking the same Custom value, so an explicit button is required for the re-open path). Re-entering Custom mode from a different preset auto-opens the calendar via_filterCalendarOpenstate set in the date-dropdown's onChange. SimpleGroup height bumped 230→260 to make room for the Apply button. Location:GUI/DateRangePicker.lua,GUI/MailLogSubTab.lua,GUI/TradeLogSubTab.lua,GUI/GuildLogSubTab.lua.Guild Log entries carry a stable cross-addon row id — Sync between guild-history addons needs a deterministic identifier so two clients can recognise the same event across their independent captures. Added an
idfield to each guild-log entry, computed at ingest as"glog|<guildKey>|<type>|<target>|<actor>|<rank>"— composed entirely from the stable dedupe-key fields plus the guild context, with NO timestamp component (the ymdh tuple drifts every query, the synthesisedtsdrifts further for old events due to 30-day month / 365-day year approximations). Two addons reading the same Blizzard buffer for the same guild compute the sameidfor the same logical event; sync code can compare on it directly. Backwards-compatible: existing SV entries withoutidget one computed and stored on first read via lazy migration inGetEntries. The"glog|"prefix leaves room for future"mlog|"/"tlog|"ids on Mail / Trade if sync expands; for now only Guild Log carries an id because that's where the user identified the use case. Not surfaced in the default UI — internal data for sync framework. Location:Modules/GuildLog/GuildLog.lua.Per-sub-tab help blocks for the Logs nexus + i-icon dispatch — Each log sub-category (Mail / Trade / Guild) now carries its own
help = { title, lines }block describing its specific UI and behaviour. The main window's bottom-row help (i) icon detects when the active outer tab islogsand looks up the active SUB-TAB's help viaaddon.logCategories[<subKey>].helpinstead of falling back to the generic outer-Logs blurb. Sub-tab help replaces the parent help whenever it exists; the bottom-row icon descriptions still append on the tail. New tab → addhelpnext tosubKey/labeland it lights up automatically. Location:Modules/MailLog/MailLog.lua,Modules/TradeLog/TradeLog.lua,Modules/GuildLog/GuildLog.lua,GUI/MainWindow.lua.Column header tooltips on every log sub-tab —
headerTip+headerTipDescpopulated on every column of Mail / Trade / Guild RowLists. Hovering a column header surfaces a tooltip explaining what the column shows, sort behaviour, and any precision caveats (e.g. Guild Log's hour-coarse time precision is called out on the Time column). Mirrors the existing pattern from the Addon Load tab. Location:GUI/MailLogSubTab.lua,GUI/TradeLogSubTab.lua,GUI/GuildLogSubTab.lua.Guild Log time column shows date AND time — was showing just
MM/DD/YYfor entries older than 7 days, which made it impossible to distinguish multiple events on the same day. Now formats asMM/DD HH:00for entries inside ~11 months andMM/DD/YY HH:00for older. Minute is hardcoded:00because Blizzard'sGetGuildEventInfoAPI only exposes hour precision. Recent entries still use the relative "Just now" / "Nh ago" forms. Location:GUI/GuildLogSubTab.lua./togt glcheckdiagnostic command — Slash subcommand that prints API availability, in-guild status, current Blizzard buffer contents, and runs an instrumented inline ingest reporting how many events the dedupe/readEvent gates accept vs. reject. Kept around past the v0.4.0 triage in case Guild Log silently stops capturing in a future patch — much faster to triage than addingprintstatements live. Documented in the Guild sub-tab's help block. Location:Modules/GuildLog/GuildLog.lua(Diagnosemethod),SlashCommands.lua.Guild Log dropped the manual Refresh button + tightened the periodic ticker from 30 min to 2 min —
GUILD_ROSTER_UPDATEis the responsive discovery path for every guild event type EXCEPT "invite sent but not yet accepted" — every join, leave, kick, promote, and demote fires a roster update, which we already opportunistically re-query on. The periodic ticker only exists for the pending-invite case.GuildEventLog_QueryGuildis a local-buffer read (not a server round-trip), so the tighter 2 min cadence is cheap, and combined with the roster-update path it makes the manual Refresh button redundant. Also dropped the unused_tickerlocal —C_Timer.NewTickerkeeps its own internal reference so we don't need to hold the handle. Empty-state copy updated to mention the auto-capture cadence instead of pointing at the now-removed button. Location:Modules/GuildLog/GuildLog.lua,GUI/GuildLogSubTab.lua.
New Features
Guild Log (phase 3) — Third Logs sub-tab snapshotting Blizzard's in-game guild event log on demand and persisting entries beyond the ~100-entry cap Blizzard keeps in its buffer. Per-guild buckets keyed by
GuildName-PlayerRealmso an account with alts in multiple guilds keeps independent histories (alts in the same guild on different realms also disambiguate cleanly because the realm suffix is included). Engine recipe: onPLAYER_LOGIN(3 s deferred to land after Ace's loaded line + other addons' login spam), on eachGUILD_ROSTER_UPDATE(cheap opportunistic re-query — roster changes often coincide with log changes), and on aC_Timer.NewTickerevery 30 minutes (catch-up for long sessions), callGuildEventLog_QueryGuild(); the matchingGUILD_EVENT_LOG_UPDATEevent then triggersingest()which walksGetNumGuildEvents()+GetGuildEventInfo(i), dedupes against the persisted entries via a(y,m,d,h,type,target,actor)tuple key, and appends new ones. Sorted newest-first after each ingest. ManualRefreshbutton on the sub-tab exposes the re-query without waiting for the periodic tick.GetGuildEventInfo's player1 / player2 / rank semantics are normalised toactor/target/rankat read time so the UI doesn't need to know the per-event-type swap: actor-driven events (promote/demote/remove/invite) put the officer in player1 and the target in player2; self-events (join/leave/quit) put the player in player1 and leave player2 nil. Timestamps are coarse — Blizzard only exposes (year, month, day, hour), no minute / second — so the tuple is stored verbatim alongside a synthesisedtsfor sort order; the year-offset interpretation handles both the "years-ago" form (Classic 1.15) and a literal-year form (older clients) via a magnitude check. Sub-tab UI: filter row (Guild / Type / Date range / Refresh / Clear) over a 6-column RowList — Time · Guild · Type · Player · Actor · Detail. Type column is colour-coded per event type (join = green, leave = yellow, kicked = red, promote = blue, demote = orange). Guild column strips the realm suffix when it matches the player's own realm to keep the column compact for the single-realm common case. Default guild filter is the character's current guild (or All when no current guild). Date range default is "All" (guild events are sparse — last 7d would frequently show empty even with months of history). Permission failure silent: if the player lacks event-log read privilege Blizzard returns zero entries and the module sits idle until they get the rank or log into an alt that has access. SV partition:db.global.guildLog.guilds[GuildKey].entries. Settings > Modules > Log categories grows a |cffffd700Guild Log|r toggle at order 30; inline group still greys out when the Logs master toggle is off. Location:Modules/GuildLog/GuildLog.lua,GUI/GuildLogSubTab.lua.Trade Log (phase 2) — Second Logs sub-tab capturing every completed player-to-player trade. Two-sided entry shape — separate
given/receiveditem lists, separatemoneyGiven/moneyReceivedtotals, and a dedicatedenchantGiven/enchantReceivedpair for trade slot 7 (the enchant slot — applying an enchant to/from a partner's item goes there, not into the regular 1-6 item slots). Hook recipe:TRADE_SHOWopens a per-trade staging cache;TRADE_PLAYER_ITEM_CHANGED(slot)/TRADE_TARGET_ITEM_CHANGED(slot)mutate the slot-keyed item maps;TRADE_MONEY_CHANGEDrefreshes both money totals;TRADE_ACCEPT_UPDATEre-snapshots every slot (covers any change event missed during drag-and-drop frame storms);UI_INFO_MESSAGEwith arg2 ==ERR_TRADE_COMPLETEcommits to entries;TRADE_CLOSEDdiscards the staging when a trade is cancelled. Arena/BG trades are skipped (IsInInstance() == "pvp" / "arena") per the MailLogger convention — they're noisy and rarely useful. Trades with no items either side AND no money AND no enchant are discarded as inspection-only opens. Trade partner name comes fromUnitName("npc")with the second return preserved for connected-realm clusters. Slot-keyed staging maps reduce to ordered lists on commit with same-link counts merged (mirrors Mail Log's stack-merge for compact rendering). Sub-tab UI: filter row (Character / Date range / Clear) over a 6-column RowList — Time · Character · Partner · Given · Received · Enchant — with the Enchant column visualising slot 7 contents on either side using->/<-arrows. Settings > Modules > Log categories grows a |cffffd700Trade Log|r toggle at order 20; the inline group still greys out when the Logs master toggle is off. SV partition:db.global.tradeLog.entries, account-wide with per-entryplayerfield for the alt filter. Location:Modules/TradeLog/TradeLog.lua,GUI/TradeLogSubTab.lua.Logs nexus tab — Mail Log (phase 1) — New top-level "Logs" tab that hosts a nested AceGUI
TabGroupwhose sub-tabs come from a per-category registry (addon.logCategories). Phase 1 ships the |cffffd700Mail|r sub-tab; Trade and Guild follow as phases 2 / 3 in the same v0.4.0 release. Outer tab registers via the standard module pattern (addon.modules["logs"]) so it picks up the bottom-row Help/Settings icons, the per-tab help block, and the Settings > Modules master toggle for free. Sub-engines self-register at file-load time:addon.logCategories["mail"] = MailLog. The Logs Draw builds its inner tab strip from the registry, filtered on the per-category enabled flag —addon:IsLogCategoryEnabled(subKey)ANDs the Logs master toggle with the sub-category toggle so disabling either silently kills the sub-engine. Per-character last-viewed sub-tab is cached atdb.char.frames.logsActiveSubTabso opening Logs returns to the last sub-tab the user was on. Deep-linking viaTab.pendingSubTablets slash commands target a specific sub-tab. Location:Modules/Logs/Logs.lua,GUI/LogsTab.lua.Mail Log engine + sub-tab — Captures every mail received (inbox takes) and sent (composed via
SendMail) intodb.global.mailLog.entries. Entry shape:{ ts, dir, player, other, subject, items = {{link, count}}, money, cod }. Hook recipe:hooksecurefunc("TakeInboxItem")/"AutoLootMailItem"/"TakeInboxMoney"for receive,hooksecurefunc("SendMail")+MAIL_SEND_INFO_UPDATEevent +UI_INFO_MESSAGE(ERR_MAIL_SENT) for send. Receive merging is timestamp-based — consecutive takes from the sameplayer+senderwithinMERGE_WINDOW_SEC(5 s) collapse into the most-recent entry, so "Take All" on a 12-item mail produces one row, not 12. Within an entry, same-itemLinkitems are merged into stacks. Items get merged on link match so a multi-stack mail shows as one row "[Link] ×N more" rather than N rows. Per-account-wide retention viadb.global.logs.retentionDays(default 90); each capture callsLogs:PruneEntrieswhich walks the head of the list dropping expired entries. UI: filter row of three AceGUI dropdowns (Character / Direction / Date range) + two-step Clear-data confirm button, over aRowListtable with columns Time · Dir · Character · Other · Subject · Items · Money. Items column shows the first item's link with "+N more" for multi-item mails. Time column is relative ("3m ago", "2h ago", "Yesterday", thenMM/DD/YY). Money column is gsc colour-formatted. Engine refreshes the sub-tab viaMainWindow:Refresh()when a new entry lands (only when Logs is the active outer tab AND Mail is the active sub-tab, so other tabs aren't disturbed). Location:Modules/MailLog/MailLog.lua,GUI/MailLogSubTab.lua.Shared date range picker —
addon.DateRangePicker— Reusable widget for Logs sub-tabs. Exposes:Create(opts)(returns AceGUI Dropdown),:Resolve(key, customTs)(returnsfromTs, toTs), and:CreateCalendar(opts)(returns AceGUI SimpleGroup hosting an inline year/month dropdowns + 7×6 day grid). Range presets: Last 24 hours / 7 days / 30 days / All / Custom day. Picking |cffffd700Custom day...|r expands the filter row to show the calendar below; clicking a day filters the log to that day's midnight-to-midnight window. The calendar's availability set is built from the current entries (filtered by character / direction / type — the non-date filters), so days without any matching event render greyed out and can't be picked — mirrors MailLogger's calendar UX without copying its code (custom-built using AceGUI SimpleGroup + native UIDropDownMenu + raw CreateFrame buttons for the grid cells). Year dropdown shows years with data descending; month dropdown is Jan-Dec; the 7×6 grid plus weekday header renders inside a 200 px-tall host frame parented to the SimpleGroup. Day-1-of-month weekday computed viadate("%w", time({year, month, day=1, hour=12})). Per-sub-tab_filterCustomDatestate persists the picked day across rebuilds (per-session, not SV). Location:GUI/DateRangePicker.lua,GUI/MailLogSubTab.lua,GUI/TradeLogSubTab.lua,GUI/GuildLogSubTab.lua.Slash command deep-links —
/togt logsopens the Logs tab to the last-viewed sub-tab;/togt logs mail//togt logs trade//togt logs guilddeep-link to a specific sub-tab viaaddon.modules["logs"].pendingSubTab. The Logs tab Draw reads and clears the pending key on its first paint./togt mlretained as a shorthand for/togt logs mail. Sub-keys are matched case-insensitively. Location:SlashCommands.lua.Settings > Modules: inline "Log categories" sub-group — Renders right below the Logs master toggle as an
inline = trueAceConfig group. Phase 1 contains a single |cffffd700Mail Log|r toggle (Trade and Guild added in phases 2 / 3). The whole sub-group isdisabled = function() return not addon:IsModuleEnabled("logs") endso it greys out when the Logs master toggle is off — visually expressing the AND relationship. Sub-toggles writedb.global.<configKey>.enabledand callMainWindow:Refresh()so the inner tab strip rebuilds on the spot. Location:GUI/Settings.lua.Per-module on/off toggles (Settings > Modules) — The Settings panel is now split into two AceConfig sub-groups rendered as tabs at the top of the Blizzard Interface Options page: |cffffd700General|r (the existing minimap + debug settings) and |cffffd700Modules|r (one toggle per registered tab/module). Disabling a module both hides its tab from the main window's tab strip AND short-circuits its engine on the same
db.global.<configKey>.enabledflag — capture handlers, login alerts, and cross-module readers (e.g. Login Digest's mail field calling Mailbox:GetExpiringSoon) all silently no-op. Data already captured (mailbox snapshots, NamePrefix nickname, Login Digest field selections) is preserved and reappears verbatim on re-enable. The Modules group's args are populated dynamically fromaddon.modulesat registration time so new modules added in future patches automatically grow a toggle without per-module wiring in Settings.lua — they just need to declareTab.configKey = "<sectionName>"alongsidetabKey/label. Newaddon:IsModuleEnabled(tabKey)helper reads the flag via the module'sconfigKeyfield (defaults to true when no configKey is declared, so legacy / freshly-registered modules stay on by default). NewMainWindow:Rebuild()rebuilds the tab strip and re-anchors the active selection when the toggle fires — closes the window entirely if every module ends up disabled (re-enter via minimap RMB or/togt settings). The existing in-tab "Enable" checkboxes on the Name Prefix and Login Digest tabs continue to work and now also callMainWindow:Rebuild()so the tab disappears immediately when toggled off from within itself. Surfaced by a user request specifically for a Mailbox kill-switch but built generally so every current and future module benefits. Location:GUI/Settings.lua,GUI/MainWindow.lua,TOGTools.lua,Modules/Mailbox/Mailbox.lua,GUI/NamePrefixTab.lua,GUI/AddonLoadTab.lua,GUI/MailboxTab.lua,GUI/LoginDigestTab.lua.Bottom-row Help (i) and Settings (gear) icons on every tab — Two new icons sit between the version-string status bar and the AceGUI Close button on the main window, adapted from the FastGuildInvite pattern. The 24×24 |TInterface\Common\help-i:14:14|t icon shows a per-tab help tooltip on hover (no click action in v0.4.0); the 20×20 |TInterface\Icons\Trade_Engineering:14:14|t gear opens the TOG Tools settings panel via
addon:OpenSettings()and survives the BlizzardCloseSpecialWindows()ESC-frame side-effect via a temporary_escProxy:SetScript("OnHide", nil)+ restore-on-next-frame workaround that mirrors FGI'sGUI/MainWindow.luagear handler. Layout math (status bar right edge -180, help -153, gear -130, Close -127) gives 3 px gaps across the row and centre-y=27 alignment with the Close button.Interface\Icons\Trade_Engineeringgets aTexCoord(0.08, 0.92, 0.08, 0.92)crop to remove the ~8% transparent border padding that otherwise makes the visible icon smaller than its hit box;help-idoesn't need it. Hit-rect insets of -2 give 2 px of click slop on every side inside the row's 3 px gap. Both icons are detached from the AceGUI Frame's underlying WoW frame on close viaaddon.UI.DetachChildren— without this the AceGUI widget pool returns the samef.frameon next Open with the leftover icons still parented, stacking fresh icons on top of stale ones. Location:GUI/MainWindow.lua,GUI/UI.lua.Per-tab help registry — Each tab module now carries a
help = { title, lines }block alongsidetabKey/label/WINDOW_SIZE/Draw. The help icon'sOnEnterreadsaddon.modules[self.activeTab].helpat hover time and dispatches automatically — new tabs add ahelptable next toDrawand immediately get a working help tooltip with no per-icon plumbing. Tabs without a help block fall back to a "No help available for this tab yet." placeholder. Help copy added for all four existing tabs (Name Prefix, Addon Load, Mailbox, Login Digest). The shared bottom-row icon description is appended at the end of every tab's help tooltip so users always see what the two icons mean. Chosen over FGI's flatTAB_HELPtable-in-MainWindow pattern because the per-module structure scales as we add tabs — MainWindow stays free of god-table accretion. Location:GUI/NamePrefixTab.lua,GUI/AddonLoadTab.lua,GUI/MailboxTab.lua,GUI/LoginDigestTab.lua,GUI/MainWindow.lua.Shared UI helpers — new
GUI/UI.lua— Addon-global helpers that future tabs (and the upcoming Logs nexus) can use without duplicating boilerplate.addon.Tooltip.Owner(frame, [budget])is an auto-flippingGameTooltip:SetOwnerthat picks ANCHOR_TOPRIGHT vs ANCHOR_BOTTOMLEFT based onGetScreenHeight() - frame:GetTop()vs the budget (default 250 px), so tooltips don't hang off the bottom of the screen for frames near the bottom edge.addon.UI.Brand(text)returns text wrapped in|c<addon.BrandColor>...|r.addon.UI.AttachTooltip(frame, title, body)HookScripts an OnEnter/OnLeave pair onto any frame (chains rather than replacing existing handlers).addon.UI.MakeIcon(parent, opts)is the factory used by the new bottom-row icons — accepts size, texture, texCoordCrop, hitRectInsets, optional onClick, and either an explicit onEnter ortooltipTitle/tooltipBodyfor the auto-tooltip path.addon.UI.DetachChildren(host, keys)is the OnClose release helper that hides, reparents to UIParent, clears points, and nils each named child reference on the host table — extracted as a global because we're about to add many more tabs each with their own icons. All six TOC files loadGUI\UI.luabetweenGUI\RowList.luaandGUI\MainWindow.lua. Location:GUI/UI.lua, all sixTOGTools*.toc.
Bug Fixes
addon:OpenSettingscrashed on Retail Midnight —GUI/Settings.luacaptured only the first return value fromAceConfigDialog-3.0:AddToBlizOptions("TOGTools", "TOG Tools")(the panel frame) and tried to open withSettings.OpenToCategory(_blizPanel.categoryID or "TOG Tools"). On Midnight builds the panel frame doesn't carry acategoryIDfield, so the string fallback"TOG Tools"was passed in —Settings.OpenToCategorycallsC_SettingsUtil.OpenSettingsPanel(openToCategoryID, ...)which now strictly validatesopenToCategoryIDas an integer in[-2^31, 2^31-1], throwingbad argument #1 to 'OpenSettingsPanel' (outside of expected range)for the string. This wasn't visible before v0.4.0 because nothing calledOpenSettingson Midnight in practice — the new bottom-row gear icon is the first caller that exposed it. Fix: capture both return values fromAddToBlizOptions(the SECOND is the opaque numeric category ID — the first/panel-frame field that Ace3's older builds populated isn't reliable on Midnight). Pass_categoryIDdirectly toSettings.OpenToCategory, called twice so the panel navigates past the Settings landing page into the TOG Tools sub-category (calling once sometimes lands on the root). Pattern lifted from FastGuildInvite'sGUI/SettingsPanel.luaafter Grouper hit the same issue. Also dropped the third-tierInterfaceOptionsFrame:Show()fallback — it opens to whatever category was last visited (not TOG Tools), and modern Retail doesn't haveInterfaceOptionsFrameanyway. Location:GUI/Settings.lua.TOGTools failed to load on current Retail (Midnight 12.0.x patches) —
TOGTools_Mainline.tocdeclared## Interface: 110207, 120001, 120000, but live Retail is on Midnight patches 12.0.5 and 12.0.7. Verified by cross-checking currently maintained Retail addons: RaiderIO ships120000, 120001, 120005, !BugGrabber ships up to120007, Ace3 covers120000, 120001and below. Clients on 12.0.5+ marked TOGTools as out-of-date, and because## Dependencies: Ace3, !TOGTrequires !TOGT to load (which had the same gap — single## Interface: 110207), users without "Load out of date AddOns" ticked saw the dep check fail and TOGTools never initialised. Fix: expanded the Mainline Interface list to110207, 120000, 120001, 120005, 120007, and added## X-Min-Interface: 110207for consistency with the other flavor TOCs. The Vanilla / TBC / Wrath / Cata / Mists TOCs are unchanged. Companion fix in !TOGT v0.1.1 covers the same Interface range. Location:TOGTools_Mainline.toc.
Improvements
addon:Debugusesaddon.UI.Brandinstead of the hardcoded"|cffFF8000TOGTools|r"literal. Pipes the debug-print tag through the global brand color so any future change toaddon.BrandColorpropagates automatically. Lookup is deferred to call time, which is always afterGUI/UI.luahas loaded. Location:TOGTools.lua.Main window title uses
addon.UI.Brand—f:SetTitle(addon.UI.Brand("TOG Tools"))replaces the literal"|cffFF8000TOG Tools|r"inMainWindow:Open. Same propagation benefit as the Debug refactor. Location:GUI/MainWindow.lua.Silenced pre-existing
unused-localLua hint —tg:SetCallback("OnGroupSelected", function(_widget, _event, group)flagged_eventas unused under the project's.luarc.jsonrules. Renamed to_to satisfy the lint check (single underscore is the canonical "ignore" convention; the surrounding_widgetkeeps its name because it's actually used). Location:GUI/MainWindow.lua.
[v0.3.5] (2026-05-23) - NamePrefix Chat-History Recall Duplicate Fix
Bug Fixes
- NamePrefix doubled the prefix when ElvUI's Up-arrow chat history recalled a previously-sent message — ElvUI's chat editbox enhancement lets the user press Up in the chat editbox to re-populate it with a previously-sent message; pressing Enter then re-sends it. The recalled text already contains the prefix that
ApplyPrefixadded on the original send (e.g.(Vishiswaz): 123), so when our hook fired again on the recall it prepended the prefix a second time, producing(Vishiswaz): (Vishiswaz): 123. Name2Chat does not double-prefix in the same scenario because its hook point inspects the outgoing message differently; ours just unconditionally prepended. Fix: inApplyPrefix, after building theprefixstring fromcfg.formatandcfg.nickname, early-exit whenstring.sub(msg, 1, #prefix) == prefix. Plainsubcomparison (not a Lua pattern) so format strings containing magic characters are safe. Edge case where the user changes their nickname between sends is acceptable: the old prefix in the recalled message won't match the new one, so the message ships as the user originally typed it rather than gaining a second prefix. Hook entry points (RetailEventRegistryand ClassicOnKeyDown) are unchanged. Location:Modules/NamePrefix/NamePrefix.lua.
[v0.3.4] (2026-05-22) - TBC /camp /exit /logout Taint Fix
Bug Fixes
/camp,/exit,/logouttyped in chat trippedADDON_ACTION_FORBIDDENon TBC / Anniversary 1.15.x with NamePrefix active — v0.3.3 replaced the per-editboxOnEnterPressedscript slot with an addon-Lua closure that invoked the captured original script viasecurecall(origScript, editBox, ...). Thesecurecallboundary clears taint at its call site, but on TBC / Anniversary the slash-dispatch chain (ChatFrameEditBoxMixin:OnEnterPressed→SendText→ParseText→ slash-command dispatcher → protectedLogout()) still failed the secure-execution check forLogout()— confirmed in the field after v0.3.3 shipped, with the failing trace[TOGTools/Modules/NamePrefix/NamePrefix.lua]:161 → [C]: securecall → ChatFrameEditBox.lua:370 → SendText:252 → ParseText:207 → SlashCommands.lua:748 → Logout(). The OnEnterPressed slot itself becomes addon-owned onceSetScriptruns on it, so the C-level taint check sees addon ownership on the dispatch path regardless of thesecurecallclear at the boundary. Switched the Classic / older-Retail hook fromSetScript("OnEnterPressed", ...) + securecall(origScript)toHookScript("OnKeyDown", ...)keyed onkey == "ENTER"or"NUMPADENTER".OnKeyDownfires in its own C dispatch frame, ahead ofOnEnterPressed, which is dispatched as a separate C-level call — our hook applies the prefix viaSetTextand returns to C beforeOnEnterPressedbegins, so addon Lua is never on the call stack whenSendText/ParseText/Logout()run. TheOnEnterPressedscript slot is no longer touched at all and stays bound to the original Blizzard handler, so the entire slash-dispatch chain executes in a fully secure context.HookScriptchains alongside any existingOnKeyDownhandler instead of claiming the slot; chat editboxes have no defaultOnKeyDownbinding, so our hook is the only one on the editbox. The retail 12.0+EventRegistry "ChatFrame.OnEditBoxPreSendText"path is unchanged. TheApplyPrefixleading-/early-exit is unchanged and continues to ensure noSetTextis issued for slash commands even though the OnKeyDown hook still runs. Updated the file-header comment block to document the rejectedSetScript + securecallapproach alongside the previously-rejectedSendChatMessage/ChatEdit_SendText/ mixin-method-replace wraps, and updated theModifyMessagedoc comment to reference the new OnKeyDown entry point. Removed the v0.3.3 temporaryOnKeyDowndiagnostic since its purpose (verifyOnKeyDownfires on TBC ahead ofOnEnterPressed) is now load-bearing in production code. Location:Modules/NamePrefix/NamePrefix.lua.
[v0.3.3] (2026-05-18) - NamePrefix /say Channel + TBC /logout Taint Fix
Bug Fixes
- NamePrefix did not fire on TBC / Anniversary 1.15.x in the v0.3.2 working tree — During pre-release work toward this version the Classic hook was experimentally rewritten twice — first to wrap the global
SendChatMessage, then to wrap the globalChatEdit_SendText(Name2Chat's pattern). Both wraps installed cleanly (verified with a temporary install-timeprint) but neither fire-time path was ever entered for user-typed chat on TBC / Anniversary 1.15.x — confirmed empirically with a temporary fire-timeprintinsideModifyMessage. On those clientsChatFrameEditBoxMixin:OnEnterPresseddispatches viaself:SendText()using a captured local reference, so addon-level global reassignment never intercepts user chat. On Classic Era 1.15.x the globalChatEdit_SendTextpath is still active and did fire during testing, which initially masked the TBC/Anniversary regression. Restored v0.3.2's per-editboxSetScript("OnEnterPressed", ...)wrap that walksChatFrame1EditBox..ChatFrameN.EditBoxviaNUM_CHAT_WINDOWS— this is the only entry point that reliably fires on every Classic flavor. Verified empirically on TBC (Anniversary,Wowhead Looterinterface 20505) and Classic Era (interface 11508). Location:Modules/NamePrefix/NamePrefix.lua. - Per-editbox
OnEnterPressedwrap trippedADDON_ACTION_FORBIDDENon/logout—SetScript("OnEnterPressed", addonFn)leaves an addon-Lua handler on the C call stack whenever Enter is pressed in a chat editbox. When the user types/logout, the originalOnEnterPressedwe invoke from inside our handler runs throughChatEdit_SendText→ChatEdit_ParseText→ slash-command dispatcher → protectedLogout(). Because addon Lua is still on the stack, taint propagates the whole way and the secure-execution layer blocksLogout()on TBC and Anniversary. Fix: invoke the captured original script viasecurecall(origScript, editBox, ...)instead of a direct call.securecallis the canonical taint-clearing dispatcher (see WoWWiki "Secure Execution and Tainting") — it saves and clears the current taint flag around the call soParseText→ slash-handler →Logout()runs untainted regardless of what sits on the C stack above. Belt-and-suspenders:ApplyPrefixalready early-exits on a leading/, so the editbox text is never modified for slash commands and itsGetTextresult stays untainted on the secure dispatch path. Location:Modules/NamePrefix/NamePrefix.lua.
New Features
- NamePrefix
Say (/s)channel toggle — New checkbox at the top of the NamePrefix tab's Active Channels list. When enabled, the configured nickname is prepended to outgoing/saymessages alongside the existing guild / officer / party / raid / instance toggles.SAYcase added to theApplyPrefixchatType dispatch table;say = falseadded toDB_DEFAULTS.global.namePrefix. Location:Modules/NamePrefix/NamePrefix.lua,GUI/NamePrefixTab.lua,TOGTools.lua. - Account-wide debug flag +
addon:Debug()helper — NewDebugsection in the Settings panel (ESC → Options → Addons → TOG Tools, or right-click the minimap button) with aVerbose debug outputtoggle. Stored atdb.global.debug, defaultfalse.addon:Debug(msg)prints|cffFF8000TOGTools|r <msg>only when the flag is true; modules call it for hook-install confirmations and fire-time diagnostics that should be silent in normal play. NamePrefix uses it for two diagnostics: the install confirmationNamePrefix: hook installed (OnEnterPressed xN)(one-shot at addon load — requires/reloadto re-fire after toggling the flag) and the per-send fire lineNamePrefix fire: chatType=X prefixed=true/false(no message echo, takes effect on the next chat send without/reload). Location:TOGTools.lua,GUI/Settings.lua,Modules/NamePrefix/NamePrefix.lua.
Improvements
- NamePrefix channel defaults are now all
false—say,guild,officer,party,raid,instanceall default tofalseinDB_DEFAULTS.global.namePrefix. Previouslyguildandofficerdefaulted totrue. Fresh installs now opt-in per channel; existing saved settings are unaffected because AceDB merges defaults without overwriting saved keys. Location:TOGTools.lua. .luarc.json/TOGTools.code-workspace— Addedsecurecall,rawget,rawsettodiagnostics.globalsfor Lua LSP coverage of the new hook code and the v0.3.2 account-wide migration block.
[v0.3.2] (2026-05-17) - NamePrefix BCC / Anniversary Fix
Bug Fixes
- NamePrefix did nothing on BCC and Anniversary (Classic Era 1.15.x) — v0.3.0 relocated the Classic hook from
ChatEdit_SendTextto the globalChatEdit_OnEnterPressedto avoid tainting OPie'ssecurecall(ChatEdit_SendText, ...)macrotext dispatch. On modern Classic builds (BCC, Anniversary 1.15.x) the chat editbox'sOnEnterPressedscript is theChatFrameEditBoxMixin:OnEnterPressedmethod, NOT the global — so the wrapped global never fired and outgoing messages went un-prefixed. Replaced the global-wrap with a per-editboxSetScript("OnEnterPressed", ...)wrap that walksChatFrame1EditBox..ChatFrameN EditBox(NUM_CHAT_WINDOWS) and prependsModifyMessageto each editbox's existing script. This catches both the legacyChatEdit_OnEnterPressedscript binding and the modern mixin-method binding without touching any global function reference. Verified safe for OPie: the wrap only touches user-visibleChatFrameNeditboxes, not OPie's synthetic Rewire editboxes, and never replacesChatEdit_SendTextorChatFrameEditBoxMixin.SendText. Confirmed against Name2Chat's own analysis thatEventRegistry "ChatFrame.OnEditBoxPreSendText"is not fired on Classic 1.15.x despiteEventRegistrybeing backported — the existinggv.isRetail120Plusgate stays correct. Location:Modules/NamePrefix/NamePrefix.lua. .luarc.json— AddedNUM_CHAT_WINDOWStodiagnostics.globals.
Improvements
- MainWindow remembers last-selected tab — Opening the main window (minimap button,
/togt, orToggle()with no arg) now restores the tab that was active the last time the user closed the window. Stored asdb.char.lastTab; per-character so different alts can have different defaults. Resolution order inMainWindow:Open(tabKey)is: explicit caller arg →db.char.lastTab(if the module still exists) → alphabetically first tab. Slash commands that pass an explicit tab (/togt np,/togt mb, etc.) still win, and clicking a different tab updates the saved value viaOnGroupSelected. Location:GUI/MainWindow.lua,TOGTools.lua. - NamePrefix is now account-wide — Moved
namePrefixfromDB_DEFAULTS.chartoDB_DEFAULTS.globalso the nickname, format string, per-channel toggles, andhideIfCharNameare configured once and shared across every alt. Toons that shouldn't self-prefix rely onhideIfCharName(now defaulting totrue) to suppress the prefix when the nickname matches the character's own name. One-shot migration inAce:OnInitializecopies any pre-upgradedb.char.namePrefixwith a non-empty nickname intodb.global.namePrefixthe first time an upgraded character logs in (only if the global table is still at defaults); subsequent alts inherit the now-global config. Leftover per-char entries are left in place since AceDB ignores keys not in the defaults. Location:TOGTools.lua,Modules/NamePrefix/NamePrefix.lua,GUI/NamePrefixTab.lua. - NamePrefix default —
hideIfCharNamenow defaults totrueinDB_DEFAULTS.global.namePrefix. New accounts no longer self-prefix on the character whose name matches the configured nickname (the common case where the nickname IS the main's name). Existing saved-variables are unaffected. Location:TOGTools.lua.
[v0.3.1] (2026-05-06) - TBC Compatibility Fix
Bug Fixes
- Addon Load tab crashed on TBC (and Wrath/Cata/Mists/Retail) —
GetNumAddOns,GetAddOnInfo,IsAddOnLoaded,GetAddOnMemoryUsage, andUpdateAddOnMemoryUsagewere called as bare globals, but TBC+ moved them all intoC_AddOns.*. Added five compat locals at the top of the module that preferC_AddOns.*when available and fall back to the bare globals for Classic Era, matching the pattern already used elsewhere in the addon. Location:Modules/AddonLoad/AddonLoad.lua.
[v0.3.0] (2026-05-04) - Mailbox Stale Watcher
New Features
- Mailbox Stale Watcher — New tab (
/togt mail/mb/mailbox) tracking mail expiration across every alt on the account so mail never gets auto-deleted at the 30-day mark for an unvisited character. Captures a per-alt inbox snapshot on eachMAIL_INBOX_UPDATEevent, stamping absoluteexpiresAt = GetServerTime() + (daysLeft * 86400)per item so the time-remaining math stays accurate even when the alt does not re-visit a mailbox for several days (same content-derived-timestamp pattern TOGPM uses). Login chat alert prints once when any alt has mail expiring within the configured threshold (default 2 days, slider-configurable 1–7 in the tab). Tab lists each alt with a row-per-mail breakdown of expiring items (sender, subject, time remaining), snapshot age display, and a- staleflag for snapshots older than 7 days. Live-refresh onMAIL_INBOX_UPDATEwhile the tab is active, so opening a mailbox in-game while the Mailbox tab is showing updates the rows in real time without forcing a manual tab-switch (addon.MainWindow:Refresh()is called from the engine's event handler whenaddon.MainWindow.activeTab == "mailbox"). Account-wide storage indb.global.mailbox.snapshots[Name-Realm]so every alt's data is visible from any character, fullName-Realmkeys throughout to prevent collisions across connected-realm clusters. Location:Modules/Mailbox/Mailbox.lua,GUI/MailboxTab.lua.
Improvements
- AceDB schema — Added
globalnamespace toDB_DEFAULTSwithmailbox = { thresholdDays, snapshots }. Account-wide scope so cross-alt mail data persists across every physical realm in a connected-realm cluster (per-db.realmwould silo each physical realm of the cluster into a separate notes table — wrong for our use case). Location:TOGTools.lua. - Slash commands —
/togt mb//togt mail//togt mailboxopen the Mailbox tab directly; help output updated. Location:SlashCommands.lua. .luarc.json— AddedGetInboxNumItems,GetInboxHeaderInfo, andChatEdit_OnEnterPressedtodiagnostics.globalsso the new mail API calls and the relocated NamePrefix hook target resolve cleanly under the Lua LSP.- TOC files — All six TOC variants (Vanilla, BCC, Wrath, Cata, Mists, Mainline) updated to load
Modules\Mailbox\Mailbox.luaandGUI\MailboxTab.luaafter the AddonLoad pair.
Bug Fixes
- NamePrefix broke OPie macrotext ring entries (custom mounts,
/castmacros) — On Classic clients, NamePrefix wrapped the globalChatEdit_SendTextto enable pre-send text modification. OPie'sLibs/ActionBook/Rewire.luadispatches each line of macro ring entries viasecurecall(ChatEdit_SendText, box, false). Replacing the global with a wrapper created in our (insecure) addon loading context tainted that function reference, so OPie's secure dispatch path picked up taint when calling it — failing for any ring slot delivered as macrotext (custom mount macros,/castslots), while direct spell-cast slots that bypass macrotext (most tradeskills) kept working. Fix: relocated the Classic hook target fromChatEdit_SendTexttoChatEdit_OnEnterPressed, which is upstream ofChatEdit_SendTextin the user-typed Enter-key flow but is not on OPie's Rewire dispatch path. The user-facing prefixing behavior is unchanged. The replaced-global pattern itself stays (hooksecurefuncfires post-call, so it can't be used to modify text before send); we just no longer taint the function OPie depends on. Location:Modules/NamePrefix/NamePrefix.lua.
[v0.2.0] (2026-05-04) - Addon Load Monitor
New Features
- Addon Load Monitor — New tab (
/togt al) showing every installed addon's memory usage, load status, and load timing. Sortable by name, memory, load time, and status (failures-first when descending). Summary bar shows totals (loaded / failed / disabled / on-demand) and total memory. Requires the!TOGTcompanion addon for full timing coverage across all addons. Location:Modules/AddonLoad/AddonLoad.lua,GUI/AddonLoadTab.lua. - !TOGT companion integration —
!TOGT(companion addon) added as a required dependency. Loads before all other addons at the very start of the loading screen (the!prefix sorts before all letters) and records load order and per-addon offset into theTOGToolsEarlyDataglobal. The Addon Load Monitor reads it at display time and shows a status-bar indicator (!TOGT active/!TOGT missing). - Shared
addon.RowListcomponent — New fileGUI/RowList.lua, ported and trimmed from FastGuildInvite'sGUI/RowList.lua. Reusable sortable-row list for any tab that needs a tabular data view: brand-colored clickable header bar, click-toggle ASC/DESC sort with per-columnsortKey/sortDescDefault/sortable/gapBeforeopts and tie-break onentry.name, alternating row banding, virtual-scroll pool that grows with the parent's height, custom Blizzard-textured scrollbar, mouse-wheel scroll. Cells useSetWordWrap(false)+SetMaxLines(1)and anchorLEFT/RIGHTto the parent so column layouts compact elastically when the user shrinks the window — no row-wrap. Public API::New(parent, opts),:SetData(arr),:SetSort(key, desc),:Refresh(),:Detach(). Registered to all six TOC files afterCompat.lua, beforeMainWindow.lua. Compat.luashared GUI helpers —addon.Tooltip.Owner/AnchorFrame(smart anchor flipping based on screen half),addon.AceGUIFrameScripts(leak-safe raw frame scripts that restore prior handlers on pooled-widget release),addon.GUI.AttachTooltip,addon.GUI.MakeColumnHeader,addon.GUI.ApplyMinResize,addon.GUI.DetachPool. Used by every tab.
Bug Fixes
- Load times reporting
0.000s—GetTime()is frame-locked, so everyADDON_LOADEDevent fired in the same loading-screen frame returned the same timestamp. Per-addon deltas collapsed to zero. Switched the timing capture in!TOGT.luaand the fallback capture inModules/AddonLoad/AddonLoad.luatodebugprofilestop(), which has sub-millisecond precision. Offsets are stored in seconds (ms / 1000) so downstream consumers are unchanged. - Load Time column sort produced random-looking order — The sort comparator in
GUI/AddonLoadTab.luawas readingrow.offset(cumulative seconds since the first event) instead ofrow.loadTime(per-addon delta). Becauseoffsetis monotonic with load order, clicking the header effectively sorted by load order regardless of direction. Comparator now readsrow.loadTime. - Addon Load tab wrapped to multiple lines on horizontal shrink — The tab used AceGUI Flow with
widget:SetWidth(...)per cell, so when the user shrunk the window narrower than the column-width sum AceGUI moved cells to a new line and the layout fell apart. Replaced the row-rendering path withaddon.RowList, whose anchor-based cells truncate instead of wrapping. The window'sminWidthwas tightened from 640 to 520 now that compaction is graceful. - Tab status bar leaked between tabs —
MainWindow:DrawTabnow resets the status text to the version string before delegating to the tab module, so a tab that overrides the status (e.g. Addon Load showing!TOGT active/missing) doesn't bleed that text into the next tab the user opens. - Tab content didn't reflow on tab switch —
MainWindow:ApplyTabSizenow callsf:DoLayout()after sizing, so when switching between tabs whoseWINDOW_SIZEdiffers, the new tab's content reflows immediately instead of inheriting stale layout.
Improvements
- Addon Load tab column widths tightened — Status reduced from 175 → 85 px (fits "Wrong Version"); Memory reduced from 110 → 65 px (fits "999.9 MB"); Load Time reduced from 110 → 60 px. The auto-width Addon Name column reclaims the freed space.
RowList'sSCROLLBAR_GUTTERreduced fromSCROLLBAR_WIDTH + 6toSCROLLBAR_WIDTH + 2so the rightmost column sits ~5 px from the scrollbar. Status column getsgapBefore = 5to break the visual collision between right-justified Load Time text and left-justified Status text. - Sort indicator removed from active column header —
RowList:_updateHeaderTextno longer appends a glyph (v/^) to the active sort column; WoW's default font doesn't render unicode triangles cleanly and ASCII letters look like typos. Click affordance is communicated via the column tooltip. - AceGUI pool-bleed protection —
RowList:Detach()orphans the row pool, header, and scrollbar toUIParenton host-widget release; a_detachedguard on:Refresh()makes sure a recycled host doesn't trigger a pool grow that would re-attach rows into the next addon's widget. Wired viabody:SetCallback("OnRelease", ...)inGUI/AddonLoadTab.lua. .luarc.jsonupdated — Addeddebugprofilestoptodiagnostics.globalsin bothTOGTools/.luarc.jsonand!TOGT/.luarc.jsonso the new timer call resolves cleanly under the Lua LSP.
[v0.1.0] (2026-05-04) - Initial Release
New Features
- Project scaffolding — TOC files for all WoW versions (Vanilla 11508, TBC 20505, Wrath 30405, Cata 40402, Mists 50503, Retail 110207/120001/120000),
.pkgmetawith BigWigs packager config,.luarc.json(Lua 5.1 LSP),.markdownlint.json, and.github/workflows/release.ymlfor tag-triggered CurseForge releases. - Core addon —
TOGTools.lua: AceAddon-3.0 instance with AceConsole-3.0, AceEvent-3.0, AceHook-3.0 mixins; AceDB-3.0 schema (charscope); version detection flags (gv.isVanilla,gv.isTBC,gv.isWrath,gv.isCata,gv.isMists,gv.isClassic,gv.isRetail,gv.isRetail120Plus). - Slash commands —
SlashCommands.lua: all/togtcommand registration and handling in a dedicated file, separate from core addon logic. Subcommands:/togt(toggle window),/togt np(Name Prefix tab),/togt settings(open settings),/togt vc(version check). - Tabbed main window —
GUI/MainWindow.lua: dynamic AceGUI Frame + TabGroup; tabs auto-populate fromaddon.modulessorted alphabetically bylabel; per-tab locked/unlocked window sizing viaWINDOW_SIZE; ESC-to-close proxy; position persistence via AceDB. - Name Prefix module —
Modules/NamePrefix/NamePrefix.lua+GUI/NamePrefixTab.lua: wrapsChatEdit_SendText(Classic) or hooksEventRegistry(Retail 12.0+) to prepend a configurable nickname to outgoing chat messages. Settings: enable/disable, nickname, format string with live preview, per-channel toggles (Guild, Officer, Party, Raid, Instance, custom channel), skip-exclamation option, suppress-if-char-name option. Per-character DB scope. Skips messages beginning with/(slash commands/macros) unconditionally. - Minimap button —
GUI/MinimapButton.lua: LibDataBroker-1.1 launcher + LibDBIcon-1.0 registration. Left-click toggles the main window; right-click opens the native addon settings panel. Icon:textures/ToGTools_PH_MMB.tga. Button show/hide state and position persisted inDB.char. - Addon settings panel —
GUI/Settings.lua: AceConfig-3.0 options table registered under ESC → Interface → Addons → TOG Tools (native Blizzard panel).addon:OpenSettings()usesSettings.OpenToCategory(Classic Era 1.15+ / Retail 10+),InterfaceOptionsFrame_OpenToCategory(older builds), orInterfaceOptionsFrame:Show()as a final fallback — detected by API presence, not version flag. - VersionCheck-1.0 integration —
VC:Enable(Ace)called on all non-Retail versions inOnInitialize;/togt vcbroadcasts a guild-wide version check and prints responses after 21 seconds. - Embedded libs —
libs/LibDataBroker-1.1.lua,libs/LibDBIcon-1.0.lua(single-file embeds, copied from TOGProfessionMaster). - MIT License —
LICENSEfile added; referenced in.pkgmetavialicense-output. - CurseForge project — Project ID
1533830set in.pkgmetaand all TOC files. - Governance files —
.github/copilot-instructions.mdandCLAUDE.mdwith full project rules (module pattern, commit/tag process, changelog format, HTML doc rules, no-tag rule).
Bug Fixes
- Name Prefix not reaching other players —
hooksecurefuncfires afterSendChatMessagehas already been called. Fixed by wrappingChatEdit_SendTextdirectly (local orig = ...; ChatEdit_SendText = function(...) ... return orig(...) end) so the prefix is applied before the message is sent. Location:Modules/NamePrefix/NamePrefix.lua. - Macros/slash commands being prefixed — Added an unconditional early-return in
ModifyMessagewhen the message starts with/. Location:Modules/NamePrefix/NamePrefix.lua.

