[FIX] Guild sync crashed with "Data too long for column 'news_source_key'" whenever a Battle.net activity type string (e.g. CHARACTER_ACHIEVEMENT) pushed the guild-activity dedup key past the original VARCHAR(64) (#387). Migration v211/widen_news_source_key widens bb_news.news_source_key to VARCHAR(191).
[FIX] Equipment sync never marked a genuine 404 as unavailable, so it kept re-polling the same missing character indefinitely. wow_api::sync_one_equipment() now writes the same 404-sentinel already used for specs/portraits.
[FIX] The "Sync Achievements"/"Update Ranks and Players" progress panels polled forever when a batch phase could never fully resolve (e.g. a Battle.net category that consistently errors). Both fetchBatch() loops now give up after 60 attempts within the same phase instead of polling without end.
[FIX] Duplicate sync-progress rows when the ACP guild-edit page's auto-sync-on-load and the manual "Update Ranks and Players from Guild API" button both raced into the same startPhase()/runBatchPhase() chain.
[NEW] Player-detail sub-tabs (#375): registered PVP (honor level; bracket ratings pending #23), Talents, and Raid Progression (both placeholder — no data source exists yet) as player-detail tabs via bbguild core's new player_detail_tab_interface. Character tab restyled to match the classic-armory.org reference layout: hero render dropped (portrait moved to the header), equipment stays two columns, stats grouped into Base/Melee/Spell as a third sidebar column instead of a flat list.
[CHG] Character page (#364): the 4 live Battle.net calls behind stats/professions/Mythic+/PvP (wow_api::fetch_character_stats()/fetch_character_professions()/fetch_mythic_keystone_profile()/fetch_pvp_summary()) no longer run synchronously inside on_player_detail_display() — they're now served by a new /bbguildwow/character-stats/{player_id} JSON endpoint, fetched by JS after initial page paint, so viewing any character page no longer blocks on up to 4 live external API round-trips. Equipment now renders in two columns flanking the character's hero render (previously stacked above it), and stats are laid out in a matching compact grid once loaded.
[FIX] Character tab equipment slots showed item names as plain text with no icons (#39) — item.ICON_URL was already synced and available, but the template only ever rendered item.ITEM_NAME/item.ITEM_LINK; equipment slots now render the item icon, quality-colored border included, with the existing bbTips/wowhead hover tooltip carried over from the text link to the icon. A full-body character render was also requested by this ticket originally, but is intentionally not restored — #375's redesign moved the portrait to the page header and put the stats sidebar in its place. Also suppressed Wowhead's own auto-injected tooltip icon on these slots (it drew a second, tiny icon over the real 32px Blizzard one) and restored the slot labels (HEAD/NECK/SHOULDER/…) to a readable size without forced uppercase, both of which only became visually obvious once the real icons landed.
[FIX] Equipment item icons 403'd on Blizzard's render CDN (#41) — wow_api.php guessed the icon URL from the equipment API's media.id (.../icons/56/{media.id}.jpg), but that id is a media-document reference, not a render-CDN file id. Added battlenet_static_data::getItemMedia(), mirroring the lookup-by-id pattern already used for emblem/border media and the character render, to resolve the real asset URL, and cache it by item_id (phpBB cache service, long TTL) since icon art is shared across every character/guild that equips the same item. Also fixed a related CI failure where the older guild-batch sync_equipment() (unlike the newer per-character sync_character()) never selected player_region, which the new icon resolver needs.
[FIX] Guild roster sync always set new characters' gender to Male (#42) — Battle.net's Guild Roster API (used by sync_guild_members()) doesn't return gender at all, so every roster-synced character was hardcoded player_gender_id = 0 regardless of the character's actual gender; gender is now resolved from the character's base profile during the per-character cron sync, so every character self-heals on its next scheduled sync.
[CHG] Character tab's BASE/MELEE/SPELL stats sidebar restyled to match the armory reference design (#43) — each group is now its own card (subtle background, rounded corners) with values right-aligned to the card edge, instead of a plain left-aligned label/value grid.
[NEW] Character-based forum avatars (#369, restores pbwowext#10 pt.1). New avatar\character_avatar_driver implements phpBB's pluggable avatar.driver interface (the real integration point — the issue's originally-cited core.memberlist_prepare_profile_params event doesn't actually exist in this phpBB install, verified against source) to display a claimed character's Battle.net render as its linked forum user's avatar, automatically. sync\character_sync_handler::sync_character() sets it after each successful per-character sync — only when the character is claimed (phpbb_user_id > 0) and the user has no avatar of their own yet (user_avatar_type empty; never overwrites a manually-chosen avatar), falling back from player_render_url to player_portrait_url if the render isn't available, with real dimensions fetched via phpBB's own upload_imagesize service (same as its built-in remote driver). The new driver's prepare_form() always returns false, so — verified against ucp_profile.php's actual driver loop — it never appears as a manual UCP choice; it's system-managed only.
[NEW] Guild activity feed sync (#10). New api\battlenet_guild::getActivity() (mirrors the existing getRoster()) hits the Battle.net Guild Activity endpoint. wow_api::fetch_guild_activity()/sync_guild_activity() fetch and map entries into bb_news, deduplicated by a new news_source/news_source_key column pair (migration v210b1/add_news_source.php, cross-extension depends_on into bbguild core's bb_news-creating migration) — the dedup key hashes the raw activity entry, so it's correct independent of the display-text mapping. cron\task\sync_guild's per-guild loop now also syncs activity feed as an independently try/caught step (a roster failure doesn't block activity sync or vice versa), rolled into the existing last-sync-result summary. Caveat: the exact Battle.net Guild Activity response shape (activity type strings, nested field names beyond activity.type/timestamp/character.name) hasn't been verified against a live guild, so the generated news headline/message text is deliberately generic (type + character name only) rather than guessing at per-type field mappings — worth revisiting once a real response is available.
[NEW] wow_api::sync_character() for per-character sync (#362). Extracted the per-player Battle.net-call-and-DB-write bodies of sync_specs()/sync_equipment()/sync_portraits()'s guild-batch loops into three shared protected methods (sync_one_specs(), sync_one_equipment(), sync_one_portrait(), each returning ['success', 'error_code', 'stop_batch']), then composed them into a new sync_character(array $player_row): bool that syncs one character's specs, equipment, and portrait in a single call. The three existing batch methods now delegate to the shared methods instead of duplicating the per-player logic, with no change to their external behaviour (same SQL, same time-budget loop, same returned ['success','message','count','errors'] shape, same 404/5xx handling). get_game_from_db()/get_ext_path() widened from private to protected so tests can stub them. Provides the orchestration that the separate character_sync_handler adapter class delegates to when implementing bbguild core's character_sync_interface (#362), so its per-character cron task can drive WoW syncs.
[NEW] Registered the WoW character-sync handler (avathar.bbguildwow.character_sync_handler) for #362, implementing character_sync_interface and delegating to wow_api::sync_character(). Tagged with bbguild.character_sync so bbguild core's character-sync cron task registry picks it up automatically, making per-character WoW syncs runnable for the first time.
[NEW] Character gear tooltips via bbTips, with bonus IDs (+gems/enchants) captured for accurate tooltips (#363). parse_equipped_item() now extracts enchant_id (first PERMANENT enchantment slot), gem_ids, bonus_ids, and set_item_ids from the Battle.net equipment response's enchantments[]/sockets[]/bonus_list[]/set.items[] fields (migration v210b1/add_equipment_detail.php widens bb_player_equipment for these, plus per-slot item stats move to a new bb_player_item_stat table via v210b1/add_player_item_stat.php). The character page's equipped-gear list renders through bbTips' [item] anchor builder (avathar.bbtips.linker, soft-detected — no hard dependency) instead of a hand-rolled data-wowhead attribute, passing bonus/ench/gems/pcs through so tooltips reflect the character's actual itemization, not just the base item; a hand-rolled data-wowhead="item=<id>" fallback remains for when bbTips isn't installed. The wowhead JS runtime (power.js) already loads on the player page independent of bbTips' own runtime injection (deduped against it since the #33 fix in 2.0.0-rc4), so no separate runtime-loading change was needed here.
[NEW] Scheduled guild roster sync via phpBB cron (#11). New cron\task\sync_guild periodically syncs every WoW guild's roster (members, faction, emblem) from the Battle.net API, reusing the existing wow_api::fetch_guild_data()/sync_guild_members() sync path. Config-driven: bbguild_wow_sync_enabled (default off), bbguild_wow_sync_interval (default 6h, ACP dropdown 1h–24h), bbguild_wow_last_sync/bbguild_wow_last_sync_result. ACP (Battle.net API page) gained an enable toggle, interval selector, last-run status, and a manual "Sync Now" button. Scope deliberately excludes character-profile refresh (ilvl/spec) — now owned by bbguild core's per-character character_sync cron (#361/#362). Guild activity feed sync landed separately (#10).
[CHG] Converted game/wow_provider.php's remaining hardcoded labels (#37 follow-up) — game name, spec label, and the 4 armor-type labels (Cloth/Leather/Mail/Plate) now go through the same set_language()/lang()/LANG_FALLBACK pattern used for wow_api/achievement, wired via services.ymlcalls: so the existing wow_provider_test.php (which constructs it directly with 3 fixed constructor args) keeps working unmodified. These were deliberately deferred from the earlier follow-up pass; all three consumers (bbguild core's roster/player-detail views and game registry) only ever display these values, never compare against them, so translating them is safe.
[CHG] Follow-up audit + fix for #37: swept the rest of the extension for the same hardcoded-string problem beyond the 3 controllers above. game/wow_api.php (17 strings — sync result messages, error_label(), default rank name, faction names) and model/achievement.php (19 strings — same "sync result message" shape) both gained a set_language() setter (wired via calls: in services.yml, not a constructor arg, so the several unit/integration tests that construct these classes directly keep working unmodified) and a private lang() helper that resolves through phpBB's language framework when wired, falling back to English constants (LANG_FALLBACK, kept in sync with language/en/wow.php) when not — preserving exact existing text for every test assertion. acp/achievement_module.php picked up two missed strings (RETURN_ACHIEVLIST, ACP_GUILD_OPTION_NONE in language/en/info_acp_achievement.php) plus an unrelated bug fix: two trigger_error('ERROR_NOGUILD', ...) calls passed the bare key string instead of $this->user->lang['ERROR_NOGUILD'], so an admin would have literally seen "ERROR_NOGUILD" on screen. controller/asset_controller.php (not touched by the original #37 fix) now takes an injected language too, for its three 'Not found' 404 responses. Also added the previously-undefined NO_METHODS key referenced by api/battlenet_resource.php. Deliberately left game/wow_provider.php's 'Alliance'/'Horde'-adjacent labels (armor types, spec label, game name) unconverted — lower-value proper-noun/interface-label text shared with bbguild core, not worth the added fallback-duplication complexity for this pass.
[CHG] Replaced hardcoded English strings in the sync/achievement AJAX controllers with phpBB language keys (#37) — portrait_controller, achievement_controller, and achievement_sync_controller returned raw English literals (e.g. 'API credentials not configured.') straight in JsonResponse payloads and admin-log details. All three now take an injected \phpbb\language\language (wired via @language in services.yml), load wow/avathar/bbguildwow on each entry point, and resolve messages through $this->language->lang(...) against new keys in language/en/wow.php (WOW_SYNC_*, WOW_ACHIEV_*). Dynamic $e->getMessage() passthroughs are left as-is.
[FIX] Roster sync never deactivated characters who left the guild (#35). update_wow_roster() computed only the add and update sets, never array_diff($oldplayers, $newplayers), and player_status was written in exactly one place in the extension (=> 1 on insert) — so ex-members stayed on the roster indefinitely, inflated the guild's player count, and kept consuming Battle.net calls in the portrait/spec/equipment syncs, all of which select WHERE player_status = 1. Departed characters are now soft-deleted (player_status = 0) inside the existing transaction, and a character back in the roster response is reactivated. Rows are never deleted — bbguild core references them from DKP and raid history — and player_outdate is left alone, since that is a date the user sets in the UCP character form.
[FIX] WowHead tooltips stayed on screen and never disappeared on mouseout when bbTips was also enabled (#33). Both extensions injected the same WowHead library into overall_header_head_append — bbGuildWoW as //wow.zamimg.com/widgets/power.js, bbTips as https://wow.zamimg.com/js/tooltips.js (byte-identical files, md5 86996e76b3f38091320b9b448d629c59). Two instances initialise, but the library only binds its hide handler when the link has no onmouseout yet, so the first instance bound it and the second showed a second set of tooltip elements that nothing could hide. bbGuildWoW now skips its own copy when S_BBTIPS_RUNTIME is set — that flag is assigned only when bbTips actually injects the script, so it honours bbTips' third-party/GDPR toggle and page-scope setting, and bbGuildWoW still loads its own copy whenever bbTips does not.
[CHG] Removed a dead $sql assignment in the player-detail event handler — an abandoned draft (INNER JOIN … ON 1=0, an unformatted %s) that was overwritten four lines later before execution. No behaviour change; it also cleared one of EPV's SQL-injection warnings. The remaining 33 are documented as false positives in tests/epv-tests.md.
[FIX] Roster grid showed a broken image for players whose class does not resolve to a known class_id (#27). portal/modules/roster.php builds the grid icon from images/roster_classes/<imagename>.png, and wow_installer.php seeds class_id 0 with imagename = wow_unknown, but no roster_classes/wow_unknown.png existed. Added one at 184x184 to match the rest of that set — the existing class_images/wow_unknown.png is a 25x25 icon and would have rendered at a quarter the size of its neighbours, since the template caps with max-width:100px and does not upscale.
[CHG] Now requires bbGuild core >= 2.0.0-rc5, enforced in ext.php::is_enableable() (new BBGUILDWOW_REQUIRES_BBGUILD_VERSION message in all languages); composer soft-require bumped to >= 2.0.0-rc5. Pair this release with core rc5.
[CHG] The BattleNet API ACP module moved into the new Game settings category (ACP_BBGUILD_GAMESETTINGS) introduced by bbguild core rc4, out of General Settings. Migration v200rc2 re-parents it and depends_on the core rc4 migration so the category exists first.
[CHG] Soft-requires avathar/bbguild >= 2.0.0-rc4 (was rc3) — pair this release with bbguild core rc4, which carries the matching ACP guild-edit fixes and the new Game settings category.
[FIX] Guilds and characters whose name contains an accented/multibyte character (e.g. "Bête Noire") failed every Battle.net sync with a 404 (#32) — the name slug was concatenated into the request URL with the raw multibyte byte (…/guild/silvermoon/bête-noire), which is not a valid URL. consume() now percent-encodes each path segment (…/silvermoon/b%C3%AAte-noire); Blizzard keeps the accent (lowercased), it is not stripped (verified 200 against the live API).
[DIAG] Roster-sync API failures now record the full Battle.net request URL (region host, namespace, realm + guild slug) in the bbGuild ACP log and the on-screen error, instead of a bare "API error 404" — consume() already computed the URL but it was discarded. Makes region/slug mismatches self-diagnosing (#32).
[FIX] Guild faction reset to Horde on every Battle.net armory sync whose response lacked faction.type (#29, core) — process_guild_data() defaulted the faction to Horde and only flipped to Alliance on an explicit faction.type === 'ALLIANCE', so any incomplete guild response silently overwrote the faction chosen in the ACP. Faction is now only reported when the API actually provides it (both ALLIANCE and HORDE handled explicitly), letting update_guild_battleNet() preserve the guild's stored value otherwise; a local fallback is retained solely for emblem ring rendering.
[FIX] Security/standards audit: all six sync AJAX routes (roster, specs, portraits, equipment, achievement categories, achievements) had no permission check at all — anyone with the URL could trigger a sync. Both controllers now require a_bbguild.
[FIX] acp/achievement_module.php's listachievements POST handler was missing CSRF form-key validation on its three state-changing actions
[FIX] Replaced trigger_error() in the Battle.net SDK layer (api/*.php) with a proper battlenet_api_exception — trigger_error() isn't catchable, so a mid-request API failure rendered phpBB's HTML error page instead of the JSON payload the sync UI's polling JS expects
[FIX] One unescaped SQL interpolation in model/achievement.php's get_achievement(), inconsistent with the rest of the file
[FIX] game/wow_api.php's raw @mkdir/@file_put_contents/file_exists calls (10 sites, several silently swallowing failures) now go through phpBB's filesystem service
[FIX] 3 undeclared dynamic properties in acp/achievement_module.php (PHP 8.2+ deprecation)
[FIX] Fresh-install failure: bbguild_wow_version was never created via config.add, only ever updated — same bug class already fixed in bbguild core (#353)
[CHG] Version tracking moved out of phpbb_config entirely into ext::BBGUILDWOW_VERSION, matching bbguild core's pattern
[CHG] Soft-requires avathar/bbguild >= 2.0.0-rc1
[NEW] First functional test suite: sync route authorization (tests/functional/sync_routes_authz_test.php), 5 smoke tests (extension lifecycle, services.yml class resolution, ACP modules load, routes don't 500, migration idempotency), and an OAuth token lifecycle integration test against a local mock Battle.net server (the SDK uses raw curl, not an injectable HTTP client, so this was the only way to cover it for real)
[CHG] CI now runs functional and smoke tests, not just unit tests — the webserver infrastructure was already there, just unused
[DOCS] README: fixed stale PHP >= 7.4.0 requirement (actual requirement has been 8.1.0 since the PHP 8.x compatibility pass)
wow_provider implements specialization_provider_interface; static spec_catalog() is the single source of truth
wow_installer::install_specs() seeds specs and translation rows on fresh install
Backfill migrations for existing installs (seed_specializations, seed_spec_translations)
[NEW] Specialization icons — 39 PNGs in images/spec_icons/ sourced from Battle.net's playable-specialization/{id}/media endpoint
[NEW] Specialization translations for de / fr / it / es_x_tu, sourced from Battle.net's official name object. Polish and Dutch fall back to canonical English (Blizzard doesn't officially translate WoW into those locales).
[CHG] Soft-requires avathar/bbguild >= 2.0.0-b4 for the specialization plumbing
[CHG] Repo + composer name dropped to no-separator form (avatharbe/bbguildwow, avathar/bbguildwow); PHP namespace is avathar\bbguildwow. DB-stored config keys (bbguild_wow_version, bbguild_wow_oauth_token_*) and cache keys preserved with the original underscore form.
[CHG] Unit-test workflow now checks out bbguild core alongside bbguildwow so tests that instantiate plugin classes (which implement core interfaces) can resolve them on CI