TOGProfessionMaster

TOG Profession Master should help you and your guild to know faster which player has which profession and who can craft which item or who has learned which enchantment.

File Details

TOGProfessionMaster-v0.7.2

  • R
  • May 29, 2026
  • 939.90 KB
  • 12
  • 5.5.3+4
  • Classic + 2

File Name

TOGProfessionMaster-TOGProfessionMaster-v0.7.2.zip

Supported Versions

  • 5.5.3
  • 4.4.2
  • 3.4.5
  • 2.5.5
  • 1.15.8

TOG Profession Master Changelog

[v0.7.2] (2026-05-28) — Bogus cooldown entries (mage talents, portals) evicted + transmute popup correctness + banker exclusion

Bug Fixes

  • Non-profession spells (e.g. Impact rank 5 / spell 12360, Portal: Undercity) rendered as cooldown rows under alchemist / other profession characters. Root cause was a missing whitelist on two paths: (1) GUI/CooldownsTab.lua's BuildRows walked every spell ID in gdb.cooldowns[charKey] and fell through to GetSpellInfo/GetItemInfo for the row label, so any spell ID happily rendered as a "cooldown" — including Mage talents and class spells with no profession meaning. (2) Scanner:OnGuildDataReceived's cooldown: leaf handler accepted any spell ID from a peer broadcast with no validation against the addon's known cooldown set. Stale entries from old v0.6.x code paths or buggy peer broadcasts therefore stuck around forever, displaying nonsense rows on every reload. Fix: two-part. (a) BuildRows now guards the single-spell branch with data.cooldowns[spellId] or spellId == data.saltShakerItem — anything not in Data/CooldownIds.lua's whitelist gets silently skipped. Same defensive-display pattern as IsVisibleCrafter. (b) A one-shot addon:RemoveBogusCooldowns sweep runs at every OnInitialize after migration — walks gdb.cooldowns, strips any spell ID not in the whitelist (data.cooldowns / data.transmutes / groupBySpell / saltShakerItem), and stops the entries from re-broadcasting forward. Idempotent. Existing affected players see their cooldown DB cleaned on the next /reload; the display guard catches any new ones that arrive via future inbound payloads.

  • Transmute popup showed transmutes the alchemist doesn't know. The cooldown-derived emit branch in GUI/CooldownsTab.lua accepted any spell ID in tg.spellIds whose live GetSpellInfo name matched "Transmute" — even when the alchemist's gdb.recipes[171] cross-reference returned no hit. Stale cooldown records (transmutes they had on CD before unlearning, or pre-v0.7.0 leftovers, or peer-broadcast pollution under wrong charKey) therefore surfaced as phantom popup rows for transmutes the char no longer knows. Fix: the cooldown-derived branch now requires recipeBySpellId[sid] to resolve (i.e., the spell ID is one the char actually owns per the recipe DB cross-reference). The "Transmute"-name fallback is removed — anything not backed by current recipe knowledge is silently filtered.

  • Transmute popup showed different per-row timers for spells that share one cooldown. In Classic Era / TBC / Wrath, all alchemy transmutes share a single ~20-hour cooldown — but the game only records the CD under the spell ID the alchemist actually cast. The popup's per-row time column used charCds[spellId] to look up the time per entry, so the cast spell showed (e.g.) "5h 17m" while every other transmute in the same popup showed "Ready" — visually inconsistent and misleading (the others aren't actually castable). Fix: transmute-group popup rows now use the group's shared row.expiresAt (the max future expiry across every transmute spell the char knows) for every entry. Non-transmute group popups (Mooncloth tier, etc.) still resolve per-spell — those CDs are genuinely independent. Location: GUI/CooldownsTab.lua ShowGroupPopup.

  • TOGBankClassic banker alts surfaced spurious Salt Shaker cooldown rows. A bank toon with no cooking skill had a stale Salt Shaker (item 15846) CD record in gdb.cooldowns — most likely from a pre-repurposing scan, or peer-broadcast pollution under the wrong charKey, or pre-v0.7.0 data that survived migration. The CD was display-only noise (bankers can't cast Salt Shaker anyway), but it cluttered the Cooldowns tab with a row that always read "Ready" and could never actually go anywhere. Fix: added addon.Bank.IsBanker(charKey) helper in Compat.lua that compares the charKey's short-name against TOGBankClassic_Guild:GetBanks(). The Salt Shaker branch of BuildRows in GUI/CooldownsTab.lua now skips emit when the row's charKey is a banker. No-op when TOGBank isn't loaded.


[v0.7.1] (2026-05-28) — Vanilla / Classic Hardcore scan-key bug + minimap position persistence

Bug Fixes

  • Vanilla / Classic Hardcore recipes scanned but didn't display. On TBC and later, GetTradeSkillRecipeLink returns |Henchant:SPELLID|h… for every profession, so Scanner:ExtractTradeSkillId returns the spell ID — which is the same key addon.recipeDB is built around, and the cross-reference at display time works. On Vanilla (and 1.15.x Classic Hardcore which inherits the same APIs), the link format is |Hitem:ITEMID|h… for everything except Enchanting, so ExtractTradeSkillId returns the crafted item ID instead. The scanner then wrote gdb.recipes[197][2996] = { crafters = … } (Bolt of Linen Cloth's item ID) while BrowserTab / DumpRecipe / every consumer looked up gdb.recipes[197][2963] (the spell ID). Same recipe, two different keys, lookup misses, UI shows nothing. A character could scan 60 Tailoring recipes and see none of them. Enchanting always worked because it uses enchant: links on Vanilla too; BS only "worked" on TBC for the same reason. Fix: a reverse lookup addon:GetSpellIdForCraftedItem(profId, craftedItemId) (built lazily per profession from addon.recipeDB[profId].craftedItemId). MergeRecipesIntoGdb and MergeCraftersIntoGdb now resolve item IDs to spell IDs before storing, so future scans + peer broadcasts land on the right key. Plus a one-shot recovery pass addon:RemapItemKeysToSpellIds runs at every OnInitialize — idempotent, walks gdb.recipes, and moves any item-ID-keyed entry whose value matches a craftedItemId in the shipped DB onto the corresponding spell-ID slot (unioning crafter sets when both keys carry data). Existing affected users get their data recovered on the next /reload. Location: TOGProfessionMaster.lua (helper + recovery), Scanner.lua (MergeRecipesIntoGdb + MergeCraftersIntoGdb).

  • Minimap button always reset to its default angle (220°) on every /reload. GUI/MinimapButton.lua's SetupMinimapButton was passing a FRESH local table (minimapData = { hide = ..., minimapPos = ... }) to LibDBIcon:Register on each load. LibDBIcon does write the new angle back into the table when the user drags the button — but the throwaway local goes out of scope at function end and the updated minimapPos never reaches Ace.db.profile. On the next reload, SetupMinimapButton reads the original default value and re-pins the button there. Fix: give LibDBIcon a sub-table that lives directly on the AceDB profile (Ace.db.profile.minimap). LibDBIcon's writes now land in a persisted location, so the button stays where you put it across reloads, character switches, and full client restarts. The legacy profile.minimapPos field stays in place as a one-time seed for existing users so nobody loses their last-set position on the first v0.7.1 launch.


[v0.7.0] (2026-05-28) — Flat universal recipe DB + libguildroster visibility gate + slim sync protocol (backwards-breaking)

This is a major architectural rework. The SV schema flattens from per-guild buckets to a single universal recipe table, the sync protocol drops the recipe-metadata leaf entirely (metadata lives in the shipped addon.recipeDB now), and crafter visibility is gated against libguildroster so departed members get swept out automatically. Wire-protocol incompatible with v0.6.x and earlier — the DeltaSync namespace bumps from TOGPmv2 to TOGPmv3, so old clients and new clients silently don't sync with each other during the transition window.

Architectural rework

  • Flat universal recipe table. TOGPM_GuildDB.global.guilds[guildKey] is gone. Recipes live in a single gdb.recipes[profId][recipeId] = { crafters = { [charKey] = guildTag } } table — one row per recipe, every crafter (across every guild) tagged inline with the guild they were sync'd via. Per-character data (cooldowns, skills, specializations, factions, accountChars, altGroups, syncTimes) moves to the top level (charKey is globally unique, no guild scope needed). The recipe-metadata fields (name, icon, reagents, itemLink, recipeLink, isSpell, spellId) that used to be stored per recipe are no longer in the SV at all — they're looked up from the shipped addon.recipeDB at render time via the new addon:GetRecipeName / GetRecipeIcon / GetRecipeReagents / GetRecipeCraftedItemId helpers. Location: TOGProfessionMaster.lua (new GUILD_DB_DEFAULTS, recipe-meta accessors).

  • Guild registry with FNV-1a tags. Each guild gets registered in gdb.guildRegistry[tag] = { name, faction, key } where tag is a 6-hex-char FNV-1a-32 hash of the guildKey ("Faction-GuildName"). Deterministic — every client computes the same tag for the same guild, so a crafter sync'd from Alice's client and Bob's client gets the same tag locally. The reserved tag "personal" covers guildless own alts (only visible to the owning player). Location: TOGProfessionMaster.lua (fnv1aHash6, GetGuildTagFor, GetCurrentGuildTag).

  • libguildroster visibility gate at every display site. New addon:IsVisibleCrafter(charKey, crafterTag) rule:

    1. Own alts (tracked via accountChars) are ALWAYS visible regardless of guild.
    2. Tag mismatch with the player's current guild → hidden AND queued in gdb.pendingPurge for the timed sweep.
    3. Tag matches but charKey isn't in GuildCache:IsInGuild → hidden + queued, unless they're an alt of someone in the roster (bank alts of in-guild mains stay visible via IsAltOfInRosterCharacter). Covers the "left the guild" + "switched guilds" cases symmetrically: when the user is guildless or in a new guild, every old-guild crafter fails rule 2 and gets queued for purge. Location: TOGProfessionMaster.lua.
  • Timed purge sweep on OnRosterReady + 60s. GuildCache:RegisterCallback("OnRosterReady", ...) schedules addon:RunPendingPurge() 60 seconds after the initial guild roster scan completes. The 60-second buffer covers straggler GUILD_ROSTER_UPDATE events on large rosters (>500 members) where the roster trickles in over multiple ticks. The sweep walks gdb.pendingPurge and for each charKey strips every reference across gdb.recipes (crafter sets), cooldowns, skills, specializations, factions, syncTimes, and altGroups (own entry + sibling references). Empty guildRegistry entries get dropped as their last crafter is removed. Net effect: the DB stays trimmed to active guild members forever, no long-term bloat. Location: TOGProfessionMaster.lua (RunPendingPurge), OnEnable wires the callback.

  • Slim sync protocol. DeltaSync namespace bumps from TOGPmv2 to TOGPmv3. The recipemeta:<profId> leaf is REMOVED entirely (every byte of name/icon/reagents/links was redundant since receivers already have it in addon.recipeDB). The crafters:<profId> leaf shrinks to bare {[recipeId] = {[charKey] = true}} — guild tags are derived locally at receive time from the receiver's own current guild context (inbound data always arrives through the receiver's guild channel, so all crafters in the payload share the receiver's tag). Per-recipe payload drops by ~90%; hash-leaf negotiation gets much faster. Cooldowns leaf is unchanged. Location: Scanner.lua (BuildLeafPayload, OnGuildDataReceived, MergeCraftersIntoGdb).

  • Receiver hides unknown recipeIds silently. If a sender's addon DB knows a recipe the receiver's (older or different-version) DB doesn't ship, the receive path skips it — no "Unknown #N" placeholder, no SV pollution. The right answer is "ship an addon update" rather than "sync metadata at runtime."

New Features

  • Dutch (nlNL) locale added. Same override-only mechanism as Thai and Filipino — nlNL isn't a WoW-recognized locale code (Dutch-speaking players play on enGB or enUS clients), so AceLocale never auto-selects it. Dutch users opt in via the Settings "UI Language Override" dropdown which now lists "Nederlands" alongside the existing 14 languages. Best-effort translation; native-speaker review welcome. Location: Locale/nlNL.lua, GUI/Settings.lua.

  • "Show all recipes" toggle on the Browser tab toolbar. Default off (today's behavior — only recipes someone in the guild knows appear). When on, every recipe in the shipped addon.recipeDB appears in the list with no-crafter rows rendered greyed out, so officers can scan the gaps at a glance.

  • "Show Missing" entry in the View dropdown. Surfaces ONLY recipes nobody in the guild knows (across the selected profession or all professions). The dropdown entry is hidden until "Show all recipes" is on, so the two controls operate independently — toggle "Show all recipes" to see what's missing inline, switch View to "Show Missing" for a focused gap list. Location: GUI/BrowserTab.lua (new toolbar checkbox + View dropdown entry, BuildRecipeList accepts { showAll = bool } opts + new missing view-mode).

Migration

  • v0.6.x → v0.7.0 is one-shot at first OnInitialize. MigrateGuildDb() runs once when gdb.schemaVersion is missing or < 7. Cooldown timers are preserved (merged out of every old gdb.guilds[guildKey].cooldowns table into the new flat gdb.cooldowns) — those are time-sensitive and re-scanning loses the active expiry. Everything else is wiped: recipes / skills / specializations / hashes / lastScan all rebuild organically on the next trade-skill scan and DeltaSync exchange (cheap now that we're not syncing metadata). The old gdb.guilds tree is dropped. accountChars and syncLog survive in place (already top-level). There is no rollback — going back to v0.6.x on a v0.7.0-migrated SV would see an empty DB.

  • Bugs caught during pre-ship testing:

    1. UI Language Override was completely broken. ApplyLocaleOverride read self.db.profile.uiLanguageOverride, but self is addon and the AceDB instance lives on the AceAddon object (addon.lib), so the read was always nil. The function fell through to "auto" and never applied the override — so picking Spanish, French, anything in Settings did nothing. Fix: read from addon.lib.db correctly. This bug pre-dated v0.7.0 (introduced in v0.6.2 when the override was added) and was just never noticed until a user tested with a non-default locale during v0.7.0 pre-ship.
    2. addon.PROF_NAMES froze English strings at module load. The table was populated with [171] = L["ProfAlchemy"], ... at file-load time, before ApplyLocaleOverride had a chance to mutate the AceLocale table. So even with the override fixed, profession dropdowns + tooltips would still show English. Fix: PROF_NAMES is now rebuilt via addon:RebuildLocalizedTables() (driven by a PROF_LOCALE_KEYS lookup), and ApplyLocaleOverride calls the rebuild after mutating the locale table.
    3. TAB_DEFS in GUI/MainWindow.lua had the same module-load-time L capture problem. Tab labels stayed English even when the AceLocale table had been mutated to Spanish. Fix: converted to a getTabDefs() function that reads L["..."] at tab-creation time.
    4. schemaVersion was wrongly in defaults. AceDB applies defaults BEFORE OnInitialize runs, so on the first v0.7.0 launch the default schemaVersion = 7 got written into the SV before MigrateGuildDb could run — the migration's early-return check then tripped, the cooldown-merge walk never executed, and active cooldown timers appeared wiped (the data was still in gdb.guilds[X].cooldowns but stranded). Fix: removed schemaVersion from defaults so it's set ONLY at the end of a successful migration. Migration trigger also widened to re-run when gdb.guilds still exists, so SVs that hit the first-launch bug recover their cooldown data on the next reload after patch.
    5. Thai entry in the UI Language Override dropdown rendered as boxes. WoW's default fonts only ship glyphs for Blizzard's officially supported locales (Latin / Cyrillic / Han / Hangul) — Thai script falls outside that set, so the native-script label ไทย couldn't render. Cyrillic (Русский), Korean (한국어), and both Chinese variants all work because those scripts have native WoW font support. Fix: dropdown label changed to the Latin-script Thai. The Thai UI strings themselves are unaffected — selecting Thai still applies the thTH locale; only the dropdown LABEL changed.
    6. accountChars / altClaims collision. The flat schema collapsed two formerly-separate accountChars semantics — the local-only boolean flag set ({[charKey] = true} for IsMyCharacter) and the per-broadcaster sync'd alt-group array ({[broadcasterKey] = [...]}) — into one table. When the same charKey was both an own char AND a broadcaster, the array stomped the boolean for its own slot but other charKeys stayed as true, causing a runtime crash in BuildLeafPayload when broadcasting accountchars:<charKey> for those entries (#group on a boolean). Fix: split the two semantics into separate fields — gdb.accountChars[charKey] = true (local flag, unchanged) and the new gdb.altClaims[broadcasterKey] = [...] (sync'd array). All read/write sites (OnPlayerEnteringWorld, BuildLeafPayload, OnGuildDataReceived, RebuildAltGroups, HashManager.ComputeAccountCharsHash, HashManager.HasContent, HashManager.RebuildOnFirstLoad) updated to use altClaims for the array semantics. The DeltaSync leaf key (accountchars:) is unchanged on the wire — only the internal field name moved.
  • ForEachGuildBucket + FindBucketForChar compatibility shims — both old helpers now operate over the single global table, so any GUI consumer that hadn't been migrated yet keeps working without code changes. The walk-every-bucket model collapses to a single virtual "bucket" that exposes the same field names (recipes, skills, cooldowns, etc.).

Cleanup

  • Dead-code removal. recipemeta:<profId> hash + leaf handlers removed from HashManager. MergeRecipeMetaIntoGdb, BackfillBogusRecipeNames, BackfillReagentItemIds, ScrubObsoleteRecipeNames, isObsoleteItemName, isBogusName, cleanRecipeName, GetReagentScraper, and the namespace-collision/obsolete-name guards in Scanner are now dead — left in place but unreferenced (will be stripped in a follow-up patch once we're confident the new path is stable). /togpm backfill is now a no-op that just prints a notice ("metadata lives in addon.recipeDB").

Multi-version safety

The new schema and visibility gate are entirely additive on every supported client (Vanilla / TBC / Wrath / Cata / MoP). The shipped addon.recipeDB already loads per-game-version via the TOC includes, so each client sees the right recipe metadata at render time. The libguildroster gate falls back to "visible" if GuildCache:IsInGuild is unavailable (defensive: better to over-display than to hide a real crafter).


[v0.6.3] (2026-05-28) - Profession-spec bonus-output indicator on the Cooldowns tab (TBC + Wrath) + every supported WoW locale + Thai/Filipino via UI Language Override

New Features

  • 8 new locale files + UI Language Override. Locale coverage now spans every WoW-supported locale (enUS, enGB, deDE, esES, esMX, frFR, itIT, ptBR, ruRU, koKR, zhCN, zhTW) plus two community-contributed locales that Blizzard's client doesn't natively support but TOGPM ships translations for (thTH Thai, filPH Filipino). The new UI Language Override dropdown in Settings (default Auto) lets any player force the TOGPM UI into any of the 14 languages regardless of their WoW client's actual locale — so a German player on an enUS Anniversary client can run the addon UI in German, and a Thai-speaking player on any client can run it in Thai. In-game item / spell / recipe / NPC names still come from Blizzard's APIs and render in the WoW client's actual language since those aren't shipped by this addon — only TOGPM's own UI strings get overridden. Dropdown labels show each language's name in its native script (한국어, 简体中文, ไทย, etc.) so they're recognizable regardless of the current UI language.

    • How it works: new Locale/_init.lua defines addon.NewLocale(code) which writes every L["key"] = "value" to BOTH AceLocale's standard table (for the auto-detect path) AND a parallel addon.Locales[code] store (for the override path). At OnInitialize, addon:ApplyLocaleOverride mutates AceLocale's L table in place — so every existing local L = LibStub("AceLocale-3.0"):GetLocale(...) reference picks up the override without code changes elsewhere. A /reload hint prints on change since some captured strings may already be formatted into widgets. Existing 5 locale files (enUS / deDE / esES / esMX / frFR / ruRU) were migrated to the new helper — header-line swap only, body of L[...] = "..." assignments untouched.
    • Bug fix bundled in: v0.6.2's esES → esMX mirror block was broken (it lived inside esES.lua AFTER an if not L then return end guard, so on an esMX client the file exited at line 7 before reaching the mirror — net effect: esMX clients fell back to enUS, not Spanish, despite us claiming Spanish support for esMX). v0.6.3 ships a dedicated Locale/esMX.lua with identical content to esES, so esMX clients now get Spanish at addon load just like esES clients do. Divergence can be introduced later if any LATAM phrasing differs.
    • Translation quality: all 8 new locales (and the 2 community ones) are best-effort following the German-locale precedent — official Blizzard glossary for the 15 profession names per language, best-effort everywhere else. Native-speaker review welcome on every one.
    • Multi-version safety: all 5 TOCs (TOGProfessionMaster.toc, _TBC, _Wrath, _Cata, _Mists) updated to load the full 14-locale set + the _init.lua bootstrap. The override mechanism's runtime cost is one table mutation at OnInitialize (and again on each Settings change) — zero per-frame overhead, zero impact on the auto-detect path.
  • Spec-bonus indicator on the Cooldowns tab. Small spec icon (12×12, sourced live from GetSpellTexture so no new art assets) renders to the left of the crafter name on cooldown rows where the crafter's profession spec gives bonus output on that specific cooldown. Hovering the icon shows a tooltip with the spec name (auto-localized via GetSpellInfo) and whether the bonus is guaranteed 2x output (tailoring cloth specs) or a chance to proc extra output (Transmutation Master). The detection works from any single TBC/Wrath alt running the addon — IsSpellKnown writes gdb.specializations[charKey][profId] = specSpellId and the value piggybacks the existing crafters: DeltaSync leaf for guild-wide propagation (same opportunistic-merge model as skill ranks, no extra hash leaves). Coverage:

    • Transmutation Master (Alchemy spec 28683) → indicator on every TBC + Wrath transmute row, including the collapsed transmute-group row.
    • Mooncloth Tailoring (spec 26797) → indicator on Primal Mooncloth (spell 26751).
    • Shadoweave Tailoring (spec 26801) → indicator on Shadowcloth (spell 36686).
    • Spellfire Tailoring (spec 26802) → indicator on Spellcloth (spell 31373).
    • Intentionally out-of-scope: Elixir Master and Potion Master procs apply to elixirs/flasks/potions, none of which are shared-cooldown crafts (no row on the Cooldowns tab to indicate on). Engineering Gnomish/Goblin specs gate exclusive recipes rather than producing bonus output. The TBC cloth-spec proc was changed to a flat 2x at patch 2.1 — listed as "guaranteed" in the tooltip rather than "chance".
    • Multi-version safety: the indicator render is gated on addon.isTBC or addon.isWrath only. Vanilla never had the proc system; Cata 4.0.1 removed it. On non-applicable clients the icon slot isn't reserved and column widths are unchanged (additive). Detection runs on all versions (writes empty specs = {} on Vanilla/Cata/MoP since no IsSpellKnown hits) so the addon never errors there. Location: Data/CooldownIds.lua (new SPEC_BONUSES map exposed via addon:GetCooldownData().specBonuses), Scanner.lua (new Scanner:DetectSpecializations + SPEC_SPELLS catalog, wired into login deferred scan and OnTradeSkillEvent; specializations payload added to the crafters: leaf in BuildLeafPayload and parallel merge logic in OnGuildDataReceived), GUI/CooldownsTab.lua (new getSpecBonus helper + 14px reserved icon column in DrawRow).

Code Reuse

  • Re-introduced the spec detection infrastructure that v0.6.0 ripped out. v0.6.0's cleanup deliberately preserved the gdb.specializations AceDB schema entry and the purge logic in GUI/Settings.lua as "zero-cost plumbing for re-introducing spec-aware filtering as a proper feature later" — that bet paid off this patch. The new SPEC_SPELLS catalog mirrors the structure of the previous one but trims out the speculative dead code (no profession filter side, no SPEC_SPELLS-as-filter-input wiring); just detection → store → render.

[v0.6.2] (2026-05-27) - Three new EU locales: Spanish (esES), French (frFR), Russian (ruRU)

New Features

  • Three new EU locale files shipped. Extends v0.6.0's AceLocale-3.0 infrastructure with esES (Spanish — Spain), frFR (French), and ruRU (Russian). Each file mirrors the 265-key structure of Locale/enUS.lua so any missing key falls back to English automatically. Profession display names use the official Blizzard glossary for each language (Alchemy → Alquimia / Alchimie / Алхимия, Tailoring → Sastrería / Couture / Портняжное дело, etc., all 15 professions). The esES file also mirrors its translations into the esMX (Latin-American Spanish) locale slot so esMX clients pick them up directly instead of falling all the way back to enUS — same Blizzard glossary, no need for a parallel file. Translations for the rest of the UI are best-effort following the German precedent (ship + iterate from player feedback); native-speaker review welcome. Location: Locale/esES.lua, Locale/frFR.lua, Locale/ruRU.lua.

Improvements

  • All four non-English locales now load on every supported expansion. v0.6.0 wired German only into the main (TOGProfessionMaster.toc) Classic Era TOC — the four variant TOCs (_TBC.toc, _Wrath.toc, _Cata.toc, _Mists.toc) still only included Locale\enUS.lua, meaning a German player on TBC Anniversary or Wrath got the English UI even though Locale/deDE.lua was sitting right there in the addon folder. Backfilled the Locale\deDE.lua include in all four variant TOCs alongside the three new EU locale entries; all five TOCs now ship the same 5-locale load list (enUS, deDE, esES, frFR, ruRU).

[v0.6.1] (2026-05-26) - Defensive pcall around SetItemByID + German profession-name validation + manual TBC phase-override mechanism

Data Quality

  • Manual TBC content-phase override mechanism (tools/manual_phase_overrides.json) — corrects recipes the ATT-derived phase map (v0.5.4) misclassified. ATT sometimes derives a recipe's phase from an awp patch tag or reputation gate that reflects when the recipe's DATA was last touched rather than when it became obtainable, so world-drop formulas available since TBC launch can wrongly end up gated behind a later phase. The override wins over the ATT value in build_authoritative_data.py's emit_recipe_file; a phase=1 override strips the phase field entirely so the recipe is always visible. First entries (user-reported by a TBC player): Enchant Boots - Cat's Swiftness (spell 34007) and Enchant Boots - Boar's Speed (spell 34008) — both BoE world-drop formulas available since TBC launch (Phase 1) but ATT had tagged them Phase 4, hiding them from the Missing Recipes tab on Phase 2. Now always-visible. Future phase misclassifications get the same one-line-JSON-entry treatment (report → add → regenerate) rather than re-deriving the whole ATT dataset.

Improvements

  • All 15 German profession names now native-speaker validated. v0.6.0 shipped with 9 names provided by the German contact and 6 my-best-guess standard WoW-DE terms (Herbalism, Skinning, Jewelcrafting, Inscription, Fishing, Smelting). Native-speaker review confirms all 6 guesses were correct as-shipped (Kräuterkunde, Kürschnerei, Juwelenschleifen, Inschriftenkunde, Angeln, Verhütten) — and corrected one of the original 9: Tailoring changes from Schneiderkunst to Schneiderei (the noun form for the profession). Updated Locale/deDE.lua L["ProfTailoring"] and the FilterProfessionDesc example string accordingly.

Bug Fixes

  • 88x error storm in BugSack when opening Missing Recipes tabs against broken peer addons. User report after v0.6.0 ship: opening the Leatherworking Missing Recipes tab generated 88 RecipeMaster errors (RecipeHandler.lua:43: attempt to index local 'recipe' (a nil value)) in BugSack. Investigation: RecipeMaster's TooltipHandler globally hooks the tooltip-set event chain, then calls getAllCharactersRecipeStatus which iterates the character's known spell IDs and indexes its own recipeDB[spellId] without nil-checking. When RecipeMaster's recipeDB isn't initialised for the current profession context (the locals dump showed rm.recipeDB = {}, rm.displayedProfession = ""), every lookup returns nil and the hook crashes. The bug exists in RecipeMaster, not in TOGPM, but our GameTooltip:SetItemByID call at GUI/MissingRecipesTab.lua:821 is what fires the tooltip-set event chain that RecipeMaster's broken hook listens on. v0.6.0's Missing Recipes accuracy fixes (the craftedItemId known-recipe fallback in particular) changed which recipes appear in the missing list — recipes the user genuinely doesn't have are now correctly shown instead of recipes they already knew. That shifted the set of hover targets toward exactly the subset RecipeMaster's broken nil-check trips over, exposing a latent bug at high volume.
  • Fix: narrowly pcall-wrap the GameTooltip:SetItemByID(f._itemId) call only — same defensive pattern as the existing comment block at the same site contemplated for LoonBestInSlot. Blizzard's C-level SetItemByID runs to completion before third-party hook callbacks fire, so the tooltip itself still populates correctly; the pcall catches errors that bubble back up from the hook chain so they don't pile up in BugSack. Other call paths (SetSpellByID, text fallback, the surrounding OnEnter/OnLeave scripts) are unchanged. Tradeoff: pcall would hide future errors from our own code if we ever break the SetItemByID call site, but the pcall is narrowed to that single line to keep the blast radius small. The underlying RecipeMaster bug still needs reporting upstream — fix on their end is a one-line nil-check at RecipeHandler.lua:43 before indexing recipe.

[v0.6.0] (2026-05-26) - Multi-language support: German (deDE) is the first translation + Classic Era Missing Recipes accuracy fixes + spec dead-code cleanup

New Features

  • Multi-language support; German (deDE) is the first translation. Wired AceLocale-3.0 through every user-facing string in the addon — 265 keys covering the main window, all four tabs (Professions, Cooldowns, Reagents, Missing Recipes), the settings panel, all tooltips (column headers, button hovers, recipe rows, minimap), the supply-mail composer (subject + body), the bank-request dialog, every error / status print, slash-command help output, and every alert. Recipe names / item names / reagent names come back from the WoW client's GetItemInfo / GetSpellInfo localized for free because they're keyed by ID — zero work to ship those. The German Locale/deDE.lua mirrors the structure of Locale/enUS.lua exactly; any missing key falls back to English automatically via AceLocale's default-locale mechanism. The 9 core profession names (Alchemy → Alchimie, Blacksmithing → Schmiedekunst, Cooking → Kochkunst, Enchanting → Verzauberkunst, Engineering → Ingenieurkunst, First Aid → Erste Hilfe, Leatherworking → Lederverarbeitung, Mining → Bergbau, Tailoring → Schneiderkunst) were provided by a German-native contact; the remaining 6 (Herbalism, Skinning, Jewelcrafting, Inscription, Fishing, Smelting) use standard WoW-DE terminology pending native-speaker review. Native-speaker review for the full string set is also welcome. Adding additional locales (frFR, esES, ruRU, zhCN, etc.) is now a copy-template-and-translate task — no further code changes needed. Location: Locale/deDE.lua, Locale/enUS.lua, TOGProfessionMaster.toc.

  • Profession names propagated through the locale table. addon.PROF_NAMES (in TOGProfessionMaster.lua) now reads from L["ProfAlchemy"] etc. instead of carrying hardcoded English. The table is the single source of truth for profession display names across the entire addon — every dropdown (Profession Browser, Cooldowns filter, Missing Recipes filter), the [TOGPM] item tooltip line, and the crafter-online alert text all pull from it. One locale change propagates to every site automatically.

Bug Fixes

  • Missing Recipes on Classic Era flagged recipes you already know as missing. User report on a Cooking character: 82 recipes shown as missing despite knowing ~99% of them. Same on Leatherworking: 243 missing despite knowing nearly all of them. Two distinct issues stacked:
    1. Known-recipe detection mismatch. For non-Enchanting professions, gdb.recipes[profId] is keyed by crafted-item ID (e.g. Charred Wolf Meat lives at key 2679, the item id, not key 2538, the spell id). The match against the authoritative addon.recipeDB then relies on rd.spellId being populated on the scanned entry. BuildSpellNameCache in Scanner.lua walks the player's spellbook to map recipe-name → spell-id, but on Classic Era this enumeration misses some profession recipe spells (they're not flagged as "spell" spellbook items the way TBC+ flags them), leaving rd.spellId nil. Without rd.spellId, knownByChar(spellId) returned false for recipes the player actually knows, and they showed up as "missing."
    2. Post-Vanilla recipes leaking onto Era clients. The recipe DB has minExpansion coverage gaps — Vanilla baseline recipes correctly carry no tag (intentional, they show on every client), but a handful of TBC / Cata / SoD recipes were also untagged in v0.5.7 (Crystal Throat Lozenge spell 30047 TBC, Smoked Redgill 470370 post-Cata, Prowler Steak 1225758+ SoD/Anniversary, etc.) and slipped past the existing cross-expansion gate. Fixes in GUI/MissingRecipesTab.lua BuildMissingList:
    • craftedItemId fallback in the known-recipe check. When iterating addon.recipeDB[profId] by spell ID, also check knownSpells[data.craftedItemId] — since the gdb table key IS the crafted-item ID for non-Enchanting professions, the lookup hits via the table key even when rd.spellId is nil. Sits alongside the existing data.teaches fallback.
    • Defensive cross-expansion gate for untagged recipes on Classic Era. When data.minExpansion is nil AND clientExp == 1 (Vanilla) AND the recipe's spell ID is > 25000, hide it. Every Vanilla recipe spell ID sits in the 2000–25000 range; anything higher with no tag is post-Vanilla content the Era client can't learn. Belt-and-suspenders to the existing requiredSkill > clientMaxSkill filter, which only catches reqSkill > 300 (low-skill TBC recipes like Crystal Throat Lozenge slip past that check). Both fixes are purely additive (elseif branches in the existing skip-decision chain) — TBC / Wrath / Cata / MoP behavior is unchanged because the defensive gate is clientExp == 1 only, and the craftedItemId fallback only adds matches, never removes them.

Data

  • 1 additional never-shipped recipe excluded: Leatherworking spell 19106 "Onyxia Scale Breastplate" — the planned matching chest piece to the shipped "Onyxia Scale Cloak" (spell 19092, Onyxia attunement reward). The Breastplate was cut before going live; recipe scroll item 15780 and crafted item 15141 also never shipped. Removed from Data/Recipes/Leatherworking.lua and added to MANUAL_EXCLUDED_SPELLS in tools/build_authoritative_data.py so future rebuilds don't reintroduce it. User-reported on Classic Era.

Code Cleanup

  • Removed unused specializations filter infrastructure. Identified during the subclass-aware-filtering investigation that data.specialization is never populated on any recipe in the shipped DB — the filter line in BuildMissingList that checked it, along with the playerSpec / specs / skillsBucket local lookups feeding it, plus Scanner:DetectSpecializations and its SPEC_SPELLS catalog, was speculative future-proofing with no data source behind it. The dead-code path created a latent bug too: when playerSpec was nil (Vanilla, or any character pre-spec-quest on TBC+), the filter data.specialization ~= playerSpec would have hidden every spec-tagged recipe — except it never fired because no recipe was tagged. Ripped out the read path in GUI/MissingRecipesTab.lua and the write path in Scanner.lua. The gdb.specializations AceDB schema entries and the purge logic in GUI/Settings.lua are kept in place — zero-cost plumbing for re-introducing spec-aware filtering as a proper feature later (would need a per-recipe spec data source that doesn't currently exist in any upstream source we mine).

Known Gaps (intentional, deferred)

  • Profession-subclass filtering (LW: Tribal / Elemental / Dragonscale, Engineering: Goblin / Gnomish, BS: Armorsmith / Weaponsmith, Alchemy TBC: Potionmaster / Elixir / Transmutation, etc.) is deferred to a follow-up patch. Requires authoring per-recipe spec tags that aren't in any upstream data source we currently mine (wago.tools SkillLineAbility doesn't expose spec gating). The dropdown UI infrastructure can be wired up cheaply once the data exists. User-driven dropdown (with auto-detect default via IsSpellKnown) is the intended UX shape.
  • Specific NPC names in the Missing Recipes "Sources" tooltips still render in English on a deDE client. Recipe / item / reagent names already localize automatically via the WoW client's API (keyed by ID), but creature names aren't in any DBC (they're server-side data). Phase 2 work: extend tools/build_authoritative_sources.py to emit a Data/NPCNames_deDE.lua from the creature_template_locale tables that ship in the AzerothCore / TrinityCore TDB dumps we already have. Same template covers frFR / esES / esMX / ruRU.
  • A few diagnostic slash-command outputs stay English (/togpm status, /togpm versioncheck, /togpm spellcache, /togpm dumprecipe). These reference internal protocol terms (DeltaSync, P2P sessions, namespace, hash-cache, etc.) that don't translate cleanly and are power-user / maintainer tools.

[v0.5.7] (2026-05-26) - Per-profession manual curation pass — 100% TBC Phase 2 requiredSkill coverage + 8 expansion-rebalance corrections + 9 never-shipped recipes excluded

Data Quality

  • TBC Phase 2 visible recipes: 100% authoritative requiredSkill coverage across all 10 craftable professions (2,271 recipes; was 94.9% / 117 gaps in v0.5.6). Filled via a per-profession manual review pass with cited values — every entry in tools/manual_skill_overrides.json carries the reqSkill, the source attribution (Apprentice auto-grant / TRAINER_SHOW capture / Wowhead / quest reward / SoD content / etc.), the verified_by user, and the recipe's name for human readability when grepping the file. Coverage now stops at the edge of the in-game data — every recipe the player can see in their Missing Recipes tab on TBC Phase 2 has an authoritative skill requirement.

  • 8 expansion-rebalance corrections. TBC Anniversary's trainers enforce HIGHER ReqSkillRank values for several Vanilla Mining / Engineering recipes than the emulator SQL (AzerothCore Wrath + TrinityCore Cata) we pull from — Blizzard rebalanced trainer requirements between Vanilla and TBC and the emulators carry the pre-rebalance values. Validated via the v0.5.6 TRAINER_SHOW capture on Galdof; corrections now ship via manual_skill_overrides.json at top priority over the emulator SQL: Smelt Iron 100→125, Smelt Gold 115→155, Smelt Steel 125→165, Smelt Mithril 150→175, Smelt Truesilver 165→230, Smelt Thorium 200→250, Coarse Blasting Powder 65→75, Coarse Dynamite 65→75.

  • 9 never-implemented spells removed from the recipe DB entirely. Blizzard's DBC retains some planned-but-cut recipes that never shipped in any live patch. Previously these appeared as un-learnable rows in the Missing Recipes tab. Added to MANUAL_EXCLUDED_SPELLS at the build pipeline so they're filtered out before emit:

    • Enchanting: 22434 "Charged Scale of Onyxia" (sibling of the v0.5.5-excluded Refined Scale of Onyxia)
    • Alchemy: 11447 "Elixir of Waterwalking" (DBC tagged as Alchemy but actually a daily quest reward)
    • Engineering: 12719 "Explosive Arrow", 12720 (truncated placeholder name), 12722 "Goblin Radio", 12900 "Mobile Alarm", 12904 "Gnomish Ham Radio", 30561 "Goblin Tonk Controller", 30573 "Gnomish Tonk Controller"
  • 119 hand-curated requiredSkill entries shipped. Up from 12 in v0.5.6. Breakdown:

    • 27 Apprentice auto-grants across all 10 professions (Linen Cloak, Linen Bandage, Bolt of Linen Cloth, Light Leather, Handstitched Leather Boots, Rough Sharpening Stone, Copper Bracers, Smelt Copper, Rough Blasting Powder, Rough Dynamite, Minor Healing Potion, Elixir of Lion's Strength, Charred Wolf Meat, Roasted Boar Meat, Runed Copper Rod, Delicate Copper Wire, etc.) — all reqSkill=1 (Apprentice tier).
    • 6 Mining smelt rebalance corrections + 2 Engineering powder rebalance corrections (see above).
    • 11 Vanilla mid-tier Mining / BS / LW / Tailoring / Cooking / Engineering / Alchemy quest / drop / vendor recipes (Smelt Dark Iron 230, Mooncloth Boots 290, Gordok Ogre Suit Tailoring+LW 285, Crafted Light Shot 30, The Mortar: Reloaded 205, Dimensional Ripper - Everlook 285, Ultrasafe Transporter - Gadgetzan 285, Tranquil Mechanical Yeti 250, Goldthorn Tea 175, Smoked Desert Dumplings 285, Crystal Throat Lozenge 300, Restorative Potion 215, Gurubashi Mojo Madness 315, Enchant Bracer - Minor Health 70).
    • 6 TBC Alchemy primal transmutes (all 385).
    • 6 TBC Alchemy flasks + Super Rejuvenation Potion (all 390).
    • 5 TBC Alchemy Major Protection cauldrons — the Discovery-system recipes (all 360).
    • 47 SoD / Anniversary Phase 1-7 content recipes across all professions — values from Wowhead / community sources, tagged with the SoD phase in the JSON source field.
  • Inscription stays out of TBC visibility scope. Inscription was introduced in Wrath; its recipes are correctly filtered out by minExpansion ≥ 3 for TBC clients. The 50.5% Inscription coverage from v0.5.6 doesn't affect what TBC users see (it's all hidden). Future manual curation passes can extend the overrides to Wrath/Cata/MoP-visible recipes as Anniversary advances expansion content.

Manual Override Workflow (for future maintainers)

When a user reports a missing-skill recipe:

  1. Check the Missing Recipes tab — note the spell ID from the [TOGPM] itemId=... spellId=... debug line on the tooltip (enable via Settings → Item tooltip).
  2. Look up the real requirement on Wowhead / trainer / in-game.
  3. Add to tools/manual_skill_overrides.json with reqSkill, source, verified_by, and name fields. Group with the appropriate _comment_* separator for readability.
  4. Re-run python tools/build_authoritative_data.py and verify with a quick coverage probe.
  5. For recipes that are in DBC but never shipped in live, add to MANUAL_EXCLUDED_SPELLS in build_authoritative_data.py instead.

[v0.5.6] (2026-05-25) - Recipe-skill data quality pass — name-match scroll linker + hand-curated overrides + in-game trainer observation capture + tooltip toggles off by default

New Features

  • Recipe-scroll name-match supplemental linker in build pipeline. Closes a long-standing data gap: many WoW recipe scrolls implement their teach effect via a generic "Learning" spell (spell 483) whose effect is then chained server-side to grant the specific craft spell. Blizzard's ItemEffect table only captures the item→Learning link, NOT the item→specific-craft link — so our pipeline previously missed ~80-100 recipe-scroll links per profession (e.g. Pattern: Mooncloth Robe → Craft Mooncloth Robe spell). The new linker exploits WoW's predictable recipe-scroll naming convention ("Pattern:" / "Plans:" / "Recipe:" / "Formula:" / "Schematic:" / "Design:" / "Manual:" / "Technique:" + craft name), strips the prefix, and matches the remainder against SpellName. 97% hit rate on Vanilla (1,047 of 1,082 recipe scrolls match a spell). The 35 unmatched are mostly singular/plural mismatches, legacy "Imbue X" enchant naming, and skill-rank books that aren't recipes. Validated at the recipe level: every name-matched scroll now flows its ItemSparse.RequiredSkillRank into the recipe's requiredSkill, AND the scroll's item ID becomes the recipe's itemId (so MissingRecipesTab can render the scroll icon + chat-insert link). Coverage on TBC-visible recipes jumped from ~80% to 94.9% (only 117 of 2,280 visible recipes still show - for unknown skill requirement). Location: tools/build_authoritative_data.py spell_by_name lookup + recipe-prefix walk + items_by_spell augmentation.

  • Hand-curated manual_skill_overrides.json as top-priority requiredSkill source. For the residual recipes whose data isn't in ANY DBC dump OR emulator SQL source (true apprentice auto-grants like Linen Cloak / Smelt Copper / Delicate Copper Wire, plus a few quest-direct-grants whose scrolls don't exist in the current DBC like Smelt Dark Iron / Gordok Ogre Suit). Each entry includes reqSkill, source (e.g. "Apprentice JC auto-grant"), verified_by (whose call), and name (for human readability when grep-ing the JSON). Wired as the top of the priority chain: manual override > scroll RequiredSkillRank > trainer SQL ReqSkillRank > - (unknown). Lets the maintainer correct any of the lower-tier sources by adding a single JSON entry without rebuilding the whole pipeline. Initial seed (JC apprentice auto-grants): Delicate Copper Wire 25255, Braided Copper Ring 25493, Woven Copper Ring 26925, Rough Stone Statue 32259 all = 1. Per-profession curation pass to follow incrementally. Location: tools/manual_skill_overrides.json, tools/build_authoritative_data.py _load_manual_skill_overrides.

  • In-game trainer observation capture (gdb.trainerObservations). New TRAINER_SHOW / TRAINER_UPDATE event hook in Scanner.lua OnTrainerShow that captures the EXACT ReqSkillRank Blizzard's server enforces for every spell a trainer offers (via GetTrainerServiceSkillReq(i) — same numeric value as the trainer's "Requires Tailoring (100)" tooltip line). One trainer visit captures every spell that trainer teaches in a single event. Stored as gdb.trainerObservations[spellId] = { reqSkill, moneyCost, profId, observedBy, observedAt } — top-level table, deliberately named to be grep-able in SavedVariables for easy extraction by the maintainer. Three-tier link resolution from the trainer service link: (a) direct spell-ID extraction from enchant: / spell: link prefixes; (b) reverse-lookup via craftedItemId against shipped recipeDB when the link is an Hitem: (the TBC pattern — service link is the produced item, not the spell); (c) name match for spells with no craftedItemId (Enchanting recipes). Single-user validation across 9 trainers (BS / Tailoring / LW / Engineering / Alchemy / Mining / Cooking / Enchanting / JC) captured 369 entries: 361 confirmed agreement with shipped values, 8 revealed legitimate TBC-era Blizzard rebalances on Mining smelts (Smelt Iron 100→125, Smelt Mithril 150→175, Smelt Truesilver 165→230, Smelt Gold 115→155, Smelt Thorium 200→250, Smelt Steel 125→165) plus 2 Engineering powders. DeltaSync wireup so observations propagate guild-wide is deferred to a later patch — the apprentice / quest-grant gap doesn't close via trainer visits anyway (trainers don't list those), so the marginal value of full guild propagation is low until we fill more of the manual override file. Setting toggle for opt-out also deferred. Location: Scanner.lua OnTrainerShow, TOGProfessionMaster.lua schema doc.

UX Changes

  • Both [TOGPM] tooltip lines (crafters list + item/spell IDs) now default OFF. Previously v0.5.5 shipped them ON, which added two lines to every item tooltip the user hovered (bags / AH / chat links / vendors / mailbox). Default flipped to off so the addon stays silent on tooltips until the user opts in via Settings → Item tooltip section. Existing users who installed v0.5.5 keep their explicit value (AceDB doesn't overwrite already-saved profile entries). New users / fresh profiles get the off-default. Location: TOGProfessionMaster.lua SETTINGS_DEFAULTS.

Investigation Notes (for future v0.5.x work)

  • Confirmed dead-ends for the residual requiredSkill gap: DBC SpellLearnSpell table doesn't exist on wago, SpellEffect[Effect=36] (LEARN_SPELL) has no rows linking profession-train spells to their auto-granted apprentice recipes, AllTheThings Professions.lua only lists recipes with notable sources (drops/quests), Blizzard's Battle.net Game Data API exists for Classic namespaces but the recipe endpoint isn't implemented (and even retail's recipe endpoint exposes no required_skill field). The auto-grant logic lives entirely in emulator C++ source (e.g. AzerothCore's LearnDefaultProfessionRecipes). Hand-curation via manual_skill_overrides.json is the path forward — small per-profession passes as the maintainer has bandwidth.
  • Per-recipe coverage diagnostics added: TBC Phase 2 visible breakdown shows JC at 100%, BS 97.7%, Cooking 95.9%, LW 95.1%, Tailoring 95.1%, First Aid 94.1%, Enchanting 93.8%, Engineering 93.1%, Mining 90.5%, Alchemy 86.2% (laggard, 27 of 196 still -).

[v0.5.5] (2026-05-25) - TBC tooltip rewrite + recipe icons + shift-click link insert + authoritative skill requirements + obsolete-name scrub + cross-expansion bleed + "My Characters" crafter list

New Features

  • Recipe rows in the Profession Browser now show the actual crafted-item icon. Previously the rows displayed whatever rd.icon happened to hold — historically populated by stub creation paths that often stored a generic spell icon (blue spell-scroll for BS, net for Tailoring, ? for JC, violin/bag for LW, NPC face for Black Dragonscale, etc.). Icon resolution now goes: (1) crafted item ID parsed from rd.itemLink (Hitem:N) — this works for both Classic Era item-keyed entries AND TBC spell-keyed entries where the recipe key is the spell ID but the itemLink still carries the produced item's Hitem:N. (2) GetItemIcon(recipeId) for Classic Era item-keyed fallback. (3) cached rd.icon last-resort. Spell-id/item-id collision detection: when the parsed itemLink ID equals the recipe's spell ID (common for Enchanting where stub creation poisoned the link with an unrelated item — e.g. spell 13522 / Enchant Cloak got Hitem:13522 = "Recipe: Flask of Chromatic Resistance", spell 13868 / Enchant Gloves got Hitem:13868 = "Frostweave Robe"), the collision is rejected and we fall back to the proper enchant scroll icon. Locations: GUI/BrowserTab.lua BuildRecipeList icon block.

  • Recipe rows now produce a working chat-insertable link on shift-click. Previously shift-click silently did nothing on trainer-taught recipes and any stub-created entry where rd.itemLink and rd.recipeLink were both nil. New ResolveRecipeLink(entry) helper falls through itemLink → recipeLink → GetItemInfo(id) → GetSpellLink(spellId) → synthetic "item:<id>". Applies to both the recipe-row hover handler in the list AND the drilldown header button. Location: GUI/BrowserTab.lua ResolveRecipeLink.

  • Global item tooltips now append a TOGPM IDs line as a second branded line under the existing crafters line. The new line shows [TOGPM] itemId=N spellId=N (in the addon brand color) so users can paste IDs into Wowhead etc. to troubleshoot icon/link issues. Appears on hover anywhere an item tooltip shows — bags, AH, vendors, chat links, mailbox. Two new Settings toggles in the Game Options panel let users disable each line independently: "Show guild crafters on item tooltips" and "Show item ID / spell ID on item tooltips" (both default on). The same brand-color lines also appear at the bottom of the Profession Browser's recipe-row and drilldown-header tooltips for consistency across every TOGPM tooltip surface. Locations: Tooltip.lua AppendCraftersNow / AppendBrandIdsLine, GUI/Settings.lua, GUI/BrowserTab.lua AppendBrandTooltipLines.

  • New per-recipe craftedItemId field shipped in the recipe DB. Populated at build time from SpellEffect[Effect=24].EffectItemType — the canonical "this spell creates this item" mapping. Lets trainer-taught crafts (which have no scroll item) render the produced item's icon in the Missing Recipes tab instead of falling to a generic spell icon. Example: Heavy Weightstone (spell 3117) now ships with craftedItemId=3241 and renders the actual stone icon; Coarse Sharpening Stone with craftedItemId=2863; Silver Rod with craftedItemId=6338; etc. Location: tools/build_authoritative_data.py created_item_by_spell, GUI/MissingRecipesTab.lua trainer-only icon branch.

  • Skill column in Missing Recipes now shows the literal "Requires Blacksmithing (175)" tooltip value, sourced from authoritative data. Three-tier resolution: (1) recipe-scroll ItemSparse.RequiredSkillRank — the LITERAL value the in-game tooltip shows on the scroll. (2) Trainer SQL trainer_spell.ReqSkillRank — the LITERAL value the trainer NPC enforces; sourced from AzerothCore Wrath + TrinityCore Cata + NEW CMaNGOS Classic emulator data. (3) When neither source has data, the column shows - so the gap is visible — we explicitly do NOT fall back to SLA MinSkillLineRank (which is the placeholder 1 for many recipes) or the green-threshold proxy (which runs 0-5 points off the real value). Coverage: 80% of 5,864 recipes (4,693) have authoritative values; the remaining 20% (1,171, mostly auto-learned Apprentice-tier and a chunk of Inscription) show -. Validation against known recipes: Silver Rod=100, Silver Skeleton Key=100, Golden Rod=150, Arcanite Rod=275, Heavy Weightstone=125, Coarse Sharpening Stone=65, Adamantite Cleaver=330 — all match the in-game trainer/scroll tooltips exactly. Locations: tools/build_authoritative_data.py required_skill resolution, tools/extract_emulator_trainers.py extract_all_skill_ranks + new CMaNGOS Classic flat-layout parser, GUI/MissingRecipesTab.lua.

  • Cooldowns tab cloth-spec icons fixed. Primal Mooncloth / Spellcloth / Shadowcloth were showing a generic net icon because GetSpellTexture returns a placeholder for those craft spells. Added per-spell entries to ICON_OVERRIDES mapping each cloth spec spell to its produced bolt's item icon (Primal Mooncloth → Bolt of Primal Mooncloth item 21845, Spellcloth → Bolt of Spellcloth 24272, Shadowcloth → Bolt of Shadowcloth 24271). Location: Data/CooldownIds.lua ICON_OVERRIDES.

Bug Fixes

  • TBC Anniversary global item tooltip — the [TOGPM] crafter1, crafter2 line never appeared. Two distinct issues:

    1. Hook never fired in TBC. The legacy OnTooltipSetItem hook was registered only in the else branch when the modern TooltipDataProcessor API wasn't available. TBC Anniversary advertises TooltipDataProcessor (so hasModern=true) but never actually fires the PostCall — so we registered ONLY the modern hook, which never ran, and the legacy hook was skipped. Fix: register OnTooltipSetItem UNCONDITIONALLY alongside the modern path; the _togpmAppended dedup in AppendCrafters keeps the dual registration from double-appending. Location: Tooltip.lua PLAYER_LOGIN handler.
    2. FindCrafters returned nil for every item hover even when the crafters were present in gdb.recipes. On TBC, GetTradeSkillRecipeLink returns |Henchant:SPELLID|h[...]|h|r links for EVERY profession (not just Enchanting). Scanner:ExtractTradeSkillId matches the enchant: prefix first and returns (spell_id, isSpell=true), so every TBC recipe gets stored in gdb.recipes keyed by spell ID with isSpell=true. FindCrafters filtered if not rd.isSpell and recipeId == itemID — which matched nothing because EVERY TBC recipe has isSpell=true. Validated: user's Anniversary SavedVariables has 1,494 recipes with isSpell=true vs 11 with isSpell=false (Classic Era is the inverse). Fix: new ResolveRecipeForItem(profRecipes, itemID) helper tries the fast item-keyed lookup first (Classic Era pattern), then falls back to walking the spell-keyed entries and matching via the Hitem:N ID parsed out of rd.itemLink (TBC pattern). Non-destructive — doesn't require rescan or breaking sync hashes. Verified: Roasted Kodo Meat (spell 6414, isSpell=true, itemLink contains Hitem:5474) now resolves crafters when hovering the Roasted Kodo Meat item 5474. Location: Tooltip.lua ResolveRecipeForItem / FindCrafters.
  • Settings panel — clicking the gear icon's close button threw a Lua error: AceConfigRegistry-3.0:ValidateOptionsTable(): TOGProfessionMaster.args.tbcAnniversaryPhase.sortByValue: unknown parameter. The v0.5.4 TBC Anniversary phase dropdown used sortByValue = true which isn't a valid AceConfig field. Replaced with sorting = {1, 2, 3, 4} — explicit order array — so the dropdown lists Phase 1 → 4 instead of AceConfig's default alphabetical-by-label sort. Location: GUI/Settings.lua.

  • Missing Recipes — Classic-Era-exclusive recipes (Season of Discovery / Anniversary content) appeared on TBC/Wrath/Cata/MoP clients with broken icons. Examples: spell 427061 "Mantle of the Second War" (? icon, no scroll), spell 1224639 "Scarlet Soldier's Stompers" (#238329 (loading…) placeholder, scroll item that doesn't exist in TBC's client DB). These are 1.15.x-only recipes that ship in our unified recipeDB because they appear in Vanilla's SkillLineAbility, but the older-expansion clients have no record of the spells OR the scroll items. Filter added in BuildMissingList: skip any recipe where GetSpellInfo(spellId) returns nil (the spell isn't in this client) AND the scroll item is also missing from the client (GetItemInfoInstant(itemId) returns nil — synchronous, distinguishes "item doesn't exist on this client" from "cache cold"). Conservative — only fires when BOTH spell AND item are unknown to the client. Location: GUI/MissingRecipesTab.lua BuildMissingList.

  • Missing Recipes trainer-only icon improvement. When a trainer-taught recipe has no scroll, GetSpellTexture(spellId) often returned a generic/wrong icon. Now uses entry.craftedItemId (newly shipped in the recipe DB — see New Features) as the authoritative icon source via GetItemIcon. Falls back to GetSpellTexture only when no craftedItemId is available (Enchanting craft spells with no produced item — correctly shows the enchant scroll icon). Location: GUI/MissingRecipesTab.lua FillList trainer-only branch.

  • Dev-sync PowerShell script (wow-version-replication.ps1) was copying tools/, .claude/, CLAUDE.md, and .markdownlintignore to non-source WoW installs. The script's hardcoded SkipPatterns list had drifted from .pkgmeta's authoritative ignore: list — tools/, .claude/, **/*.bat, .markdownlintignore, and CLAUDE.md were never excluded. Fix: aligned SkipPatterns to mirror .pkgmeta (so anything excluded from the CurseForge package is also excluded from local dev sync). Cleanup pass deleted previously-replicated artifacts from _classic_ and _anniversary_ installs. Includes a note that future ignores need to be added to BOTH files. Location: wow-version-replication.ps1.

  • Recipe rows display obsolete Blizzard internal item names like "59 TEST Green Shaman Chest", "ZZOLD Design: Bracing Earthsiege Diamond", "DEPRECATED Formula: Enchant Chest - Exceptional Mana", "Manual: Crystal Infused Bandage [PH]" — user reported "Green Shaman Chest" displaying as "59 TEST Green Shaman Chest" with "tens of recipes like this". Two distinct root causes:

    1. Data side (~29 shipped recipes): tools/build_authoritative_data.py collects items_by_spell from Blizzard's ItemEffect table then picks items[0] as the recipe's itemId. For many recipes Blizzard's DBC retains an obsolete variant of the recipe scroll (ZZOLD/TEST/DEPRECATED/UNUSED/[PH]) with a lower item ID than the live version — so sorted items[0] returned the obsolete one. Worst offender: 20 Wrath Jewelcrafting designs whose itemId pointed at ZZOLD Design: ... items (41403-41422) instead of the live 41397-41396 set. Fix adds an is_obsolete_item_name() filter (patterns: \bTEST\b, \bQA\b, \bDEPRECATED\b, \bUNUSED\b, ^ZZ, \[PH\], \bOLD$) that drops the bad item from the per-spell set before picking items[0]. Build pipeline now reports e.g. skipped 386 obsolete item-teach rows (ZZOLD/TEST/DEPRECATED variants) per build. Re-emitted Data/Recipes/*.lua — only 1 of ~29 bad entries survives (Crystal Infused Bandage, item 23689, whose [PH] suffix exists only in Wrath+ DBC; on Vanilla/TBC clients GetItemInfo(23689) returns the clean name).
    2. Runtime side (spell-id / item-id namespace collision + cross-guildmate poison spread): Scanner.lua MergeCraftersIntoGdb creates a stub recipe entry when crafters:<profId> arrives before recipemeta:<profId>, and BackfillBogusRecipeNames runs at PEW to heal "? <id>" placeholders. Both call GetItemInfo(recipeId) where recipeId is a spell ID — and the spell-ID namespace overlaps the item-ID namespace. Example: spell 26926 = "Heavy Copper Ring" (Jewelcrafting) but item 26926 = "59 TEST Green Shaman Chest" in TBC/Wrath/Cata/MoP ItemSparse. GetItemInfo happily returns the TEST name and we cached it as stub.name, so every render shows the bad name forever. Worse: once cached, the bad name was broadcast via recipemeta:<profId> and every other client's MergeRecipeMetaIntoGdb stamped it in too — one poisoned guildmate could re-infect every other user every time they synced. Fix has four parts:
      • New isObsoleteItemName() Lua helper (mirror of Python pipeline patterns: TEST / QA / DEPRECATED / UNUSED / ZZ-prefix / [PH] / trailing OLD).
      • MergeCraftersIntoGdb stub creation: when GetItemInfo(spellId) returns an obsolete-marker name, treat as nil and fall through to GetSpellInfo (the correct API for a spell-id stub).
      • BackfillBogusRecipeNames recovery: same GetItemInfo guard so the recovery pass doesn't re-stamp the TEST name.
      • MergeRecipeMetaIntoGdb (sync receive): drop incoming obsolete names from peers; treat existing local obsolete names as bogus and re-heal them. Kills the cross-guildmate re-infection loop — even if one guildmate is still running a pre-v0.5.5 version and broadcasts poisoned recipemeta, our merge no longer accepts it.
      • New ScrubObsoleteRecipeNames() one-time PEW pass: walks gdb.recipes and clears rd.name / rd.icon / rd.itemLink on any entry whose cached name matches an obsolete marker (left over from pre-v0.5.5 stubs in SavedVariables). Runs BEFORE the backfill schedule so the next BackfillBogusRecipeNames pass repopulates the cleared names via GetSpellInfo. Self-heals on /reload — no /togpm purge required. Logs Scrubbed N obsolete recipe names (TEST/ZZOLD/DEPRECATED/[PH]) from cache when something is actually cleaned. Locations: Scanner.lua isObsoleteItemName / MergeCraftersIntoGdb / BackfillBogusRecipeNames / MergeRecipeMetaIntoGdb / ScrubObsoleteRecipeNames, tools/build_authoritative_data.py _OBSOLETE_NAME_PATTERNS / is_obsolete_item_name.
  • "My Characters" view on the Profession Browser shows guildmate crafter names in each recipe's crafter list, making it look like the addon is treating guildmates as your alts — user reported seeing "Gumshots and 29+ others for anti-venom" with the view set to My Characters. Root cause is NOT in the visibility filter (mineVisible correctly requires at least one MY-character crafter on line 207-213) and NOT in IsMyCharacter / accountChars (a new /togpm myalts diagnostic confirmed global.accountChars is clean: only the logged-in toon is flagged as MINE, every guildmate correctly returns false). The bug is that the CRAFTER LIST RENDER on each visible recipe still iterates every entry in rd.crafters and adds non-mine entries as plain crafter names alongside the user's own alts — so a recipe that one of YOUR alts knows AND that 30 guildmates also know renders with 30 guildmate names next to your alt. Reads exactly like "TOGPM thinks these guildmates are my alts" even though they're not. Fix: in BuildRecipeList's render loop, the else branch that appends guildmate crafters is now gated on viewMode ~= "mine" — in mine view, only own-alt crafters render. guild view shows everyone as before. Location: GUI/BrowserTab.lua BuildRecipeList.

  • Recipes from later expansions leaking into earlier clients' Missing list (Wrath transmute spell 53771 "Transmute: Eternal Life to Shadow" showing on TBC; Wrath/Cata/MoP recipes generally surfacing as unobtainable rows on TBC Anniversary). Root cause exposed by user testing: the v0.5.3 cross-expansion filter (requiredSkill > clientMaxSkill) wasn't working because requiredSkill is sourced from MinSkillLineRank in wago.tools' SkillLineAbility, which defaults to 1 for many recipes (it represents the "becomes orange" threshold, NOT the learn requirement). The check 1 > 375 is never true, so nothing got filtered. Recipes were leaking because the build pipeline unions data forward across all 5 expansion builds (Vanilla → MoP), and wago.tools' MoP-build SkillLineAbility contains every spell ever shipped — meaning Wrath/Cata/MoP recipes ended up in the merged DB with no per-expansion gate. The fix adds an explicit minExpansion field per recipe (1-5 indexing Vanilla through MoP) set during the build-time merge to the EARLIEST build index where the spell first appeared in SkillLineAbility. A new BuildMissingList filter hides any recipe whose minExpansion > clientExp regardless of skill cap or phase tag — that's the primary cross-expansion gate now, with the skill-cap check kept as a belt-and-suspenders fallback. Validation: spell 53771 → minExpansion=3 (Wrath), correctly hidden on TBC. Inscription drops to 0 visible recipes on TBC (it didn't exist pre-Wrath). Total: 3,388 of 5,865 recipes hidden on TBC clients that were previously visible. Locations: tools/build_authoritative_data.py (merge_expansions + emit_recipe_file), GUI/MissingRecipesTab.lua BuildMissingList, Data/Recipes/*.lua re-emitted.

  • Hand-curated MANUAL_EXCLUDED_SPELLS exclusion list in the build pipeline for recipes that exist in wago.tools / DBC data but were never actually obtainable in the live game (planned-but-cut content, leftover dev entries). First entry: spell 22430 "Refined Scale of Onyxia" — Alchemy recipe planned for Vanilla AQ40 but never implemented (would have bottlenecked Onyxia loot turn-ins). User-reported; verified non-obtainable via Wowhead comments + community sources. Removed entirely from Data/Recipes/Alchemy.lua at emit time (count drops 363 → 362). Future user reports of similar phantom recipes get appended to the list with a # source comment so the next maintainer can audit the entry. Location: tools/build_authoritative_data.py MANUAL_EXCLUDED_SPELLS.

Known Gaps

  • Alchemy cauldron recipes show source "Unknown" when they should show "Discovery" (made via Major Protection Potion discovery). Needs a new source kind + small hardcoded list. v0.5.6 candidate.

[v0.5.4] (2026-05-25) - TBC Anniversary phase filter (hide future-phase recipes like Sunwell on Phase 2)

New Features

  • TBC Anniversary phase filter on the Missing Recipes tab. TBC Anniversary is currently live on Phase 2 (Serpentshrine Cavern + Tempest Keep), but the recipe DB we ship contains every TBC recipe including Phase 3 (Black Temple, Hyjal), Phase 3.5 (Zul'Aman), and Phase 4 (Sunwell, Magisters' Terrace, Shattered Sun). Users on Phase 2 were seeing Sunwell jewelcrafting designs and Black Temple drops in their Missing list with no way to actually obtain them. New Settings entry "TBC Anniversary phase" (visible only on TBC clients via addon.isTBC) lets the user pick the current live phase; BuildMissingList then hides any recipe whose phase field exceeds the selected value. Default = 2 (matches Anniversary live state on v0.5.4 release date). Users bump the setting as Blizzard advances phases. Locations: GUI/Settings.lua, GUI/MissingRecipesTab.lua, Locale/enUS.lua, TOGProfessionMaster.lua (default).

  • Per-recipe phase data sourced from AllTheThings at build time. Phase numbers are derived in a dev-only Python pipeline that reads ATT's per-expansion Lua data files via lupa (an embedded Lua VM in Python — chosen over regex-based parsing because executing ATT's actual Lua code is more reliable than trying to re-implement Lua semantics for ATT's compressed nested-call format). The pipeline merges recipe fields across 5 ATT files (Professions.lua, Instances.lua, Zones.lua, WorldDrops.lua, Craftables.lua) since the phase-relevant data (minReputation, awp) is scattered — e.g. spell 42588 (Design: Kailee's Rose) has only basic fields in Professions.lua but its Shattered Sun reputation gate minReputation={935: 9000} only appears in Zones.lua. Phase derivation combines three signals in confidence order: (1) boss-drop containment inside a known TBC raid inst() wrapper (Karazhan→1, SSC→2, Tempest Keep→2, Hyjal→3, Black Temple→3, Magisters' Terrace→4, Sunwell Plateau→4); (2) faction reputation gate (Shattered Sun Offensive→4, Scale of the Sands→3, Ashtongue Deathsworn→3); (3) ATT's awp patch-added tag (≥2.4.0→4, ≥2.3.0→3). Fallback: no phase field emitted, addon treats as always-visible. Coverage on the 2,252 shipped TBC recipes: 16 tagged Phase 2, 109 tagged Phase 3, 87 tagged Phase 4. Locations: tools/att_probe.py, tools/att_extract_phase.py, tools/build_authoritative_data.py, Data/Recipes/*.lua re-emitted.

Known Gaps

  • Recipes ATT doesn't tag with phase signals default to Phase 1 (always shown). 88 of our 2,252 shipped TBC recipes have no entry in ATT at all; ATT also doesn't tag every Sunwell-era recipe with awp or minReputation (their schema only requires phase metadata on a subset). These slip past the filter and remain visible on earlier phases — opposite of the user's complaint, but the failure mode this release is biased toward (show extra rather than hide what shouldn't be hidden) since we can't distinguish "ATT doesn't know" from "ATT knows it's launch content."
  • Phase 3.5 (Zul'Aman) is currently bucketed as Phase 3 or 4 depending on the recipe's other signals — the patch table doesn't separate 2.3.0 (Zul'Aman) from 2.4.0 (Sunwell) cleanly enough for a sub-phase split. When Anniversary advances to Phase 3.5 in autumn 2026 we'll add a separate setting value and re-derive.
  • Wrath / Cata / MoP have no phase tags yet. Same pipeline will produce them when those Anniversary cycles approach phase changes that matter; v0.5.4 is TBC-scoped because that's the immediate user-reported bug.

[v0.5.3] (2026-05-23) - Cross-expansion recipe bleed + persistent "(loading)" placeholders

Bug Fixes

  • Recipes from later expansions appearing in the Missing list on earlier-expansion clients (e.g. MoP First Aid spell 102697 showing for a TBC Anniversary player who can never learn it; Wrath/Cata Jewelcrafting designs on TBC) — v0.5.0's recipe database is a UNION across every expansion's data shipped as a single Data/Recipes/*.lua set, loaded by every TOC variant. So a TBC client loads MoP recipes too even though they're unreachable. Fix in GUI/MissingRecipesTab.lua BuildMissingList: hide any recipe whose requiredSkill exceeds the current client's expansion cap. Caps: Vanilla 300, TBC 375, Wrath 450, Cata 525, MoP 600. Detected via the existing addon.isVanilla / isTBC / etc. flags from Compat.lua; no Data file changes needed. A future-expansion recipe shows up automatically once the player is on a later TOC variant that supports its required skill. Note: this is an expansion-level filter only — TBC content from later PHASES (e.g. Shattered Sun Offensive recipes during Phase 2 of TBC Anniversary) still surfaces because we don't have per-recipe phase metadata; that's a separate v0.5.4+ problem requiring phase tagging.

  • Rows persistently showing #22430 (loading…) instead of the recipe nameGetItemInfo returns nil when an item is not yet in the WoW client cache (triggers an async load) AND returns nil indefinitely if the item id genuinely doesn't exist on the current client (e.g. some Vanilla recipe scrolls were removed from the game over the years even though wago.tools still has the spell). The previous fallback was a static "(loading…)" placeholder that never resolved in the latter case. Fix in GUI/MissingRecipesTab.lua FillList: when the item name can't be resolved, fall back to the SPELL name (GetSpellInfo(entry.spellId)) and the spell icon (GetSpellTexture). The row now shows e.g. "Brown Linen Vest" via the spell name instead of "#22589 (loading…)" via the missing item. When the item later loads from the cache, GET_ITEM_INFO_RECEIVED triggers a refresh and we render the proper item display.


[v0.5.2] (2026-05-23) - Skill-rank-up spells in the Missing list + cache-cold item crash

Bug Fixes

  • Skill-rank-up + utility spells appearing in the Missing Recipes list ("Master Alchemy", "Find Minerals", "Apprentice Jewelcrafting", etc. surfacing as missing "recipes") — SkillLineAbility in wago.tools is wider than just craftable recipes: it also includes the rank-up spells trainers hand out at each tier (Apprentice / Journeyman / Expert / Artisan / Master / Grand Master / Illustrious), profession specialisation spells, and utility toggles like "Find Minerals" / "Find Herbs" / "Find Fish". v0.5.0 / v0.5.1 emitted every SLA row, so these non-recipes leaked into the list. Filter in tools/build_authoritative_data.py's emit_recipe_file: skip any spell where reagents is empty AND no items (recipe-scroll items) exist. Every real craft has at least one reagent (you need materials); every scroll-taught recipe has at least one teaching item. Anything missing BOTH is by definition not a craftable recipe. Re-ran Phase A; 149 non-recipe spells filtered across all professions (Mining -23, Engineering -14, Tailoring -13, Inscription -13, Blacksmithing -13, Cooking -9, etc.). New recipe total: 5,865 (was 6,014).

  • Blizzard_ObjectAPI/Classic/Item.lua:320: table index is nil crash storm STILL firing at higher spell IDs (e.g. 27667, 33209, 35752+) even after v0.5.1's nil-itemId fix — different root cause this time: we now correctly pass a real, valid item ID to GameTooltip:SetItemByID, but when the item is not yet in the WoW client's item cache, LoonBestInSlot's post-hook (AceHook-3.0 wrapper around SetItemByID) reads GameTooltip:GetItem() and gets back nil — the cache hasn't populated yet — then crashes calling ContinueOnItemLoad(nil). Symptom: Tooltip: fallback Show-hook fired for itemID = N appears in chat (proving the item id IS valid) but BugSack still shows 289x Item.lua errors from LoonBestInSlot's stack. Fix in GUI/MissingRecipesTab.lua OnEnter handler: use GetItemInfo(itemId) as both a cache-presence test and an async-load trigger BEFORE deciding to call SetItemByID. When cached → call SetItemByID as before. When not cached → fall back to spell tooltip (SetSpellByID) AND the GetItemInfo call queues the item for fetch, so the next mouseover on the same row succeeds. Effectively the row "warms up" on first hover then renders the proper item tooltip on subsequent hovers. No more SetItemByID on cache-cold items, no more crash in LoonBestInSlot.


[v0.5.1] (2026-05-23) - Lua error storm in Missing Recipes from passing spell ids to item-id APIs

Bug Fixes

  • Blizzard_ObjectAPI/Classic/Item.lua:320: table index is nil Lua error firing 148-247x per Missing Recipes session, with the stack rooted in TOGProfessionMaster/GUI/MissingRecipesTab.lua:683 (the row OnEnter handler calling GameTooltip:SetItemByID) — v0.5.0 rebuilt the recipe DB keyed by recipe SPELL id (was the crafted-item id in the legacy PS data). The row code in GUI/MissingRecipesTab.lua still treated entry.spellId as an item id and passed it to every item-id API: GameTooltip:SetItemByID(spellId), GetItemInfo(spellId), GetItemIcon(spellId), GetItemQualityColor. Blizzard's SetItemByID silently sets an empty tooltip when given an id that doesn't match an item — but downstream addons that hook SetItemByID (e.g. LoonBestInSlot's tooltip-update path) then crash trying to load the empty item, surfacing as the storm of table index is nil errors in BugSack. Fix has three parts:

    1. Add a real itemId field to the runtime recipe DB. tools/build_authoritative_data.py was already capturing items (list of recipe-scroll item ids that teach the spell, via ItemEffect) but stripping the field from emit_recipe_file. Now keep the first id as entry.itemId (or omit when the recipe has no scroll — trainer-taught entries like "Brown Linen Vest" spell 2385 are correctly nil). Re-ran Phase A; verified e.g. Red Linen Robe (spell 2389) → itemId 2598 (Pattern: Red Linen Robe).

    2. Route every item-id API call through entry.itemId with a spell-id fallback. GUI/MissingRecipesTab.lua FillList, OnEnter, OnMouseDown, the AH-scan getItems collector, and the search filter all updated. When itemId is nil (trainer-only recipe with no scroll), fall back to GetSpellInfo for the row name, GetSpellTexture for the icon, GameTooltip:SetSpellByID for the hover tooltip, and GetSpellLink for the shift-click chat link. Bank + AH buttons hide when itemId is nil (no scroll to bank or auction).

    3. Guard the row tooltip handler against any nil id. OnEnter now early-returns unless f._itemId or f._spellId is non-nil. The previous guard checked only f._itemId, which never triggered the early-return for trainer-only rows because we were also storing the spell id IN f._itemId (the bug).

    Knock-on benefit: search filter now finds recipes by spell name too, so trainer-taught recipes are searchable by their actual name instead of only by their (non-existent) scroll item name. Location: GUI/MissingRecipesTab.lua, tools/build_authoritative_data.py, Data/Recipes/*.lua (re-emitted with itemId field).


[v0.5.0] (2026-05-23) - Authoritative recipe + source data from Blizzard DBCs + emulator world DBs

Bug Fixes

  • Recipes the player already knows showing up as "Missing" on TBC / Anniversary / MoP (a class of bugs that v0.4.6 / v0.4.7 only partially addressed) — root cause was a keyspace mismatch between recipeDB and gdb.recipes. Scanner:ExtractTradeSkillId (Scanner.lua:869) returns the SPELL id for Enchanting (its recipe links carry enchant:SPELLID) but the CRAFTED ITEM id for every other profession (Tailoring / BS / LW / Cooking / etc. recipe links carry item:ITEMID for the produced item). The scanner stores each row keyed by what that returns, so gdb.recipes[197][2589] = {...spellId=2385...} for "Brown Linen Vest". Our recipeDB[197][2385] (keyed by recipe spell id) then did a direct bucket.recipes[profId][2385] lookup in knownByChar — which missed every time because the bucket key is 2589, not 2385. The previous knownByChar walked every guild bucket (v0.4.7's cross-bucket fix) but the key was wrong in every bucket. Fix: build a knownSpells set once per BuildMissingList call that indexes BOTH the table key (Enchanting hit) AND the stored rd.spellId field (every other profession's hit). O(N) one-time build, O(1) lookup, works for all professions. The v0.4.7 fix is preserved on top — the new index iterates every bucket too. Location: GUI/MissingRecipesTab.lua.

UX Changes

  • Missing Recipes tab now surfaces recipes even when we don't have a known source for them — previously the tab filtered out any recipe without a sourceDB entry on the assumption that the recipe wasn't actually obtainable. With v0.5.0's authoritative recipeDB (sourced from Blizzard's DBC), that assumption is wrong: we KNOW the recipe exists in the game even when no emulator we read from catalogs the NPC. Recipes without source data now display with an "Unknown" source tag (L["MissingSrcUnknown"]) instead of being hidden, so the user still sees "I'm missing this" and can look it up externally. This is the main lever closing the visibility gap for MoP-introduced recipes (since we don't have a MoP emulator source yet). The "Include trainer-only" toggle still hides trainer-only entries when off — that's an unchanged UX choice tied to the tab's AH-hunting use case. Locations: GUI/MissingRecipesTab.lua, Locale/enUS.lua.

New Features

  • Recipe database rebuilt from authoritative sources — 6,014 recipes (up from ~2,479 in the previous PM/PS-derived ports) across TBC / Wrath / Cataclysm / Mists of Pandaria — the static recipeDB was Vanilla-only inherited from a PersonalShopper port in v0.3.0; intermediate v0.4.x patches considered porting from the ProfessionMaster addon's crowd-sourced data but on per-profession audit PM was missing 44% of the recipe universe (e.g. Cata Tailoring: 540 recipes in Blizzard's DBC vs 292 in PM, +248 missed; MoP Leatherworking: 952 vs 306, +646 missed). v0.5.0 replaces the PM path entirely with a two-phase Python pipeline (dev-only, not packaged) that pulls authoritative data directly from Blizzard:

    1. Phase A — Recipe metadata from wago.tools' API, which exposes Blizzard's actual client-shipped DB2 tables for every Classic build. We hit one build per expansion (Vanilla 1.15.8, TBC 2.5.5, Wrath 3.4.5, Cata 4.4.2, MoP 5.5.3) and join SkillLineAbilitySpellNameSpellReagentsItemEffectItemSparse for each profession's skill line. Output: recipe name, difficulty thresholds (orange / yellow / green / grey), required skill, reagent list (item id → count), and the item that teaches the spell (recipe scroll). Union-merged across all 5 expansions, latest-wins on rebalanced difficulty.
    2. Phase B — Source data from emulator world DBs. Trainer NPCs from AzerothCore WotLK (trainer_spell × creature_default_trainer) and TrinityCore Cata Classic TDB (trainer_spell × creature_trainer). Vendor NPCs from each emulator's npc_vendor. Drop NPCs from creature_loot_template with reference_loot_template chain expansion. Game-object drops (nodes / chests) from gameobject_loot_template with the same chain expansion. The TrinityCore Cata TDB 7z is downloaded once (~60 MB) and stream-filtered to extract only the 8 tables we need, cached at tools/emulator_data/trinity_cata_*.sql. Same approach for AzerothCore via raw GitHub SQL downloads. Recipe item ids from Phase A drive the reverse lookup that converts item-keyed source rows into spell-keyed source entries.

    Total: 3,696 recipes now ship with at least one source entry (61.5% of the 6,014 recipe universe). Per-profession sourced count / total: Blacksmithing 570/934, Tailoring 462/732, Leatherworking 569/1063, Jewelcrafting 661/995, Enchanting 346/454, Inscription 276/653, Engineering 314/445, Alchemy 248/375, Cooking 200/258, First Aid 21/34, Mining 28/57, Fishing 1/14. Per source kind: 1,780 trainer entries, 1,197 vendor, 796 drop (creature + game-object), 355 container (clams / lockboxes / Crafty Quest Reward Boxes), 74 quest. The remaining ~2,300 recipes lacking sources are MoP-only (no maintained MoP emulator yet; deferred to v0.5.1), have specialization gates we don't yet parse, or sit in emulator-uncatalogued world-drop tables. Data files ship at ~8 MB total (1.4 MB largest, Blacksmithing sources) — Blizzard-authoritative comes with size weight.

    New TOC entries: Data/Recipes/Inscription.lua, Data/Recipes/Jewelcrafting.lua, and matching Data/Sources/ files. Wired into TBC (Jewelcrafting only), Wrath / Cata / MoP (both). Vanilla TOC unchanged — neither profession existed pre-TBC. New dev tools: tools/wago_probe.py, tools/build_authoritative_data.py, tools/build_authoritative_sources.py; existing tools/extract_emulator_trainers.py extended to cover 7 new TDB tables (vendor, loot, container, reference, quest, creature, item_loot) AND a rewritten SQL row parser that's now string-quote-aware — the original regex-based tuple splitter bailed on the first ( or ) inside a quoted column (which quest_template is full of: descriptions like $N (level X)), parsing only 248 of AC Wrath's 9,464 quest rows; the new state-machine walker handles every row cleanly. Locations: Data/Recipes/, Data/Sources/, tools/, all 4 expansion TOGProfessionMaster_*.toc files.

Known Gaps

  • MoP-specific trainer / vendor / drop data is absent — TrinityCore dropped active MoP support after TrinityCore 5.4.8, so we'd need to locate a maintained MoP emulator fork (PandaCore or similar) to extract its world DB. MoP recipes are present in the recipe DB (so they appear in Browser and Cooldowns), but they won't surface in the Missing Recipes tab without a source entry. This is the single biggest contributor to the remaining 38.5% source-gap and the v0.5.1 priority.
  • Specialization-locked recipesdata.specialization filtering in BuildMissingList works against a field we don't yet populate, so specialization gates are effectively off (a non-Transmute Master Alchemist still sees Master Transmute recipes as "missing"). Cosmetic, not functional.
  • Some "world drop" recipes still uncatalogued — Blizzard puts a chunk of recipe scrolls in generic world-drop loot tables that emulators reference but don't always populate at the leaf level. A future pass on reference_loot_template chain depth may close some of this gap.

[v0.4.7] (2026-05-21) - Gear + help-i icons bleeding into other AceGUI addons

Bug Fixes

  • Help "i" icon and Settings gear icon bleeding from TOGPM into other AceGUI-based addons (TOGBankClassic, PersonalShopper, Grouper, etc.) — both icons are raw CreateFrames parented to the MainWindow's AceGUI Frame at GUI/MainWindow.lua:214 and GUI/MainWindow.lua:313. The OnClose callback at GUI/MainWindow.lua:192 correctly called addon.GUI.DetachPool on both icons before AceGUI:Release, BUT OnClose only fires when the user clicks the X button. Any other exit path — the ESC key (line 119), the /togpm toggle slash command, or any programmatic MainWindow:Close() — routed through MainWindow:Close at line 477 which called AceGUI:Release(self.frame) directly without detaching. AceGUI then recycled the frame (with our icons still parented) into the next AceGUI:Create("Frame") caller. Fix: extracted the detach + release logic into a shared MainWindow:_ReleaseFrame(widget) helper that both the OnClose callback and Close() now route through, so the icons are Hide() + SetParent(UIParent) + ClearAllPoints regardless of which close path the user takes. The DetachPool helper itself was correct (no change to GUI/SharedWidgets.lua) — the bug was that one of the two close paths skipped it entirely. Location: GUI/MainWindow.lua.

[v0.4.6] (2026-05-21) - MoP cross-bucket known-recipes fix + packaging cleanup

Bug Fixes

  • Recipes the user already knows showing up in the "Missing" list on MoPBuildMissingList pinned its known-recipes lookup to a single guild bucket via addon:FindBucketForChar(charKey, "skills"), but a character's recipes can live in a different bucket than where their skills happened to be cached (e.g., skills in the synthetic NoGuildBucketKey from a brief no-guild moment while the latest scan landed in the current guild's bucket; or stale skill data in an old guild bucket while current scans are elsewhere). Pinning to one bucket caused recipes the player actually has to show as "missing". Fix: rewrite knownByChar to walk EVERY bucket in addon.guildDb.global.guilds and return true the moment ANY bucket has this charKey as a crafter for this recipe. Same correctness logic the Cooldowns/Browser tabs already use in their "My Characters" views. skillMax lookup also rewritten to walk all buckets and take the max. Location: GUI/MissingRecipesTab.lua.

Improvements

  • .pkgmeta cleanup — added .claude/, CLAUDE.md, .markdownlint.json, .markdownlintignore, and tools/ to the package-ignore list so dev tooling and AI-assist guidance files don't ship in the CurseForge package. Removed a redundant *.bat entry (already covered by **/*.bat).

[v0.4.5] (2026-05-20) - Modern Auction House API support (Cata / MoP) + TBC tooltip fix

Bug Fixes

  • Crafter line missing from item tooltips on TBC Anniversary — the tooltip-hook setup had two paths, a modern TooltipDataProcessor.AddTooltipPostCall path (Cata / MoP / Retail) and a legacy OnTooltipSetItem script path (pre-modern-engine Classic). On TBC Anniversary, the modern API doesn't behave the same way and the legacy script doesn't fire on the modern client engine, so neither path triggered. Result: hovering an item showed no [TOGPM] crafters: ... line. Fix: added a third universal fallback that uses hooksecurefunc(GameTooltip, "Show", ...) plus GetItem() — fires after the tooltip is fully populated, works on every Classic and Retail client. Also tightened the de-duplication: the previous check (if tooltip._togpmAppended then return end) was buggy because on the modern client engine, OnTooltipCleared may not fire between tooltips, leaving the flag set from a previous item and silently swallowing every subsequent hover. The check is now if tooltip._togpmAppended == itemID then return end — bails only when THIS item was already appended to THIS tooltip frame. Added /togpm debug instrumentation showing which tooltip path was wired up at PLAYER_LOGIN. Location: Tooltip.lua.

  • Auction House integration broken on Cata Classic and MoP Classic — our Modules/AHScanner.lua used only the legacy AuctionFrame + QueryAuctionItems API. Cata Classic and MoP Classic (and Retail) use the modern C_AuctionHouse API with AuctionHouseFrame, so all the [AH] buttons, "Scan AH" controls, and price lookups silently no-op'd on those clients. Vanilla, TBC Classic, and Wrath Classic still use the legacy path and were unaffected. Fix: feature-detect at file-load (AH._isModernAH = C_AuctionHouse and C_AuctionHouse.SendSearchQuery ~= nil and C_AuctionHouse.MakeItemKey ~= nil) and branch every dispatch path. AH.IsOpen() checks AuctionHouseFrame:IsShown() on modern; AH.SearchFor(itemName) uses AuctionHouseFrame.SearchBar instead of BrowseName + AuctionFrameBrowse_Search; the scan loop uses C_AuctionHouse.SendSearchQuery(MakeItemKey(itemId), {}, false) instead of QueryAuctionItems; result collection branches between ITEM_SEARCH_RESULTS_UPDATED (unique items) and COMMODITY_SEARCH_RESULTS_UPDATED (commodities — most reagents). New AH._completeCurrentItem(listings) helper holds the post-scan dispatch logic so both API paths converge on the same continuation. Modern AH's built-in server throttling makes the legacy CanSendAuctionQuery() gate irrelevant on that path, but the version-aware _scanDelay + Settings override carries over so users on either API can tune scan pacing. Safety timer (delay + 5s) advances the scan if no result event fires, protecting against item-cache misses on modern. Location: Modules/AHScanner.lua.


[v0.4.4] (2026-05-19) - Cross-profession sync gap + AH scan stall + Poisons removed

Bug Fixes

  • Recipes for professions the local character doesn't have were never received from guildmates who do — the DeltaSync OFFER protocol is broadcaster-driven: a peer only offers data for keys that appear in the broadcaster's hash list. A player who has Enchanting / Tailoring locally syncs that data fine (mismatched hashes → offer → fetch), but never receives Engineering / BS / LW recipes from guildmates who have them, because the broadcaster has no recipemeta:202 / crafters:202 entry to advertise. Confirmed by /togpm status on a TBC Anniv user showing 7 profession buckets locally and catchUpCycles=5 (max retries exhausted) with zero sync events for profIds 164 / 165 / 202 across an entire debug log. Fix: new HashManager:PadMissingProfessionPlaceholders(DS, map) injects a placeholder hash entry (the stable hash of an empty table) for every available crafting profession the local player has no content for. Peers see the placeholder mismatch their real hash, offer, we accept, we merge — and on the next broadcast the real computed hash naturally replaces the placeholder. Placeholders live only in the broadcast map, not in gdb.hashes, so they don't conflate "I want this" with "I have this." Wired into both broadcast sites (getMyHashes in InitDeltaSync and Scanner:BroadcastHashes). Location: Modules/HashManager.lua, Scanner.lua.

  • AH scan stalled after the first item on TBC / Wrath / Cata / MoP (Classic Era was unaffected) — debug logs from a TBC Anniversary user showed query 1 completing instantly, query 2 firing, then no AUCTION_ITEM_LIST_UPDATE ever arriving for it. The 1.5s scan delay from v0.4.3 was tuned to Classic Era's looser server throttle; TBC and later servers enforce a stricter window, and the second query was being silently dropped by the rate limiter without firing a response event, stalling the scan. Three-part fix: (1) the scan delay is now version-aware — defaults to 1.5s on Classic Era / Anniversary (where it always worked) and 3.0s on TBC / Wrath / Cata / MoP (matches what other Classic AH-scan addons use as a safe default). (2) Every QueryAuctionItems call is gated on CanSendAuctionQuery() returning true — when the API says throttled we put the item back at the front of the queue and retry in 0.5s slices until it agrees. This handles the case where the default delay still isn't enough for a particular server's current throttle state. (3) New Settings entry "AH scan delay (seconds)" lets users override the version default in the range 0.5–10s — empty / 0 / "off" uses the version-appropriate default. Lets each guild dial in the right value for their server. Location: Modules/AHScanner.lua, GUI/Settings.lua, TOGProfessionMaster.lua, Locale/enUS.lua.

Improvements

  • Poisons profession removed entirely — Poisons (skill 40) was Rogue-only and became automatic-from-trainer in Wrath 3.1, so the data was dead weight on Vanilla / TBC and irrelevant on Wrath / Cata / MoP. Removed from addon.PROF_NAMES, addon.PROF_AVAILABILITY, and addon.CRAFTING_PROFS; deleted Data/Recipes/Poisons.lua and Data/Sources/Poisons.lua; dropped the two Poisons load lines from all five TOC files (vanilla, _TBC, _Wrath, _Cata, _Mists); updated docstring references in Browser and Missing Recipes tabs. Location: TOGProfessionMaster.lua, GUI/BrowserTab.lua, GUI/MissingRecipesTab.lua, all TOC files.

[v0.4.3] (2026-05-19) - Profession Browser "My Characters" filter walks all buckets + shared bucket-walk helpers

Bug Fixes

  • "My Characters" filter on the Profession Browser tab still ignored alts outside the current guild after v0.4.2 — v0.4.2 fixed the same bug on the Cooldowns and Missing Recipes tabs but didn't carry the pattern over to the Browser. BuildRecipeList was still reading gdb.recipes from the current guild bucket only, so a recipe that an alt in Guild B (or in no guild) could craft never appeared in Main-in-Guild-A's "mine" view. No data migration is needed — the existing scans are already keyed correctly per bucket. Fix: new CollectRecipesForView(viewMode) helper that, in "mine" mode, walks every bucket in addon.guildDb.global.guilds and unions the per-(profId, recipeId) crafter sets across buckets. First bucket's metadata wins on duplicate recipe rows; crafters from every bucket are unioned. In "guild" mode and any other view mode the helper returns the current bucket's recipes table unchanged, so guild-view behavior is identical to v0.4.2. Local read only — no sync-protocol implications. Location: GUI/BrowserTab.lua.

Improvements

  • Shared cross-bucket walk primitives in addon namespace — v0.4.2 introduced three near-identical bucket-walking helpers (CollectCooldownsByChar in CooldownsTab, FindCharBucket in MissingRecipesTab, plus an inline walk in the transmute popup) and v0.4.3 was about to add a fourth (CollectRecipesForView in BrowserTab). Promoted the common pattern to two reusable primitives in TOGProfessionMaster.lua: addon:ForEachGuildBucket(callback) iterates every bucket in addon.guildDb.global.guilds, and addon:FindBucketForChar(charKey, field) returns the first bucket whose field sub-table contains an entry for charKey (works for any per-character sub-table — "skills", "cooldowns", "specializations", etc.). All four call sites refactored to use them: the Browser's CollectRecipesForView, the Cooldowns tab's CollectCooldownsByChar and transmute-popup lookup, and the Missing Recipes tab's GetCharactersWithProfessions / GetProfessionsForCharacter / BuildMissingList. Future tabs that need the same access pattern can hit the helpers directly instead of reinventing the iteration. Location: TOGProfessionMaster.lua, GUI/BrowserTab.lua, GUI/CooldownsTab.lua, GUI/MissingRecipesTab.lua.

[v0.4.2] (2026-05-18) - Dynamic broadcast debounce + "My Characters" filter works for cross-guild and guildless alts

Bug Fixes

  • "My Characters" filter on Cooldowns and Missing Recipes tabs only showed alts that were in the same guild as the currently-logged-in character — the whole point of the filter was account-wide visibility, but both tabs only read from addon:GetGuildDb() (the current guild's bucket). An alt in a different guild had their cooldowns/skills stored in that guild's bucket, invisible here. An alt with no guild scanned nothing at all because the scanner early-returned on if not addon:GetGuildKey() then return end. Fix has three parts: (1) addon:GetGuildDb() now falls back to a synthetic addon.NoGuildBucketKey = "__noguild" bucket when the player has no guild, so guildless scans always have somewhere to write. (2) Every broadcast helper (BroadcastHashes, BroadcastLeafToGuild, BroadcastSubhashesToGuild) now gates explicitly on addon:GetGuildKey() returning a real (non-nil) value as belt-and-suspenders protection — the synthetic bucket's contents never reach the wire. (3) The Cooldowns tab in "mine" view walks every bucket in addon.guildDb.global.guilds and merges rows for own characters (latest expiresAt wins on duplicates from stale buckets); the transmute-group popup uses the same bucket walk. The Missing Recipes tab gets a FindCharBucket(charKey) helper that does the same walk so GetCharactersWithProfessions, GetProfessionsForCharacter, and BuildMissingList all surface cross-guild and guildless alts. Sync protocol is unchanged: only real-guild data ever crosses the wire. Location: TOGProfessionMaster.lua, Scanner.lua, GUI/CooldownsTab.lua, GUI/MissingRecipesTab.lua.

Improvements

  • Broadcast debounce now scales with the number of addon users in the guild — the static 30s floor on Scanner:BroadcastHashes was sized for busy guilds, but it suppressed legitimate post-scan recipe broadcasts in any guild small enough that the 10-min periodic catch-up tick couldn't reliably reach an online peer (the bug v0.4.1 worked around with a MoP-only static 3s value). Replaces the MoP gate with a VersionCheck-1.0-driven recount: at InitDeltaSync and at the top of every 10-min tick, fires VC:FireBatch() and 21 seconds later (after the VC10_REQ broadcast + 8s jitter window + 12s collect period have all settled) sets Scanner._broadcastSeconds = max(3, min(30, addonUsersOnline)). Linear scale: a 2-person test guild gets a 3s floor so recipe hashes propagate within seconds; a 30+ active-addon-user guild keeps the original 30s ceiling to protect the GUILD channel from saturation. The new count from each tick's batch applies to the next 10-min cycle; the current cycle uses whatever the previous recount produced (steady state converges in one tick). Falls back to the 30s default until the first recount lands, and degrades to the default if VersionCheck-1.0 isn't loaded. Helper: Scanner:ScheduleAddonUserRecount. Location: Scanner.lua.

[v0.4.1] (2026-05-18) - MoP profession availability + MoP guild sync fixes

Bug Fixes

  • Jewelcrafting and Inscription missing from profession dropdowns on MoPaddon.PROF_AVAILABILITY[755] and [773] (the JC / Inscription availability predicates) referenced addon.isMists, but Compat.lua only defines addon.isMoP. On a MoP client isCata is false and isMists is nil, so IsProfessionAvailable(755 / 773) returned false and every dropdown that filters via that helper dropped both professions. Fix: rename both references to addon.isMoP so the predicate matches the flag actually set at load time. Other professions were unaffected (they have no PROF_AVAILABILITY entry and default to always-available). Location: TOGProfessionMaster.lua.

  • Recipe sync between MoP peers silently blocked by broadcast debounceScanner._broadcastSeconds = 30 puts a 30s floor on Scanner:BroadcastHashes, intended as a guard against bursty rapid-fire ScheduleBroadcast calls. On TBC/Classic/Cata with larger guilds, the 10-min periodic catch-up tick at Scanner.lua:405-409 reliably lands while some peer is online, so recipe hashes eventually escape. On MoP Classic with smaller guilds, peers rarely overlap during the 10-min window, and the post-scan broadcast (which is what carries newly-invalidated recipemeta:<profId> / crafters:<profId> hashes) gets suppressed for the entire 30s wall after every successful broadcast. Result: per-character cooldown sync works (those leaves are keyed by charKey, so each broadcaster's first login hash list carries them cleanly), but guild-wide recipe leaves never propagate between MoP peers — both sides only ever see their own scans. The v0.2.0 protocol's offer/handshake dance requires the requesting side's hash list to reach the provider so the provider can WHISPER an OFFER back; with broadcasts suppressed, the dance never starts. Fix: gate the debounce on addon.isMoP — 3s on MoP (enough to coalesce TRADE_SKILL_SHOW + TRADE_SKILL_UPDATE pairs beyond the 0.5s ScheduleBroadcast coalescer), 30s elsewhere. The count == 0 differential check at Scanner.lua:1287-1290 already provides content-based throttling, so a 3s floor is purely a burst guard. Location: Scanner.lua.


[v0.4.0] (2026-05-18) - Cooldown-ready alerts + Cooldowns view filter + settings gear + scroll-pool / tab-pool fixes

New Features

  • Per-row "!" cooldown-ready alarm on the Cooldowns tab — same "!" toggle pattern as the Profession Browser's shopping-list alert, but only renders on your own characters (gated on addon:IsMyCharacter(row.charKey)). Cyan when armed, grey when off. When the cooldown becomes ready, the alarm prints a chat line and plays SOUNDKIT.ALARMCLOCKWARNING_3 (5274) — deliberately distinct from the existing gold-coloured crafter-online alert + PlaySound(878) so the two are immediately distinguishable in the heat of guild chat. Toggling "!" on a cooldown that is already ready fires immediately so the user gets a confirmation ping. New engine module: Modules/CooldownAlerts.lua, 30-second polling tick driven by Ace:ScheduleRepeatingTimer; armed-set persisted in Ace.db.char.cooldownAlerts keyed by charKey:spell:<id> / charKey:transmute / charKey:group:<groupKey> with metadata captured at toggle time. Login-time pre-stamp avoids the burst of pings on UI reload. Location: GUI/CooldownsTab.lua, Modules/CooldownAlerts.lua, Locale/enUS.lua, TOGProfessionMaster.lua.

  • Recurring cooldown-ready reminders — new setting "Cooldown ready reminder" (free-text minutes, 0/empty/off disables, validated 1–1440 range with friendly error). When set, every armed cooldown re-fires the alarm every N minutes while the cooldown stays ready, anchored to the first-fire wall-clock time so the cadence stays regular even if the first ping was delayed (login, instance exit, etc.). Crafting clears the dedup clock so the next ready transition fires a fresh first alert. Implemented by replacing the dedup boolean with a _lastFiredAt[key] timestamp in CooldownAlerts:Check — same value gates both the initial fire and the recurring re-fire window. Location: Modules/CooldownAlerts.lua, GUI/Settings.lua.

  • "Mute alerts in instances" setting — defaults ON. When the user is inside any IsInInstance() instance (raid / dungeon / battleground / arena / scenario) the alarm stays silent. Pending alerts fire the moment the user steps out — Check doesn't set _lastFiredAt[key] while suppressed, so the next un-suppressed tick treats it as fresh. Capital cities and sanctuaries are deliberately NOT suppressed: the user still gets pinged while AFK in Stormwind. Location: Modules/CooldownAlerts.lua, GUI/Settings.lua.

  • Guild / My Characters dropdown on the Cooldowns tab — mirrors the Browser tab's view-mode dropdown so the two tabs behave the same way when the user wants to focus on their own cooldowns. Sits in the toolbar after the Profession / Cooldown filter dropdowns. Filter applied in FillRows after the profession+cooldown filter pass so both compose naturally; the same view filter also gates Scan AH's getItems so the scan only walks reagents the user can actually see in the list. State session-only (CooldownsTab._viewMode), matching the existing profession/cooldown filter pattern. Location: GUI/CooldownsTab.lua.

  • Settings gear icon on the main window — between the help "i" icon and the AceGUI Close button. 20×20 Button with Interface\Icons\Trade_Engineering texture + ButtonHilight-Square highlight, anchored to helpIcon's BOTTOMRIGHT + (3, 2) for vertical centring against the 24-tall help icon. Click opens the AceConfig settings panel via addon:OpenSettings() — same target as /togpm settings and Shift+left-click on the minimap button. Wraps the open call in a TOGPMEscProxy save-and-restore so the Blizzard Settings panel's CloseSpecialWindows() doesn't slam the main window shut when the panel opens. Bottom strip layout: [status text...(-183)] -6- [help 24] -3- [gear 20] -3- [Close 100 (-27)]; statusbg's right edge moved from -163 to -183 to make room. Detached in OnClose via addon.GUI.DetachPool so it doesn't bleed into the next addon that acquires the recycled AceGUI Frame. Location: GUI/MainWindow.lua.

Bug Fixes

  • Cooldowns tab showed "Veil of Shadow" instead of "Salt Shaker" — the Salt Shaker item-based cooldown is stored under its item ID 15846, but spell ID 15846 is a generic NPC ability named "Veil of Shadow." BuildRows resolved cdName via data.cooldowns[spellId] or GetSpellInfo(spellId) or GetItemInfo(spellId) or tostring(spellId), and GetSpellInfo(15846) won the lookup chain before GetItemInfo(15846) could be tried. Fix: special-case spellId == data.saltShakerItem to go straight to GetItemInfo. Location: GUI/CooldownsTab.lua.

  • Open profession dropdown closed on every guild sync (3-second cadence in active guilds)GUILD_DATA_UPDATEDMainWindow:QueueRefreshtabs:ReleaseChildren() → AceGUI released the toolbar's Dropdown widget mid-interaction, which closes its pullout. Fix: new helper addon.GUI.IsAnyDropdownPulloutOpen() walks AceGUI's global pullout pool (_G["AceGUI30Pullout"..N]:IsShown()) and MainWindow:Refresh defers the redraw by 250ms when one is open, re-queuing itself until the user dismisses the pullout. Same defense protects Cooldowns / Browser / Missing dropdowns for free. Location: GUI/MainWindow.lua, GUI/SharedWidgets.lua.

  • Cooldowns tab scrollbar disappeared after switching from Browser — Browser's virtual-scroll trick (scroll.LayoutFinished = function() end to prevent AceGUI from overwriting its manually-set content:SetHeight(#recipes * ROW_HEIGHT)) installs the override on the widget INSTANCE. AceGUI's ScrollFrame stores methods as instance fields (Constructor copies the methods table directly onto each widget), so the override survives AceGUI:Release and follows the recycled widget into whichever tab acquires it next. When Cooldowns received the polluted scroll, its AceGUI children laid out fine but the no-op LayoutFinished prevented content.height from being set, FixScroll saw viewheight==0, hid the scrollbar. Fix: PersistentScroll.Acquire (the new shared helper) captures the original LayoutFinished from a freshly-pulled scroll once per session and restores it on every acquire — Browser's no-op override is wiped before Cooldowns / Missing see the widget. Location: GUI/SharedWidgets.lua.

  • Cooldowns tab scrollbar ALSO disappeared after Missing → Cooldowns — different root cause, same symptom. Missing installs container.LayoutFinished = AnchorAll on the TabGroup container to drive its custom section + scroll-frame anchoring. The TabGroup widget stays alive across tab switches (only its children get released), so Missing's AnchorAll closure persisted on it and fired during Cooldowns' subsequent layout passes. The closure referenced Missing's stale self._scroll.frame / self._headerFrame — and because the AceGUI ScrollFrame pool recycled Missing's released scroll widget into Cooldowns, AnchorAll re-anchored Cooldowns' (same widget instance) scroll frame to Missing's hidden header / section frames, collapsing the viewport to ~0 px → no scrollbar. Fix: MainWindow:DrawTab clears container.LayoutFinished = nil before every tab Draw, wiping the previous tab's override. Missing / Browser still re-set their own anchoring overrides as part of their own Draw. (Earlier attempt to restore the class default failed because TabGroup's class LayoutFinished shrinks content during intermediate AddChild passes via OnHeightSet, which collapsed Missing's section to 0 px and made the rows vanish — see the next entry.) Location: GUI/MainWindow.lua.

  • Missing tab rows disappeared after the scroll-pool fix above — Missing had silently been free-riding on Browser's scroll.LayoutFinished = function() end pollution. AceGUI's FixScroll calls self:DoLayout() on every scrollbar visibility transition (AceGUIContainer-ScrollFrame.lua:108/118), and DoLayout runs the scroll's "List" layout — which calls scroll.LayoutFinished(_, _, 0) because Missing's scroll has no AceGUI children (raw frame pool inside scroll.content). The class default content:SetHeight(0 or 20) was clobbering Missing's manually-set scroll.content:SetHeight(#list * ROW_HEIGHT) whenever the scrollbar toggled. Pre-fix this didn't manifest because Browser's pool pollution made Missing's scroll silently inherit the no-op LayoutFinished whenever the user had visited Browser first. After PersistentScroll.Acquire started restoring the class default on every acquire (correct for Cooldowns, which DOES have AceGUI children), Missing had to install its own no-op explicitly — matches Browser's virtual-scroll pattern. Location: GUI/MissingRecipesTab.lua.

  • Missing tab scroll position resetting on every redraw — same _scrollStatus persistence pattern that Cooldowns and Browser already use, applied to Missing via the new shared helper. Includes dropdown / filter / character changes — your scroll position now survives every redraw. Location: GUI/MissingRecipesTab.lua via GUI/SharedWidgets.lua.

Improvements

  • addon.GUI.PersistentScroll shared helper — three primitives (Acquire(tab, opts), Restore(scroll, saved, afterFn?), Reset(tab, scroll)) replacing the previously-duplicated scroll-status plumbing in BrowserTab, CooldownsTab, and MissingRecipesTab (was ~25-40 lines per tab). Acquire captures the saved scrollvalue into a local BEFORE any synchronous FixScroll clobber, resets the status table to zero so FixScroll's implicit writes are harmless, then re-attaches the persistent status; Restore does SetScroll(saved) + scrollbar visual nudge + optional afterFn callback for virtual-pool tabs that need to call UpdateVirtualRows; Reset is the explicit jump-to-top primitive for filter-change call sites. Also restores the captured class LayoutFinished on every acquire (see the Cooldowns scrollbar fix above). Location: GUI/SharedWidgets.lua.

  • Locale label fixes for truncation — "Suppress cooldown alerts in instances" (38 chars) → "Mute alerts in instances" (24 chars, matches neighbouring "Suppress alerts on login"). "Cooldown ready reminder (minutes)" (33 chars) → "Cooldown ready reminder" (23 chars), with units moved into the description so the tooltip carries the "1–1440 minutes (24 hours)" detail. Dropped the now-redundant SettingsCooldownReminderUsage line so the AceConfigDialog tooltip consolidates onto a single description line. Location: Locale/enUS.lua, GUI/Settings.lua.

  • Gear-icon tooltip arrow swapped to ASCII (\226\134\146, U+2192) doesn't render in the tooltip font on Classic Era. Replaced with > like FastGuildInvite uses, wrapped in |cffffd700...|r for visual hierarchy. Location: GUI/MainWindow.lua.


[v0.3.5] (2026-05-17) - Browser tab scroll position persists across guild syncs

Bug Fixes

  • Profession Browser recipe list snapped back to the top every few seconds while scrolling — in any active guild, peers broadcast sync deltas every few seconds; each SYNC_RECV fires GUILD_DATA_UPDATED, which MainWindow:QueueRefresh debounces into a MainWindow:Refresh call that does tabs:ReleaseChildren() and re-runs BrowserTab:Draw(). The freshly-acquired AceGUI ScrollFrame widget always starts at scroll value 0 (its OnAcquire does SetScroll(0)), so the user got yanked back to the top mid-scroll on every sync. CooldownsTab solved this in an earlier release with a persistent _scrollStatus table re-attached via SetStatusTable on each Draw (GUI/CooldownsTab.lua:991), but BrowserTab never got the same treatment. Fix applies the same persistent-status pattern, with one extra wrinkle: BrowserTab:FillList calls scroll:FixScroll() synchronously, and FixScroll calls scrollbar:SetValue(0) on the recycled widget — if the scrollbar's residual value from a previous use was non-zero, that fires the default OnValueChangedSetScroll(0) → writes scrollvalue=0 into our status table, destroying the saved position before the restore code can read it. So the saved scroll value is now captured into a local at the top of BrowserTab:Draw() BEFORE the scroll widget is created (and before any FixScroll clobber can happen), then restored via SetScroll(saved) + UpdateVirtualRows() after FillList completes and content height is set. RefreshList (profession dropdown + search-box changes) explicitly resets to scroll position 0 — a filter change should always show the top of the new result set, not whatever offset the previous list was scrolled to. Location: GUI/BrowserTab.lua.

[v0.3.4] (2026-05-05) - Help-icon widget bleed fix + DetachPool single-frame form + consolidated cleanup

Bug Fixes

  • Help "i" info tooltip on the main window bleeding into other Ace3 addons — the help icon next to the close button is a raw CreateFrame("Frame", nil, f.frame) parented to the AceGUI Frame's underlying frame. The OnClose callback released the AceGUI Frame back to its widget pool but never detached our helpIcon, so the icon — texture, mouse handlers, and tooltip body that reads "Profession Browser / Cooldowns Tracker / Missing Recipes" — stayed parented to the recycled Frame. The next addon that called AceGUI:Create("Frame") (any Ace3 config window, BigWigs options, etc.) acquired our Frame from the pool with the "i" icon riding on it, visibly inside their UI and serving up TOGPM's tab-help tooltip on hover. Same class of bug as v0.3.3's shopping-list widget bleed, just on a different raw frame. Fix: stash helpIcon on self._helpIcon at creation and route through the shared addon.GUI.DetachPool helper inside the existing OnClose callback, before AceGUI:Release(_widget). Location: GUI/MainWindow.lua.

  • _detailOuter (Browser tab's recipe detail panel) missing ClearAllPoints on cleanup — the inline OnRelease block at the recipe-scroll widget did :Hide() and :SetParent(UIParent) on the detail panel, but not :ClearAllPoints(). Old anchors (TOPRIGHT → headerBar BOTTOMRIGHT, BOTTOMRIGHT → container.content BOTTOMRIGHT) lingered after the detach. If those anchor target frames were the AceGUI parents being recycled, the detail panel could carry a stale point reference into the next addon's frame layout. Now routed through addon.GUI.DetachPool which always does the full Hide + UIParent + ClearAllPoints triple. Location: GUI/BrowserTab.lua.

Improvements

  • addon.GUI.DetachPool now accepts a single raw frame as well as an array — the helper introduced in v0.3.3 only handled arrays of pooled frames (via ipairs). For one-off raw frames parented to AceGUI widgets — like the main window's help icon, the Browser tab's _headerBar, and the detail panel's _detailOuter — call sites had to inline the same Hide + UIParent + ClearAllPoints triple instead of using the helper. Extended addon.GUI.DetachPool(poolOrFrame) to detect a single frame by type(poolOrFrame.Hide) == "function" and apply the same cleanup. Result: every raw-frame cleanup site in the codebase now flows through one function. Location: GUI/SharedWidgets.lua.

  • Consolidated four inline cleanup sites in BrowserTab to use the helperBrowserTab:Draw() had a defensive _headerBar cleanup at the top, the recipe-scroll's OnRelease had another _headerBar cleanup plus a _detailOuter cleanup, all written as 4-5 line if frame then frame:Hide() ... end blocks. Each replaced with a single addon.GUI.DetachPool(self._<frame>) call. The helper handles the nil check, the right cleanup operations, and matches the surrounding BrowserTab:DestroyPool / BrowserTab:DetachShoppingListPool / MissingRecipesTab:DetachPool / MainWindow OnClose call sites — five places in the addon, one helper. Future raw-frame parented to an AceGUI widget = one-line cleanup. Location: GUI/BrowserTab.lua.


[v0.3.3] (2026-05-04) - Shopping-list widget bleed fix + rank-book filter + addon.GUI.DetachPool helper

Bug Fixes

  • Missing Recipes tab listed Expert / Artisan rank-up books even when the character's skillMax was already above the rank's cap — Cooking, First Aid, and Fishing have rank-up book entries (teaches = "Expert" / "Artisan" / etc.) in our static recipe DB. The build pass had no way to filter them: knownByChar(spellId) and knownByChar(data.teaches) both miss because the rank books aren't in gdb.recipes and data.teaches is a string, not a spell ID. Result: a First Aid 300 character saw both "Expert First Aid" (raises max 150→225) and "Artisan First Aid" (raises max 225→300) listed as missing — even though the only way they got to 300 was by consuming both. Fix: new RANK_CAPS map ("Expert" = 225, "Artisan" = 300, "Master" = 375, etc. — exhaustive across all expansion ranks for future-proofing) plus a skip check in BuildMissingList that hides rank-book entries whose cap is at or below the character's skillMax. WoW has no API to detect "did the player use this consumable" so skillMax is the proxy: if it's at the cap, the book must have been used. Location: GUI/MissingRecipesTab.lua:BuildMissingList.

  • Shopping-list rows on the Profession Browser tab bleeding into other Ace3 addons — the shopping-list section's _slPool and _slReagentPool are pooled raw CreateFrame rows parented to the AceGUI InlineGroup's content frame (line 533/534 in FillShoppingListSection). When the InlineGroup was released back to AceGUI's pool — on tab switch, window close, or empty-list redraw — our pool frames were never detached, so they stayed SetParent'd to the InlineGroup. AceGUI then recycled the InlineGroup into another addon's UI with our rows still attached, visibly bleeding into that addon. Fix: new BrowserTab:DetachShoppingListPool() method wired to the InlineGroup's OnRelease callback. Locations: GUI/BrowserTab.lua, CLAUDE.md.

Improvements

  • Consolidated pool-detach into one shared helper — the same Hide + SetParent(UIParent) + ClearAllPoints loop was duplicated in three places (BrowserTab:DestroyPool for the recipe scroll, the new BrowserTab:DetachShoppingListPool, and MissingRecipesTab:DetachPool). Extracted to addon.GUI.DetachPool(pool) in GUI/SharedWidgets.lua. All three call sites now route through the shared function so the next pool we add only requires a one-line OnRelease wiring instead of inventing the cleanup loop again. New CLAUDE.md rule documents the pattern: any tab that parents raw CreateFrame rows to an AceGUI widget's content frame MUST register addon.GUI.DetachPool(pool) on the parent widget's OnRelease. Location: GUI/SharedWidgets.lua, CLAUDE.md.

[v0.3.2] (2026-05-04) - Multi-version TOC sync (hotfix)

Bug Fixes

  • Lua error on opening the Profession Browser on TBC / Wrath / Cata / MoP clients (attempt to index field 'GUI' (a nil value) at GUI/BrowserTab.lua:316) — TOGProfessionMaster_TBC.toc, _Wrath.toc, _Cata.toc, and _Mists.toc had been frozen at the v0.2.7 file list and never picked up the v0.3.0 additions. Missing entries on every non-Vanilla TOC: 22 Data/Recipes/*.lua + Data/Sources/*.lua data files, Modules/AHScanner.lua, GUI/SharedWidgets.lua, and GUI/MissingRecipesTab.lua. Without SharedWidgets.lua loading, addon.GUI was never created, and BrowserTab's call to addon.GUI.AttachTooltip(...) blew up the tab. Synced all four non-Vanilla TOCs to match the Vanilla TOC's file list. Locations: TOGProfessionMaster_TBC.toc, TOGProfessionMaster_Wrath.toc, TOGProfessionMaster_Cata.toc, TOGProfessionMaster_Mists.toc.

[v0.3.1] (2026-05-03) - Version-aware profession dropdowns + de-duplicated profession lookup + offline-peer chat spam fix

Bug Fixes

  • No player named 'X' is currently playing. chat spam when guildmates log out — peers broadcast their L0 hash list on the GUILD channel; our addon (and DeltaSync's internal P2P logic) responded by whispering them OFFER / RequestData messages. If the peer logged out between their broadcast and our reply landing on the wire, the server rejected the whisper and printed the system error to chat. A guild with active sync produced this spam dozens of times per logout event. Two-pronged fix: (1) added a Scanner.GuildCache:IsPlayerOnline(sender) gate at every DS:RequestData(sender, ...) call site in Scanner.lua — three sites total: onSyncAccepted for both the subhashes-roll-up and the leaf-data branches, plus the per-character leaf-data request inside the subhashes-response handler. Mirrors the TOGBankClassic pattern in Modules/DeltaComms.lua where every send site has an IsPlayerOnline guard. (2) Added a CHAT_MSG_SYSTEM filter via ChatFrame_AddMessageEventFilter that swallows "No player named X is currently playing." (both quoted and unquoted variants) and "Player not found" system messages — race-window safety net for the gap between our online check and the actual send, plus DeltaSync's own internal sends that we don't directly control. Pattern matches the TOGBankClassic implementation in Modules/Events.lua including the fast plain-text prefix check before the full pattern match (cheap on the high-volume CHAT_MSG_SYSTEM stream). Trade-off: a manual /w to a player who logged off also won't show the error, accepted because the addon generates orders of magnitude more such errors than user typos do, and the lack of typing response in the chat window already signals the failed send. Location: Scanner.lua:InstallChatFilter / Scanner.lua:Init / Scanner.lua:onSyncAccepted / Scanner.lua subhashes-response handler.

  • Jewelcrafting and Inscription missing from the Profession Browser dropdown on TBC / Wrath / Cata / MoP clientsBrowserTab.ALL_PROFESSIONS was a hardcoded static list of 9 Vanilla professions (the comment literally said "Static list of all Vanilla crafting professions"). Users on later clients couldn't filter by JC (added in TBC) or Inscription (added in Wrath) even when guildmates had broadcast recipes for them. Now the Browser dropdown derives from the shared addon.PROF_NAMES master table filtered by addon.IsProfessionAvailable(profId) and addon.CRAFTING_PROFS set, so JC appears on TBC+ and Inscription appears on Wrath+ automatically. Location: GUI/BrowserTab.lua:GetProfDropdownEntries.

Improvements

  • De-duplicated profession lookup tablesPROF_NAMES was previously copy-pasted in 4 places (addon.PROF_NAMES in TOGProfessionMaster.lua, plus local PROF_NAMES in CooldownsTab / MissingRecipesTab, plus ALL_PROFESSIONS in BrowserTab) and had drifted out of sync (Cooldowns had JC + Inscription, Missing didn't, Browser had neither). Consolidated into one master table on addon.PROF_NAMES with all 16 known professions. Adding a profession or fixing a display name is now one edit instead of four, and the three tabs can't drift apart again. Location: TOGProfessionMaster.lua, GUI/BrowserTab.lua, GUI/CooldownsTab.lua, GUI/MissingRecipesTab.lua.

  • Version-aware profession dropdowns across every tab — new addon.PROF_AVAILABILITY map declares which client versions a profession exists on (default = always-available / Vanilla). New addon.IsProfessionAvailable(profId) helper returns true/false based on the current addon.is* flags. Every tab's profession dropdown filters by this:

    • Jewelcrafting (755) — TBC, Wrath, Cata, Mists only (hidden on Vanilla)
    • Inscription (773) — Wrath, Cata, Mists only (hidden on Vanilla / TBC)
    • Poisons (40) — Vanilla, TBC only (hidden on Wrath+ since the WotLK 3.1 patch made Rogue poisons automatic)

    No data migration: profession IDs and saved selections stay valid; the existing "selected profession not in list → fall back to first available" guards in each tab handle stale per-character state silently. Location: TOGProfessionMaster.lua.

  • New addon.CRAFTING_PROFS set — explicit list of professions that produce learnable recipes (i.e. belong in the Browser / Missing Recipes lists). Excludes pure gathering (Herbalism / Skinning / Fishing). Mining stays in because Smelting produces craftable bars. Used by BrowserTab to filter its dropdown; Cooldowns and Missing have their own implicit category filters (cooldown definitions / character skill data). Location: TOGProfessionMaster.lua.

Known Gaps

  • Missing Recipes data for Jewelcrafting and Inscription — the dropdown now shows JC on TBC+ and Inscription on Wrath+, but our static recipeDB / sourceDB (copied from PersonalShopper, which is Vanilla-only) doesn't contain entries for these professions yet. A character with JC / Inscription will see the profession in the dropdown but no scrolls listed. Curating the recipe-scroll universe and source tags for these professions is planned for v0.3.2.

[v0.3.0] (2026-05-03) - Missing Recipes tab + AH scanner + shared widget factories

New Features

  • New "Missing Recipes" tab — third tab in the main window, modeled on PersonalShopper's Collector. Picks a character from a dropdown (defaults to the currently logged-in toon, includes any tracked alts on the account), then a profession from a second dropdown (filtered to professions that character has scanned), and renders every recipe scroll the character hasn't yet learned. Each row shows the scroll icon, color-coded item name, required skill, and source tags (Vendor / Drop / Quest / Crafted / Container / Fishing). Hover the icon or name for the standard item tooltip (anchored via addon.Tooltip.Owner); shift-click the row to insert the scroll's hyperlink into the active chat edit box. A [Bank] button appears at the right of each row when TOGBankClassic reports the recipe scroll in stock — click to open the standard bank-request dialog (matches the BrowserTab / CooldownsTab / ShoppingListTab [Bank] pattern). Search box filters by name; "Include trainer-only" checkbox unhides recipes obtainable only from a trainer (off by default to match PS, since trainer-only scrolls aren't AH-buyable). Column headers (count, Skill, Sources) now have hover tooltips explaining each column. Location: GUI/MissingRecipesTab.lua.

  • Embedded recipe-universe databaseData/Recipes/<Profession>.lua and Data/Sources/<Profession>.lua, copied verbatim from PersonalShopper's curated dataset (11 professions: Alchemy, Blacksmithing, Cooking, Enchanting, Engineering, First Aid, Fishing, Leatherworking, Mining, Poisons, Tailoring). The Lua local _, addon = ... upvalue is per-addon, so addon.recipeDB and addon.sourceDB here are private to TOGPM and don't collide with PS even when both addons are loaded simultaneously. No optional dependency on PS — the tab works fully standalone. Location: TOGProfessionMaster.lua, Data/Recipes/, Data/Sources/.

  • ReagentWatch:IsWatching(itemId) API — sibling to RW:Watch / RW:Unwatch. Returns true when the item is currently on the watch list. Lets the new tab toggle the + button between "add" and "already watching" states without poking at the underlying SavedVariable directly. Location: Modules/ReagentWatch.lua.

  • addon.AH module — new shared module that mirrors the addon.Bank pattern for the auction house. addon.AH.IsOpen() tracks AH visibility via AUCTION_HOUSE_SHOW/AUCTION_HOUSE_CLOSED; addon.AH.SearchFor(itemName) switches the AH to Browse, populates the name field, clears any narrowing filters (level/quality/usable), and fires AuctionFrameBrowse_Search. addon.AH.StartScan(items, opts) queues {itemId, itemName} pairs and walks them at 1.5s intervals (rate-limit-safe), calling QueryAuctionItems per item with exactMatch=true and collecting results from AUCTION_ITEM_LIST_UPDATE; results cache per itemId for the session. Custom AH_OPEN_STATE_CHANGED and AH_SCAN_COMPLETE callbacks (via addon.callbacks) let each tab refresh its UI live as scan state changes. Auto-clears scan results when the AH closes (listings go stale fast). Targets Vanilla → MoP via the legacy AuctionFrame UI; retail (8.0+) needs a separate C_AuctionHouse path that's out of scope. Location: Modules/AHScanner.lua.

  • Scan AH on every tab + per-row [AH] button — Browser, Cooldowns, Missing Recipes, and ShoppingList all gained "Scan AH" toolbar buttons that walk the items relevant to that tab (shopping-list reagents / cooldown reagents / missing recipe scrolls). After scan completes, rows whose item has live listings reveal an [AH] button next to [Bank], gated on addon.AH.GetListingsFor(itemId).count > 0 exactly like [Bank] is gated on Bank.GetStock(itemId) > 0. Click [AH] to jump the AH browse search to that item. Toolbar button label flips to Scanning N/M while running and click cancels. The Cooldowns tab's [+] Transmute popup also got per-row [AH] buttons mirroring its existing [Bank] pattern, so multi-reagent transmutes (Arcanite needs Thorium Bar + Arcane Crystal) get one [AH] per reagent row. Locations: GUI/BrowserTab.lua, GUI/CooldownsTab.lua, GUI/MissingRecipesTab.lua, GUI/ShoppingListTab.lua.

  • Cooldowns tab — two-level Profession / Cooldown filter dropdowns — two new AceGUI dropdowns on the toolbar (next to the Ready Only button) let you narrow the cooldown list by profession then by specific shared-timer cooldown. Profession dropdown lists professions that have cooldowns in the current game version; cooldown dropdown is contextual to the selected profession and is hidden when profession is "All". Selecting a different profession resets the cooldown selection to "All" within it. Full taxonomy across all expansions: Alchemy (Transmute group, Alchemy Research), Tailoring (Mooncloth, Specialty Cloth — combined for TBC Spellcloth/Shadowcloth/Primal Mooncloth + Wrath Spellweave/Moonshroud/Ebonweave since they're spec-locked and a single tailor only has one, Glacial Bag, Dreamcloth group, Imperial Silk), Leatherworking (Salt Shaker — filed here because Refined Deeprock Salt is an LW reagent despite the misleading name, Magnificence group), Enchanting (Magic Sphere — Prismatic + Void combined, Sha Crystal), Jewelcrafting (Brilliant Glass, Icy Prism, Fire Prism, JC Daily Cut group), Inscription (Inscription Research group, Forged Documents — both faction variants combined, Scroll of Wisdom), Blacksmithing (Titansteel Bar, Smelting group — Balanced Trillium + Lightning Steel), Engineering (Jard's Energy). For alchemists, all transmute spells across all expansions collapse to a single "Transmute" entry (one shared timer per character) so the dropdown doesn't get janky. Driven by COOLDOWN_BY_PROFESSION at the top of GUI/CooldownsTab.lua; the per-profession match predicate is the union of its cooldown entries' matches, so adding a new entry automatically extends both the cooldown dropdown options AND the parent profession's coverage with no UI plumbing changes. Location: GUI/CooldownsTab.lua, Locale/enUS.lua.

  • Per-tab window sizing — Cooldowns and Missing tabs are now LOCKED to a fixed 720×500 window (resize grip hidden) so switching between them produces no visible jump; only the Browser tab is resizable. Browser's last user-chosen size persists separately in frames.mainWindow.browserWidth/browserHeight so locked tabs can't overwrite it. Each tab declares its size policy as WINDOW_SIZE = { width=W, height=H, locked=true } or { minWidth=W, minHeight=H }; MainWindow:ApplyTabSize reads the spec on Open and on every tab switch. Locations: GUI/MainWindow.lua, GUI/BrowserTab.lua, GUI/CooldownsTab.lua, GUI/MissingRecipesTab.lua.

  • Shared GUI widget factories — three reusable factories in a new GUI/SharedWidgets.lua collapse what was previously ~240 lines of copy-pasted plumbing across the three tabs into ~10-line call sites: (1) addon.GUI.MakeScanAHButton(opts) — owns the Button widget, its label refresh closure, the OnClick handler that drives addon.AH.StartScan, the tooltip, and ONE module-level pair of AH callbacks that route via a small _activeButtons registry instead of N callbacks accumulating; (2) addon.GUI.MakeColumnHeader(opts) — InteractiveLabel + brand color + no-wrap + optional tooltip + optional sort onClick, enforcing the column-header rule from CLAUDE.md structurally instead of by convention; (3) addon.GUI.AttachTooltip(widget, title, desc) — standard hover tooltip with the Dropdown/EditBox label-area mouse trick built in (those widgets put their SetLabel fontstring above the body and Control_OnEnter fires only for the body, so hovering the label produced no tooltip; the helper detects widget.type == "Dropdown" or "EditBox" and adds a wrapper-frame mouse handler via addon.AceGUIFrameScripts). Location: GUI/SharedWidgets.lua.

Improvements

  • Cooldowns tab — fixed-width columns for smooth resizing — the tab originally tried responsive column widths driven by a WINDOW_RESIZED callback, but AceGUI's Flow layout reflowed mid-drag and the user saw rows visibly stacking into 2-3 lines and snapping back when the drag stopped. Switched to fixed widths (Char 140 / Cooldown 360 / Time 80 = 580 total) — same approach as Missing/Browser tabs, which use raw-frame virtual-scroll pools that never reflow. Combined with the per-tab window sizing, the Cooldowns tab is now locked to a single window size, so columns fit perfectly without any responsive math. Removed: ComputeColWidths, GetAvailableWidth, the WINDOW_RESIZED handler. Location: GUI/CooldownsTab.lua.

  • Cooldowns tab — ready cooldowns now sort A-Z by name within the ready cohort — every ready row had the same sort value (-math.huge) under the time column, and Lua's table.sort is not stable, so the ready cohort shuffled into a different order every redraw. With dozens of crafters running the addon, opening the window saw the same set of ready cooldowns appear in unpredictable orders, which was disorienting. Added a tiebreaker (cooldown name → character name, both ascending) that fires whenever two rows compare equal under the active sort column, so the Ready Only view stays in a stable A-Z order across refreshes. Also stabilises ties under the Character and Cooldown column sorts. Location: GUI/CooldownsTab.lua:SortRows.

  • Tab-routing extended for the new tabMainWindow.TAB_DEFS and MainWindow:DrawTab() now include a missing branch wired to addon.MissingRecipesTab:Draw(container). The shared help-icon tooltip gains a missing entry describing the filters, trainer toggle, row actions, and source-tag legend so the in-game documentation matches the existing browser/cooldowns help text. Location: GUI/MainWindow.lua.

  • TOC load order extended — 22 new data files (11 recipes + 11 sources) load after Data/CooldownIds.lua and before Scanner.lua; Modules/AHScanner.lua joins the modules group; GUI/SharedWidgets.lua loads after GUI/MainWindow.lua so all tabs can use the factories; GUI/MissingRecipesTab.lua loads after GUI/ShoppingListTab.lua. Location: TOGProfessionMaster.toc.

Bug Fixes

  • Cross-addon AceGUI tooltip leak — TOGPM tooltips appearing in OTHER addons' UIs — the toolbar dropdown / search / scan-button tooltips, plus the cooldown row's right-click whisper handler, all set widget.frame:SetScript("OnEnter", ...) directly on AceGUI widgets. AceGUI clears widget.events (the SetCallback registry) on Release but does NOT reset raw frame scripts, and AceGUI pools widgets account-wide across every addon — so leftover scripts kept firing in whatever addon next acquired the recycled widget. Two-part fix: (1) new addon.AceGUIFrameScripts(widget, scripts) helper in GUI/MainWindow.lua installs the scripts AND wires up an OnRelease cleanup that RESTORES the prior script (not nils — many AceGUI Constructors install internal Control_OnEnter dispatchers there that drive the SetCallback registry, and nilling them would break widget:SetCallback("OnEnter") for whoever recycles the widget next). (2) Migrated all toolbar tooltip attachments to use widget:SetCallback("OnEnter") instead of frame scripts — Button, Dropdown, EditBox, CheckBox all wire Control_OnEnter to fire the SetCallback registry, and AceGUI clears the registry on release for free. The frame-scripts helper is now used only for SimpleGroup right-click handlers (which have no native dispatcher). New CLAUDE.md rule documents the pattern. Locations: GUI/MainWindow.lua, GUI/CooldownsTab.lua, GUI/BrowserTab.lua, GUI/MissingRecipesTab.lua, CLAUDE.md.

  • Scan AH button stayed greyed when AH opened on the active tab — each per-tab AH_OPEN_STATE_CHANGED handler called _refreshScanBtnLabel UNCONDITIONALLY before the active-tab early-out, operating on a stale closure that captured a scanBtn from a prior tab visit (released the moment the user switched away). Calling SetText/SetDisabled on a recycled widget either no-op'd or stomped a pooled widget another addon now owned. Fixed by collapsing all three per-tab handlers into ONE module-level pair of handlers in addon.GUI.MakeScanAHButton that route via a small _activeButtons[tabName] registry — only the current tab's live button is touched. Locations: GUI/SharedWidgets.lua, per-tab handlers removed from GUI/CooldownsTab.lua / GUI/BrowserTab.lua / GUI/MissingRecipesTab.lua.

  • Tooltips on dropdown / editbox LABELS (not bodies) didn't fire — Dropdown and EditBox put their SetLabel("...") fontstring at the TOP of widget.frame and the actual interactive body (the dropdown button / input field) BELOW it. AceGUI's Control_OnEnter is wired only to the body, so hovering the label area produced no callback — the user mouseover on "Profession Filter" / "Search Recipes" labels never showed a tooltip. The new addon.GUI.AttachTooltip detects widget.type == "Dropdown" or "EditBox" and additionally enables mouse on widget.frame + wires a raw OnEnter via addon.AceGUIFrameScripts — covering the label area while preserving release-time cleanup. Location: GUI/SharedWidgets.lua.

  • Cooldowns transmute popup — multi-reagent transmutes collapsed to one row — the cooldown branch in BuildRows used the hardcoded single-reagent data.transReagents map for every cast spellId, then marked seenSpellIds to block the multi-reagent recipe-DB branch from re-emitting. So Arcanite (Thorium Bar + Arcane Crystal) showed ONE row with whichever reagent the hardcoded map happened to list, instead of TWO adjacent rows. Restructured: the cooldown branch now consults the recipe-DB (which captures multi-reagent data from the alchemist's actual trade-skill scan) FIRST, falling back to data.transReagents only when the recipe scan didn't cover that spell. The recipe-DB branch then only runs for spells the cooldown branch didn't already emit. Location: GUI/CooldownsTab.lua:BuildRows transmute group section.

  • Cooldowns tab — Scan AH didn't include transmute reagentsgetItems iterated row.reagentItemId, but transmute group rows have reagentItemId = nil because each transmute inside the group has its own reagent (sometimes multiple). The scan only picked up standalone non-transmute cooldowns (Salt Shaker, Mooncloth, etc.) — Arcane Crystal, Thorium Bar, Iron Bar, etc. were never queried. Now iterates row.transmuteEntries[].reagentId for transmute group rows so the scan covers everything the user can actually craft. Location: GUI/CooldownsTab.lua:MakeScanAHButton getItems.

  • Enchanting recipes broadcast as ? <id> with "Retrieving item information" tooltips, never resolved by /togpm backfill — Enchanting recipeIds are enchant SPELL IDs, but MergeCraftersIntoGdb's stub creation only tried GetItemInfo(recipeId), which returns nil for spell-only IDs. The resulting nameless stub left isSpell/spellId unset, and BackfillBogusRecipeNames then refused to call GetSpellInfo on it because its spell branch was gated on rd.isSpell == true or rd.spellId (both nil for the stub). BrowserTab's tooltip code fell through to SetHyperlink("item:<id>"), which is exactly what surfaces the "Retrieving item information" message in WoW for IDs the client treats as items it doesn't have cached. Two-part fix: (1) MergeCraftersIntoGdb now tries GetSpellInfo as a fallback when GetItemInfo fails, populating the stub's name/icon/isSpell/spellId in one shot. (2) BackfillBogusRecipeNames's spell branch now runs unconditionally (bounded only by the bogus-name + numeric-recipeId guards). GetSpellInfo returns nil for non-spell IDs so there are no false positives. Locations: Scanner.lua:MergeCraftersIntoGdb, Scanner.lua:BackfillBogusRecipeNames.

Performance

  • Missing Recipes tab — virtual-scroll rendering with raw frame pool (35 rows) — initial implementation rendered every visible missing-recipe row as ~6 AceGUI widgets, so a profession with 500+ missing recipes spawned 3000+ AceGUI children and the layout pass froze the WoW client. Even capping at 100 rendered rows still produced 600 widgets — enough to choke AceGUI's layout. Switched the result body to mirror GUI/BrowserTab.lua's pattern: a pool of 35 raw CreateFrame rows parented to the AceGUI ScrollFrame's content frame, repositioned and re-skinned as the user scrolls. Total widget count is now bounded at 35 regardless of list size. Pool is persistent across RefreshList (search keystrokes / trainer toggle / profession switch) via a new DetachPool helper that re-parents to UIParent on ScrollFrame release rather than destroying frames — WoW frames are session-lifetime and never GC'd, so destroying-and-rebuilding 35 frames on every search keystroke would leak. Location: GUI/MissingRecipesTab.lua.

  • Missing Recipes tab — build pass no longer calls GetItemInfo — initial implementation called GetItemInfo(spellId) inside the build loop to filter rows by recipe-scroll prefix; for Tailoring (~3000 recipes) this triggered thousands of simultaneous async cache loads, locking the WoW client for ~20 seconds and tripping Script from "Bagnon" has exceeded its execution time limit warnings (Bagnon registers for GET_ITEM_INFO_RECEIVED and re-runs its handler for every cache fill). Restructured: build pass uses sourceDB presence as the recipe-scroll proof (no GetItemInfo), all gdb lookups are hoisted out of the per-recipe loop. Per-row GetItemInfo runs only inside UpdateVirtualRows for the 35 currently-visible rows, so cache-miss volume is bounded by the visible window. A debounced GET_ITEM_INFO_RECEIVED handler refreshes the list 0.5s after the cache-fill burst settles so placeholder names get replaced once items finish loading. Search filtering uses GetItemInfoInstant (does not trigger async loads) so typing in the search box never re-fires the cache-miss firestorm. Search keystrokes are debounced 200ms so each character typed doesn't trigger a full rebuild. Location: GUI/MissingRecipesTab.lua.


[v0.2.7] (2026-05-01) - Transmute popup overhaul: known-transmute list, multi-reagent rows, anchored positioning

New Features

  • The transmute group popup now lists every transmute the alchemist knows, not just the ones currently on cooldown — clicking the [+] Transmute row used to show only the spells with active cooldown records, which meant if Alice had cast Earth-to-Water 3 hours ago and you wanted to send her materials for Iron-to-Gold (which she hadn't cast recently and so had no cooldown record), the row simply wasn't there. The popup now augments the cooldown-derived spell list with every transmute recipe in gdb.recipes[171] where crafters[charKey]=true — the alchemist's full learned-transmutes set. Each row shows time-remaining when on cooldown, "Ready" in green otherwise, with its own reagent label, [Bank], and mail icon. Location: GUI/CooldownsTab.lua:BuildRows transmute group section.

  • Multi-reagent transmutes render as one row per reagent — Arcanite Bar requires both 1 Thorium Bar and 1 Arcane Crystal; previously the popup collapsed it into a single row showing only one reagent because the hardcoded data.transReagents map only stores one reagent per spell. Now each transmute emits one row per reagent from the alchemist's actual rd.reagents scan, with name and time-remaining shown only on the first row of the group so the visual grouping stays clean. Each row's [Bank] and mail buttons act on its own reagent independently — useful when you have one reagent in stock but not the other. Location: GUI/CooldownsTab.lua:ShowGroupPopup, entry-build path.

  • addon.Tooltip.AnchorFrame(frame, source) helper — sibling to addon.Tooltip.Owner (Compat.lua). Anchors any popup/dialog to a source widget using the same screen-half logic that places GameTooltips: source in upper half → popup below source; source in lower half → popup above source. Accepts either a raw Frame or an AceGUI widget (unwraps via widget.frame). The transmute popup uses this to sit adjacent to the row that opened it instead of centering on the screen — the user can mouse straight onto it without losing context. Reusable for any future click-popup that wants tooltip-like positioning.

Bug Fixes

  • Transmute popup appeared centered on the screen far from the row that opened itpopup:SetPoint("CENTER", UIParent, "CENTER", 0, 0) always anchored to the middle, requiring the user to drag their mouse across the UI to interact with it. Switched to addon.Tooltip.AnchorFrame(popup, sourceWidget), where sourceWidget is the AceGUI InteractiveLabel that received the click. The popup now sits directly below the row when the row is in the top half of the screen, and above it when the row is in the bottom half — same ergonomics as a tooltip. Location: GUI/CooldownsTab.lua:ShowGroupPopup.

  • Reagent label and [Bank] button stacked on top of each other in the transmute popup — both were anchored at SetPoint("RIGHT", entry, "RIGHT", -(mailW + 2), 0), the same offset, so they overlapped. Moved the reagent label (bankW + 2) further left so it sits cleanly to the LEFT of the bank button. Also bumped popup width 400→460 for breathing room. Location: GUI/CooldownsTab.lua:ShowGroupPopup reagent label SetPoint.

  • Tooltips on the transmute popup rows rendered visually behind the row text instead of on top — the popup is at TOOLTIP strata, which is also GameTooltip's default strata. Same strata means whichever has higher frame level wins, and the popup's child labels/buttons win by default. Added a showAbovePopup() helper called after every GameTooltip:Show() in the popup that bumps the GameTooltip's frame level above the popup's. Spell, item, [Bank], and mail tooltips all now render on top. Location: GUI/CooldownsTab.lua:ShowGroupPopup.

  • [Bank] buttons missing from the first transmute popup of a session, but appearing in subsequent popups — TOGBankClassic constructs _G.TOGBankClassic_Guild.Info.alts lazily; the first addon.Bank.GetStock(reagentId) call during the popup creation loop hit that uninitialized state and returned 0, so no [Bank] button was created for any row. Subsequent popups queried after TOGBank had populated and worked normally. Fixed by always creating the bank button (initially hidden) and registering a per-row visibility refresher that runs on popup:OnShow plus a deferred C_Timer.After(0.1, ...) tick — so even when TOGBank's data lands asynchronously after our popup opens, the buttons reveal themselves without requiring the user to close and reopen. Also changed popup:Show() to follow an explicit popup:Hide() after CreateFrame, so OnShow fires the actual hidden→shown transition. Location: GUI/CooldownsTab.lua:ShowGroupPopup bank-refresher block.

  • Non-alchemist viewers couldn't see proper spell tooltips for transmutes the alchemist had broadcast with rd.spellId = nilRefreshTransmuteCatalogueFromRecipes had only one resolution path: GetSpellLink(rd.name), which only works for spells the local player knows. On a non-alchemist viewer, GetSpellLink returns nil for every transmute, so rd.spellId stayed nil and hover tooltips fell through to the output-item link. Added a static-catalogue name-match fallback that runs first: walk data.transmutes (the cumulative VANILLA / TBC / WRATH / CATA / MOP transmute table), find the entry whose name matches rd.name, and assign that spellId. Resolution works for non-alchemists because it's pure string compare against hardcoded data — no spell-knowledge required. Location: Data/CooldownIds.lua:RefreshTransmuteCatalogueFromRecipes.

Improvements

  • Removed the MINOR>=N runtime version check on DeltaSync-1.0 — the addon previously gated guild sync on a hard-coded LibStub.minors["DeltaSync-1.0"] >= N check in Scanner:InitDeltaSync, which made the addon's source contain library version assumptions that drift over time. Library version compatibility is the responsibility of the ## Dependencies declaration in the .toc, not a runtime check. Removed the check entirely; if a too-old DeltaSync is installed, behavior degrades gracefully via the existing if not DS then return end guard. Location: Scanner.lua:InitDeltaSync.

  • Hover tooltip on every popup row, even when no spellId is available — for transmute rows that propagated with rd.spellId = nil, the name zone now falls back to GameTooltip:SetHyperlink("item:" .. recipeId) which shows the output item's tooltip (since recipeId IS the output item ID for non-spell recipes). Less informative than the spell tooltip, but better than nothing. The static-catalogue name-match fallback above means most rows actually do get the proper spell tooltip now. Location: GUI/CooldownsTab.lua:ShowGroupPopup name-zone OnEnter.

  • "Ready" label in green for transmutes castable right now — replaced the previous ? placeholder when a spell had no cooldown record. Both "no record" and "expired record" now render as Ready in |cff00ff00 so the user can tell at a glance which transmutes are available to send materials for. Location: GUI/CooldownsTab.lua:ShowGroupPopup row time-string formatting.


[v0.2.6] (2026-04-30) - Recipe display recovery + lazy item-cache backfills + sync robustness

Bug Fixes

  • Tailoring (and other) recipes showed as ? <id> in the recipe browser — when a peer's crafters:<profId> leaf arrived before the matching recipemeta:<profId> leaf, MergeCraftersIntoGdb created a stub {crafters={}} entry with no name/icon/links. The browser row then rendered the default question-mark icon plus tostring(recipeId) as the name — visually "? 10002" — even though hovering the row showed the proper name (BrowserTab falls back to SetHyperlink("item:<id>") for tooltips, which WoW resolves from its own item cache). Three-pronged fix: (1) new cleanRecipeName helper extracts the real name from the [...] portion of itemLink/recipeLink when the raw scan name is a placeholder; (2) MergeCraftersIntoGdb now populates name/icon/itemLink from GetItemInfo(recipeId) at stub creation so even the first-paint render is correct when the item is cached; (3) MergeRecipeMetaIntoGdb self-heals on receive — if the stored name is a placeholder, replace it with the cleaned-up version from the new payload. Locations: Scanner.lua:cleanRecipeName, Scanner.lua:MergeCraftersIntoGdb, Scanner.lua:MergeRecipeMetaIntoGdb.

  • Clicking [Bank] on a reagent row in the recipe drilldown did nothing for some users (the button was visible, the click just didn't open the popup) — the recipe drilldown's reagent row is a Button frame and had SetScript("OnClick", ...) for shift-click-to-insert-link. The child [Bank] button is also a Button with its own OnClick handler. On some WoW builds the parent-Button's OnClick swallows the click before the child-Button's OnClick can fire — explaining why the recipe-row [Bank] button (whose parent uses OnMouseDown, not OnClick) always worked, while the reagent-row [Bank] button intermittently didn't. Same fix as the recipe row: switch the parent's shift-click handler from OnClick to OnMouseDown. Different event = no conflict, child's OnClick fires normally. Location: GUI/BrowserTab.lua:DrawDetail reagent-row mouse-script setup.

  • Transmute on the cooldown tab showed the specific spell name (e.g., "Earth to Water") instead of grouping into a generic "[+] Transmute" row with the per-spell popup, on non-alchemist viewers — the cooldown tab's BuildRows checks data.transmutes[spellId] to recognise a cooldown as a transmute. The runtime augmentation that adds Anniversary client spell IDs to data.transmutes (addon:RefreshTransmuteCatalogueFromRecipes) only ran inside ScanCooldowns, which fires off the LOCAL player's scan events. Non-alchemists rarely trigger it, so when an alchemist's transmute spell ID arrived via guild sync it never made it into the catalogue, and the row fell through to a regular per-spell display with no popup, no per-transmute reagents, and no per-transmute mail button. Fixed by calling the augmentation at the start of BuildRows itself — idempotent, cheap, runs every render. The recipe DB (populated from the alchemist's broadcast carrying spellIds backfilled via GetSpellLink) supplies all the IDs needed. Once augmented, the transmute group + click-to-expand popup with per-spell reagents and [Bank] / mail buttons works as designed. Location: GUI/CooldownsTab.lua:BuildRows.

  • Roll-up sync sessions could stay in ACTIVE state after a subhashes drill-down dispatched its child leaf-data requests — when our addon received a subhashes response for guild:cooldowns or guild:accountchars and dispatched per-character leaf-data follow-ups in response, the parent session was never explicitly marked complete; it lingered until the underlying delivery timeout fired. With the parent's lifecycle now bracketed correctly — child leaf sessions are tracked independently and complete on their own data arrival — the roll-up's session slot is freed as soon as its drill-down dispatches, restoring full session-slot availability for the leaf fetches that follow. Location: Scanner.lua:OnGuildDataReceived subhashes branch.

Improvements

  • Recipe-name and reagent-itemId backfills now run multiple passes with cache-loading fallback — both backfills are scheduled at 3s/30s/120s post-login. The reagent backfill adds a third recovery path that calls GetItemInfo(name) (cache-loading variant), which returns nil on first call but issues an async server-side load — the next retry pass picks up the result via GetItemInfoInstant once the load resolves. The recipe-name backfill walks gdb.recipes and re-runs the full recovery chain (link [...] extract → GetItemInfo(recipeId) for items → GetSpellInfo(spellId) for spells), so both placeholder names and crafters-only stubs heal as the WoW item cache fills. Each pass logs only when something was actually checked, so silent runs after the data is healed don't spam chat. Locations: Scanner.lua:BackfillReagentItemIds, Scanner.lua:BackfillBogusRecipeNames, Scanner.lua:Init.

  • Diagnostic ENTRY prints in guild-sync handlers — when /togpm debug is on, Scanner: onDataReceived ENTRY ..., Scanner: onDataRequest ENTRY ..., Scanner: OnGuildDataReceived ENTRY ..., and Scanner: onSyncAccepted ... fire at every handler entry, plus a BAIL — <reason> line on each early-return path of OnGuildDataReceived. Without entry-level prints, when a sync handler silently no-op's (malformed payload, own echo, no guild, etc.) nothing is logged — making receive-side failures invisible. Now any future receive-path issue is observable from the user's chat without instrumenting code. Locations: Scanner.lua:InitDeltaSync, Scanner.lua:OnGuildDataReceived.

  • P2P concurrency limits raised for active guilds — increased inbound and outbound concurrency from 3 to 8 and stretched the offer-collect window from 10s to 30s. In active 30+ member guilds the prior caps saturated within seconds because every peer was at the same limit, queuing requests faster than they drain. Higher caps plus a longer collect window let more sessions dispatch in parallel and accumulate offers from more peers before picking, increasing the odds of finding a peer that isn't at its own cap. Location: Scanner.lua:InitDeltaSync.

  • /togpm backfill slash command runs both backfills — runs the reagent-itemId backfill and the recipe-name backfill back-to-back. Useful for re-attempting on demand after the WoW item cache has had more time to populate (e.g., after opening a few trade-skill windows). Location: TOGProfessionMaster.lua:RunBackfill.


[v0.2.5] (2026-04-29) - GetSpellLink transmute-ID fallback + personal bank / mail in reagent counts

Bug Fixes

  • Transmutes still didn't show on the cooldown tab on Classic Era Anniversary even with v0.2.4 — the v0.2.4 runtime-augment path required rd.spellId to be populated by BuildSpellNameCache (spellbook tab iteration via GetNumSpellTabs / GetSpellBookItemInfo). On Anniversary, transmute spells don't appear in that enumeration, so ScanTradeSkillInto stored the recipe with rd.spellId = nil and the augment path had nothing to do. Section 5 of /togpm transmutedebug showed transmute recipes present but with nil spellIds, confirming the spellbook-scan miss. Fixed by adding a GetSpellLink(rd.name) fallback in RefreshTransmuteCatalogueFromRecipesGetSpellLink works for any known spell by name regardless of spellbook presentation, returns |Hspell:NNNNN|h from which we extract the ID, and the ID is also backfilled onto rd.spellId so the rest of the cooldown chain (knownTransmutes filter, cooldown-tab grouping) works. Location: Data/CooldownIds.lua:RefreshTransmuteCatalogueFromRecipes.

Improvements

  • Reagent Tracker and Reagent Watch now show personal bank + mail alongside bag count — the have total is now bags + cached personal bank + cached mail, where personal bank is scanned on BANKFRAME_CLOSED and mail is scanned on MAIL_CLOSED (mirroring TOGBankClassic's scan-on-close pattern). COD mail attachments are excluded (not really yours until you pay, matching TOGBank). TOGBankClassic guild-bank stock stays separate, surfaced as a +<N> annotation in the row — the user previously asked to keep guild-bank stock visually distinct from "in possession", so personal bank/mail join the have side and only TOGBank stays separate. New Ace.db.char.bankCounts and Ace.db.char.mailCounts cache per-character. First-visit-required: until the user opens their bank or mailbox once with v0.2.5 installed, the caches are empty (same first-time UX as TOGBank). Locations: Modules/ReagentWatch.lua, GUI/ReagentTracker.lua:GetPlayerBagCount, GUI/ShoppingListTab.lua:FillReagentWatch.

  • Reagent Tracker color coding now reflects bag-vs-bank-vs-shortage — green = bags alone satisfy the recipe; yellow = bags fall short but bag + bank covers it (request from bank); orange = bag + bank still short, partial; red = nothing in bags or bank. The +<N> bank annotation is light blue and always shown when bank stock > 0, deliberately separated from the have/need count so the player can read the bank contribution without it being conflated with personal possession. Display widened (COUNT_W 48 → 90, WIN_W 280 → 320) to fit the new format. Location: GUI/ReagentTracker.lua:Refresh.

  • Reagent Watch panel applies the same color scheme — green = in your bags, yellow = only in the guild bank, grey = nowhere. Same +<N> annotation pattern. Location: GUI/ShoppingListTab.lua:FillReagentWatch.

  • /togpm transmutedebug now also runs the runtime augmentation up front — so the diagnostic reflects post-refresh state, including any spellIds resolved via the new GetSpellLink fallback. Prints a confirmation line when new IDs were added. Location: TOGProfessionMaster.lua:DumpTransmuteDiag.


[v0.2.4] (2026-04-29) - Runtime-augmented transmute catalogue + extended transmutedebug

Bug Fixes

  • Transmutes never showed up on the cooldown tab on Classic Era Anniversary (and any client whose actual spell IDs don't match VANILLA_TRANSMUTES) — the static spell-ID catalogue in Data/CooldownIds.lua was the only thing ScanCooldowns consulted. If the alchemist's actual transmute spells used different spell IDs than the ones we hard-coded (a real condition on Anniversary, where the recipe DB shows transmutes with spellIds that aren't in our catalogue), GetSpellCooldown was called with the wrong IDs, the recipe-DB filter found zero matches, and nothing got stored. New addon:RefreshTransmuteCatalogueFromRecipes() walks gdb.recipes[171] for any recipe whose name contains "Transmute" and adds its spellId to data.transmutes at runtime. Idempotent, cheap, runs at the start of every ScanCooldowns. The catalogue self-heals against any client-specific or locale-specific ID variation, picks up newly-learned transmutes from guildmate broadcasts, and the rest of the cooldown chain (cooldown-tab grouping, transmute popup, mail integration) works as a side effect. Locations: Data/CooldownIds.lua:RefreshTransmuteCatalogueFromRecipes, Scanner.lua:ScanCooldowns.

Improvements

  • /togpm transmutedebug now also dumps the actual alchemy recipe DB and a spellbook walk — v0.2.3's diagnostic only showed which transmutes matched our catalogue. If the catalogue was stale, the diagnostic showed all zeros without a way to tell why. v0.2.4 adds two more sections: (5) total alchemy recipes in gdb.recipes[171] for the local char with their actual spellId values (filtered to anything whose name contains "Transmute"), and (6) a full spellbook walk for any spell whose name contains "Transmute" with the spell ID the client is actually using. If section 6 prints IDs that aren't in the catalogue, the new runtime augmentation will pick them up automatically; it's still a useful triage signal. Location: TOGProfessionMaster.lua:DumpTransmuteDiag.

[v0.2.3] (2026-04-29) - /togpm transmutedebug diagnostic command

New Features

  • /togpm transmutedebug — one-shot diagnostic for the transmute-cooldown chain. Prints what the WoW API says is on cooldown, what's in the recipe DB for the local character, what IsSpellKnown says, and what's actually stored in gdb.cooldowns. No spell IDs to look up — just run it and paste the output. Useful for triaging "transmute isn't showing on the cooldown tab" reports without making the user dig up spell IDs. Location: TOGProfessionMaster.lua:DumpTransmuteDiag.

[v0.2.2] (2026-04-29) - Cumulative cooldown ID loading across expansions

Bug Fixes

  • Transmute cooldowns from earlier expansions never showed up on Cata/MoP clientsData/CooldownIds.lua:Build() loaded transmute and cooldown spell IDs version-exclusively: only CATA_TRANSMUTES on Cata, only WRATH_TRANSMUTES on Wrath, etc. But spell IDs from earlier expansions stay valid on later clients — a Cata alchemist still has the 18 Wrath Eternal transmutes, the TBC primal transmutes, and the Vanilla element transmutes in their spellbook, all sharing the same 24-hour cooldown. Casting any one of them was invisible to our scan because we never iterated those IDs in ScanCooldowns. Symptom: Cata alchemist casts Eternal Fire to Water → cooldown tab shows nothing for them. Same root cause for non-transmute cooldowns (Mooncloth, Spellcloth, Icy Prism) on later-expansion clients. Fixed by changing Build() to load IDs cumulatively — the current expansion plus every earlier one. Doesn't change behavior on Classic Era (only Vanilla IDs load, same as before). Location: Data/CooldownIds.lua:Build.

[v0.2.1] (2026-04-29) - v0.2.0 sync convergence fixes

Bug Fixes

  • Cooldowns weren't syncing between peers — drill-down chain never firedHashManager:HasContent returned false for guild:cooldowns and guild:accountchars roll-up keys, on the (incorrect) reasoning that we don't directly serve roll-up data. But DeltaSync's OnHashListReceived gates offers on hasContent(itemKey) — peers with stale data wouldn't offer for the roll-up because hasContent said no, the broadcaster never received an offer for guild:cooldowns, onSyncAccepted never fired, and the subhashes drill-down never happened. Symptom: PC with 10 cooldowns broadcasting; PC with 3 cooldowns receiving the broadcast but never reporting back. Fixed by returning true for guild:cooldowns when we have any cooldowns in gdb.cooldowns, and similarly for guild:accountchars. We don't serve the roll-up data — the offer triggers onSyncAccepted which calls BroadcastSubhashesToGuild, sending the per-character sub-hash list. Location: Modules/HashManager.lua:HasContent.

  • Idle peers never broadcast — protocol can't push to them — v0.2.0's broadcasts only fire on event triggers (cooldown scan, recipe scan, login). With differential broadcasting, an idle peer's "no changes since last broadcast" results in a skipped send. Other peers never see the idle peer's hashes, so they never offer fresh data, so the idle peer never receives anything. The protocol doc specified a 10-minute periodic broadcast for exactly this case but the implementation only had event-driven broadcasts. Added a 10-minute repeating timer in Scanner:Init that resets _lastBroadcastHashes = nil and broadcasts the full L0 hash list (non-differential), guaranteeing every peer is on the wire at least every 10 minutes regardless of local activity. Location: Scanner.lua:Init.

  • Cooldowns tab scroll bar always visible and extending ~2x the window height below the bottom edgeCooldownsTab:Draw set the container's layout to "List", but AceGUI's List layout doesn't honor child.height == "fill" — it only manages widths. Only the Flow layout reads SetFullHeight(true) and anchors the child's BOTTOM to the parent content. Without that anchor, the AceGUI ScrollFrame's outer frame grew unbounded past the window edge and the scrollbar grew with it. Fixed by switching the container layout from "List" to "Flow". Toolbar, headers, and scroll all already had SetFullWidth(true), so Flow stacks them vertically the same way List did — Flow just additionally constrains the scroll's height to fit the remaining space. Location: GUI/CooldownsTab.lua:Draw.

How to Force Sync on Already-Stale Data

If you upgraded between PCs and one is missing data, the periodic tick will catch up within 10 minutes. To force immediate sync, run /togpm forcebroadcast on the less-data PC — that broadcasts its hashes, peers see the mismatch, peers offer, your PC fetches the subhashes and missing leaves, and merges within seconds.


[v0.2.0] (2026-04-29) - Hash-then-fetch sync protocol, content-aware merge, relay-capable cooldowns + recipes

Major Protocol Overhaul

  • Replaced full-payload broadcast with hash-then-fetch sync — v0.1.x broadcast each peer's full ~30 KB profession + cooldown payload to the guild every 30 seconds, multiplied by every active broadcaster. v0.2.0 broadcasts a tiny ~600 B leaf-hash list per peer every 10 minutes (differential — only leaves whose content has changed). Peers compare hashes; on mismatch they whisper a short handshake; the chosen sender broadcasts only the differing leaf's data on the GUILD/BULK channel, where every peer with stale data merges for free. Steady-state guild traffic drops by orders of magnitude. See docs/v0.2.0-protocol.md for the full design. Locations: Scanner.lua, Modules/HashManager.lua.

  • Content-aware merge replaces destructive overwriteOnGuildDataReceived now merges per leaf type so anyone with cached data can serve it without risk of clobbering fresher data: cooldowns merge with max(local.expiresAt, incoming.expiresAt) per (charKey, spellId); recipe metadata merges richest-non-nil per field via the existing mergeReagents helper; crafter sets union-add for relayed payloads and wipe-then-re-add when the broadcaster claims an authoritative own-scan; account-char groups replace authoritatively for the broadcaster's own slot and union for relayed slots. Receiving from any peer always converges to the same state. Locations: Scanner.lua:OnGuildDataReceived, Scanner.lua:MergeRecipeMetaIntoGdb, Scanner.lua:MergeCraftersIntoGdb.

  • Cooldowns and recipes now relay through any peerHashManager:HasContent returns true for any locally-cached leaf, not just owner-owned. If Alice's alchemist is offline, Bob's cached copy of cooldown:Alice-Realm can serve the leaf to Carol when she logs in. Recipe metadata + crafter membership relay similarly. Cooldown coverage no longer requires the data owner to be online. Location: Modules/HashManager.lua:HasContent.

  • Hash + timestamp invariant: both immutable per data state — Each leaf entry {hash, updatedAt} is a co-determined function of the data: both change atomically when content changes, both stay frozen otherwise. updatedAt is content-derived from gdb.lastScan[charKey][scope], never GetServerTime() at a no-op site. The v0.1.x HashManager:RebuildAll re-stamped every leaf's updatedAt on every receive — even no-op merges — which was the root cause of the "stale relayer with high updatedAt suppresses fresh owner's offer" routing bug. Replaced with targeted Invalidate* helpers that no-op when the new tuple matches existing. Location: Modules/HashManager.lua:setEntry.

New Hash Leaf Taxonomy

Replaces v0.1.x's cooldown:<charKey> + recipes:<profId> + guild:cooldowns + guild:recipes with:

  • recipemeta:<profId> — immutable recipe metadata for one profession (rare-change, bootstrap-only after first sync).
  • crafters:<profId> — crafter membership map for one profession (frequent, deltas).
  • cooldown:<charKey> — full cooldown bucket for one character.
  • accountchars:<charKey> — alt group claimed by one broadcaster.
  • guild:cooldowns and guild:accountchars — structured roll-ups over per-character leaves; broadcast at L0 with per-character leaves drilled-down on roll-up mismatch.

L0 broadcast carries 9 + 9 + 2 = 20 hashes × ~30 B per peer ≈ 600 B. Per-character leaves stay out of L0 to avoid 300-500-leaf broadcast bloat for large guilds.

Channel Allocation

GUILD/BULK for hash list broadcasts and per-leaf data responses (high throughput, throttle-tolerant); WHISPER for handshake control messages only (offers, requests). Whisper throttling no longer constrains bulk transfer.

Storage Changes

Two new top-level fields on each guild bucket; existing data is preserved verbatim:

  • gdb.accountChars[broadcasterKey] = { charKey, ... } — per-broadcaster authoritative alt group. gdb.altGroups becomes a derived view rebuilt from this.
  • gdb.lastScan[charKey][scope] — content-derived timestamps (where scope is a profId, "cooldowns", or "accountchars"). HashManager reads these to compute leaf updatedAt.

Wire Format

Bumped DeltaSync namespace TOGPmv1TOGPmv2 to prevent v0.1.5 ↔ v0.2.0 cross-talk during rollout. v0.1.5 peers don't see v0.2.0 broadcasts and vice versa; once everyone upgrades, the v0.1.5 namespace dies.

Per-leaf payload format (payload.leaves[itemKey] = { data, hash, updatedAt }) carries content + the source's hash tuple. Multiple leaves can ride in one broadcast. Sub-hash drill-down responses (payload.type = "subhashes") carry per-character hashes for one roll-up parent.

Dependency Bump

Requires DeltaSync-1.0 MINOR>=9 (shipped in DeltaSync v2.0.3, 2026-04-29). The new offer condition (hash-mismatch instead of updatedAt > peer's) is required for the relay-capable sync model; older DeltaSync versions still load the addon but Scanner:InitDeltaSync refuses to enable sync and prints a clear error. Location: Scanner.lua:InitDeltaSync.

New Diagnostic Commands

  • /togpm dumphashes — print the local L0 hash list (itemKey, hash, updatedAt) for cross-peer comparison.
  • /togpm dumpcooldowns [charKey] — print stored cooldown bucket for a character (no arg = list every character with cooldowns).
  • /togpm forcebroadcast — bypass the 10-min debounce and broadcast a full (non-differential) hash list immediately.

Bug Fixes

  • Cooldowns tab letter (mail) icon wrapping under the cooldown name — AceGUI Flow's wrap math (framewidth + usedwidth > width) is strict-greater, but in practice the mail icon was wrapping to a new row even when the inner widget widths summed exactly to col2's 456px. Reserved 12 px of slack in the cdNameW calculation so even a small rounding/padding discrepancy in any AceGUI Label widget can't push the row total past col2 width. Location: GUI/CooldownsTab.lua:611-622.

Migration Notes

No existing data is destroyed. On first v0.2.0 load gdb.accountChars and gdb.lastScan initialize empty and populate as scans run + broadcasts arrive. gdb.altGroups is rebuilt from gdb.accountChars whenever it changes. Old recipes:<profId> hash entries become unused garbage in gdb.hashes and can be cleaned up in a future version. Existing recipes, cooldowns, and skill ranks remain usable through the merge.


[v0.1.5] (2026-04-29) - Transmute cooldowns, reagent itemId capture, non-destructive merge, Reagent Tracker bag-vs-bank fix

Bug Fixes

  • Transmute cooldown was detected but never stored — Cooldowns tab showed nothing while the recipe still appeared in the Browser tabScanner:ScanCooldowns ran two loops: the first found the active transmute via GetSpellCooldown, the second wrote the expiry into gdb.cooldowns[charKey]. Both branches of the second loop (active-CD store and Ready seed) were gated on IsSpellKnown(spellId, false). On Classic Era that call returns false for transmute spell IDs (documented in docs/bugs.md DATA-004 — same root cause that bit the upstream ProfessionMaster fork), so transmuteExpiry was computed correctly but immediately discarded — the recipe still appeared in gdb.recipes from the trade-skill scan, but no cooldown row ever materialised. Fixed by deriving "known transmutes" from gdb.recipes[171] (alchemy recipes carry spellId from the spellbook scan via BuildSpellNameCache) and force-including the spell ID that was actually found on cooldown so the active CD shows even on first login before any trade-skill window has been opened. IsSpellKnown is kept as a third fallback path for any client where it does work. Location: Scanner.lua:731-781.

  • Reagent [Bank] button and Reagent Tracker silently broken because reagent itemLinks were nilGetTradeSkillReagentItemLink and GetCraftReagentItemLink return nil on Classic Era for reagents that aren't in the local item cache (e.g. items the player has never owned), even though the equivalent tooltip APIs SetTradeSkillItem(i, r) / SetCraftItem(i, r) work fine. With no link captured, the bank-stock check in BrowserTab.lua (drilldown panel + shopping-list expansion) and the Reagent Tracker's BuildReagentList (GUI/ReagentTracker.lua:54) had no item ID to key off and either hid the row entirely (Reagent Tracker) or skipped the [Bank] button (drilldown). Fixed by routing every reagent through a hidden GameTooltip scraper (SetTradeSkillItem / SetCraftItemGetItem()) when the link API returns nil, and by also resolving itemId directly via GetItemInfoInstant(name) as a third-tier fallback for items that happen to be cached. Both fields are now stored on every reagent. Locations: Scanner.lua:467-491 (TradeSkill), Scanner.lua:631-655 (Craft).

  • One peer with the broken reagent-link API would wipe the rich reagent data guild-wideScanner:MergeRecipesIntoGdb was overwriting existing.reagents wholesale on every receive: if rd[6] ~= nil then existing.reagents = asTable(rd[6]) end. If a peer with GetTradeSkillReagentItemLink returning nil broadcast their version of a recipe, every receiver's previously-rich reagent table (with itemLink + itemId populated) got replaced by name+count-only entries — silently breaking the bank lookup, reagent tracker, and tooltip popups for everyone. Replaced with a non-destructive mergeReagents that matches incoming entries to existing ones by reagent name and preserves itemLink / itemId whenever the incoming payload lacks them. Location: Scanner.lua:580-636.

  • Reagent Tracker counted guild bank stock as if it were in your bags — "0 in bags, 945 in bank" displayed as 945/30 greenRT:Refresh set have = bagCount + bankCount, so a reagent sitting in TOGBankClassic's bank was indistinguishable from one in your character's bags for satisfaction display. Bank stock is still surfaced separately by the [Bank] button on each row (only shown when stock > 0), so collapsing it into have was double-signalling. Fixed by setting have = GetPlayerBagCount(item.id) only. The colour code (green/yellow/red) now reflects what you actually have on your character; the [Bank] button signals that more is available via guild-bank request. Location: GUI/ReagentTracker.lua:146-149.

Improvements

  • Login-time reagent backfillScanner:BackfillReagentItemIds runs on PLAYER_ENTERING_WORLD (3 s after PEW so guild + realm context are stable), walks every recipe's reagent table, and resolves itemId from itemLink (parse) or GetItemInfoInstant(name) for any reagent missing both. Best-effort: items still uncached on this character can't be resolved at login, but they get filled in on the next trade-skill scan via the new tooltip scraper. Location: Scanner.lua:856-898.

  • BrowserTab reagent rendering tolerant of missing links — Two new helpers ResolveReagentItemId(r) and ResolveReagentItemLink(r) lazy-resolve and cache item identity on each reagent table, so renderers transparently use whichever data is available. The detail-panel reagent row falls back to GameTooltip:SetItemByID(rItemId) when itemLink is nil, and the bank-stock check keys off the resolved itemId rather than only itemLink. Location: GUI/BrowserTab.lua:45-79.

  • New diagnostic commands/togpm dumprecipe <name> prints a recipe's stored fields and full reagent table to chat (used to diagnose the missing-itemLink bug above). /togpm backfill runs the reagent backfill on demand and prints checked=N fixed=N missed=N. Locations: TOGProfessionMaster.lua:140-141, TOGProfessionMaster.lua:377-419.


[v0.1.4] (2026-04-28) - Hand DeltaSync our AceAddon so sync goes through AceCommQueue

Bug Fixes

  • aceComm=false in /togpm status — sync was bypassing AceCommQueue throttling — When the v0.1.1 externalization moved DeltaSync out of libs/, the new external library expects the host addon to pass its AceAddon instance into Initialize({ aceAddon = ... }). Without it, DeltaSync falls back to raw C_ChatInfo.SendAddonMessage instead of routing through self.aceAddon:SendCommMessage — so chunked payloads aren't throttled by AceCommQueue-1.0 and can interleave + CRC-fail silently under sync load. The Scanner's Initialize call was missing this key entirely. Fixed by passing aceAddon = addon.lib (the AceAddon-3.0 instance with AceCommQueue already embedded onto it at TOGProfessionMaster.lua:46). After this fix, /togpm status reports aceComm=true and chunked sync should be reliable. Location: Scanner.lua:87-93.

[v0.1.3] (2026-04-28) - GuildCache consolidation, "You (Alt)" disambiguation, sync-log datestamp

Improvements

  • LibGuildRoster-1.0 removed; all guild-roster work now goes through GuildCache-1.0 — Deleted the embedded libs/LibGuildRoster-1.0/ folder (~300 lines) and rewired the OnMemberOnline crafter-alert callback at TOGProfessionMaster.lua:179 to register on LibStub("GuildCache-1.0") instead. GuildCache-1.0 (bundled inside the standalone DeltaSync addon, MINOR ≥ 2) is now a true superset: query API (IsPlayerOnline, IsInGuild, GetOnlineGuildMembers, NormalizeName, GetNormalizedPlayer) plus CallbackHandler-1.0 transition events (OnMemberOnline, OnMemberOffline, OnMemberJoined, OnMemberLeft, OnRosterReady, OnRosterUpdated) plus real-time CHAT_MSG_SYSTEM parsing plus login-race retry. One library, one source of truth. Requires the DeltaSync addon at a build that ships GuildCache-1.0 MINOR=2 (already a hard ## Dependencies since v0.1.1). Locations: all five *.toc files, TOGProfessionMaster.lua, CLAUDE.md, docs/FEATURES.md, .luarc.json.

  • Sync log entries now show full date+time, not just time[14:23:11] was useful for "what just happened" but not for "did this sync happen today or yesterday?" Switched the format string in GUI/Settings.lua:317 from "%H:%M:%S" to "%Y-%m-%d %H:%M:%S". The underlying e.ts (UNIX epoch seconds set at time()) didn't need any data change.

  • "You" disambiguation when several own alts appear in the same list — In the Cooldowns tab and the Browser tab's recipe-row crafter list, every one of your characters used to render as a single "You" label. With ten alts that meant ten rows all called "You" — useful for color-coding, useless for telling the alts apart. Now the currently-logged-in character still shows You, and every other own alt shows You (AltName) (short name without realm). The Browser tab also expands the previously-consolidated single "You" entry into one entry per own crafter so each alt that can craft a given recipe is listed individually. Locations: GUI/CooldownsTab.lua:550-557, GUI/BrowserTab.lua:127-172.

Bug Fixes

  • /togpm status was silently hiding the online-roster sectionPrintStatus runs as function addon:PrintStatus() (so self is addon), but the GuildCache handle is stashed on Scanner.GuildCache. The diagnostic read self.GuildCache (always nil), the if GuildCache then block silently skipped, and the user saw two ---- separators with nothing between them — easy to misread as "0 people online." Fixed by reaching across to Scanner.GuildCache explicitly. Location: Scanner.lua:289.

  • /togpm status showed AceComm=nil AceCommQueue=nil after the v0.1.1 DeltaSync externalization — The external DeltaSync no longer exposes useAceComm / useAceCommQueue as direct fields on the lib handle; that data moved into DS:GetCommStats(). Replaced the stale field reads with aceComm/registered/p2p/guildCache line built from GetCommStats() plus an explicit Scanner.GuildCache ~= nil check so it's obvious at a glance whether the GuildCache library actually loaded. Location: Scanner.lua:246-253.


[v0.1.2] (2026-04-28) - Type-guard for malformed recipe wire data

Bug Fixes

  • Browser tab crashed with attempt to call method 'match' (a nil value) on opening — Six call sites in GUI/BrowserTab.lua (the recipe-row renderer at line 1466, the shopping-list color line at 594, two tooltip SetHyperlink paths at 909/940, and the detail-pane title/header-link block at 1199/1203) called :match / :find on entry.itemLink (and entry.recipeLink) after a plain truthy check. If any peer's wire payload landed a non-string at position [5] or [7] of a recipe array, the merged gdb.recipes[*][*].itemLink became non-string, the truthy check passed, and the method call crashed the UI. All six sites now gate on type(entry.itemLink) == "string". Belt-and-suspenders type-guard added at the merge site in Scanner.lua:530 so future malformed wire data is coerced to nil instead of being stored as-is — asString(rd[5]) for itemLink, asTable(rd[6]) for reagents, asString(rd[7]) for recipeLink.

[v0.1.1] (2026-04-28) - DeltaSync externalized as a standalone addon

Improvements

  • DeltaSync-1.0 is now an external dependency, not an embedded copy — Removed the entire libs/DeltaSync-1.0/ folder (DeltaSync.lua, GuildCache.lua, DeltaOperations.lua, P2PSession.lua) and switched to loading DeltaSync-1.0 from the standalone DeltaSync addon via LibStub. This is the same pattern TOGPM already uses for AceCommQueue-1.0 and VersionCheck-1.0. The benefit: when multiple addons consume DeltaSync, LibStub picks one shared copy at the highest MINOR instead of each addon shipping its own fork — exactly the conflict that the v0.1.0 mod 7 convergence was working around. The standalone DeltaSync also includes a newer GuildCache-1.0 library and an optional DeltaSyncChannel.lua transport (TOGPM doesn't use either directly). Locations: all five *.toc files, .pkgmeta.

  • Dependency declaration updated everywhere it lives## Dependencies in TOGProfessionMaster.toc, _TBC.toc, _Wrath.toc, _Cata.toc, and _Mists.toc now lists DeltaSync alongside Ace3, AceCommQueue-1.0, and VersionCheck-1.0. .pkgmeta required-dependencies adds deltasync so CurseForge enforces installation. The 4 libs\DeltaSync-1.0\*.lua lines are gone from every TOC.

  • Roster helpers re-routed through the new GuildCache-1.0 LibStub handle — In the embedded copy, GetOnlineGuildMembers, NormalizeName, GetNormalizedPlayer, IsInGuild, and IsPlayerOnline were registered onto the DeltaSync-1.0 LibStub handle itself (the embedded GuildCache.lua declared local MAJOR = "DeltaSync-1.0"). The external lib promotes GuildCache to its own LibStub library (MAJOR = "GuildCache-1.0") bundled inside the DeltaSync addon. Scanner now resolves LibStub("GuildCache-1.0", true) alongside DeltaSync and stashes it as Scanner.GuildCache; all call sites in Scanner.lua, Modules/HashManager.lua, Tooltip.lua, GUI/BrowserTab.lua, and GUI/CooldownsTab.lua were updated to call through the new handle. Wire format and the rest of the DeltaSync public surface (Initialize, InitP2P, BroadcastData, RequestData, SendData, SerializeData, ComputeHash, ComputeStructuredHash, etc.) are unchanged.

Breaking Changes

  • Users must install the standalone DeltaSync addon — Without it, TOGPM still loads (the LibStub("DeltaSync-1.0", true) call uses the silent variant), but guild sync silently disables and you'll see "DeltaSync-1.0 not found — guild sync disabled" in the debug log. CurseForge will prompt for the dependency automatically once the v0.1.1 release ships with the updated .pkgmeta. Manual installs need to grab DeltaSync separately.

[v0.1.0] (2026-04-19) - DeltaSync-1.0 mod 7 convergence

New Features

  • DeltaSync-1.0 bumped to MINOR=7, merging the TOGPM (mod 2) and PersonalShopper (mod 6) forks into a single shared library — Previously each addon shipped an incompatible fork at the same DeltaSync-1.0 MAJOR. LibStub always loaded whichever had the higher MINOR (PS mod 6), so TOGPM's P2P calls into a lib that didn't have them — forcing users to disable one addon. Mod 7 is the superset: kept PS mod 6's NormalizeSender, host-supplied self.aceAddon model, CHANNEL-distribution hooks, snifferFrame, and DebugStatus; ported in TOGPM mod 2's OFFER/HANDSHAKE channel types, OnComm_OFFER/OnComm_HANDSHAKE handlers, BroadcastItemHashes/SendHashOffer/SendHandshake/InitP2P public API, and CRC+stop-marker wire format (SerializeWithChecksum/DeserializeWithChecksum) with a legacy AceSerializer-only fallback so old mod 2 messages still decode. GuildCache hooks (GetNormalizedPlayer, NormalizeName, guildRoster whisper-offline guard) are soft deps guarded by presence checks so PS can run without GuildCache.lua. Location: libs/DeltaSync-1.0/DeltaSync.lua.

Improvements

  • DeltaSync no longer embeds Ace libraries into its own lib object — Mod 2 called AceSerializer:Embed(lib) and AceCommQueue:Embed(lib) at load time, which coupled DeltaSync to Ace's MINOR upgrades and duplicated methods the host addon already had. Mod 7 references AceSerializer-3.0 via LibStub(...) at call-time inside SerializeWithChecksum/DeserializeWithChecksum (cached in a file-local upvalue), and delegates throttling to the host addon's own SendCommMessage. The library is now a pure consumer of Ace via LibStub, never an embedder. Location: libs/DeltaSync-1.0/DeltaSync.lua.

  • AceCommQueue throttling moved from the library to the host addon — Because DeltaSync mod 7 calls self.aceAddon:SendCommMessage(...) instead of its own lib:SendCommMessage, the wrap target has to be on the host addon. Added LibStub("AceCommQueue-1.0"):Embed(Ace) immediately after NewAddon(...) so every DeltaSync send from TOGPM is still queued and throttled — preventing CRC corruption from chunk interleaving under sync load. ## Dependencies: AceCommQueue-1.0 was already listed in every TOC, so no new runtime deps. Location: TOGProfessionMaster.lua.

Breaking Changes

  • Wire format changed for existing TOGPM users on mod 2 — The merged mod 7 format is still AceSerializer + CRC + stop-marker (same as mod 2), but OnComm_* receive paths now normalize sender names via NormalizeSender and route through the new checksum helpers. Mod 2 ↔ mod 7 messages remain decodable via the legacy fallback in DeserializeWithChecksum. No action required for existing users.

[v0.0.17] (2026-04-19) - Global [TOGPM] Tooltip & Bank Button Fix

New Features

  • [TOGPM] line on every item tooltip — Hovering any crafted item anywhere in the game (bags, AH, loot, merchant, chat links, comparison tooltips) now appends a single line at the bottom of the tooltip: [TOGPM] name1, name2, ... showing every guildmate (and your own alts) who can craft it. Online names are white, offline are grey. A blank row separates it from the item's own info. Works across all supported clients via TooltipDataProcessor (MoP Classic+) or OnTooltipSetItem/OnTooltipCleared (Vanilla → Cata Classic) on GameTooltip, ItemRefTooltip, and the three ShoppingTooltips. Location: Tooltip.lua.

Bug Fixes

  • Tooltip crafter feature silently disabled since day oneAceHook-3.0 was never listed in the NewAddon mixins, so Ace.HookScript was nil and Tooltip.lua early-returned on load. The global tooltip hook never ran in the addon's lifetime. Fixed by adding AceHook-3.0 to the mixin list. Location: TOGProfessionMaster.lua.

  • FindCrafters traversing the wrong schema — Walked gdb.guildData[charKey].professions[].recipes[].craftedItemId, which only exists in pre-migration SavedVariables. Rewritten to walk gdb.recipes[profId][recipeId] where recipeId IS the crafted item ID when not rd.isSpell, collecting charKeys from rd.crafters. Location: Tooltip.lua.

  • [Bank] button showing on every recipe row — The recipe-row bank button was iterating entry.reagents and lighting up whenever any reagent had bank stock, so ~every row got a button that requested the wrong thing (e.g. Barbaric Belt asked for Leather). Replaced with a single check on entry.id so the button only appears when the crafted item itself is in bank stock, and the request dialog receives the crafted item's name/link. Suppressed entirely for enchants (no craftable item). Location: GUI/BrowserTab.lua.

  • Custom recipe tooltip missing the [TOGPM] line — The BrowserTab reagent-list tooltip path builds its content manually with ClearLines() + AddLine(), bypassing all tooltip hooks. Added an explicit addon.Tooltip.AppendCrafters(GameTooltip, entry.id) call before Show() in that path. Location: GUI/BrowserTab.lua.

Improvements

  • PROF_NAMES lookup promoted to addon namespace_PROF_NAMES was file-local in TOGProfessionMaster.lua, which prevented Tooltip.lua from showing profession names. Exposed as addon.PROF_NAMES. Location: TOGProfessionMaster.lua.

  • AppendCrafters exposed for explicit callers — BrowserTab's custom tooltip path bypasses hooks, so AppendCrafters is now assigned to addon.Tooltip.AppendCrafters and callable directly. A per-tooltip _togpmAppended flag prevents the post-hook from double-adding when the custom path also fires a subsequent Show(). Location: Tooltip.lua.

  • Blank-line separator embedded via |n — Two-line approach (AddLine(" ") + AddLine("[TOGPM]...")) was being reordered by the tooltip's internal build, landing at the top instead of the bottom. Switched to a single AddLine("|n[TOGPM]...") so the blank row can't be repositioned. Location: Tooltip.lua.

  • .luarc.json globals — Added TooltipDataProcessor and Enum so the LSP stops warning on the MoP Classic+ branch. Location: .luarc.json.

  • Shopping list tooltips use the smart anchor helper — Three OnEnter callbacks in GUI/ShoppingListTab.lua were hardcoding GameTooltip:SetOwner(frame, "ANCHOR_TOPRIGHT"), which clipped off-screen when the window was near the top or right edge. Swapped for addon.Tooltip.Owner(frame) so the tooltip anchors above or below based on which half of the screen the widget is in. Location: GUI/ShoppingListTab.lua.

  • Help tooltip rewritten for the current UI — Browser and Cooldowns help blocks were written before the master-detail layout, the ! alert toggle, and the global [TOGPM] tooltip line existed, and the [Bank] description was stale after the v0.0.17 scoping fix. Rewrote both blocks with current section layout (Filters / Shopping list / Recipe area / Detail area / Everywhere else on Browser; Columns / Row actions / Controls on Cooldowns), consolidated sub-bullets into wrap-friendly paragraphs, and added GameTooltip:SetMinimumWidth(480) so the tooltip lays out wide and short instead of tall and narrow. Location: GUI/MainWindow.lua.

  • Help-icon tooltip anchor kept as ANCHOR_TOP — The help icon lives in a fixed position at the bottom-right of the main window, so centered-above reads better than the helper's TOPLEFT/BOTTOMLEFT picks. Left the raw SetOwner in place and added a comment so it isn't "fixed" back to the helper later. Location: GUI/MainWindow.lua.

  • Transmute cooldown scan simplified — The transmute branch of ScanCooldowns had a fragile cross-addon dependency on the global GetCooldownTimestamp, which is defined by the separate ProfessionCooldown addon (not by WoW). When ProfessionCooldown wasn't loaded, we fell back to GetSpellCooldown; when it was loaded, we took a different code path that could behave differently. Removed the GetCooldownTimestamp branch entirely so the scan uses GetSpellCooldown on every client, matching the simpler pattern known to work in production. Location: Scanner.lua.

  • [Bank] button added to the transmute popup — When you click a transmute group row in the Cooldowns tab, the popup lists each individual transmute with its reagent and a Mail icon. It was missing the [Bank] button that the main cooldown rows have. Added it (visible only when TOGBankClassic has stock of that specific reagent), wired to the same addon.Bank.ShowRequestDialog as the main rows. Widened the popup from 340 → 400 px to fit. Location: GUI/CooldownsTab.lua.


[v0.0.16] (2026-04-19) - Enchanting Tooltip Fixes & Crafter Alerts

New Features

  • Crafter online alerts — When a guild member who can craft an item on your shopping list comes online, a chat message is printed and (unless suppressed) a sound plays and the screen flashes gold. Each shopping list row has a ! toggle button (gray = off, gold = on) to arm alerts per recipe. Alt-group awareness: if the online player is an alt of a crafter, the alert still fires with an "(alt of X)" note. Three settings in ESC → Options → TOG Profession Master → Crafter Alerts: master on/off toggle, suppress sound & flash, suppress on login (default on to avoid the login burst). Location: TOGProfessionMaster.lua, GUI/BrowserTab.lua, GUI/Settings.lua.

Bug Fixes

  • Enchanting tooltip showing wrong item — On Vanilla Classic Era, enchanting recipes scanned via the Craft frame stored only name and icon (no isSpell, no reagents). The tooltip fallback chain would reach the last else branch and call SetHyperlink("item:" .. spellId), resolving the enchant spell ID to a random item like "Sentinel's Leather Pants". Fixed by capturing reagents from the Craft frame (GetCraftNumReagents/GetCraftReagentInfo/GetCraftReagentItemLink) and setting isSpell = true so the data format matches the TradeSkill path. Location: Scanner.lua.

  • Enchanting tooltip not showing reagent list — The Professions tab tooltip priority checked recipeLink before reagents, but enchanting stores an enchant:SPELLID link there (not a displayable item link). Added |Hitem: guards on recipeLink and itemLink usage, and moved the spellId fallback to after the reagent branch so enchanting now shows the same reagent-list tooltip as leatherworking. Location: GUI/BrowserTab.lua.

  • Shopping list alert toggle always staying enabled — The ! button on shopping list rows used cur and nil or true to toggle, which always evaluates to true in Lua because nil is falsy. Replaced with an explicit if/else. Location: GUI/BrowserTab.lua.

Improvements

  • VersionCheck-1.0 version field wired correctlyAce.Version was nil, so VersionCheck-1.0 fell back to GetAddOnMetadata to read the version string. Fixed by setting self.Version = addon.Version on the Ace object in OnInitialize before calling VC:Enable(self), so the library reads the version directly without the fallback. Location: TOGProfessionMaster.lua.

[v0.0.15] (2026-04-19) - Reagent Tracker & Professions Tab Master-Detail Layout

New Features

  • Reagent Tracker window — Standalone floating window (no backdrop or border) opened by right-clicking the minimap button or /togpm reagents. Consolidates every reagent across all shopping list entries (e.g. 1 Runecloth from one recipe + 10 from another = 11 required). Each row shows the item icon, name coloured by item rarity, a have/need count (green = satisfied, yellow = partial, red = none), and a [Bank] button when a TOGBankClassic banker alt has stock. "Have" is live player bags + all banker alt stock via TOGBankClassic_Guild. Window position is saved per character. Refreshes automatically on BAG_UPDATE and whenever the shopping list changes. Location: GUI/ReagentTracker.lua.

  • Master-detail split layout in Professions tab — The floating recipe popup is replaced by a persistent right-side detail panel (268 px wide) inline in the Professions tab. Clicking any recipe row populates the panel without opening a separate window. The panel shows: recipe icon + name (hover for item tooltip, shift-click to insert link), right-justified shopping list qty controls ( qty + ×), per-reagent [Bank] buttons, and full crafter list with right-click-to-whisper. Location: GUI/BrowserTab.lua.

  • [Bank] button in recipe list rows — Each left-column recipe row now shows a [Bank] button when any reagent is in TOGBankClassic stock. Recipe name column widened from 150 to 160 px; crafter column narrowed to RIGHT−56 to accommodate. Location: GUI/BrowserTab.lua BuildPool(), UpdateVirtualRows().

Bug Fixes

  • Bank buttons missing for ~5 minutes after loginTOGBankClassic_Guild.Info is nil until GUILD_RANKS_UPDATE fires. Fixed by registering a one-shot event watcher in FillList() that triggers a deferred refresh of the recipe list, detail panel, and shopping list section once bank data is ready. Location: GUI/BrowserTab.lua.

  • ESC proxy cleanup — Removed stale popup check from the ESC proxy OnHide handler; the recipe popup no longer exists as a floating frame. Location: GUI/MainWindow.lua.


[v0.0.14] (2026-04-19) - Restore BrowserTab, CooldownsTab, MainWindow & Compat Work

Bug Fixes

  • Restored Apr 18 evening work — A version-sync script bug was self-copying the wrong directory, silently discarding an evening's worth of changes. Recovered and recommitted: BrowserTab virtual scroll pool, CooldownsTab group/transmute popup, MainWindow ESC proxy wiring, and Compat API shims. Location: GUI/BrowserTab.lua, GUI/CooldownsTab.lua, GUI/MainWindow.lua, Compat.lua.

  • Version-sync script self-copy bug — The wow-version-replication.ps1 sync script was incorrectly including itself in the source glob, causing it to overwrite the destination copy with stale content. Fixed source path exclusion. Location: .vscode/tasks.json.


[v0.0.13] (2026-04-18) - P2P Sync, Transmute Scan & Version Check Command

Bug Fixes

  • P2P sync reliability — Multiple DeltaSync handshake and delta-apply edge cases fixed: hash mismatches on first contact, offer/response sequencing under concurrent peers, and stale session state after a guild member relogged. Location: libs/DeltaSync-1.0/.

  • Transmute cooldown scan — Transmute spell IDs were scanned against the wrong API path on some client builds, causing all transmutes to report as "Ready" immediately after use. Scanner now validates expiry against GetSpellCooldown with a 30-day sanity cap. Location: Scanner.lua.

  • /togpm version command — Added version subcommand; prints the running addon version and broadcasts a version check request to online guildmates. Location: TOGProfessionMaster.lua.


[v0.0.12] (2026-04-17) - BAG_UPDATE Storm, Guild Key Migration & Online Display Fixes

Bug Fixes

  • BAG_UPDATE_COOLDOWN broadcast storm — Every bag slot change was triggering a full guild broadcast. Added a 30-second coalescing debounce so rapid inventory changes collapse into a single send. Location: Scanner.lua.

  • Guild key migration — Characters whose data was stored under the old Faction-Realm-GuildName key were invisible after the key format change in v0.0.11. Added a one-time migration pass on OnEnable that moves existing entries to the new Faction-GuildName key. Location: Scanner.lua.

  • Alt online display — When a crafter's main was offline but an alt on the same account was online, the alt's name was not being shown in the crafter column. Fixed display logic to show AltName (CrafterName) format when the online alt is detected. Location: GUI/BrowserTab.lua.


[v0.0.11] (2026-04-17) - Debug Timestamps & Guild Key Format Refactor

Improvements

  • HH:MM:SS timestamps on debug output — All addon:DebugPrint() calls now prefix output with the current wall-clock time, making it easier to correlate debug lines with in-game events. Location: TOGProfessionMaster.lua.

Internal

  • Guild key format changed — Guild DB key changed from Faction-Realm-GuildName to Faction-GuildName. Realm is intentionally omitted so connected-realm clusters share a single key regardless of which realm a member appears on. Location: Scanner.lua, TOGProfessionMaster.lua.

[v0.0.10] (2026-04-17) - Mining Profession & Reagent Wire Payload

New Features

  • Mining added to profession browser — Mining (profession ID 186) added to the profession filter dropdown and static profession list. Location: GUI/BrowserTab.lua.

Bug Fixes

  • Reagent data missing for guild peersitemLink and reagents arrays were not included in the DeltaSync wire payload, so recipients could not show item tooltips or reagent details for recipes learned by guildmates. Both fields now serialized and merged on receipt. Location: Scanner.lua.

[v0.0.9] (2026-04-17) - Alt Detection & Account Character Tracking

New Features

  • Alt detection — Characters on the same account are now detected and linked. Own characters are shown as You (brand-coloured) in the crafter list and are sorted first. When a crafter's main is offline but a known alt is online, the crafter column displays OnlineAlt (CrafterName). Location: GUI/BrowserTab.lua, Scanner.lua.

Bug Fixes

  • accountChars registration timing — Account character list was being registered in OnInitialize, before PLAYER_ENTERING_WORLD had fired and guild data was available. Moved to PLAYER_ENTERING_WORLD to ensure the roster is populated before alt matching runs. Location: TOGProfessionMaster.lua.

[v0.0.8] (2026-04-17) - Connected-Realm Sender Normalization & Broadcast Storm Fix

Bug Fixes

  • Connected-realm sender names not normalized — Guild members appearing on connected realms were stored under their raw Name-ConnectedRealm key instead of the canonical normalized realm, creating duplicate entries and breaking online-status detection. All incoming sync messages now pass through GetNormalizedRealmName() before storage. Location: Scanner.lua.

  • Sync broadcast storm from cross-realm cluster members — Receiving a sync payload from a cross-realm cluster member was triggering a re-broadcast of the full dataset back to the guild, causing exponential message traffic. Fixed by gating re-broadcast on a "data changed" flag rather than "data received". Location: Scanner.lua.


[v0.0.7] (2026-04-17) - AceComm Sync Fixes

Bug Fixes

  • AceComm handler signature mismatch — The registered OnCommReceived handler had an incorrect parameter order (prefix, message, channel, sender vs the actual AceComm dispatch of prefix, message, distribution, sender), silently discarding all incoming sync messages. Corrected signature. Location: Scanner.lua.

  • AceComm handler parameter shift — A secondary handler registration was using a closure that shifted all parameters by one, causing the sender field to be read as the channel and vice versa. Fixed parameter binding. Location: Scanner.lua.

  • Broken sort indicator on Cooldowns tab headers — Column header sort arrow textures were referencing a path that doesn't exist on Classic Era, leaving a broken texture visible at all times. Removed the sort indicator until a valid asset is identified. Location: GUI/CooldownsTab.lua.


[v0.0.6] (2026-04-17) - HashManager & DeltaSync Stability

New Features

  • HashManager hierarchical hash system — New Modules/HashManager.lua implements a Merkle-style hash cache: per-member cooldown leaf hashes, per-profession recipe leaf hashes, and guild-level roll-ups (guild:cooldowns, guild:recipes). DeltaSync uses these hashes to skip transfers when both peers already agree. Location: Modules/HashManager.lua, Scanner.lua.

Bug Fixes

  • DeltaSync Serialize nil on early send — AceSerializer-3.0 was being embedded inside Initialize(), so any send that fired before Initialize completed caused attempt to call Serialize (nil). Moved library embedding to load time. Location: libs/DeltaSync-1.0/DeltaSync.lua.

  • BroadcastItemHashes nil guard — A startup timer could fire before the P2P session was fully constructed, causing a nil-access crash in BroadcastItemHashes. Added existence guard. Location: Scanner.lua.


[v0.0.5] (2026-04-17) - Cooldowns Tab UI Polish

Improvements

  • Cooldowns row layout — Fixed column width calculations so character name, cooldown name, reagent, and time-left columns no longer overlap at narrow window widths. Sort arrow positioning corrected. Location: GUI/CooldownsTab.lua.

  • Header tooltips and brand color — Cooldowns tab column headers now show descriptive tooltips on hover and use the addon brand color (FF8000) for header text, matching the Professions tab style. Location: GUI/CooldownsTab.lua.

  • Header bleed fix — Column header row was rendering 2 px outside the tab content frame at the bottom, causing a thin line of header background to bleed into the first data row. Fixed via explicit height clamp. Location: GUI/CooldownsTab.lua.


[v0.0.4] (2026-04-17) - Package Metadata & TOC Fixes

Bug Fixes

  • Incorrect CurseForge .pkgmeta slugs — External library slugs in .pkgmeta were pointing to wrong CurseForge project paths, preventing the packager from embedding Ace3 and companion libraries correctly on release builds. Location: .pkgmeta.

  • TOC interface version mismatchesTOGProfessionMaster_TBC.toc, _Wrath.toc, _Cata.toc, and _Mists.toc had incorrect ## Interface: values that caused the client to flag the addon as out-of-date on those versions. Corrected to the appropriate build numbers. Location: all .toc files.

Internal

  • Added .gitignore entries for legacy and copyright-encumbered source files that must not be committed to the public repository.

[v0.0.3] (2026-04-16) - Recipe Browser Tooltip Overhaul

New Features

  • Rich recipe tooltips — Hovering a recipe row in the Professions tab now shows a fully custom tooltip: profession name + recipe name header (WoW yellow), reagent list with quantities, and full item data (quality, stats, binding, flavor text) scraped from a hidden GameTooltipTemplate frame without triggering other addon hooks. Location: GUI/BrowserTab.lua, Tooltip.lua.

  • Crafter line in tooltips — Tooltip footer lists all known crafters with the current player shown as gold You sorted first. Online crafters are shown in white; offline in grey. Location: GUI/BrowserTab.lua.

  • Centralized UI color paletteaddon.BrandColor (Legendary orange FF8000), ColorYou, ColorCrafter, ColorOnline, ColorOffline defined once on the addon table and used throughout all GUI files and Tooltip.lua. Location: TOGProfessionMaster.lua.

  • Smart tooltip anchoring — Tooltip anchors below the hovered row when in the top half of the screen (ANCHOR_BOTTOMLEFT) and above when in the bottom half (ANCHOR_TOPLEFT), preventing clipping. addon.Tooltip.Owner() helper added to Compat.lua for consistent anchoring across all modules. Location: Compat.lua.

Improvements

  • L["You"] locale key — Added to Locale/enUS.lua for consistent localization of the self-reference label. Location: Locale/enUS.lua.

[v0.0.2] (2026-04-16) - Complete Clean-Room v1.0 Build

New Features

  • Profession browserGUI/BrowserTab.lua: virtual-scroll recipe list (35-row pool), profession dropdown filter, text search, Guild/Mine view toggle, shopping list integration. Location: GUI/BrowserTab.lua.

  • Cooldowns trackerGUI/CooldownsTab.lua: displays all guild members' tracked profession cooldowns with character name, cooldown name, reagent, and time remaining. Right-click any row to whisper. Location: GUI/CooldownsTab.lua.

  • Shopping list — Per-character shopping list with quantity controls, reagent expansion, and missing-reagents tracking. Location: GUI/ShoppingListTab.lua, Modules/ReagentWatch.lua.

  • P2P guild sync via DeltaSync-1.0 — Custom embedded library broadcasting profession recipes, skills, cooldowns, specializations, and alt-group data peer-to-peer over guild addon channels. Full payload on first contact; hash-based delta sync thereafter. Location: libs/DeltaSync-1.0/, Scanner.lua.

  • Scanner — Scans TRADE_SKILL_SHOW, BAG_UPDATE_COOLDOWN, and related events to capture recipe and cooldown data, merges into the guild DB, and fires GUILD_DATA_UPDATED callbacks. Location: Scanner.lua.

  • AceDB storageTOGPM_GuildDB (account-wide, guild-scoped): recipes, skills, cooldowns, specializations, altGroups, hashes. TOGPM_Settings (per-character): shopping list, reagent watch, alerts, frame positions. Location: TOGProfessionMaster.lua.

  • Minimap button — LibDataBroker + LibDBIcon launcher. Left-click opens profession browser; right-click opens reagents; Shift+Left-click opens settings. Location: GUI/MinimapButton.lua.

  • Settings panel — AceConfig-3.0 options registered under ESC → Options → Addons → TOG Profession Master: minimap button toggle, persist profession filter, debug output, force re-sync, purge data, sync log viewer. Location: GUI/Settings.lua.

  • Sync log — Scrollable log of last 200 sync events (send/recv/request/version) with timestamps and byte counts. Location: Modules/SyncLog.lua, GUI/Settings.lua.

  • Multi-version TOC — Supports Vanilla (Classic Era / Anniversary), TBC, Wrath, Cata, and Mists via separate .toc files. Version flags (addon.isVanilla, addon.isTBC, etc.) set at load time from GetBuildInfo(). Location: Compat.lua, all .toc files.

  • Slash commands/togpm, /togpm sync, /togpm debug, /togpm purge, /togpm version, /togpm minimap. Location: TOGProfessionMaster.lua.


[v0.0.1] (2026-04-16) - Initial Scaffold

Internal

  • Repository initialized. Clean-room project structure established: libs/, Data/, GUI/, Modules/, Locale/, docs/. Core addon frame (TOGProfessionMaster.lua), AceAddon skeleton, and placeholder TOC created. No functional game code.