# Cerebro

## [v0.10.29](https://github.com/Shezzannn/Cerebro/tree/v0.10.29) (2026-03-20)
[Full Changelog](https://github.com/Shezzannn/Cerebro/compare/v0.10.28...v0.10.29) 

- Merge pull request #35 from Shezzannn/feature/guild-roster-sync-dashboard  
    feat(standings): Guild Roster Sync Dashboard with all PR review fixes  
- chore: Bump version to 0.10.29  
    Guild Roster Sync Dashboard: combat guard on guild scan and heartbeat UI refresh; LWW sentinel for auto-discovered players; semver version comparison; cached online-member lookup; DRY sync health helper; correct no\_addon (red) / unknown (gray) colors; stale addonStatus pruning; config hash convergence fix (sync.lastUpdate excluded, fixes 25/25 to 1/25 every-5-min bounce)  
- fix(standings): apply all 13 PR #35 review fixes  
    Critical:  
    - Fix 1: Add InCombatLockdown() guard in HandleHeartbeat addon-state refresh timer;  
      add PLAYER\_REGEN\_ENABLED post-combat flush in CommunicationV2  
    - Fix 2: Add not InCombatLockdown() guard before DiscoverFullGuildRoster in  
      GROUP\_ROSTER\_UPDATE (prevents mid-boss DB writes + RecalculateAllPR)  
    - Fix 3: Correct comment GUILD\_ROSTER\_UPDATE -> GROUP\_ROSTER\_UPDATE  
    - Fix 4: Remove duplicate INACTIVE\_THRESHOLD local; expose as  
      CommunicationV2.INACTIVE\_THRESHOLD module constant  
    - Fix 5: DiscoverFullGuildRoster creates Pending+lastUpdate=0 stubs (LWW sentinel)  
      instead of Core+real timestamp to prevent auto-discovery winning over officer data  
    Important:  
    - Fix 6: Add CommunicationV2:CompareVersions() semver helper; replace all  
      lexicographic v < maxVersion comparisons in GetAddonState + ShowSyncHealthTooltip  
    - Fix 7: Add CommunicationV2:GetOnlineGuildMembers() with 10s TTL cache;  
      replaces O(N) guild scan per standings row in GetAddonState  
    - Fix 8: Add StandingsFrame:ComputeSyncHealth() DRY helper; refactor Draw()  
      and Refresh() to call it instead of duplicating the sync-pct block  
    - Fix 9: Split no\_addon (red, online+no addon) from unknown (dim gray,  
      offline/indeterminate) in GetAddonStateColor per design intent  
    Minor:  
    - Fix 10: Prune addonStatus entries older than 7 days in InitSyncState  
    - Fix 11: Add GetMaxAddonVersion() with \_cachedMaxVersion cache; invalidate on HB  
    - Fix 12: Anchor \_dev pattern to end of string (\_dev$) everywhere  
    - Fix 13: Remove tools/inspect\_pb.ps1 (unrelated debug utility)  
- chore(tasks): add PR #35 fix task tracker and implementation plan  
    13 fixes across Core.lua, CommunicationV2.lua, StandingsFrame.lua:  
    - Critical: combat guards, LWW safety, class on restore  
    - Important: semver helper, guild lookup cache, sync health DRY, color split  
    - Minor: addonStatus pruning, version cache, \_dev anchor, ps1 removal  
- fix(standings): resolve 4 bugs found in deep-dive PR review  
    Bug 1 (StandingsFrame.lua:184) — broken WoW color code  
      GetAddonStateTooltip used `|cfffe8<000` for the outdated state label.  
      The `<` is not valid hex; WoW ignores the color and renders as plain  
      text. Fixed to `|cfffe8700` (matching the tooltip row color).  
    Bug 2 (Core.lua:728) — DiscoverFullGuildRoster skips Pending stubs  
      Race condition: GROUP\_ROSTER\_UPDATE fires immediately on login and  
      DiscoverGuildGroupMembers creates Pending stubs (lastUpdate=0) for  
      all raid members. The 10s login timer then fires DiscoverFullGuildRoster,  
      which skipped anyone already in players{} — including those fresh stubs.  
      Result: visible-rank raiders stayed Pending, defeating the feature's  
      primary purpose. Fix: detect the sentinel (status=Pending, lastUpdate=0)  
      and upgrade stubs to Core, preserving ep/gp/class from the stub.  
    Bug 3 (StandingsFrame.lua:407) — Restore creates classless record  
      ShowRemovedPlayersPopup Restore path created the new Core record  
      without a class field. Fixed: look up class from guild roster before  
      creating the record (same pattern as MergeRosterData/Admit to Core).  
    Bug 4 (StandingsFrame.lua:1104) — \_removedPlayersLabel shows "0 removed"  
      After the last purged/blocked player is restored/unblocked, Refresh()  
      kept updating the label text to "| 0 removed" with no way to hide it  
      short of closing the window. Fixed: clear the label text to "" when  
      count == 0 so the widget takes no visual space.  
    Luacheck: 0 warnings / 0 errors in 2 files  
- fix(sync): exclude sync.lastUpdate from config hash computation  
    sync.lastUpdate is a merge-metadata LWW timestamp stored inside  
    config.sync. ComputeConfigHash()'s recursive serializer was including  
    it, causing two officers with identical settings but different save  
    timestamps to compute permanently different config hashes — the  
    ping-pong loop seen in debug logs (0b565077 vs 690de2b8 vs c0583603).  
    Fix: add EXCLUDED\_NESTED\_KEYS = { lastUpdate = true } inside the  
    serialize() closure so LWW timestamps are never part of the content  
    hash, regardless of which config section contains them.  
    Luacheck: 0 warnings / 0 errors  
    validate\_docs.py: all checks passed  
- fix(standings): sync bar now requires both roster AND config hash match  
    Previously only rosterHash was checked, causing 100% (9/9) to display  
    even when configHash diverged on 6 of 9 peers. Now a peer is only  
    counted as fully synced when both hashes match. The tooltip detailed  
    breakdown (roster vs config separately) is unchanged for diagnostics.  
- fix(standings): apply Offline steel blue-gray to guild rank column  
    no\_addon and unknown states now both use {0.45, 0.50, 0.62} in  
    GetAddonStateColor, matching the tooltip Offline color. Red (no\_addon)  
    and dim gray (unknown) are consolidated since both represent offline.  
- fix(standings): improve Offline tooltip color readability  
    Changed from near-black (0.28, 0.28, 0.28) to steel blue-gray (0.45, 0.50, 0.62).  
    Readable against dark tooltip background, cool tone suggests 'disconnected',  
    clearly distinct from the warm neutral gray of Inactive.  
- feat(standings): rewrite addon compliance tooltip counts  
    - Compliance now scoped to visible guild roster (visibleRanks filtered,  
      excluding purged/blocked) instead of raw addonStatus table  
    - Self (local officer) now counted in 'Synced & current'  
    - Replaced 'No addon detected' with 'Offline' bucket (dimmer gray):  
        synced   = in addonStatus, recent, current version  
        outdated = in addonStatus, recent, old version  
        inactive = in addonStatus, >90s ago (darker gray)  
        offline  = never in addonStatus at all (darkest gray, 0.28)  
    - Total now matches visible standings player count  
    - Sync: 100% label turns green in both Draw() and Refresh()"  
- feat(standings): split sync bar into two separate hover zones  
    - ShowSyncHealthTooltip: removed the inline removed-players list  
    - ShowRemovedPlayersTooltip: new fn showing only purged/blocked names  
    - Draw(): sync bar is now two InteractiveLabels:  
        'Sync: X% (N/M)' -> hover = hash/compliance/epoch stats  
        'N removed'      -> hover = removed players list, click = popup  
    - Refresh(): updates both labels independently"  
- fix(standings): include self in tooltip peer counts  
    ShowSyncHealthTooltip had its own peerHashes loop that also omitted  
    self, showing 3/3 peers instead of 4/4 when 4 officers were online.  
    Adds +1 to rMatch and cMatch after the loop (self never differs).  
- fix(standings): remove blank spacer between filter row and table  
    The spacer Label was invisible before the sync health bar was added.  
    Once the bar caused the filter row to wrap to two lines, the spacer  
    became a visible blank gap between the search bar and column headers.  
- fix(standings): include self in sync peer count  
    peerHashes only contains other players' heartbeats — the local officer  
    is never in it. Was showing 3/3 when 4 officers were online. Now adds  
    +1 to both syncedPeers and totalPeers after the loop so the local  
    player is always counted as synced (self-check already returns 'synced').  
- fix(standings): self red color and stale sync bar  
    - GetAddonState: short-circuit to 'synced' for local player — HandleHeartbeat  
      self-excludes so the local officer never had an addonStatus entry, causing  
      them to fall through to the guild online check and return 'no\_addon' (red)  
    - Refresh(): update \_syncHealthLabel text with live peerHashes data on every  
      call — the label was created once in Draw() and never updated, causing the  
      sync percentage to stay stale until a tab reopen"  
- fix(standings): sync dashboard bug fixes  
    - addonStatus keys now normalized to full Name-Realm before write  
      (AceComm passes short name on same-realm; lookup was always missing)  
      → fixes all rank colors showing red (no\_addon)  
    - Removed Players popup: added ScrollFrame container so 10+ entries  
      scroll instead of overflowing  
    - Removed Players popup: fixed 350px height keeps AceGUI close button  
      always visible regardless of entry count  
    - Removed Players popup: added duplicate guard (self.\_removedPlayersPopup)  
      so re-clicking closes old popup before opening new one"  
- feat(standings): guild roster sync dashboard  
    - Auto-discover all visible-rank guild members on login (DiscoverFullGuildRoster,  
      10s delay, 60s throttle). Purge/block lists take absolute priority. Core  
      status assigned directly (visibleRanks IS the admission gate).  
    - Guild rank text color now encodes 5-state addon compliance for officers:  
      green=synced, yellow=outdated, dark gray=inactive, red=no addon, dim=unknown.  
      Raiders continue to see the original binary green/gray color.  
    - Sync health bar added to filter row top-right of Standings (officer-only).  
      Shows 'Sync: X% (N/M)  |  N removed'. Hover reveals per-hash breakdown,  
      addon compliance counts, epoch, last HB age, and named purged/blocked players.  
    - Clicking the 'N removed' label opens a Removed Players popup with  
      per-row Restore/Unblock buttons (RBAC-gated, audit-logged).  
    - Real-time UI reactivity: state-change detection in HandleHeartbeat  
      triggers a 1s debounced RefreshAllUI() — no window reopen needed.  
    - addonStatus stored in syncStateV2 (NOT players) — hash-immune by design.  
    - \_dev version suffixes excluded from max version computation."  
