promotional bannermobile promotional banner

TOGTools

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

File Details

TOGTools-v0.4.0

  • R
  • May 24, 2026
  • 1.99 MB
  • 14
  • 12.0.7+9
  • Retail + 3

File Name

TOGTools-TOGTools-v0.4.0.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.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 rowappendReceive merged 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-itemLink items 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-MailIndex cache. Items, money, and COD updates for the same MailIndex still merge into one entry (correct for a single Take-All on a multi-item mail); takes on a different MailIndex always 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 on MAIL_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's SimpleGroup has a LayoutFinished hook (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 runs DoLayout. The calendar has no AceGUI children (only raw CreateFrame buttons + dropdowns parented to .content), so LayoutFinished(0) reset height to 0 right after my explicit SetHeight(230). The parent's Flow layout then placed body at Y=0 thinking the calendar was zero-height — body overlapped the buttons. Diagnosed via temporary addon:Debug prints hooking SetHeight and LayoutFinished. Fix: set group.noAutoHeight = true before any layout pass (the escape-hatch flag Ace3 itself reads at line 28: if self.noAutoHeight then return end). Also SetLayout("Fill") so subsequent DoLayout calls don't recurse through the (unused) "List" default. Diagnostic prints removed after confirming frame.h=230 survives 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 unused availMonth local. 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 Draw after adding the calendar when _filterRange == "custom" and _filterCustomDate is 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 (QueryGuildEventLog instead of the legacy GuildEventLog_QueryGuild UI-helper) the log stayed empty on login. Diagnostic /togt glcheck showed Blizzard's buffer correctly held 100 events post-query and our ingest() happily appended all 100 when invoked manually — but the wired GUILD_EVENT_LOG_UPDATE event 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 and GUILD_EVENT_LOG_UPDATE no longer fires after QueryGuildEventLog(). Fix: requestQuery now schedules an explicit C_Timer.After(1.5, ingest) immediately after calling QueryGuildEventLog(), 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; ingest is 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" offsetsGetGuildEventInfo returns (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 from ymdh=0/0/0/3 to 0/0/0/4. tupleToTs was treating them as absolute and calling time({year=0, month=0, day=0, hour=4, ...}) which gave nonsense timestamps (Lua's time() normalises month=0 to December-of-previous-year etc.). Fix: subtract from GetServerTime()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 old Blizzard_GuildUI standalone 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 — our if not GuildEventLog_QueryGuild then return end guard silently no-op'd and never queried Blizzard's buffer. The actual underlying API global is QueryGuildEventLog() (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 calls QueryGuildEventLog() from its retail-shipping codebase). Fix: rename the call site, drop the (now mistaken) isRetailUnsupported short-circuit added during the misdiagnosis, swap .luarc.json globals entry to QueryGuildEventLog. GetNumGuildEvents and GetGuildEventInfo were 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 Sender for received, |cffffaa55->|r Recipient for 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 bumped Logs.WINDOW_SIZE width 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/D and endY/M/D on 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: CreateCalendar opts defaultFrom / defaultTo (replace default), 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; nil customTo is 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 / pickedD on 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 _filterCalendarOpen state 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 id field 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 synthesised ts drifts 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 same id for the same logical event; sync code can compare on it directly. Backwards-compatible: existing SV entries without id get one computed and stored on first read via lazy migration in GetEntries. 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 is logs and looks up the active SUB-TAB's help via addon.logCategories[<subKey>].help instead 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 → add help next to subKey / label and 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-tabheaderTip + headerTipDesc populated 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/YY for entries older than 7 days, which made it impossible to distinguish multiple events on the same day. Now formats as MM/DD HH:00 for entries inside ~11 months and MM/DD/YY HH:00 for older. Minute is hardcoded :00 because Blizzard's GetGuildEventInfo API only exposes hour precision. Recent entries still use the relative "Just now" / "Nh ago" forms. Location: GUI/GuildLogSubTab.lua.

  • /togt glcheck diagnostic 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 adding print statements live. Documented in the Guild sub-tab's help block. Location: Modules/GuildLog/GuildLog.lua (Diagnose method), SlashCommands.lua.

  • Guild Log dropped the manual Refresh button + tightened the periodic ticker from 30 min to 2 minGUILD_ROSTER_UPDATE is 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_QueryGuild is 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 _ticker local — C_Timer.NewTicker keeps 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-PlayerRealm so 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: on PLAYER_LOGIN (3 s deferred to land after Ace's loaded line + other addons' login spam), on each GUILD_ROSTER_UPDATE (cheap opportunistic re-query — roster changes often coincide with log changes), and on a C_Timer.NewTicker every 30 minutes (catch-up for long sessions), call GuildEventLog_QueryGuild(); the matching GUILD_EVENT_LOG_UPDATE event then triggers ingest() which walks GetNumGuildEvents() + 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. Manual Refresh button on the sub-tab exposes the re-query without waiting for the periodic tick. GetGuildEventInfo's player1 / player2 / rank semantics are normalised to actor / target / rank at 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 synthesised ts for 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 / received item lists, separate moneyGiven / moneyReceived totals, and a dedicated enchantGiven / enchantReceived pair 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_SHOW opens a per-trade staging cache; TRADE_PLAYER_ITEM_CHANGED(slot) / TRADE_TARGET_ITEM_CHANGED(slot) mutate the slot-keyed item maps; TRADE_MONEY_CHANGED refreshes both money totals; TRADE_ACCEPT_UPDATE re-snapshots every slot (covers any change event missed during drag-and-drop frame storms); UI_INFO_MESSAGE with arg2 == ERR_TRADE_COMPLETE commits to entries; TRADE_CLOSED discards 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 from UnitName("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-entry player field 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 TabGroup whose 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 at db.char.frames.logsActiveSubTab so opening Logs returns to the last sub-tab the user was on. Deep-linking via Tab.pendingSubTab lets 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) into db.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_UPDATE event + UI_INFO_MESSAGE (ERR_MAIL_SENT) for send. Receive merging is timestamp-based — consecutive takes from the same player+sender within MERGE_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-itemLink items 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 via db.global.logs.retentionDays (default 90); each capture calls Logs:PruneEntries which 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 a RowList table 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", then MM/DD/YY). Money column is gsc colour-formatted. Engine refreshes the sub-tab via MainWindow: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) (returns fromTs, 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 via date("%w", time({year, month, day=1, hour=12})). Per-sub-tab _filterCustomDate state 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 logs opens the Logs tab to the last-viewed sub-tab; /togt logs mail / /togt logs trade / /togt logs guild deep-link to a specific sub-tab via addon.modules["logs"].pendingSubTab. The Logs tab Draw reads and clears the pending key on its first paint. /togt ml retained 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 = true AceConfig group. Phase 1 contains a single |cffffd700Mail Log|r toggle (Trade and Guild added in phases 2 / 3). The whole sub-group is disabled = function() return not addon:IsModuleEnabled("logs") end so it greys out when the Logs master toggle is off — visually expressing the AND relationship. Sub-toggles write db.global.<configKey>.enabled and call MainWindow: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>.enabled flag — 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 from addon.modules at registration time so new modules added in future patches automatically grow a toggle without per-module wiring in Settings.lua — they just need to declare Tab.configKey = "<sectionName>" alongside tabKey / label. New addon:IsModuleEnabled(tabKey) helper reads the flag via the module's configKey field (defaults to true when no configKey is declared, so legacy / freshly-registered modules stay on by default). New MainWindow: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 call MainWindow: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 Blizzard CloseSpecialWindows() ESC-frame side-effect via a temporary _escProxy:SetScript("OnHide", nil) + restore-on-next-frame workaround that mirrors FGI's GUI/MainWindow.lua gear 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_Engineering gets a TexCoord(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-i doesn'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 via addon.UI.DetachChildren — without this the AceGUI widget pool returns the same f.frame on 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 alongside tabKey / label / WINDOW_SIZE / Draw. The help icon's OnEnter reads addon.modules[self.activeTab].help at hover time and dispatches automatically — new tabs add a help table next to Draw and 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 flat TAB_HELP table-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-flipping GameTooltip:SetOwner that picks ANCHOR_TOPRIGHT vs ANCHOR_BOTTOMLEFT based on GetScreenHeight() - 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 or tooltipTitle / tooltipBody for 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 load GUI\UI.lua between GUI\RowList.lua and GUI\MainWindow.lua. Location: GUI/UI.lua, all six TOGTools*.toc.

Bug Fixes

  • addon:OpenSettings crashed on Retail MidnightGUI/Settings.lua captured only the first return value from AceConfigDialog-3.0:AddToBlizOptions("TOGTools", "TOG Tools") (the panel frame) and tried to open with Settings.OpenToCategory(_blizPanel.categoryID or "TOG Tools"). On Midnight builds the panel frame doesn't carry a categoryID field, so the string fallback "TOG Tools" was passed in — Settings.OpenToCategory calls C_SettingsUtil.OpenSettingsPanel(openToCategoryID, ...) which now strictly validates openToCategoryID as an integer in [-2^31, 2^31-1], throwing bad argument #1 to 'OpenSettingsPanel' (outside of expected range) for the string. This wasn't visible before v0.4.0 because nothing called OpenSettings on Midnight in practice — the new bottom-row gear icon is the first caller that exposed it. Fix: capture both return values from AddToBlizOptions (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 _categoryID directly to Settings.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's GUI/SettingsPanel.lua after Grouper hit the same issue. Also dropped the third-tier InterfaceOptionsFrame:Show() fallback — it opens to whatever category was last visited (not TOG Tools), and modern Retail doesn't have InterfaceOptionsFrame anyway. Location: GUI/Settings.lua.

  • TOGTools failed to load on current Retail (Midnight 12.0.x patches)TOGTools_Mainline.toc declared ## 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 ships 120000, 120001, 120005, !BugGrabber ships up to 120007, Ace3 covers 120000, 120001 and below. Clients on 12.0.5+ marked TOGTools as out-of-date, and because ## Dependencies: Ace3, !TOGT requires !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 to 110207, 120000, 120001, 120005, 120007, and added ## X-Min-Interface: 110207 for 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:Debug uses addon.UI.Brand instead of the hardcoded "|cffFF8000TOGTools|r" literal. Pipes the debug-print tag through the global brand color so any future change to addon.BrandColor propagates automatically. Lookup is deferred to call time, which is always after GUI/UI.lua has loaded. Location: TOGTools.lua.

  • Main window title uses addon.UI.Brandf:SetTitle(addon.UI.Brand("TOG Tools")) replaces the literal "|cffFF8000TOG Tools|r" in MainWindow:Open. Same propagation benefit as the Debug refactor. Location: GUI/MainWindow.lua.

  • Silenced pre-existing unused-local Lua hinttg:SetCallback("OnGroupSelected", function(_widget, _event, group) flagged _event as unused under the project's .luarc.json rules. Renamed to _ to satisfy the lint check (single underscore is the canonical "ignore" convention; the surrounding _widget keeps 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 ApplyPrefix added 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: in ApplyPrefix, after building the prefix string from cfg.format and cfg.nickname, early-exit when string.sub(msg, 1, #prefix) == prefix. Plain sub comparison (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 (Retail EventRegistry and Classic OnKeyDown) are unchanged. Location: Modules/NamePrefix/NamePrefix.lua.

[v0.3.4] (2026-05-22) - TBC /camp /exit /logout Taint Fix

Bug Fixes

  • /camp, /exit, /logout typed in chat tripped ADDON_ACTION_FORBIDDEN on TBC / Anniversary 1.15.x with NamePrefix active — v0.3.3 replaced the per-editbox OnEnterPressed script slot with an addon-Lua closure that invoked the captured original script via securecall(origScript, editBox, ...). The securecall boundary clears taint at its call site, but on TBC / Anniversary the slash-dispatch chain (ChatFrameEditBoxMixin:OnEnterPressedSendTextParseText → slash-command dispatcher → protected Logout()) still failed the secure-execution check for Logout() — 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 once SetScript runs on it, so the C-level taint check sees addon ownership on the dispatch path regardless of the securecall clear at the boundary. Switched the Classic / older-Retail hook from SetScript("OnEnterPressed", ...) + securecall(origScript) to HookScript("OnKeyDown", ...) keyed on key == "ENTER" or "NUMPADENTER". OnKeyDown fires in its own C dispatch frame, ahead of OnEnterPressed, which is dispatched as a separate C-level call — our hook applies the prefix via SetText and returns to C before OnEnterPressed begins, so addon Lua is never on the call stack when SendText / ParseText / Logout() run. The OnEnterPressed script 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. HookScript chains alongside any existing OnKeyDown handler instead of claiming the slot; chat editboxes have no default OnKeyDown binding, so our hook is the only one on the editbox. The retail 12.0+ EventRegistry "ChatFrame.OnEditBoxPreSendText" path is unchanged. The ApplyPrefix leading-/ early-exit is unchanged and continues to ensure no SetText is issued for slash commands even though the OnKeyDown hook still runs. Updated the file-header comment block to document the rejected SetScript + securecall approach alongside the previously-rejected SendChatMessage / ChatEdit_SendText / mixin-method-replace wraps, and updated the ModifyMessage doc comment to reference the new OnKeyDown entry point. Removed the v0.3.3 temporary OnKeyDown diagnostic since its purpose (verify OnKeyDown fires on TBC ahead of OnEnterPressed) 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 global ChatEdit_SendText (Name2Chat's pattern). Both wraps installed cleanly (verified with a temporary install-time print) but neither fire-time path was ever entered for user-typed chat on TBC / Anniversary 1.15.x — confirmed empirically with a temporary fire-time print inside ModifyMessage. On those clients ChatFrameEditBoxMixin:OnEnterPressed dispatches via self:SendText() using a captured local reference, so addon-level global reassignment never intercepts user chat. On Classic Era 1.15.x the global ChatEdit_SendText path is still active and did fire during testing, which initially masked the TBC/Anniversary regression. Restored v0.3.2's per-editbox SetScript("OnEnterPressed", ...) wrap that walks ChatFrame1EditBox..ChatFrameN.EditBox via NUM_CHAT_WINDOWS — this is the only entry point that reliably fires on every Classic flavor. Verified empirically on TBC (Anniversary, Wowhead Looter interface 20505) and Classic Era (interface 11508). Location: Modules/NamePrefix/NamePrefix.lua.
  • Per-editbox OnEnterPressed wrap tripped ADDON_ACTION_FORBIDDEN on /logoutSetScript("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 original OnEnterPressed we invoke from inside our handler runs through ChatEdit_SendTextChatEdit_ParseText → slash-command dispatcher → protected Logout(). Because addon Lua is still on the stack, taint propagates the whole way and the secure-execution layer blocks Logout() on TBC and Anniversary. Fix: invoke the captured original script via securecall(origScript, editBox, ...) instead of a direct call. securecall is the canonical taint-clearing dispatcher (see WoWWiki "Secure Execution and Tainting") — it saves and clears the current taint flag around the call so ParseText → slash-handler → Logout() runs untainted regardless of what sits on the C stack above. Belt-and-suspenders: ApplyPrefix already early-exits on a leading /, so the editbox text is never modified for slash commands and its GetText result 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 /say messages alongside the existing guild / officer / party / raid / instance toggles. SAY case added to the ApplyPrefix chatType dispatch table; say = false added to DB_DEFAULTS.global.namePrefix. Location: Modules/NamePrefix/NamePrefix.lua, GUI/NamePrefixTab.lua, TOGTools.lua.
  • Account-wide debug flag + addon:Debug() helper — New Debug section in the Settings panel (ESC → Options → Addons → TOG Tools, or right-click the minimap button) with a Verbose debug output toggle. Stored at db.global.debug, default false. 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 confirmation NamePrefix: hook installed (OnEnterPressed xN) (one-shot at addon load — requires /reload to re-fire after toggling the flag) and the per-send fire line NamePrefix 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 falsesay, guild, officer, party, raid, instance all default to false in DB_DEFAULTS.global.namePrefix. Previously guild and officer defaulted to true. 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 — Added securecall, rawget, rawset to diagnostics.globals for 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_SendText to the global ChatEdit_OnEnterPressed to avoid tainting OPie's securecall(ChatEdit_SendText, ...) macrotext dispatch. On modern Classic builds (BCC, Anniversary 1.15.x) the chat editbox's OnEnterPressed script is the ChatFrameEditBoxMixin:OnEnterPressed method, NOT the global — so the wrapped global never fired and outgoing messages went un-prefixed. Replaced the global-wrap with a per-editbox SetScript("OnEnterPressed", ...) wrap that walks ChatFrame1EditBox..ChatFrameN EditBox (NUM_CHAT_WINDOWS) and prepends ModifyMessage to each editbox's existing script. This catches both the legacy ChatEdit_OnEnterPressed script binding and the modern mixin-method binding without touching any global function reference. Verified safe for OPie: the wrap only touches user-visible ChatFrameN editboxes, not OPie's synthetic Rewire editboxes, and never replaces ChatEdit_SendText or ChatFrameEditBoxMixin.SendText. Confirmed against Name2Chat's own analysis that EventRegistry "ChatFrame.OnEditBoxPreSendText" is not fired on Classic 1.15.x despite EventRegistry being backported — the existing gv.isRetail120Plus gate stays correct. Location: Modules/NamePrefix/NamePrefix.lua.
  • .luarc.json — Added NUM_CHAT_WINDOWS to diagnostics.globals.

Improvements

  • MainWindow remembers last-selected tab — Opening the main window (minimap button, /togt, or Toggle() with no arg) now restores the tab that was active the last time the user closed the window. Stored as db.char.lastTab; per-character so different alts can have different defaults. Resolution order in MainWindow: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 via OnGroupSelected. Location: GUI/MainWindow.lua, TOGTools.lua.
  • NamePrefix is now account-wide — Moved namePrefix from DB_DEFAULTS.char to DB_DEFAULTS.global so the nickname, format string, per-channel toggles, and hideIfCharName are configured once and shared across every alt. Toons that shouldn't self-prefix rely on hideIfCharName (now defaulting to true) to suppress the prefix when the nickname matches the character's own name. One-shot migration in Ace:OnInitialize copies any pre-upgrade db.char.namePrefix with a non-empty nickname into db.global.namePrefix the 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 defaulthideIfCharName now defaults to true in DB_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, and UpdateAddOnMemoryUsage were called as bare globals, but TBC+ moved them all into C_AddOns.*. Added five compat locals at the top of the module that prefer C_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 each MAIL_INBOX_UPDATE event, stamping absolute expiresAt = 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 - stale flag for snapshots older than 7 days. Live-refresh on MAIL_INBOX_UPDATE while 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 when addon.MainWindow.activeTab == "mailbox"). Account-wide storage in db.global.mailbox.snapshots[Name-Realm] so every alt's data is visible from any character, full Name-Realm keys throughout to prevent collisions across connected-realm clusters. Location: Modules/Mailbox/Mailbox.lua, GUI/MailboxTab.lua.

Improvements

  • AceDB schema — Added global namespace to DB_DEFAULTS with mailbox = { thresholdDays, snapshots }. Account-wide scope so cross-alt mail data persists across every physical realm in a connected-realm cluster (per-db.realm would 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 mailbox open the Mailbox tab directly; help output updated. Location: SlashCommands.lua.
  • .luarc.json — Added GetInboxNumItems, GetInboxHeaderInfo, and ChatEdit_OnEnterPressed to diagnostics.globals so 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.lua and GUI\MailboxTab.lua after the AddonLoad pair.

Bug Fixes

  • NamePrefix broke OPie macrotext ring entries (custom mounts, /cast macros) — On Classic clients, NamePrefix wrapped the global ChatEdit_SendText to enable pre-send text modification. OPie's Libs/ActionBook/Rewire.lua dispatches each line of macro ring entries via securecall(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, /cast slots), while direct spell-cast slots that bypass macrotext (most tradeskills) kept working. Fix: relocated the Classic hook target from ChatEdit_SendText to ChatEdit_OnEnterPressed, which is upstream of ChatEdit_SendText in 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 (hooksecurefunc fires 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 !TOGT companion 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 the TOGToolsEarlyData global. The Addon Load Monitor reads it at display time and shows a status-bar indicator (!TOGT active / !TOGT missing).
  • Shared addon.RowList component — New file GUI/RowList.lua, ported and trimmed from FastGuildInvite's GUI/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-column sortKey/sortDescDefault/sortable/gapBefore opts and tie-break on entry.name, alternating row banding, virtual-scroll pool that grows with the parent's height, custom Blizzard-textured scrollbar, mouse-wheel scroll. Cells use SetWordWrap(false) + SetMaxLines(1) and anchor LEFT/RIGHT to 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 after Compat.lua, before MainWindow.lua.
  • Compat.lua shared GUI helpersaddon.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.000sGetTime() is frame-locked, so every ADDON_LOADED event fired in the same loading-screen frame returned the same timestamp. Per-addon deltas collapsed to zero. Switched the timing capture in !TOGT.lua and the fallback capture in Modules/AddonLoad/AddonLoad.lua to debugprofilestop(), 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.lua was reading row.offset (cumulative seconds since the first event) instead of row.loadTime (per-addon delta). Because offset is monotonic with load order, clicking the header effectively sorted by load order regardless of direction. Comparator now reads row.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 with addon.RowList, whose anchor-based cells truncate instead of wrapping. The window's minWidth was tightened from 640 to 520 now that compaction is graceful.
  • Tab status bar leaked between tabsMainWindow:DrawTab now 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 switchMainWindow:ApplyTabSize now calls f:DoLayout() after sizing, so when switching between tabs whose WINDOW_SIZE differs, 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's SCROLLBAR_GUTTER reduced from SCROLLBAR_WIDTH + 6 to SCROLLBAR_WIDTH + 2 so the rightmost column sits ~5 px from the scrollbar. Status column gets gapBefore = 5 to break the visual collision between right-justified Load Time text and left-justified Status text.
  • Sort indicator removed from active column headerRowList:_updateHeaderText no 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 protectionRowList:Detach() orphans the row pool, header, and scrollbar to UIParent on host-widget release; a _detached guard 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 via body:SetCallback("OnRelease", ...) in GUI/AddonLoadTab.lua.
  • .luarc.json updated — Added debugprofilestop to diagnostics.globals in both TOGTools/.luarc.json and !TOGT/.luarc.json so 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), .pkgmeta with BigWigs packager config, .luarc.json (Lua 5.1 LSP), .markdownlint.json, and .github/workflows/release.yml for tag-triggered CurseForge releases.
  • Core addonTOGTools.lua: AceAddon-3.0 instance with AceConsole-3.0, AceEvent-3.0, AceHook-3.0 mixins; AceDB-3.0 schema (char scope); version detection flags (gv.isVanilla, gv.isTBC, gv.isWrath, gv.isCata, gv.isMists, gv.isClassic, gv.isRetail, gv.isRetail120Plus).
  • Slash commandsSlashCommands.lua: all /togt command 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 windowGUI/MainWindow.lua: dynamic AceGUI Frame + TabGroup; tabs auto-populate from addon.modules sorted alphabetically by label; per-tab locked/unlocked window sizing via WINDOW_SIZE; ESC-to-close proxy; position persistence via AceDB.
  • Name Prefix moduleModules/NamePrefix/NamePrefix.lua + GUI/NamePrefixTab.lua: wraps ChatEdit_SendText (Classic) or hooks EventRegistry (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 buttonGUI/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 in DB.char.
  • Addon settings panelGUI/Settings.lua: AceConfig-3.0 options table registered under ESC → Interface → Addons → TOG Tools (native Blizzard panel). addon:OpenSettings() uses Settings.OpenToCategory (Classic Era 1.15+ / Retail 10+), InterfaceOptionsFrame_OpenToCategory (older builds), or InterfaceOptionsFrame:Show() as a final fallback — detected by API presence, not version flag.
  • VersionCheck-1.0 integrationVC:Enable(Ace) called on all non-Retail versions in OnInitialize; /togt vc broadcasts a guild-wide version check and prints responses after 21 seconds.
  • Embedded libslibs/LibDataBroker-1.1.lua, libs/LibDBIcon-1.0.lua (single-file embeds, copied from TOGProfessionMaster).
  • MIT LicenseLICENSE file added; referenced in .pkgmeta via license-output.
  • CurseForge project — Project ID 1533830 set in .pkgmeta and all TOC files.
  • Governance files.github/copilot-instructions.md and CLAUDE.md with full project rules (module pattern, commit/tag process, changelog format, HTML doc rules, no-tag rule).

Bug Fixes

  • Name Prefix not reaching other playershooksecurefunc fires after SendChatMessage has already been called. Fixed by wrapping ChatEdit_SendText directly (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 ModifyMessage when the message starts with /. Location: Modules/NamePrefix/NamePrefix.lua.