Skip to content

Power Infusion Helper: the rework — its own designer tab, real sources, named players, a combat-safe gate - #266

Open
Krathe82 wants to merge 66 commits into
DanderBot:mainfrom
Krathe82:krathe/pi-helper-rework
Open

Krathe82 wants to merge 66 commits into
DanderBot:mainfrom
Krathe82:krathe/pi-helper-rework

Conversation

@Krathe82

Copy link
Copy Markdown
Contributor

66 commits on top of main, all field-tested on Krathe's raid lane over the last week. Two things are deliberately not in here: the battle-res slot-park guard (that is #265 against stable, and will reach main through the stable merge — if that lands first, this rebases with one small conflict in _applyHelperGate, which I'll resolve), and the local -krathe toc suffix.

Where it lives now

The helper is a fourth pool tab inside the Aura Designer (Any Buff / Debuff / … / Power Infusion Helper), not a page beside it and not a band on the Any Buff tab. Two sub-tabs: Triggers (what makes it fire, for whom) and Effects (the designer's own Effects tab, scoped to the helper's records). Its records still live in the Any Buff pool because the pool decides the caster filter — that is plumbing the user never sees. The Any Buff tab no longer shows the helper's indicators, and the alpha.6 changelog line saying it "lives on the Any Buff tab" will want updating.

Triggers

  • Classes and Cooldowns in one box: the class ticks narrow the cooldown list they sit beside, with an Edit button into the Filter Designer and a live on/total count on the header.
  • Additional Filters — Trinkets, Potions, Racials — each a tick with its own count and a pencil straight into its list. Racials is a curated list of four we seed (the category has thirteen, nine of them the opposite of worth infusing behind), so it can be edited like the others.
  • These are references, not copies: ticking Trinkets writes f.includes on the helper's filter, and FilterRegistry:ResolveSelection folds includes one level. The helper's list never gains their spells; edit them in their own lists. (selection.noIncludes opts a consumer out.)
  • Roles, asked positively ("watch these roles").
  • Players — a named-player allowlist on the same compact roster widget as pinned frames (GUI:CreateCompactRosterWidget, narrowed for the column), with an "Only watch these players" switch so a raid team stays written down through a pug night. Absent means on; an empty list is still everyone.
  • "Show when Power Infusion is on Cooldown" (off by default) and "Show in combat only", both with two-line tooltips, one state each.
  • The sound tick is just "Enable" with a tooltip.

Effects

  • Icon asks which picture, with pictures: PI Icon, Icon (the cooldown's own art) or Icons — a row of one icon per cooldown up, with Max Icons and Icon Size on the card. Icons is a normal effect in Active Indicators, not a layout group, and closes with an ✕ like every other picker.
  • Both the icon effect and the Icons group have a SHOW block — cooldowns / trinkets / potions / racials — that follows the Triggers tab until you override it, with a "Following the Triggers tab" footer and a way back.
  • Placed effects are named apart: "PI Helper — PI Icon / Icon / Icons". Power Infusion and "Their CD" can coexist on one signal.
  • Preview tiles show real example cooldown icons instead of ?.
  • Edit pencils were under the row's own hover rect; fixed.

The gate (the part that matters in a raid)

  • pihShouldShow(inCombat) takes the event-carried combat truth rather than racing the handler order; DF.playerInCombat, never InCombatLockdown(), per the standing rule.
  • The gate reaches placed slots in combat: _applyHelperGate attempts the push and defers only on an actual refusal (it used to defer always, so in a raid — combat for the whole pull — no edge on a placed icon ever landed). It arms the processor after the push so a placed icon clears with the rest.
  • The dead filter is { maxDuration = 0 } — the same verified park lever as SLOT_PARK_CF, evaluated outside the identity gate. It was an includeSpellIDs map, which the identity gate skips when it declines, so the "dead" filter failed open exactly when it was needed; every intermittent "icon stayed on after I cast PI" report was this.
  • The gate follows a retargeted frame (re-pushed from SetSlotOwnerUnit / Handle:SetUnit), reads the live designer (auto-layout preset overlays honoured), and is a no-op on non-priests (records untouched, everything suppressed through PIH_SetEnabled(false)).
  • One log line per gate edge, in the persisted debug log: pushed/deferred/skipped counts split by lane, group and slot status, every condition in force, and the player-list mode. A reload after a raid is the whole reporting procedure.
  • /df debug pi shows what each placed slot is carrying.

Also in here

  • Border animations: the blend mode is a setting, not a constant — DF Chase and Proc default to ADD as before, everything else to BLEND, with an Animation Blend Mode dropdown. Chase was tinting to the frame border's colour on the overlap.
  • Filters: a list we seeded toggles and resets instead of being torn apart; the count follows the ticks; the curated mark is stamped on a helper that already exists.
  • A sweep retiring every x and nil or y in both addons — nil never wins an and/or, so the or value always did. Two of them were the helper's own ticks; the other six are listed in the commit.

Verification

Every file compiles (226 non-Libs Lua files), no unexplained global-name changes per commit, line endings and BOM state preserved throughout, and one cherry-pick conflict against main's Rows.lua (the retired in-row helper band) resolved to the removal.

Krathe, 2026-09-08: "it's not very clear how to use it or even how to find it.
It's under AD any buff and it's settings are confusing and don't merge well…
Even the main settings and the added indicator are split."

Both complaints came from the same decision. The helper's records must live in
the Any Buff pool -- the pool decides a record's caster filter before anything
else, and the helper watches OTHER people's cooldowns -- and the card was put
next to the records so the button and its result sat together. Reasonable in
code, and it also decided where the user had to LOOK: priest-only, four levels
deep, behind a tab that is not the default, with the behaviour in one panel and
the appearance of what it created in a different card further down the page.

★ THE CARD DOES NOT HAVE TO LIVE WHERE THE RECORDS LIVE. The pool constraint is
real and unchanged; the location was never required by it. So the feature moves
and the plumbing stays exactly where it was.

  * New page: Auras > Power Infusion Helper, a SIBLING of the Aura Designer
    rather than a mode of it -- this is a feature with its own rules, and
    sitting in the same category is also what gives it the designer's chrome
    for nothing. AuraDesigner/UI/PIHelperPage.lua composes the parts Cards.lua
    already owns (S.BuildPIHelperCard / S.PIHelperSections /
    S.BuildPIHelperPane); not one builder was rewritten.
  * Priest-gated through CreateSubTab's own fourth argument, so a non-priest
    never has the entry built rather than seeing it greyed.
  * NOT gated on the helper existing. A page that appears only once you have
    added the helper is a page you cannot use to add it -- the exact trap this
    move exists to undo.
  * Both old mounts removed: the classic pane in the Effects head area
    (Cards.lua) and the popout row band (Rows.lua, 215 lines). Each site keeps
    a DO-NOT-REMOUNT note saying where it went and, more importantly, what did
    NOT change -- so nobody re-derives the pool rule from an empty space.

⚠ THE WIDTH DANCE IS THE ONLY GENUINELY NEW MACHINERY, and it is worth knowing
about. GUI/Panel.lua's Add records layoutHeight/layoutCol and nothing else; the
actual SetWidth happens later, in PageRefreshStates' layout pass. But
S.BuildPIHelperPane needs a real width AT BUILD TIME -- the card measures its
wrapped description against it and falls back to 320 without one, which is
right for the 260px popout pane and wrong by half on a full-width page. So the
page builds lazily on the first size it is given, re-measures only when the
width actually moves, then publishes the measured height and asks for one more
layout pass. That cannot loop: the rebuild is gated on WIDTH and the second
pass changes only HEIGHT, with a re-entrancy flag as a belt.

Teardown goes through GUI._trashFrame, the same way BuildPage retires its own
children -- a rebuild that left the old column parented here would stack two
sets of controls writing the same settings.

Verification: 170 files parse. No new globals anywhere -- Cards.lua and
Rows.lua each LOST one (UnitClass, select) as the class checks left them, and
the new file reads only CreateFrame/DandersFrames/UIParent/UnitClass/math/
pcall. Every identifier defined inside the deleted 215-line band was checked
for use after it: none escaped. Load order is safe because the CreateSubTab
call runs inside DF._SetupGUIPagesPart3 at GUI-build time, long after
PIHelperPage.lua has loaded. All files CRLF, no BOM, blob-to-blob.

⚠ Not verified in game. Two increments still to come, both agreed: hiding the
helper's records from the designer's own lists, and per-signal appearance plus
a preview on the new page.

(cherry picked from commit 0fa0e84ae7d184ddff427b6f9acd4fc41cc35130)
…ns the signal

Two halves of the same complaint, both from 2026-09-08: "Anything added should
show just on the PI helper page and not in AD itself" and "the main settings
and the added indicator are split".

THE RECORDS LEAVE THE DESIGNER'S LISTS

A record carrying a `pihSignal` mark belongs to the helper. Left in the
designer's own lists it reads as a stray indicator the user does not remember
making, and deleting one there silently half-dismantles a feature configured
somewhere else entirely.

  * CollectAllEffects (Active Indicators, both layouts) now takes an opts table
    and drops helper-owned rows unless asked for them.
    ☠ AN OPTION, NOT A HARD SKIP: the helper's own page wants exactly these
    rows -- it is the one surface where they ARE the subject -- so the filter is
    the caller's choice, and the PIH_SignalLabel derivation below it stays live
    rather than becoming unreachable code that still looks maintained.
  * Layout Groups: a new P.VisibleLayoutGroups, used by the classic list, its
    hasGroups gate, and the popout layout's list.
    ☠ FILTERED AT THE DISPLAY SITES AND NOT IN CurrentLayoutGroups, and that
    distinction is the whole reason the helper still works: two of that
    function's callers are LOGIC, not display -- GetIndicatorLayoutGroup
    resolves an indicator's owning group and DeleteLayoutGroup removes one by
    id. Filtering at the source would have made the helper's own icon group
    unreachable and undeletable by its own Remove path. The store stays whole.

THE COLOUR JOINS THE SIGNAL

A signal's behaviour was set on the helper panel and its colour on the
generated indicator's card further down the page, with nothing saying the two
belonged together. That division was consistency with the designer, where
appearance lives on effect rows -- on a page whose only subject IS the helper
it has nothing left to justify it. Each signal row now carries its own colour.

  * Bound straight to the record, so no accessor pair was needed: the colour
    already lives at cfg[pihColorKey(surface)] and CreateColorPicker writes
    through a table+key like every other colour in the addon.
  * Re-fetched per build rather than captured. A surface swap changes both the
    record and the key -- a border keeps its colour under BorderColor -- so a
    captured binding would write to the record the signal had just left.
  * Offered ONLY where a colour exists. An Icon has none (pihPlace says so when
    it declines to carry one) and "None" has no record at all; a swatch on
    either is a control that writes where nothing reads, the same class of
    lying control this feature was moved away from.
  * Writes end at P.PIH_Apply + pihRefresh, the chokepoint every other helper
    mutation already uses -- the table alone would leave the frames on the old
    colour until something unrelated repainted them.

Verification: 170 files parse, no global changes in any of the five files, all
CRLF, no BOM, blob-to-blob.

⚠ Not verified in game.
⚠ THE PREVIEW IS NOT DONE, and deliberately not guessed at. CreateFramePreview
is reusable standalone (the scope-card tiles already do it, with thumb +
placement=false), so the plumbing is not the obstacle -- opts.Paint is, because
what a helper preview should PAINT is a design question I would be inventing an
answer to: one frame per signal, or one frame showing whichever signal wins the
contested surface. Worth asking rather than building twice.

(cherry picked from commit a70c4dc993f2543eba0c24c96daf9b992e392451)
Three asks from 2026-09-08, after checking what the standalone PIHelper addon
actually does: its entire visual vocabulary is a glow on the raid frame plus
optional duration text. Nothing placed. That is the shape this was drifting
towards anyway.

SQUARE IS RETIRED AS A HELPER SURFACE (schema 3)

The helper answers "who should I infuse", and that is a STATE OF THE FRAME, not
a thing placed on it. Border, health bar, background and the two texts are all
the frame saying something about the unit; a Square is a coloured block at a
coordinate, which is a PLACEMENT decision -- and placement is the Aura
Designer's job, not this feature's.

  ⚠ MIGRATED, NOT DELETED. Someone running a Square helper would otherwise open
  the panel to a signal reading "None" with no explanation, which is
  indistinguishable from their settings having been lost. pihSweep step 4
  converts it to Border, carrying the colour across, because Border is the
  nearest honest equivalent -- a contended surface, like the Square was
  competing for attention.
  ⚠ The square HANDLING code stays (pihPlace, pihCreateSignal, pihDeleteSignal
  all still know the type). It is what the migration runs through, and a
  half-removed surface is how a legacy record becomes unreachable.

  ⭐ ICON SURVIVES, NARROWED TO ONE MEANING, and is relabelled to say it:
  "Power Infusion icon". It earns its place because it answers something no
  colour can -- on the already-infused signal it is the buff you would be about
  to waste, on the cooldown signal it is the spell you are being told to cast.
  A picture of Power Infusion means Power Infusion; a gold border means
  whatever the user decided gold means today. "Icon" was accurate and told them
  nothing -- it read as a sibling of Square, i.e. another placement, which is
  the exact reading this cut removes.

AN ENABLE TICK, NOT AN ADD/REMOVE CARD

"Add" and "Remove" were the implementation talking. Literally true -- the
helper creates and deletes AD records -- but that is plumbing the user was
never meant to know about, and on a page whose whole subject IS the helper, a
card offering to add the thing you came here for is a step with nothing on the
other side of it. It now reads like the designer's own enable: same banner,
same styled check button, same left-aligned label.

  ⚠ Settings still survive the round trip, as they already did: unticking
  routes to PIH_Remove, which STASHES every customisation, and PIH_Create lays
  the stash back over fresh defaults. The tick just stops making it look like a
  destructive act.
  ⚠ The handler reads the POOL, not the box. A double click or a profile switch
  landing mid-build would otherwise create a second helper or remove one that
  is already gone.
  The explaining sentence moved onto the banner and stays visible while the
  tick is OFF -- which is exactly when someone needs to read it.

A REAL PREVIEW

CreateFramePreview, the same factory the designer mounts in both its layouts
and the scope-card tiles already use standalone. Reusing it is the only way
this page can promise the picture matches the live frame; a second renderer is
a second thing to drift.

  ☠ PAINTED FROM THE HELPER'S OWN RECORDS. RefreshPreviewEffects now takes an
  optional pool, defaulting to exactly what it always used, and the page hands
  it S.PIH_PreviewPool -- the SAME cfg tables, filtered to those carrying a
  helper mark. The Any Buff pool also holds the user's unrelated work, and a
  preview on a page about one feature that quietly renders another's effects is
  worse than no preview.
  ⚠ S.framePreview is a singleton and this takes it. Safe for the same reason
  the designer's two layouts can both assign it: one options page renders at a
  time, and whichever builds next claims it back.

Four now-orphaned locale strings removed rather than left for translators to
work on text nobody will ever see.

Verification: 170 files parse, no global changes in any file, all CRLF, no BOM.

⚠ Not verified in game.

(cherry picked from commit e7738df9c6eff7467016222fd8d4099983868ece)
…, Icon out

Krathe, 2026-09-08, with the designer's page beside it for comparison: "the
preview should be the same with a side by side, settings on right side preview
on left, the page should look almost exactly like AD… right side should be the
PI helper options with triggers then indicators".

THE SPLIT

A 50/50 column: preview left, settings right, mirroring the designer's own
splitContainer -- including its 6px gutter, borrowed rather than picked so three
split panels in this addon do not drift to three different gaps.

  ⚠ MEASURED, NOT ANCHORED TO CENTER. The designer can anchor its halves to a
  CENTER point because they live inside an island of known size; this column is
  measured by the page harness after the fact, so the halves take an explicit
  width off the host's real width -- which is why nothing builds until the first
  size arrives.
  ⚠ The column is as tall as its TALLER half. Sizing to the settings alone would
  clip the preview whenever the helper is off and the panel is two rows high;
  sizing to the preview alone would clip the settings in every other case.
  ⚠ No border on the left panel -- the preview container draws the visible dim
  border, and one on both stacks into a brighter doubled line. The designer's own
  note, and the reason it says it.

TRIGGERS, THEN INDICATORS

The old grouping opened with "What to Show" -- the answer -- and buried the
question two boxes down, which is why the panel read as a pile of settings
rather than one decision followed by another. Now: Triggers (roles + the
cooldown gate), Classes and Cooldowns, Indicators (the signal rows), Sound.

  ⚠ THE GATE MOVED. "Hide the helper while your own Power Infusion is on
  cooldown" is not a display choice -- it is a condition on whether the helper
  has anything to say at all, which is what a trigger is.
  ⚠ Classes and Cooldowns is a SIBLING of Triggers, not a child: t.group draws a
  box, and a box inside a box for a thirteen-row list reads as a nested pane
  rather than a fold.

ICON OUT, SQUARE BACK (schema 4)

☠ THIS REVERSES THE PREVIOUS COMMIT, AND THE REVERSAL IS THE POINT. I cut Square
as "a placement, which is the designer's job" and kept Icon as "the one picture
that carries meaning". That was SHAPE reasoning. Krathe's is SCOPE reasoning:
"we don't need to track each buff just the fact someone has popped a CD and we
highlight in some form." An icon shows a specific buff's artwork, so offering
one implies the helper tracks WHICH cooldown each player popped -- detail the
feature never delivers. Scope wins over shape, and the swap is recorded in both
files so it does not later read as drift.

  ⚠ Schema 3 (Square -> Border) is deliberately NOT preserved and its number is
  burned rather than reused. It existed for part of one afternoon on one
  developer's client, so nothing in the wild ran it; reviving its inverse would
  mean tracking which of two contradictory migrations a profile had seen. A
  profile that DID run it has borders where it had squares -- a colour on a
  different surface, not lost work.
  ⚠ Schema 4 migrates Icon -> Square: the nearest thing that still marks the same
  unit in the same place. An Icon carries no colour, so the square falls back to
  the signal's default, which is what pihPlace already does with a nil carry.

S.PIHelperSections is removed -- it adapted the bodies for the popout row band,
and that band went when the helper got its own page. The per-section split is
KEPT: it is what let the page reorder Triggers before Indicators without
touching a single control. Three locale strings retired with the shapes that
used them.

Verification: 170 files parse, no global changes, every L[] key resolved, no
orphaned key still referenced, all CRLF, no BOM.

⚠ Not verified in game.
⚠ Still open: "indicators you can add like you would on AD" is implemented as
the existing one-surface-per-signal dropdown offering exactly that list. Whether
you meant MULTIPLE indicators per signal is a data-model question -- pihFound is
keyed by signal and assumes one record each -- and I have not guessed at it.

(cherry picked from commit 6fa0fe9bd9ab154969595074c51033cae7c7c77a)
The left half rendered as a transparent gap with the game world showing
through (Krathe, screenshot 2026-09-08). My bug, and a circular one.

☠ WITHOUT opts.thumb, CreateFramePreview ANCHORS ITS CONTAINER TO ALL FOUR
CORNERS OF THE PARENT -- so the canvas takes its height FROM the panel it is
built into. I built the preview into a 1px panel and then set that panel's
height FROM the preview. The two resolved against each other at 1px, so
nothing was drawn and the empty panel showed the world behind it.

The designer never hits this: its leftPanel is anchored TOPLEFT+BOTTOMLEFT
inside an island that already has a size, so the height exists before the
canvas asks for it.

⇒ Decide the height, THEN build into it. The settings half is measured first,
the column takes the larger of that and the canvas's own wanted height, and the
preview is created last into a panel that already has a size.

⚠ AND THE FLOOR IS THE CANVAS'S OWN NUMBER, not a constant I picked.
P.CanvasWantedHeight exists for precisely this case -- its comment says "the
host must size the band BEFORE calling the builder that creates it" -- and it
derives from the frame height and the live preview scale, so the canvas keeps
fitting when either moves. My first patch used a flat 260, which would have
been wrong the moment anyone touched the scale slider.

It matters most in the state a new user sees first: with the helper switched
off the settings are one banner tall, and a canvas sized to that cannot show a
unit frame at all.

Verification: 170 files parse, no global changes, CRLF, no BOM.
⚠ Not verified in game.

(cherry picked from commit b6a77aab30ac452f55c322e75100c2bad32cecab)
… as tabs

Three from the screenshot, 2026-09-08.

THE PREVIEW WAS STRANDED IN THE MIDDLE

☠ The mock frame is SetPoint("CENTER", container, "CENTER") and the container
fills its parent -- so a left panel stretched to match a long settings column
centres the frame halfway down THAT column, which is why it sat near the bottom
with a screen of empty space above it. The designer never shows this because
its two halves are the same height BY CONSTRUCTION: they fill an island sized
to the viewport and the settings side scrolls inside it. This page is a
measured column in a scrolling page, so the halves are independent -- only the
CONTENT height is shared, and the left panel takes the CANVAS's height.

⚠ That height is the canvas's own number, not one I picked: the non-compact
floor, or the height that fits the frame at the live preview scale plus the
30px the band form saves by putting the scale behind a glyph -- the file's own
accounting, quoted at the call site. Whichever is larger.

THE MISSING HEADER AND SLIDER

It was built with compact = true, which is the BAND form: label hidden, and the
scale behind a glyph in the corner rather than a slider row. That is right for
a popout band and wrong for a page asked to look like the designer. Now
non-compact, with placement = false -- the one thing suppressed, because that
flag gates the nine anchor dots, the drag hint and the three instruction rows,
all of which are about POSITIONING an indicator. Nothing here can be dragged.
It does not gate the slider.

TRIGGERS AND EFFECTS ARE TABS NOW

☠ The split is the feature's two questions, and they are answered at different
times. TRIGGERS -- roles, the cooldown gate, which classes, which spells -- is
set up once, carefully. EFFECTS -- surface, colour, sound -- gets fiddled with.
One column holding both meant scrolling past the thirteen-row class list every
time you wanted to nudge a colour.

  * GUI:StyleButton's `tab` mode, the same the designer's right panel uses, so
    this reads as the same kind of page rather than a lookalike.
  * ☠ The active tab lives on S, not in a local: this builder re-runs on every
    rebuild, and a local would drop the user back to Triggers every time they
    changed anything on Effects.
  * The enable banner sits ABOVE the tabs -- it turns the whole feature on, so
    it cannot live inside one of the two things it governs.
  * Sound moved to Effects: it is another way of being told the same thing.
  * S.BuildPIHelperPane becomes S.BuildPIHelperBody(parent, {tab=...}) -- one
    tab's worth of settings, card no longer included.

AND THE WAY IN TO THE SPELL LIST

The class ticks say WHICH classes are watched; the filter behind them holds the
individual cooldowns, and nothing on the page said that list existed. Triggers
now carries a line and a button through to the Filter Designer. A LINK, not a
second copy of the picker -- one place edits these, and a rebuilt picker here
would be a second answer to "is this spell watched".

⚠ CAUGHT BY THE _ENV DIFF, NOT BY luac: reworking the height flow left `h` as a
nil GLOBAL in the layout feedback block, which parses clean and would have
thrown on the first rebuild. It is `colH` now. This is the third time that
check has earned its place.

Verification: 170 files parse; the only new global anywhere is `ipairs` for the
tab loop; every L[] key resolves; no stale BuildPIHelperPane reference outside
one historical note; all CRLF, no BOM.

⚠ Not verified in game.

(cherry picked from commit 16601288ceadb577bed63276c5573ee5f4ce3ae3)
… to a tooltip

Four from 2026-09-08, all of them the panel saying things the wrong way round.

ROLES, NOT "NEVER SHOW ON"

DPS / Tanks / Healers, ticked to WATCH. Tanks and Healers off by default.

☠ THE STORE STAYS AN EXCLUSION SET, and that is deliberate rather than lazy.
helperExcludedRoles fails OPEN on a missing entry -- a group with no assigned
roles reads "no role" for everyone and nothing is hidden, which is the safe
direction. A positive store would invert that: an empty table would mean
"watch nobody" and the feature would go dark on any group without role
assignments. So the inversion is in the UI only.

⭐ AND THE OLD DEFAULTS ALREADY ARE THE NEW ONES. The store ships
{ TANK, HEALER } excluded, which through this inversion reads as DPS on, Tanks
and Healers off -- exactly what was asked for. No migration, and nobody's saved
choice changes meaning.

⚠ DPS was never offered before, which made "watch damage dealers" an invisible
always-on rather than a choice. DAMAGER is a real UnitGroupRolesAssigned token
(Core.lua's GetUnitRole passes it through), so unticking it excludes the same
way the other two do.

THE GATE, ALSO POSITIVELY

"Show even if your Power Infusion is on cooldown", off by default. It was the
one control in a box of enables that turned a SUPPRESSION on, which made the
box read inconsistently. Same trick as the roles: stored value untouched,
still defaults to gating, so it ships unticked and means what it always meant.

TWO LINKS TO THE FILTER DESIGNER -- ONE OF THEM MINE, AND THE WORSE ONE

pihAddClasses ALREADY ended with a Filter Designer button, and it uses
GUI:OpenFilterInDesigner, which switches the page AND scrolls to, selects and
pulses the cooldown filter. I added a bare SelectTab beside it without
checking. The comment on the existing one says exactly why mine was wrong: a
hand-written jump "landed you on the page with nothing indicated, which is
indistinguishable from a broken link". Mine is gone.

☠ Check for the affordance before adding it. The section was already complete;
I read its first half and built against the gap I assumed was there.

THE BANNER'S SENTENCE IS A TOOLTIP

It is read once, by someone deciding whether to switch the feature on, and
after that it is a line of prose in front of the settings on every visit. The
hover is the whole banner row, not the 16px box -- a tooltip on a checkbox the
size of a full stop is a tooltip nobody finds.

Verification: 170 files parse, no global changes, no orphaned key still
referenced, all CRLF, no BOM.

⚠ Not verified in game.
⚠ Still open, and the larger half of the request: Effects adding like the
designer's tiles with several indicators per signal (border + health + square
at once), and a generic square/bar that marks "needs PI" without naming the
cooldown. That is the data-model change -- pihFound is keyed by signal and
assumes ONE record each -- so it is its own commit rather than tacked onto this.

(cherry picked from commit d3f17bffa2a6f4df8ff65ddccf1de45b2af312f2)
Krathe, 2026-09-08: "the effects should add with the same button's as we use on
AD... we should be able to add multiple like border + health + square/bar etc.
Just like AD."

☠ THE STORE ALREADY ALLOWED THIS. A pool record holds many frame-level effects
and many placed instances, so "border AND health bar AND a square" was always
expressible. Two things said otherwise: pihFound wrote each hit over the last
into out[signal], and pihCreateSignal refused any second add with "already on".
Both were one-surface assumptions layered over a store that never had one.

  * pihFoundAll returns EVERY hit per signal, sorted by surface. pihFound keeps
    its old one-per-signal shape over the top, so the eleven existing consumers
    are untouched.
    ⚠ SORTED, because the pool walk is hash order -- "the primary hit" used to
    be whichever the iterator reached last. Harmless with one; three effects
    reshuffling on every redraw is not.
    ⚠ Its own rank table rather than PIH_SURFACE_ORDER: that local is declared
    ~400 lines below, so naming it would compile as a nil GLOBAL -- the
    "declared below its first caller" trap this file documents elsewhere.
  * The create gate is per SURFACE. It still refuses a duplicate of the same
    one: two borders on a record cannot both exist, and two identical squares
    would be an invisible double only the store can see.
  * pihDeleteSignal takes an optional surface. Without it, unchanged behaviour.
  * New: PIH_AddSurface / PIH_RemoveSurface / PIH_SurfacesOf, all ending at
    PIH_Apply + pihRefresh like every other helper mutation.

THE ROW BECOMES A LIST

Each signal now shows what it displays -- one entry per surface, with that
surface's colour swatch where it has one and a "Remove Border" / "Remove Health
Bar" button naming what it acts on -- then a row of buttons for what it could
also show. No "None" entry: removing the last effect IS what none meant, and an
empty list says so without a word for it.

⚠ THE CLASH WARNING IS PER EFFECT NOW. It read the signal's single surface;
with several it has to ask about the row it is drawn under, or a border clash
would be reported beside the health bar.

⚠ Two nil GLOBALS caught by the _ENV diff, neither visible to luac: `surface`
(the icons-only note still read the dropdown's variable) and, earlier, the rank
table. Fourth and fifth this week.

☠ AND I PARAPHRASED TWO LOCALE STRINGS WHILE MOVING THE CLASH BLOCK, inventing
L["Stack borders"] and a shortened warning -- keys that do not exist, which
compile fine and render BLANK. The originals are restored verbatim. Copy locale
lines; do not retype them.

⚠ PIH_SurfaceOptions and PIH_SurfaceOf are now unreferenced, and PIH_SetSurface
keeps only its own definition. Left in place rather than removed in the same
commit: PIH_SetSurface owns the swap machinery (pihSurfaceTakenBy, pihCapture,
pihPlace) and pihPlace is what schema 4's migration runs through. Untangling
that is its own job, not a tail-end cleanup.

Verification: 170 files parse, no new globals, every L[] key in the new block
checked against the locale file, all CRLF, no BOM.

⚠ Not verified in game.
⚠ Still to come, and asked for: the PI icon showing Power Infusion's own
artwork rather than the tracked buff's, and a generic square/bar with optional
"PI Me" text. Those change what the surfaces DRAW; this commit changes how many
of them a signal can have.

(cherry picked from commit b1b42390ef83dc3d302a283a9290d2a26dafa669)
Krathe, 2026-09-08, with three screenshots side by side: "what have you done...
It should basically function exactly as AD. Just without the My buffs /
Debuffs / Any Buff tabs and AD only stuff."

☠ I BUILT A LOOKALIKE TWICE AND IT WAS WRONG BOTH TIMES -- first a surface
dropdown, then a stack of plain grey buttons -- while the real thing sat one
call away. The helper's records ARE Aura Designer records; there was never a
reason to draw a second, worse version of the designer's own list.

The whole of AD's Effects tab is three lines:

    local yPos = S.BuildEffectsHeadArea(parent, -10)
    local effects = CollectAllEffects()
    for _, e in ipairs(filtered) do yPos = S.CreateEffectCard(parent, yPos, e) end

So the helper now uses those parts directly:

  * ADD AN EFFECT: GUI:CreateChoiceCardGroup for the route -- the designer asks
    route -> spell -> type, and there is no spell to choose here (the cooldown
    filter IS the spell), so it asks signal -> type. Two cards, then the
    designer's own tile grid: CreateFrameTile + PaintEffectOnThumb, the same
    miniature pictures and tooltips, three to a row like the add flow.
  * ACTIVE INDICATORS: S.CreateEffectCard per effect -- the same expandable
    shell, type badge, eye toggle and delete the designer draws, because it IS
    that list. CollectAllEffects({ includePIH = true }) is its own collector
    asked for the rows it normally hides from itself.
  * Types the signal already holds are left out of the grid, and sound has no
    tile: it is a property of the helper, not a surface.

⚠ S.activeBuffTab IS SET TO "other" AROUND THE BUILD AND RESTORED AFTER, and
that is not a hack. S.CreateEffectCard reads PoolKeyPrefix() and IsOtherTab()
off it, to key its expand state and decide whether a spec applies. The helper's
records genuinely live in the Any Buff pool, so pointing it there while we build
is telling the shared builder the truth. Restored immediately, because the
designer's own page reads the same field.

☠ AND THE ICON LISTS WOULD HAVE VANISHED. Cooldowns / Trinkets / Potions /
Racials lived inside t.signalRow, and the Effects tab stopped calling it the
moment the designer's tiles took over -- which would have removed four of
Maelareth's settings (DanderBot#263) from the UI as a silent side effect of a layout
change. They are lifted into pihAddIconLists and drawn under TRIGGERS, which is
where they belonged anyway: each tick decides which category of buff COUNTS,
not how anything is drawn.

t.signalRow is removed, with a note at its site saying where each of its four
jobs went.

Verification: 170 files parse, no new globals against the session baseline,
every L[] key in the new code checked against the locale file, all CRLF, no BOM.

⚠ Not verified in game.
⚠ Still not done, and it is the part that needs the surfaces themselves to
change rather than the panel around them: the Icon showing Power Infusion's own
artwork instead of the tracked buff's, and a generic square/bar with optional
"PI Me" text.

(cherry picked from commit 745d180e66e0d444bbb4d247696c95ad30c43b96)
Krathe's own answer, and the right one: "maybe we put it as a tab at the top of
AD next to Any Buff... and then the Power Infusion Helper in the menu just
instant links to that."

☠ I BUILT A COPY OF THE AURA DESIGNER THREE TIMES. A surface dropdown, then a
stack of grey buttons, then choice cards and a tile grid -- each one a worse
version of something sitting twenty lines away, and each one corrected in
review. The mistake was never a widget choice: it was treating the helper as a
thing BESIDE the designer when it is a POOL INSIDE it.

  My Buffs | Debuffs | Any Buff | Power Infusion Helper

Every surface the designer owns now works on it unchanged, because they all
route through four accessors in AuraDesigner/UI/Options.lua:

  * CurrentAuraPool -> a filtered view of the other pool, helper records only.
    ☠ SAFE BECAUSE THE VALUES ARE THE LIVE TABLES: consumers either iterate it
    or take CurrentAuraPool()[name] and mutate the record they get back, and
    both reach the real cfg through the shared reference.
  * CurrentAuraPoolWrite -> the REAL pool. A new record must land in the store,
    not in the temporary view -- the one thing a filtered read cannot carry.
  * IsOtherTab -> true for it. That question is really "any caster's buffs,
    shared across specs", which is exactly what the helper watches; answering
    no would give it My Buffs' caster filter, the one pool where its records
    are guaranteed to match nothing.
  * PoolKeyPrefix -> shares "other:", because it is the same record. A prefix of
    its own would key one card two ways, so expanding it on one tab would read
    as collapsed on the other.
  * VisibleLayoutGroups -> the test INVERTS on this tab. Elsewhere a helper
    group is somebody else's business; here it is the only business. One rule,
    read both ways.

The pool tab itself is one appended entry in PoolDefs -- priest-gated, and
appended rather than inserted so the other three keep the positions people
already know. Both layouts get it: they share S.BuildPoolStrip.

TRIGGERS ARE THE GLOBAL SUB-TAB

Every pool's Global tab holds what applies to the whole pool rather than to one
effect, which is precisely what roles, classes, icon lists and the cooldown
gate are. Krathe: "Triggers where people pick WHAT will show the effect... Then
HOW it shows the effects." WHAT lives there; HOW is the Effects tab, which is
the designer's own and needed nothing added to it.

The enable tick leads it, because it governs everything below and has to be
reachable while the helper is OFF -- the state a new priest arrives in.

PIHelperPage.lua: 292 lines -> 69

It is the DOOR now and nothing else: set the pool, call
DF.BuildAuraDesignerPage. The nav entry survives because the tab is four levels
deep and a priest should not have to know the helper lives inside the designer
-- which was the original complaint. Everything else it had grown is deleted,
with a note at the top saying it must never grow a page again, and why.

⚠ The tab is set BEFORE the build and deliberately NOT restored: the user is
now looking at that pool, and putting it back would leave the strip showing
Power Infusion Helper as active while the surfaces below rendered My Buffs.

Verification: 170 files parse, no global changes in any file, all CRLF, no BOM.

⚠ Not verified in game.
⚠ The two signals (burst / infused) still exist underneath. Collapsing them to
one -- "someone is worth infusing", with "already has it" as a trigger rather
than a second signal -- is the next thing, and it is a data change rather than
a layout one.

(cherry picked from commit 59f531f55705c7f0bd90f7555c0e010b04ff8f40)
Krathe: "selecting the PI helper in the menu is totally fucked up and the
trigger/effects are not showing." Both mine, both from pointing a second nav
entry at a builder that assumed it had only one.

1. THE FULL BUILD CLOBBERED THE POOL

BuildAuraDesignerPage's teardown sets S.activeBuffTab = "my" unconditionally.
The helper's page set the pool and then called that builder -- so the pool was
reset before anything rendered, and the helper's entry opened on My Buffs with
none of its controls on screen.

☠ AND IT WAS INTERMITTENT, WHICH IS WHY IT READ AS "TOTALLY BROKEN" RATHER THAN
"WRONG TAB": a REVISIT takes the reuse path, which leaves the pool alone. Same
click, two behaviours, depending on whether the island already existed.

⇒ S.pendingBuffTab: a one-shot the builder CONSUMES at the point it would
otherwise have defaulted. Requested, not assigned. Cleared on use, so a plain
visit to the designer still opens on My Buffs exactly as before.

2. TWO PAGES WERE SHARING ONE ISLAND

S.mainFrame is built into whichever page asked for it, and the reuse guard
REPARENTS it to whoever asks next. With the designer's page and the helper's
page both calling the builder, one island got handed back and forth while each
page's harness still believed it owned the widgets it had Add'd around it --
which is the sub-tab strip drawn over the Template row in the screenshot.

⇒ S.mainFrameOwner. A different page is a different build, exactly like a mode
switch, so it takes the full-build path -- which hides and unparents the old
island first and leaves nothing stranded on the page we came from. Compared by
identity, not name: the harness can rebuild a page object, and a stale
reference must read as "different" rather than matching a dead frame.

☠ WHAT I SHOULD HAVE DONE: read what BuildAuraDesignerPage does to shared state
BEFORE pointing a second caller at it. I checked that the pool accessors routed
correctly and stopped there -- the builder's own teardown, twenty lines further
down the same function, resets three of the fields I was setting.

Verification: 170 files parse, no new globals, all CRLF.
⚠ Not verified in game.

(cherry picked from commit e3b99f290b5ea3a0242a8ad8e1aa84d4045a86eb)
Three reports, one root cause each.

1. THE BLANK PAGE. Two nav entries both called the designer's builder, and
   that builder has ONE island (S.mainFrame) to parent. Giving each page its
   own build stopped them overlapping and started them blanking instead: the
   page harness does not re-run a builder on a cache hit (Panel.lua
   RefreshCached), so whichever page did not build last showed nothing at all.

   ⇒ Only the Aura Designer's page owns the island now. The Power Infusion
   Helper's nav row is a LINK -- it asks for the pool, drops the designer's
   cache and selects it -- which is what the design said from the start:
   "the Power Infusion helper in the menu just instant links to that."
   The page object survives as a stub with a banner and a button, because the
   settings search can still select any tab by name.

   WHAT I SHOULD HAVE DONE: the previous commit fixed one consequence of two
   pages sharing an island without asking whether they could share one at all.

2. THE SUB-TABS. On the helper's pool the strip is Triggers, then Effects, and
   no Layout Groups. "Global" IS "Triggers" -- relabelled, not a new tab, so
   SwitchTab and sixty call sites need no special case. Layout Groups goes
   because a layout group is a container of live aura icons and the helper has
   no per-spell display to arrange; leaving the tab would be a door back to
   the bug in (3). One definition (P.SubTabDefs) drives both layouts.

3. THE STUCK "PI Helper - Cooldown icons". A Filter Group of live cooldown
   icons, switched on by a tick captioned "Icons" filed under "Classes and
   Cooldowns" -- moved, renamed and finally removed, while its output kept
   drawing with nothing left that could reach it.

   ⇒ Retired outright (schema 5). pihSweep deletes every pihSignal group and
   the stash behind it, and both designer builders run the sweep for a priest
   so it does not wait for the right tab to be opened. The three amplifier
   ticks stay -- they are genuine triggers -- and now write into the ONE list
   the effects match on, so ticking Trinkets makes a trinket proc fire them.

AND THE EFFECTS TAB IS THE DESIGNER'S. It lists helper rows (includePIH on
this pool -- without it the collector filtered out every row the tab exists to
show) and adds through the designer's own tiles, with the spell step removed
because the cooldown list IS the spell. Icon comes back pinned to Power
Infusion's own artwork rather than the trigger's, which answers the scope
objection that cut it in schema 4: iconSpec.staticSpellID reaches a placed
indicator, and bindNative skips Blizzard's SetIcon when one is set so the
engine cannot repaint our art with the matched aura's. Bar joined the placed
branch, where it always belonged -- it was writing a frame-level key nothing
reads and could never have drawn.

Verified: 170 files parse, no new globals in any edited file except PlaySound
and SOUNDKIT in Auras.lua (both real), all CRLF, blob-to-blob diffs targeted.
NOT verified in game.

(cherry picked from commit 6cdbd9c115b3beeb217290510842c94b2563db9b)
The sweep found the retired cooldown-icon group and then failed to delete it,
silently, so it kept drawing. Krathe: "I'm still seeing PI helper - Cooldowns
on my AD despite it not being an option anymore."

A FINDER THAT DOES NOT DEPEND ON THE POOL NEEDS A DELETER THAT DOES NOT EITHER.
pihAnyIconGroup reads adDB.otherLayoutGroups directly -- the helper's groups
are always there. P.DeleteLayoutGroup resolves its store through
CurrentLayoutGroups(), which is pool-routed off S.activeBuffTab. Moving the
sweep to page-build time in the previous commit meant it now runs with the pool
still set to whatever the last session left, usually My Buffs -- so the delete
searched the SPEC store, found no such id, and removed nothing. It has no
return value, so the loop could not tell; my "refuse to spin" guard then turned
the failure into a clean break.

Two fixes, because the mismatch had two halves:

  · P.RemoveOtherLayoutGroupByID -- explicit store, clears the expand key,
    RETURNS whether it removed anything. Both the sweep and PIH_Remove use it,
    and both now warn instead of breaking quietly. PIH_Remove had the identical
    latent fault; it was only ever safe because it happened to run from an
    other-routed tab, which is a property of the caller, not of the code.

  · pihSweep pins the pool for its whole body. Step 4's Icon -> Square
    migration writes through EnsureAuraConfig, which is pool-routed too -- so
    an unswept profile could have had its migrated squares created into the
    spec pool. Same class of bug, one step earlier, not yet reported.

This also mattered more than it looks: with Layout Groups gone from the
helper's sub-tabs, a marked group is listed on no tab at all, so the sweep is
the only thing that can remove one. A silent no-op there is unrecoverable.

WHAT I SHOULD HAVE DONE: when I moved pihSweep to a new call site, checked
which of the things it calls resolve their store from the open tab. Two of them
do, and the one I hit is the one Krathe reported twice.

Verified: 170 files parse, no new globals, CRLF intact. NOT verified in game.
(cherry picked from commit d2c1463593e04dbebe990f590287ceffc893f633)
I read Krathe's SavedVariables instead of guessing again. The groups were
never in the store any of my three attempts searched.

    profiles/Healer New/auraDesignerPresets/Base/
      layoutGroups/HolyPriest        -> EIGHT "PI Helper - Cooldown icons"
      auras/HolyPriest/@Custom:cf9   -> one marked icon indicator
      otherAuras/@Custom:cf12        -> one marked icon indicator  <- the only
                                        one in the store every finder looks in

WHY THEY ARE THERE. The old icon tick called P.CreateLayoutGroup, which is
pool-routed off S.activeBuffTab, and the panel it lived on was mounted in
S.BuildEffectsHeadArea -- drawn on EVERY pool's Effects tab, not only Any Buff,
whatever the comment beside it claimed. Ticking it on My Buffs created a group
in the SPEC store; the finder then could not see it, reported icons as off, and
the next tick made another one. Eight times.

NOTHING COULD REACH THEM. Not the helper (wrong store), not the Layout Groups
tab (VisibleLayoutGroups hides marked groups from every pool but the helper's),
not the sweep. A record no control can see is a record no control can turn off.

  · pihPurgeStrayMarks walks EVERY group store (otherLayoutGroups + each
    layoutGroups[spec], legacy flat array handled) and every aura pool, removing
    all marked groups and any marked effect outside adDB.otherAuras. A spec-pool
    helper effect cannot work -- poolFilter answers HELPFUL|PLAYER before it
    consults othersOnly, so it asks for other people's cooldowns cast by me --
    and no list shows it, so it renders nothing, lists nowhere, deletes never.
  · pihIconGroup / pihAnyIconGroup / RemoveOtherLayoutGroupByID are gone. All
    three were one-store-by-id, which is the shape that failed three times.
  · Schema 6, because schema 5 stamped itself and deleted nothing -- the stamp
    says "swept" on profiles that were not.
  · The sweep now gates on the version it is coming FROM. Step 4 migrates every
    marked Icon to a Square, and Icon is a supported surface again in schema 5;
    ungated, the bump to 6 would have silently converted an icon the user had
    just added. That would have hit Krathe first.
  · The count goes in the debug log. A migration that deletes stored records
    has to say what it deleted.

CORRECTION: my last two commit messages claimed "CRLF intact". The check I used
(grep -c $'\r$') matches every line in this shell and proves nothing. Cards.lua,
Rows.lua and PIHelperPage.lua were pure LF in the working tree -- git said so in
three warnings I read as noise. Committed blobs were unaffected (autocrlf
normalises to LF either way), so the repository never moved; the working tree is
back to CRLF and the check is now a byte count.

Verified: 170 files parse, no new globals, byte-counted CRLF, blobs unchanged.
NOT verified in game.

(cherry picked from commit 73e6f3507ee8652a21389bccfaf61b8e546d6c7d)
… name the effect

Four things, three of them one bug: the island's REUSE path changes nothing but
the content.

1. THE STRIPS WERE STALE. The helper's nav row asks for its pool and reopens the
   designer's page. Its Invalidate drops the HARNESS cache, not the island's, so
   the build takes the reuse path -- which redraws the tab content and leaves
   both strips describing the pool the panel was BUILT for. Hence the helper's
   settings under a strip reading Effects / Layout Groups / Global, with Any Buff
   still lit. AuraDesigner_RefreshPage now re-syncs the pool tabs (P.SyncPoolTabs,
   lifted out of SetMainTab) and the sub-tab strip before SwitchTab -- that order,
   because ApplySubTabStrip decides which buttons exist and SwitchTab decides
   which is active.

2. ...AND IT NEVER CONSUMED THE REQUEST. S.pendingBuffTab was read only by the
   full build, so the nav row -- which almost always lands on the reuse path --
   left it standing for the next unrelated full build to pick up and pin the
   designer to the helper's pool for a visit nobody asked it of.

3. THE RAIL SAT ON THE WRONG ROW. The page is the designer's and the row clicked
   is the helper's; SelectTab lights the row that owns the page, which is true
   about the page and wrong about the click. GUI.SetNavHighlight makes the same
   three writes SelectTab's own tail makes, as a verb -- navMarker and C_TEXT are
   panel locals and a caller reaching for them would be reaching into that file's
   private state. Nothing about page state changes, and the next click on any row
   re-runs SelectTab's clear, so it cannot get stuck.

4. THE ROWS NAMED THE TRIGGER, NOT THE EFFECT. "PI Helper - Big cooldown" is what
   FIRES every helper effect -- the same words on every row, in the space where
   the row's own identity belongs, and the whole subject of the Triggers tab
   anyway. It is "PI Helper" now; the TYPE is already on the row as its coloured
   badge, which is how the designer tells two effects on one spell apart, so the
   text does not repeat it. The retired second signal keeps its own name -- the
   badge cannot say it is a different thing, and nothing creates one any more.

Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM.
NOT verified in game.

(cherry picked from commit 27ebd620765c9e1e612326a14038427d0ad509c0)
Krathe: "we don't need the others only setting or even note that on the Active
indicators either, we only care about using it on others anyway, it's a
pointless option."

He is right, and the recipe already agreed with him: pihCreateSignal STAMPS
othersOnly on every effect it builds, because a helper watching your OWN
cooldowns would be telling you to infuse yourself. So the tick was a control
over a value the user does not get to pick, and the row suffix reported a
constant as though it were a state -- the same constant-on-every-line fault the
"Big cooldown" label had, in the same summary, one field along.

P.ShowsOthersOnly = IsOtherTab() and not IsPIHelperTab(), used at the four sites
that draw either half (the tick and the suffix, in each layout). One predicate
rather than four copies of the same test, because the question is one question:
does the user decide the caster rule on this pool? On Any Buff yes -- that pool
means "any caster including you" and narrowing it is the whole point. On the
helper, no.

⚠ THE STORED VALUE IS UNTOUCHED. othersOnly is what makes the effect correct and
the engine still reads it; what goes is the control that pretends it is up for
discussion. The infused signal's deliberate inversion (it IS your own cast) is
unaffected for the same reason.

Layout-group Others Only is left alone: it is gated on IsOtherTab() too, but the
helper's pool has no Layout Groups tab to reach it from.

Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM.
NOT verified in game.

(cherry picked from commit bfb090a944d6109deeb9d9c65988006787ee77fa)
Krathe: "on the example for icon it has a ? instead of the PI icon (on the GUI,
works fine to actually show PI when their CD was active)."

The live half being right is what narrowed it: staticSpellID reaches the button
and bindNative leaves the art alone, so frames were already correct. The tile is
a different painter.

THE QUESTION MARK IS THE DESIGNER'S HONEST PLACEHOLDER. Its add flow asks for a
TYPE first and a SPELL second, so at tile-paint time there genuinely is no
artwork to show -- the picture is swapped in later through pv.spellIcon once
section 1 is answered.

☠ ON THE HELPER'S POOL THERE IS NO SECOND STEP. The cooldown list IS the spell,
so nothing ever came back to swap it, and the one tile whose entire subject is a
fixed picture was the one showing a question mark.

PaintEffectOnThumb takes an optional staticSpellID and paints that spell's
texture; the helper's Icon tile passes the same id pihCreateSignal pins, so the
tile and the effect it creates cannot disagree. Every other caller passes
nothing and is unchanged.

⚠ AND A PINNED TILE DOES NOT PUBLISH pv.spellIcon AT ALL. That field is the
handle the add pane swaps through; leaving it set would let the pane repaint
Power Infusion with whatever spell was picked -- the one thing pinned art exists
to prevent. The pane already guards on the field being present, so it simply
skips.

Verified: 170 files parse, one new global (C_Spell, guarded at both uses),
byte-counted CRLF, no BOM. NOT verified in game.

(cherry picked from commit b2b5750282538da95bacc3f1d1a4fd554ac6e305)
Audited every PIH symbol (definition vs uses, comments stripped) and every
locale key (orphaned now vs referenced before this rework started at e3b99f29).

DEAD, AND DELETED -- the retired surface dropdown and what only it used:
  P.PIH_SurfaceOf / P.PIH_SurfaceOptions / P.PIH_SetSurface, PIH_SURFACE_ORDER,
  pihCapture, pihSurfaceTakenBy, P.PIH_RemoveSurface, pihDeleteSignal, and the
  P.PIH_FoundAll export (the local is live; only the export was unread).
  They all answered one question -- "which SINGLE surface is this signal on" --
  which is why picking an occupied row had to SWAP two signals. A signal holds
  several surfaces now, added and removed one at a time through the designer's
  own tiles and cards, so the question has no answer to give.
  ⚠ pihPlace SURVIVES: pihSweep step 4 still migrates an old Icon to a Square
  through it, and it is the only reader left.

SEVEN ORPHANED LOCALE KEYS, deleted with the comments that described them: the
helper's private "ADD AN EFFECT" heading, the two route cards and their
descriptions, its empty-state line, "Big cooldown", and the dropdown's swap
label. Orphan count for this rework is now zero. (100 pre-existing orphans are
not this rework's doing and were left alone.)

NOT DEAD -- A REAL BUG THE AUDIT FOUND:
  Deleting a helper effect through the designer's ✕ never re-derived the engine.
  pihRefresh's own note records why that matters -- "Danders' review found the
  resident half left armed, events registered, sound armed, for a helper with
  nothing in it" -- and every helper mutation used to end at that chokepoint. It
  stopped being a chokepoint when the Effects tab became the designer's: its ✕
  removes the record, runs the AD refresh path, and knows nothing about the
  helper. So deleting your last PI effect left the watcher and the sound
  registered for a feature with nothing in it. Exactly the state that review
  caught, reachable again through a different door.
  ⇒ P.PIH_ReDerive, called from both layouts' delete after asking whether the
  doomed effect carried the mark (asked BEFORE the removal -- afterwards there is
  no config left to ask).

FLAGGED, NOT TOUCHED -- for Krathe to decide:
  P.PIH_ClashOn / PIH_SiblingContends / PIH_SelfContends and their three helpers
  have no callers. The warning they feed was rendered on the old per-signal rows.
  The HAZARD is still real -- border and both texts take a single winner, so a
  helper border and one of the user's own means one silently does not draw -- so
  deleting a safety warning is not a cleanup. Kept, and marked as dead-but-live-
  looking so it cannot rot unnoticed.

Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM, zero
locale orphans from this rework. NOT verified in game.

(cherry picked from commit 0e9f7e695a1df294df69f57d09c57e2ca5051667)
Krathe's call, and his border observation was right: "with border they can just
offset and be able to show two borders like you can with AD anyway?" -- exactly,
and the warning string has always named that escape hatch. Ticking "Give this
aura its own border" opts an effect OUT of the contest (collectStackedBorders
draws it alongside, sorted by priority), so both rings show. Checked, not
assumed: that checkbox and the Priority slider the text version names are both
still live controls on the effect card.

THE HAZARD, for the record: border, name text and health text resolve through
pickWinner, which takes ONE winner per surface from config alone and tears every
other candidate down. A helper border under one of the user's own borders simply
does not draw, with nothing on screen saying which lost or why. Health bar and
background are MULTI and cannot clash -- that is what PIH_CONTENDED encodes.

P.PIH_ClashText composes the message; the badge carries it. AttachWarningBadge
gained opts.text, because until now every caller had a config warning KEY and
this text is composed at render time -- it names the offending effect and how
many others contend. A key still wins when both are present (a fact about the
SPELL outranks a fact about this configuration), though they cannot collide in
practice: a helper record is filter-owned and has no spec entry.

☠ AND IT ASKS ABOUT THE ROW IT IS ON. pihSurfaceTakenBy, pihSiblingContends and
P.PIH_SelfContends all went: every one asked through pihFound(), which returns
ONE hit per signal -- correct when a signal had a single surface, wrong the day
it could hold several. "Does our border contend?" was answered about whichever
surface hash order landed on, which might have been the square. The card hands
over its own config instead. The sibling term went with them: only one signal is
creatable now and one record holds one effect per surface, so "another of OUR
signals contends here" is unreachable rather than merely unlikely.

⚠ It vanishes when the remedy is applied -- pihContends runs the real candidacy
test on our own cfg first -- because a warning that survives its own fix teaches
people to ignore warnings.

⚠ Both strings still say "signal", which is vocabulary the helper's UI no longer
uses anywhere. Left as-is rather than minting new keys mid-sweep; flagged.

Audit after this: zero unused PIH symbols, zero locale keys orphaned by the
rework (the pre-existing orphan count fell 100 -> 96, these strings coming back).
170 files parse, no new globals, byte-counted CRLF, no BOM. NOT verified in game.

(cherry picked from commit b19d5a6bc94018457f2b1359e4ad309181245b73)
Krathe: "Triggers should be Role for the DPS/Tanks etc section."

The box was named after the TAB it sits on, so the Triggers tab opened with a
box captioned Triggers. A heading that repeats its own parent says nothing --
and the one thing it could have said, that this box is the ROLE filter, was the
thing it left out.

⚠ THE COOLDOWN GATE STAYS INSIDE IT, and that was already argued rather than
decided now: pihAddGateAndNotes' own note says a whole titled group around a
single checkbox is more chrome than the setting is worth, and that "Show even if
your Power Infusion is on cooldown" says what it does without a header to lean
on. It names this exact case as the test for a control living under a heading
that does not quite describe it, so the heading changing does not disturb it.

L["Triggers"] stays -- it is the sub-tab's own label -- so nothing is orphaned.

Verified: 170 files parse, no new globals, zero locale orphans, byte-counted
CRLF, no BOM. NOT verified in game.

(cherry picked from commit ede2f3e16f0ce0300ac74aecdd7a9643c66af561)
…that are not ours

Krathe: "when I disable the PI tracker, it seems to remove my border effect I
added." It did, and there were two separate ways for that to happen.

1. THE STASH HELD ONE SURFACE PER SIGNAL, AND A SIGNAL CAN HOLD SEVERAL.
   retainedCfg[key] was a single {surface, cfg} pair, written when a signal WAS
   one effect; the multi-surface work never revisited it, and PIH_Remove walked
   pihFound(), which answers one hit per signal. So a border + an icon + a square
   went in, ONE was stashed, all three were deleted, and re-enabling rebuilt the
   one. The other two were gone with nothing anywhere that remembered them.
   ⇒ The stash is a list in menu order (pihStashHits), PIH_Create names each
   surface explicitly rather than leaving pihCreateSignal's fallback to pick one,
   and pihKeptCfg/pihKeptSurfaces read BOTH shapes -- so nobody's retained
   customisation is lost by the fix for losing retained customisations.

2. THE DELETE TOOK THE WHOLE RECORD. It nil'd the pool key outright, justified
   by "a record here is identified BY a helper spell list, so nothing of the
   user's own can be sitting on it". That is false: the designer's From a Filter
   flow will hang an effect off the Power Infusion Helper filter, and it lands on
   this exact record. Turning the helper off took it with no warning and no
   stash.
   ⇒ Marked effects are removed one at a time. The helper-owned `sound` key --
   the actual reason the wholesale delete existed, since the generic effects list
   offers no delete button for sound on a filter-owned record -- is cleared
   explicitly, and the record goes only once it holds nothing at all.

The enable tick has always promised it "behaves like an enable even though
records really are created and deleted underneath". It only does if the round
trip is lossless; it wasn't.

⚠ Also corrected the result string: it said "removed %d signal(s)" while
counting one per signal. It counts effects now, because that is what it removes.

Verified: 170 files parse, no new globals, zero unused PIH symbols, byte-counted
CRLF, no BOM. NOT verified in game.

(cherry picked from commit 24fde995aa0c362fd50c77547b229e5aad0d255b)
Krathe: "it should function like the rest of AD". The rest of AD writes ONE
boolean -- modeDB.auraDesignerEnabled -- and deletes nothing. The helper had no
enabled flag at all: its existence WAS its records, so "off" had to mean "delete
them", with a stash to fake reversibility. Every bug in this area had that one
root -- a switch pretending to be a switch while actually being a delete.

  · adDB.pihelper.enabled is the switch. The records are never touched.
  · Engine: a `pihEnabled` file-local, distinct from pihGateEnabled because they
    do not collapse -- off means forced DARK, gate-off means forced OPEN, and
    both on means the watcher drives. The gate's machinery already meant "dark
    and nothing will open it", so this costs one local and three guards: the
    watcher, the PLAYER_ENTERING_WORLD re-check, and the readiness ticker (which
    exists to REOPEN the gate and would otherwise have undone the switch at 2Hz).
  · PIH_SetGateEnabled now early-outs while the feature is off -- both its
    branches mean SHOW, and ticking the cooldown option while disabled lit the
    helper up.

BACK-COMPAT: `enabled` is absent on every existing profile. The engine reads
`s.enabled ~= false`, so absent is ON -- a profile with helper records had a
working helper and must come back working. The panel's accessor backfills once
from PIH_Exists(), so a profile with NO records comes back off and the tick reads
honestly. ⚠ `enabled` is deliberately NOT in PIH_Settings' default table: seeding
it there would make the backfill unreachable, which is the defaults-before-
migration trap this addon has hit three times.

DELETED, because nothing serves the delete model any more: P.PIH_Create,
P.PIH_Remove, pihStashHits, pihKeptCfg, pihKeptSurfaces, pihRestoreInto,
PIH_RECIPE_OWNED, pihDeepCopy. Net -100 lines.

⚠ TWO CONSEQUENCES, SAID OUT LOUD SO NEITHER IS REDISCOVERED AS A BUG:
  · The three "Power Infusion Helper" custom filters are no longer auto-deleted.
    PIH_Remove used to drop them once no mark remained anywhere. Nothing removes
    them now -- correct, because the records referencing them are no longer
    removed either. They are ordinary custom filters, visible and deletable in
    the Filter Designer. A lingering list is cruft; a silently deleted one that
    an effect still points at is a broken effect.
  · adDB.pihelper.retainedCfg survives in old profiles, inert. Not swept: a
    migration that deletes data to tidy up is a worse trade than the bytes.

⚠ First enable on a profile with nothing still seeds the default border, and that
is the one place this differs from AD's switch. AD is enabled and then you add
indicators; the helper is a recipe, and a first-ever enable landing on an empty
Effects tab would be a switch with nothing on the other side of it. Only when the
pool holds nothing of ours -- a re-enable finds its records where it left them.

Verified: 170 files parse, no new globals (the _ENV diff caught pihRestoreInto
and pihKeptSurfaces going nil mid-edit), zero unused PIH symbols, zero locale
orphans, byte-counted CRLF, no BOM. NOT verified in game.

(cherry picked from commit 34b6452b8476a35880e9b2fe1f182578a5ac906b)
Krathe: "the PI 'icon' has stacks but PI does not have stacks, you only get 1
charge so that setting should at least be off by default if not hidden."

It was worse than off-by-default: showStacks DEFAULTS TRUE for icons and squares
(AuraDesigner/UI/Options.lua), so every helper marker ever created has been
drawing one. And on the icon it is a number read off whichever cooldown matched,
printed on artwork pinned to Power Infusion -- a count describing a spell the
picture is not of.

  · The whole Stack Count GROUP is skipped, not just the tick: Font, Scale,
    Outline, Anchor, Offset X/Y and Colour are seven more controls for a number
    that is never drawn, and leaving them while unticking one is how a panel
    fills up with settings that do nothing.
  · HIDDEN, not greyed -- the opposite call from GateSWM two hundred lines up,
    deliberately. Show When Missing is greyed WITH A REASON because ticking it
    would silently break the cooldown gate and the user needs to know why. Stacks
    are not broken, they are irrelevant, and a greyed control invites a "why?"
    that has no interesting answer.
  · The stored value is forced false as well -- stamped at creation, and schema 7
    backfills the markers already out there -- so the record says what the frame
    draws rather than the panel hiding a true value.

⚠ Step 7 walks EVERY aura pool, like step 5, because an old marker can be sitting
in a spec pool; pihPurgeStrayMarks documents how they got there. Placed only: a
frame-level effect has no stacks to begin with. Set rather than cleared, because
an absent field means "inherit" and the inherited default is the true one.

What survives on a helper effect and why: Show Duration and Hide Cooldown Swipe
both describe the TRIGGERING cooldown's remaining time, which is how long the
burst window lasts -- genuinely useful. Show When Missing was already greyed with
its reason.

Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM.
NOT verified in game.

(cherry picked from commit 1df60d591dd028f48047e8ed7283a6df44d6b9e2)
Krathe: "let them pick what kind of icon it is and what it includes."

Two ticks, first group on the icon card, helper effects only:

  Show the triggering cooldown's icon   -- off = Power Infusion (default)
  Ignore trinkets, potions and racials  -- off = anything that triggered it

WHY THE SECOND ONE EXISTS. A placed icon renders ONE slot. With a class cooldown
and a trinket proc and a racial running together, which one the engine hands us
is not ours to choose and can change between parses -- and the "Also count" ticks
make that COMMON rather than rare, because racials and trinkets are things people
press ALONGSIDE a cooldown. That is what made them "amplifiers" in the first
place. So the ambiguity is worst exactly where those ticks are on, and this is
the control that resolves it.

☠ IT RIDES mutedSpellIDs -- THE SAME STORE THE TRACKED IDS TICKS WRITE, not a
parallel one. That is why it needs no new render concept: the resolver already
narrows a placement by its mutes (narrowByPlacementMutes), already treats
"everything muted" as "matches nothing", and the per-ID ticks are the same
setting at maximum resolution. A coarse control and a fine control over one
store, and unticking clears only OUR ids so a hand-set mute survives.

☠ THE TICK IS DERIVED, NEVER STORED. It reads "are ALL the currently-ticked
amplifier ids muted on this record", so ticking Racials ON in Triggers later
makes it read false BY ITSELF -- the new ids are not muted and the icon really
can show them. A stored boolean would sit there claiming "ignored" while racials
appeared. Same doctrine as the class ticks.

☠ THE PICTURE IS staticSpellID's PRESENCE, with no second field recording the
choice: the field that does the thing is the field the control reads. Structural,
so it rebuilds rather than restyles -- bindNative's SetIcon bind is once per slot
and placedStructSig already carries the flag.

⚠ THE ICON ONLY, of all the helper's surfaces. A border or a square looks the
same whichever trigger fired, so "which one is this" has no answer to give there.
Both verbs take a record rather than assuming one, so it generalises if asked.

⚠ The add tile keeps the Power Infusion artwork -- it is the default, and a tile
that changed picture per instance would be describing one effect rather than the
type. Only one icon can exist per helper, so the list stays unambiguous.

Verified: 170 files parse, no new globals, zero unused PIH symbols, zero locale
orphans, byte-counted CRLF, no BOM. NOT verified in game.

(cherry picked from commit dd7deee4929d5393bd1729016a41c8c037e090e0)
…part

Krathe: "it's a pre created list by us that should toggle on off and be able to
reset to default if someone ticks something off."

THE TWO KINDS OF CUSTOM FILTER ARE NOT THE SAME THING, and the ✕ was treating
them as if they were. A list the user built: membership IS the truth, so removing
a spell is exactly the right verb -- they put it there. A list WE seeded from a
curated set: removing a spell was destructive, unrecoverable, and there was no
way back to the default. The Power Infusion Helper's cooldown list is the second
kind and had the first kind's controls.

⭐ SO A CURATED FILTER NOW GETS WHAT A PRESET HAS, deliberately in the same shape
so the UI can offer the same controls:

    preset            curated custom          hand-built custom
    IsSpellEnabled    IsCustomSpellEnabled    (none -- keeps the ✕)
    SetSpellEnabled   SetCustomSpellEnabled
    ResetPreset       ResetCuratedFilter

  · The state lives ON THE FILTER (f.disabled), not in the preset overrides
    table: it is per-filter data with the filter's own lifetime, so it travels
    with a profile export and dies with a DeleteCustomFilter.
  · ABSENT MEANS ENABLED, so every existing custom filter resolves byte-for-byte
    as before and only one actually ticked off carries anything.
  · f.dfDefaults is the mark AND the answer -- its presence says "we seeded
    this", its contents say what default means. One field, so they cannot
    disagree. pihEnsureFilter stamps it on every call, not only on create, so a
    profile made before the mark existed picks it up.
  · ResolveSelection honours the ticks at the ONE chokepoint every custom-filter
    expansion already goes through, mirroring the preset arm three lines up.
  · The existing Reset button serves both. Shown only when there is something to
    undo, which for a curated list means `disabled` is non-empty -- the same
    question IsPresetModified asks.

⚠ RESET RESTORES, IT DOES NOT PRUNE. Everything we seeded comes back and every
tick comes back on; a spell the USER added to our list stays. "Reset to default"
is reached for to undo what you turned off -- one that also threw away your own
additions would be the destructive act this exists to replace.

Verified: 170 files parse, one new global (`next`, stdlib), byte-counted CRLF,
no BOM. NOT verified in game.

(cherry picked from commit 6055292d72c02b61e008e67158bf16d559fba05a)
The mark shipped and reached nobody. dfDefaults is what gives our seeded list the
on/off tick and the Reset button, and it is written by pihEnsureFilter -- which
only RUNS when the helper creates or repairs a signal. A helper that already
exists and is not being edited never calls it, so Krathe's list still had the
destructive ✕ and no Reset, one round after I said the stamp would reach him.

WHAT I GOT WRONG: I wrote "pihEnsureFilter stamps it on every call, not only on
create, so a profile made before the mark existed picks it up". True and useless
-- the question is not what the function does when called, it is who calls it. A
mark written by a CREATE path reaches nobody who already has the thing.

  · pihSweep step 8 stamps both seeded lists from the same seed functions
    pihEnsureFilter would have used. Not from the list's CURRENT contents:
    default means what the recipe seeds, so anything the user added since is
    theirs and stays out of what a reset restores.
  · ...and the sweep now also runs on the FILTER DESIGNER's page build. It ran
    only on the Aura Designer's, so opening the Filter Designer first -- exactly
    what someone inspecting that list does -- showed the unmarked version.
    Schema-stamped and priest-gated, so it is one comparison after the first run.

Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM.
NOT verified in game.

(cherry picked from commit 970cd22311316a4fa7dc60b36b31906a98bdf674)
Krathe: "the number does not change as I tick them on/off."

THREE PLACES COUNTED THE SAME THING, THREE WAYS, AND NONE OF THEM ASKED WHETHER A
SPELL WAS ON. The Filter Designer's left list had a local CustomSpellCount, its
right-hand header called that same local, and R:ListFilters -- which the Buff
Bar's picker reads -- walked the tables itself and set `enabled = n, total = n`
unconditionally. All three were written when membership was the only state a
custom filter had, and none was revisited when the ticks arrived. A count that
ignores the control next to it is worse than no count.

  · R:CustomFilterCounts(cfId) is the one counter now, and it honours the ticks.
    A hand-built filter answers enabled == total -- IsCustomSpellEnabled is true
    with no disabled set -- so no caller branches on which kind of list it is.
  · The left row shows "34/39" once something is off and the plain total
    otherwise, plus the modified dot a preset row carries
    (R:IsCuratedFilterModified, the same question IsPresetModified asks).
  · The header does the same: "%d spells" until something is off, then
    "%d of %d spells".
  · The local CustomSpellCount is gone rather than left beside its replacement.

⚠ An ordinary custom filter's row and header are unchanged in every case, because
enabled and total are equal for a list that cannot have anything switched off.

Verified: 170 files parse, no new globals, byte-counted CRLF, no BOM.
NOT verified in game.

(cherry picked from commit 31f6acb8d656ee67f7c92f70d95a3b765f26e747)
Krathe: "you should select them and it should show the number active next to them
in a similar way to how you pick on the buff bar", and before that the question
that exposed it -- "I assume Cooldowns is our custom filter for PI helper and the
other 3 are normal filters?"

A reasonable assumption, and wrong, which is the panel's fault. "Cooldowns" was a
HEADING above a link, with no tick to say it was on. The other three were
captioned "Also count" -- counted "also" to something the panel never named. All
four were one custom list underneath and nothing said so.

Now: Trigger Filters, four rows, four ticks, the number in each.

    ☑ Cooldowns   39/45      our curated list
    ☐ Trinkets       41      trinketsItems
    ☐ Potions         6      consumables
    ☐ Racials         4      the four offensive ones, not the category's thirteen

  · P.PIH_CooldownsOn / SetCooldownsOn give the base source the tick it never had.
    Read off the LIST, like the class ticks -- a stored boolean beside it is a
    second copy that drifts the moment anyone edits the filter by hand. Surgical
    like pihApplyClass: the seeded ids only, so a spell the user added to our list
    survives being switched off and back on.
  · Cooldowns' count is enabled/total from R:CustomFilterCounts, so ticking a
    spell off in the Filter Designer moves the number here. ⚠ That is a DIFFERENT
    question from the row's own tick: a spell ticked off is still IN the list, so
    the source stays included. The row shows both, which is why it needs both.
  · Racials says 4 because it IS 4. The `racials` category is every racial
    ability and nine of its thirteen -- Shadowmeld, Darkflight, Stoneform -- are
    the opposite of worth infusing behind, which is why the curated set exists.
  · The Filter Designer link moved down with them, so the whole filter block reads
    as one thing instead of a heading at the top and three ticks at the bottom.

⚠ STILL ONE CANDIDATE LIST UNDERNEATH, deliberately. A helper effect resolves from
ONE filter ref -- its record key -- and a placed indicator has no union at all, so
giving it one means changing the Factory's resolve path for every indicator in the
addon. The ticks go on meaning "put these spells in the list", which is what they
always did; what changed is that the panel says so, names each source and counts
it. If per-effect sources are ever wanted, that is the Factory change, not this.

☠ The _ENV globals diff earned its keep twice here: the moved block closed over
pihAddClasses' `parent` local, and a multi-line splice left 65 LF lines in a CRLF
file. Both invisible to luac -p.

Verified: 170 files parse, no new globals, zero unused PIH symbols, zero locale
orphans, byte-counted CRLF, no BOM. NOT verified in game.

(cherry picked from commit e56e040793f486a28824a28be12531da1d719822)
…nd the link says what it reaches

Three from one screenshot.

1. MULTIPLE ICONS -- the cooldown-icon group, back on purpose.
   ☠ A version of this shipped, stuck on Krathe's frames and took three attempts
   to delete. Every one of those failures was PLUMBING, not the idea:
     · created through the pool-routed CreateLayoutGroup from whatever tab was
       open, so it landed in the SPEC store where no finder looked
     · switched on by a tick captioned "Icons" buried under Classes and Cooldowns
     · the helper had no Layout Groups tab, so nothing could see or configure it
   ⇒ What is different, point by point, because "we fixed it" is not an argument:
     · created DIRECTLY into adDB.otherLayoutGroups -- never through the creator
       whose store depends on the open tab
     · added by a TILE on the helper's own add grid, beside the surfaces
     · the helper's Layout Groups sub-tab is back, and VisibleLayoutGroups
       already shows exactly the marked groups there, so it can be moved, sized
       and deleted like any other group
     · pihPurgeStrayMarks is unchanged and still finds a marked group in ANY
       store, so the recovery path that cleaned up the old one still exists
   It earns its place because a placed icon is ONE slot showing one arbitrary
   match; this shows every cooldown they actually have up, one icon each.

2. THE ICON PICKS ITS PICTURE WITH PICTURES. Krathe: "when you add an icon it
   should then have a graphic like we do for the other types to then pick the type
   i.e an actual icon or a PI icon?" Right -- those two are as different from each
   other as an icon is from a square, and every other choice on that grid is made
   by looking at a thumbnail. A SECOND STEP rather than two icon tiles, because a
   signal holds one effect per surface: two icon tiles would both be "icon" and
   the second could never be added. The tick on the card stays -- it is how you
   change your mind without deleting and re-adding.

3. THE FILTER DESIGNER LINK REACHES ALL FOUR LISTS, so it no longer sits under a
   "Cooldowns" heading naming one of them, and its note no longer says "single
   cooldowns". Krathe: "the cooldowns header/footer of the Filter Designer link is
   wrong, as really they can modify all 4 filters in it." The button belongs to
   the tick list it follows -- the escape hatch for what those four cannot do --
   so it needs no caption between them at all.

Verified: 170 files parse, no new globals, zero unused PIH symbols, zero locale
orphans, byte-counted CRLF, no BOM. NOT verified in game.

(cherry picked from commit cbf38d9c209b1f046dba870ff1d6543ac386433f)
Krathe, reading the layout: "the classes, they only effect the Cooldowns
correct? Maybe we do separate that out."

Correct, and checked rather than assumed: pihApplyClass only ever adds or removes
entries from pihSeedRecords() -- the class-cooldown set -- inside the cooldown
list. No class tick can reach the other three sources: trinkets and potions are
items with no class at all, and racials are tagged class = "ALL". So thirteen
class ticks sitting above a list of four sources said the opposite of the truth.

    Classes and Cooldowns   45          <- the count moves to the header
      ☑ Death Knight … (13)
      Untick a class to stop watching its cooldowns.

    Additional Filters
      ☐ Trinkets   41
      ☐ Potions     6
      ☐ Racials     4
      [ Filter Designer ]

☠ AND THE COOLDOWNS TICK IS GONE, because it was redundant AND harmful.
Redundant: unticking all thirteen classes IS turning class cooldowns off -- the
classes are that source's control. Harmful: both it and the class ticks READ OFF
THE LIST (no stored booleans, deliberately, so nothing can drift), so unticking
the source removed all forty cooldowns, every class tick then read off, and
ticking it back re-added all forty -- silently undoing whichever classes the user
had turned off. A switch that quietly reverts your other choices is worse than no
switch, and the alternative -- remembering the class states -- is exactly the
stored copy of a derived truth the class ticks exist to avoid.

⚠ THE HEADER COUNTS THE SEED SET, NOT THE LIST. The list also holds trinkets,
potions and racials once those are ticked, and counting them under a "Classes and
Cooldowns" header would be a number describing something else. Enabled too: a
spell ticked off in the Filter Designer is in the list and not firing.

⚠ One link, still, at the foot of the second box, and it reaches all four lists.

Verified: 170 files parse, no new globals, zero unused PIH symbols, zero locale
orphans, byte-counted CRLF, no BOM. NOT verified in game.

(cherry picked from commit 55d3719e5308a930689365f0049cb10f992a36be)
…s what it watches

Three reports from Krathe, 2026-09-10.

RACIALS SHOWED 4 AND HAD NO PENCIL. Trinkets and Potions point at preset
categories and so have a list to open; Racials pointed at four spell IDs
written out in Cards.lua, because the `racials` category is EVERY racial and
nine of its thirteen (Shadowmeld, Stoneform, Darkflight) are the opposite of
worth infusing behind. Those four are now the SEED of a curated list of ours,
so the row gets the pencil, a count that moves as you tick, and Reset to
Default -- the same treatment the cooldown list already has, and a racial we
missed can now be added. It is created wherever the cooldown list is, never
from a tick, and pihAmplifierIDs falls back to the seed until it exists, so the
trigger is right before and after.

Two things that came with it:
  · Trinkets and Potions counted the CATEGORY's size, which does not move when
    a spell is ticked off in the Filter Designer -- while pihAmplifierIDs, which
    decides what fires, honours those ticks. Same fault as the cooldown count,
    one row over. Both now read enabled/total, collapsing to one number when
    nothing is off.
  · An amplifier tick can no longer remove a CLASS COOLDOWN from the cooldown
    list. The removal universe is "every id an amplifier source could
    contribute", and now that Racials is a list the user can put anything in it
    -- including a class cooldown, which unticking Racials would then delete
    from Triggers with no control admitting to it.

"COOLDOWN ICONS ALLOWS FOR TRINKETS + THE OTHER FILTERS? DON'T SEE THE OPTION."
It does, automatically: pihSyncTriggerExtras writes the amplifier ids INTO the
cooldown list, which is the list the group links. There is no option because
there is nothing to choose -- but nothing on screen said so, and dropping the
Linked Filters block from its card left it silent about its own contents. Its
header now carries "N spells, from your Triggers", off the list's own enabled
count, so it moves when a tick over there does.

THE ICON TILE ASKS WHICH ICON. Cooldown Icons stood on the main grid beside
Border and Square, which put a CONTAINER among a row of EFFECTS -- and that
mismatch is the whole of what kept going wrong with it. Krathe: "for icon it
would be best to have a sub menu so you click Icon then it has PI Icon,
Cooldown Icons, and the other icon choices?" Three answers on two axes -- how
many (one marker, or one per cooldown up) and what picture (always Power
Infusion, or the buff they used); the fourth cell is four identical PI icons in
a row. So: three tiles behind Icon.
  · Cooldown Icons gets a thumbnail of THREE icons, drawn at the group's own
    geometry. "How many" is the axis prose keeps failing at, and the tile used
    to paint a single icon.
  · Taken answers are GREYED with the reason, not removed. The tile vanishing
    the moment you clicked it was half of why the group felt lost.
  · The Icon tile itself survives its own surface being taken, since Cooldown
    Icons is still behind it.

AND A TRAP CLOSED BEFORE BUMPING THE SCHEMA. pihPurgeStrayMarks deletes every
pihSignal-marked group in every store -- correct when a marked group could only
be the retired one, and no longer true: the Cooldown Icons group lives in
otherLayoutGroups on purpose. Any schema bump, for any reason, would have
silently deleted it on the next Aura Designer build; it has not bitten only
because the group and schema 8 shipped together. It now exempts the one group
P.PIH_IconGroup resolves to, exactly as the effect half has always exempted
otherAuras.

(cherry picked from commit 9fd6ed6750003783b9298882e8739d8559cc5f06)
Krathe: "no back use X like we do on the other AD effects."

The designer's own picker is a few hundred lines above this one
(S.BuildEffectsHeadArea's S.effectsPicker arm): a head frame with the question
on the left and GUI:CreateCloseButton on the right, captioned there as "the only
way out that does not commit to anything". The icon step is the same question in
the same place, so it takes the same control -- a Back button under the grid was
a second vocabulary for leaving, invented for one grid.

The heading is now two shapes rather than one fontstring doing both jobs: step 1
keeps the small-caps dim SECTION CAPTION (nothing to leave), step 2 gets the
picker's own head in the picker's font and colour, with the X.

L["Back"] goes with it. It was introduced for a tooltip in the designer's add
pane, that call site is long gone, and this was its only remaining user --
checked against the dynamic-lookup sites too, since a key can be reached as
L[x.name]. The equipment-slot table has a "Back" entry but renders slotName
raw, never through L.

(cherry picked from commit 69a83e1cece09b95daea8713527bcd511819d473)
…ng them

Krathe: "despite the fact those additional filters link to our actual filters,
ticking them on actually just adds those to the PI helper filter, so they are
now twice on? this is very confusing."

He is right, and the reason it could not be fixed with wording: each row has a
PENCIL that opens the real list, which promises a reference, and the tick made a
copy -- 51 spell IDs lifted out of Trinkets, Potions and Racials into ours, so
his list read 91 where it should read 40 and the same ids lived in two places.

A custom filter may now name other filters (`f.includes`), folded into the
selection by R:ResolveSelection. Our list stays the 40 class cooldowns -- which
is what "Edit Cooldowns" has always claimed to open -- and names the other three.

IN THE REGISTRY, NOT IN THE AURA DESIGNER, and that is the load-bearing choice.
Four paths resolve the helper's list: DF:ResolveADFilterRef for the placed
effects, the layout-group path for the Cooldown Icons group, Engine.lua's
pihResolvedMap for the sound registrations, and the Buff Bar's own picker if a
user selects it there. Folding in any one of them would make one list mean
different things in different places, which is worse than the copy it replaces.
It is inert for every filter without `includes` -- the caller's table is handed
straight back, no allocation -- and it folds ONE level, so no cycle is possible
and no guard is needed.

The fold runs BEFORE the empty-selection test, not after: a filter whose only
content is its includes has no presets and no customs of its own, and that
branch fails open to SHOW EVERYTHING.

WHAT GOES WITH THE COPY: the removal universe, and yesterday's guard against an
amplifier tick deleting a class cooldown that also sat in one of those lists.
Neither was defending against anything real -- they were defending against the
copy. Also gone: hand-rolling "honour the preset's own ticks". ResolveSelection
has always done that for a selected preset, so a trinket switched off in the
Filter Designer now stops firing here for free.

Schema 10 takes the copied spells back out, fenced by dfDefaults so a seeded
cooldown that also sits in a trinket list stays. A hand-added trinket goes too:
it is indistinguishable from a copied one in the store, and the include brings
it straight back for anyone with that source ticked.

The Cooldown Icons card counts P.PIH_WatchedCount now (the list plus each ticked
source) rather than the list's own size, sharing one counter with the Triggers
rows so the two screens cannot report the same feature differently.

foldIncludes unit-tested standalone: identity on a plain filter, no mutation of
the caller's table, uncategorised carried, an already-selected preset kept, a
self-include a no-op, and never shrinking the selection (which is what lets the
fail-open latch below it keep working).

(cherry picked from commit 4472b44e6ad06c71516f6a918615556277ede811)
Krathe: "can we get the preview card here to actually show some example icons
instead of ??"

The placeholder was defensible right up to the point it was looked at. The
designer's add flow asks for a TYPE first and a SPELL second, so its tiles
genuinely have no artwork yet and the `?` is honest there -- I argued the same
for these, since the helper's picture really is unknown until a cooldown
matches. But a tile is a picture of what the thing LOOKS like, and three
question marks in a row is a picture of an error. What is unknown here is WHICH
cooldown, never WHETHER there is art.

Both tiles now sample the cooldown seed: Recklessness, Adrenaline Rush and Blade
Flurry, the three lowest ids in it. Their cooldown shows the first; Cooldown
Icons shows all three, so the row that means "one per cooldown they have up"
finally looks like it.

  · FROM THE SEED, NOT THE LIVE LIST. The user's list moves with the class
    ticks, so sampling it would change these pictures when someone unticked
    Warrior. A tile is not a live readout.
  · SORTED, so the choice is the same on every client and every build rather
    than whatever pairs() happened to say first.
  · ONLY IDS THE CLIENT CAN DRAW -- GetSpellTexture returns nil for a spell
    whose data is not cached, and one `?` standing among two real icons reads
    worse than three of them. A short sample still draws a full-length row.
  · SAMPLED ONCE FOR BOTH TILES, so the single icon is the first of the row
    rather than an unrelated fourth spell. The tiles differ in HOW MANY; a
    second difference in the picture would mean nothing.

(cherry picked from commit a0ca021b49cc66e832fe20315f0c05fead9723a0)
Krathe: "Max icons should default to 4 it's showing blank but seems to look like
8? Icon size is also showing blank on the slider."

Both sliders bind the group field directly, so a missing field draws blank -- and
the factory falls back to 8 for a filter group's max, which is the 8 he was
seeing. The fields were missing because P.PIH_AddIconGroup writes its record as a
table literal instead of calling CreateLayoutGroup, and that literal listed the
fields it thought a filter group had. `iconSize` and `maxIcons` were not among
them; CreateLayoutGroup has always set both.

Not calling CreateLayoutGroup is still right -- that function picks its store
from the OPEN TAB, the one line that put eight stray groups in Krathe's spec
store and took three attempts to clean up. What was wrong was hand-writing the
record as well as the store.

P.NewLayoutGroupRecord is that record now. Both creators share it and differ only
in where they insert; the helper overrides the three things it genuinely means
differently (the mark, othersOnly, and TOPRIGHT/LEFT_DOWN so a row of cooldown
icons runs away from the unit's name rather than across it). Copying a field list
is how they drifted, and there is no longer a field list to copy.

Schema 11 fills the gaps on a group that already exists, nil-only, reading the
values off NewLayoutGroupRecord rather than typing them -- so the repair step
cannot disagree with what a fresh group is made of, which is the fault it exists
to repair. Anchor and grow direction are deliberately not in that list: filling
those from the shared record would move an existing group.

(cherry picked from commit 293eb9943cc810707aa255aa6069160f1f15b407)
Krathe: "too verbose, make it Enable with a tooltip explaining what it does in
better english."

The label was "Play a sound when someone becomes worth infusing", under a box
already captioned Sound Alert -- the feature stated twice, and wrapping to two
lines to do it. The explanation moves to where an explanation goes:

  Sound Alert
  Plays your chosen sound when a group member's cooldown makes them worth
  infusing.

A table spec rather than a bare string, so the tooltip keeps the BOX's title:
ResolveTooltipSpec defaults a string's title to the widget's label, and "Enable"
heading its own tooltip tells nobody which setting they are reading about.

L["Enable"] and L["Sound Alert"] already existed; the old sentence key is
retired. The "Only plays while the helper is showing" note underneath stays --
it is a caveat about when the sound is suppressed, not a description of it.

(cherry picked from commit 321f90e93f3c767a331e9bbd104e17c552f1c868)
Krathe: "same with this, less verbose with tooltip and more clear."

"Show even if your Power Infusion is on cooldown" spelled the whole rule out on
the row and wrapped doing it. The rule moves to the tooltip:

  Show when I can't infuse
    Off: markers appear only while your Power Infusion is ready, so you are
    never pointed at someone you cannot infuse. On: they appear whatever your
    own cooldown is doing.

Off first, because off is the default -- the reader's first line is the
behaviour they currently have, the same shape as the icon tick's explainer.

NOT "Show while on cooldown", which was shorter still and ambiguous in the one
way that matters: this box is otherwise full of OTHER people's cooldowns, so a
label that does not say whose reads as the tracked one. "I can't infuse" is what
the row has to carry; the rest is explanation.

A bare string tooltip here, so its title is the label -- unlike the sound tick,
which passes a table because "Enable" titles nothing. Both notes that cite this
label as the example of "a control that says what it does without a header to
lean on" are updated to the new wording rather than left quoting a string that
no longer exists.

(cherry picked from commit 218d6624fca0f7c4135546c896cd92d50f3bd83b)
Krathe's wording, verbatim, capital C included.

My draft was "Show when I can't infuse" -- shorter, and it identified the
cooldown only by implication. Naming the spell says it outright, which is what
the row actually has to carry: this box is otherwise full of OTHER people's
cooldowns, so a label that does not say which spell reads as the tracked one.
Same reason "Show while on cooldown" was never an option.

The tooltip is unchanged and still carries the rule.

(cherry picked from commit 467b10bf3d8ee278a0ebf1b6ae1292bb935d9145)
Krathe: "markers? it should be effects and the wording itself is not very clear
on the tooltip rethink it."

Both faults were mine. "Effects" is this panel's own word -- what the Effects tab
lists and what ACTIVE INDICATORS holds; "marker" belongs to the raid target icon
and the dispel corner mark, which are other features. And the sentence stated a
CONSEQUENCE of the rule ("so you are never pointed at someone you cannot infuse")
before it had finished stating the rule, in one long line covering both states.

  Show when Power Infusion is on Cooldown
    Off: the helper's effects only appear while your Power Infusion is off
    cooldown.
    On: they appear even while it is on cooldown.

One line per state, each a plain sentence, off first because off is the default.
"off cooldown" / "on cooldown" echoes the label rather than reaching for "ready"
-- the tick says on Cooldown, so the explanation says the same words back.

A table with no title rather than a bare string: ResolveTooltipSpec fills a
missing title from the widget's label, which is what this one wants, and a table
is what carries two lines.

Two internal comments describing the same control as a "marker" are corrected
too -- prose that teaches the wrong word is how the wrong word got into a string.
The migration notes about helper markers keep theirs: they describe what was
being reasoned about at the time.

(cherry picked from commit b8fb59c205a495a5b6250323cfea45c08a23de8b)
THE PREVIEW CANVAS NEVER LEARNED THE RULE THE EFFECTS LIST LEARNED. Krathe: "any
buff tab on AD is showing our PI helper indicators, it should not."
CollectAllEffects has hidden helper-owned records from every pool but the
helper's since 2026-09-08; the canvas was written before that and kept painting
them. Three leaks, one cause -- every one a DISPLAY site reading a STORE
accessor:

  · the filter-group placeholder loop read CurrentLayoutGroups instead of
    VisibleLayoutGroups, so the Cooldown Icons group drew on every pool. That
    accessor's own note describes this exact split; this was the site that
    ignored it.
  · the placed-instance loop iterated CurrentAuraPool without testing the mark,
    so every helper icon and square drew on the designer's canvas.
  · RefreshPreviewEffects did the same for frame-level effects -- a helper
    BORDER painted itself over the Any Buff preview. That was the half with no
    card to explain it: the effects list already hid the row, so the colour on
    the mock frame came from nowhere the panel would admit to.

P.PIHShowsMark is that rule in one place, read in both directions like
VisibleLayoutGroups: on the helper's tab the marked records are the only ones
that belong. P.PIHVisibleRecord hides frame-level effects PER TYPE KEY, not per
record -- nothing stops a user adding an effect of their own to the helper's
filter record from the Any Buff tab, and "this record is the helper's" would hide
their work with ours. No copy is made when there is nothing to hide.

TWO ICONS AT ONCE. "We can't add Power Infusion and Their CD at the same time,
we should allow this if we can?" We can, and the store always could: pihPlace's
own note says a placed target mints an INSTANCE and instances are per-id. The
refusal was the guard being coarser than its reason -- two frame effects on one
key genuinely cannot coexist, but two icons differing in art are not a duplicate.
The guard now tests the ART, and only for icons; two squares are still a
duplicate.

  · The art is decided AT CREATION. P.PIH_AddSurface used to create with the pin
    and then walk EVERY icon the signal held clearing staticSpellID -- harmless
    with one icon, destructive with two: adding "Their cooldown" would have
    unpinned the Power Infusion icon already on the frame.
  · A second icon is nudged one icon-width off the first, away from whichever
    edge its anchor names. Both take the type's default corner, so without it the
    pair arrives perfectly stacked and reads as a click that did nothing.
  · Each add tile greys on its own art now instead of the two sharing one state,
    and the Icon tile survives until all THREE answers behind it are spent.

Guard, nudge and visibility rule unit-tested standalone: the six add/refuse
combinations for the icon pair, that squares and icons do not block each other,
the offset's sign per anchor, and PIHShowsMark in both directions.

(cherry picked from commit ebfc32389f9cf92b68a8e48715f402243c686507)
Krathe: "in guild groups it would be useful to only have the PI alert for the DPS
you know who should be getting PI instead of every DPS in the raid who uses a CD."

A Players box on the Triggers tab, collapsed, with the count on its header so a
folded box still says whether it is doing anything. Empty means everyone, so
nothing changes for a profile that never opens it.

THE PICKER IS THE PINNED-FRAMES ONE AT HALF THE WIDTH, not a reuse of it: "it can
just be based around it... as long as it looks and functions in the same way, but
is adjusted for the more narrow width". The dual-column widget is 460 wide with
two 224px panes; this column is ~230, so one of its columns alone is the whole
surface. GUI:CreateCompactRosterWidget is the same rows -- role icon, class
colour, themed right-hand button, Add-by-name field -- in ONE list whose button
toggles both ways instead of two panes with an arrow between them.

Deliberately not carried over:
  · the role bulk-add buttons. "No need for an auto add function as you can pick
    class/role etc anyway" -- the helper narrows by role and class on the same tab
    already, so a button adding every DPS would be a third control saying it.
  · drag-to-reorder. Pinned frames needs an order because the order IS the layout;
    an allowlist is a set, and a hand-sorted set invites the reader to think the
    order means something.
  · the group number, the first thing that stops fitting. The list is still sorted
    by group, so the grouping shows -- it just is not labelled.

Chosen players who are not in the group lead the list, dim and with no role icon
(neither role nor class is knowable from here). They are the whole point of the
name field, and a list that only showed who is present would give no way to see
or remove what you had typed.

THE ENGINE SIDE RIDES RAILS THAT WERE ALREADY THERE. Role exclusion is a per-unit
test consumed at exactly two points in the container plus one verb for the sound
path, pushed by a setter and re-pushed on five roster/role events. The allowlist
is one more setter and one more clause. GROUP_ROSTER_UPDATE is already in that
event set, so a list written before the raid forms takes effect the moment the
named player walks in.

  · Both tests fold into ONE verb (helperUnitExcluded), so the container funnel
    and the sound path cannot answer differently for one unit. IsHelperRoleExcluded
    is renamed to IsHelperUnitExcluded with its single caller: keeping the old name
    while widening the answer is how the next reader gets it wrong.
  · The list is stored as an ARRAY (the picker's order of entry) and pushed as a
    MAP. Empty is nil, never an empty map -- the container reads a present map as
    "these players and nobody else", so an empty one would silence the helper for
    someone who added two names and removed them again.
  · It is cleared on PIH_ApplySaved's RESET branch too, or a list would go on
    narrowing a helper that is not there and then narrow the next one built.
  · The name test fails open twice: no list means everyone, and a unit whose name
    cannot be read is shown rather than hidden.

Keys are "Name-Realm", the same shape the pinned list writes, and RosterSnapshot
now treats an empty-string realm as absent -- UnitName returns the realm only when
it differs from yours, and "Bob-" would match nothing while looking like a name
that should.

Unit-tested standalone: the predicate's fail-open cases (no list, nil unit,
unreadable name), same-realm nil vs empty string, cross-realm, the array-to-map
conversion collapsing empty and junk to nil, and the toggle never mutating the
stored array.

(cherry picked from commit 70fe8b3dd98cb14172a42305d398bbaba2b7e5df)
Krathe: "if I've set it to red it will show orange when over a yellow border...
I'm sure we used to offer up a blend mode for animation?"

DF Chase is DF_ORBIT, and its sparkles were created with a hardcoded
SetBlendMode("ADD"). ADD means "add my colour to whatever is behind me", so the
effect showed its true red over the dark icon in the corner of his frame and
turned orange the moment it crossed the yellow border. Working exactly as
written, and not what a colour picker promises.

He is right that it dates from the effects rework: c4b4e5e, "drop
LibCustomGlow". Each new effect got a blend mode chosen by hand -- DF_ORBIT and
DF_PROC additive, DF_DASH / DF_PIXEL / BLINK / DF_FLASH none at all, which is
BLEND. Same colour picker, two different meanings depending on which effect you
picked, with nothing anywhere admitting to it. That inconsistency is the actual
bug; the orange was the symptom.

No blend mode for animations ever existed -- git log -S finds no such key on any
branch. What exists is Border Blend Mode, which governs the border's own EDGES
and not the effect drawn over them: an easy pair to remember as one control.

Animation Blend Mode now sits with Scale and Frequency, on every effect that
owns textures. DF Pulsate is absent: it has none, it modulates the border's own
edges, so Border Blend Mode already IS its blend mode and a second control would
leave the user watching whichever ran last.

NOBODY'S FRAMES CHANGE. An unset key means "this effect's own default", and
ANIM_BLEND_DEFAULT holds exactly what was hardcoded. The dropdown shows that
state as "Default" and maps it back to nil, so the stored shape is unchanged and
a profile exported before today imports untouched.

Three things that would each have made the setting look broken:

  · SetBlendMode moved OUT of every `if not texture then` branch. These pools are
    reused across re-applies, so a mode set at birth is the mode the first frame
    happened to get -- changing the option would have done nothing until a
    reload. Same reason SetVertexColor was already out there.
  · animSpecHash carries blendMode. StartAnimation returns early on an unchanged
    hash, so without it the new mode would land only when some OTHER tunable
    moved -- the "nudge the frequency and it appears" symptom this file already
    documents for the driver check.
  · The Aura Designer's PLACED_BORDER_KEYS and rawBorderAnimStructTok carry it
    too, or an AD border would stay stale until /reload -- and it is structural
    there, since a container button's animation groups are frozen at build.

The DF Dash OnUpdate path redraws 96 pooled quads per frame through a function
handed geometry and a colour, so the resolved mode is stashed on the border and
stamped only when it actually changes rather than threaded through four call
layers or written every frame.

The value reaches SetBlendMode, which throws on an unknown string, and it comes
out of SavedVariables -- so it is validated against the four real modes and falls
back to the effect's own default rather than erroring the animation pass.

Unit-tested standalone: every effect's unset default matches what was hardcoded,
each override is honoured, garbage/number/empty/nil all fall back correctly, and
the dropdown's DEFAULT<->nil mapping round-trips and restores the effect default.

(cherry picked from commit e36a679056b9178a5ec24a630740e9c35d98a502)
Krathe: "we should let people toggle cooldowns and the sub filters on/off so they
can pick from any of the 4, by default it should match what they have set on
triggers, but it might be the case they want to trigger from a trinket but only
show a CD etc."

Triggers answers WHEN the helper fires; this answers WHAT the row of icons then
shows. Those are the same question until you have both a marker and a row, and
different once you do.

A SHOW block on the group's card, which is what now stands where the generic
LINKED FILTERS picker was dropped. That slot was emptied because it offered a
filter picker over the helper's own plumbing -- correct, and it left the card
silent about its own contents. It holds the four real sources by name instead.

ABSENT MEANS FOLLOW, which is the whole compatibility story. With no pihSources
the group links the cooldown list and nothing else, and that list's own
`includes` bring in whatever Triggers has ticked -- exactly what it did before,
with no migration and no defaults table.

Touch a tick and it stops inheriting and names all four itself, materialised from
the EFFECTIVE set so the first click changes the one thing clicked and the other
three keep what they were showing a moment earlier. A Follow Triggers button goes
back, by DELETING the key rather than writing today's values into it -- "follow"
has to keep following.

THE FOOTER IS A STATE READOUT, not a caption. Four ticks that match the Triggers
tab look identical whether they are inheriting it or were set by hand to the same
thing, and the difference is whether a later change over there still reaches this
group. One line says which.

selection.noIncludes in the registry is what makes "cooldowns only" sayable at
all: our cooldown list NAMES the other three now, so selecting it would drag them
in behind it. A selection that lists its sources itself opts out of the fold.
Inert for everyone else -- no other selection sets it.

Two things that would have bitten:
  · PIH_AddIconGroup wrote filterSelection as a literal. It goes through the
    shared builder, so a new group and an edited one cannot disagree about the
    shape -- and with no pihSources it writes exactly what the literal did.
  · No file-scope table for the four keys. Cards.lua sits at Lua's 200-local
    ceiling in its main chunk, so `local PIH_SOURCE_ORDER = {...}` is a compile
    error rather than a style question. luac caught it; the note records why.

R:SelectionSignature hashes the RESOLVED map, not the selection's shape, so a
source toggle re-signs and rebuilds without noIncludes needing to be hashed.

Unit-tested standalone: a fresh group follows and sets no noIncludes, the
effective set mirrors Triggers, each override resolves to the right presets and
customs, following again picks up a Triggers value that moved while overridden,
and the first click preserves the other three.

(cherry picked from commit c307bb52f5975215378038067ac47664e04d34d8)
…called Icons

Two from Krathe, 2026-09-10.

"THE NUMBER OF SPELLS TRACKED DOES NOT SEEM TO UPDATE ON THE SHOW TOGGLES BUT
ONLY ON THE TRIGGERS." P.PIH_WatchedCount read P.PIH_Settings() outright, so the
header reported what the HELPER fires on while the SHOW block one row beneath it
listed what the GROUP shows. Two different questions, only one being asked.

The sources are an argument now, defaulting to the Triggers ticks for every other
caller. Cooldowns counts as a source rather than an always-on baseline: the group
can switch it off, and a count that added the class list regardless would
over-report by forty.

The wording follows the state. A group that has been given its own set is no
longer reporting "from your Triggers", and a header saying so while the block
under it says it has stopped following would be the panel contradicting itself
one row apart -- so following gets the Triggers phrasing and overridden gets the
count alone.

"MAYBE IT SHOULD BE CALLED PI HELPER - ICONS NOT COOLDOWNS AS IT CAN BE TRINKETS
ETC TOO?" Right, and more so since the SHOW block: the group can now be set to
trinkets ONLY, where the old name is not vague but wrong.

Schema 12 renames an existing group -- but ONLY if it still holds the exact old
string. The card has an editable Group Name field, so anything else is a name the
user typed, and renaming that would be the addon overwriting their words to
satisfy its own tidiness. The name stays stored data and never L[]: a translated
string in a profile is a name that changes when the client's language does.

The old name is a literal inside that migration rather than a constant beside the
new one, and the schema's own note says why: this file is at Lua's 200-local
ceiling in its main chunk, so the second local is a compile error, not a style
question. luac refused it twice today; both refusals are now documented where the
next person will hit them.

Unit-tested standalone: the count for each combination of the four sources,
including cooldowns off with amplifiers on, and his current 91.

(cherry picked from commit bcb73bc7603b3f223a49f0c44ad8283147a8ae65)
Krathe: "yes build the icon block the same".

The icon card had ONE tick for this -- "Ignore trinkets, potions and racials" --
which is the same mechanism at a coarser grain: per-record mutes, applied to the
resolved include map by narrowByPlacementMutes. It is now the same four sources
the Cooldown Icons group got, so the two cards read alike.

NOTHING TO MIGRATE, which is why the old API could simply go rather than being
converted. A record saved by that tick carries exactly the mutes these readers
read, so "all three ignored" reads back as three ticks off -- no conversion step,
no schema bump. P.PIH_IgnoresAmplifiers / P.PIH_SetIgnoreAmplifiers are deleted
with the control they served, and the note above their old home says so.

IT CAN ONLY SUBTRACT, and the footer says so rather than the panel pretending
otherwise. A placed effect is keyed by ONE filter reference, so it cannot carry a
selection of its own the way a group can -- it can show less than Triggers
watches and never more. Showing more needs a filter of its own, which is what the
group is for.

A source Triggers has switched off is GREYED with its own reason, not hidden:
hiding it would leave the icon card and the group card disagreeing about how many
sources this feature has.

Class cooldowns is mutable too, which costs forty entries in the record for "show
trinkets only". Accepted deliberately -- the alternative is a fourth source the
box has to explain the absence of.

☠ THIRD luac REFUSAL TODAY on the 200-local ceiling in Cards.lua. P.PIH_SourceIDs
is a table field rather than a `local function` for that reason alone, and says
so, because it reads like a style smell and is actually a compile error.

Unit-tested standalone: a fresh record reads all four on and writes no mute
table, each toggle leaves the other three alone, turning one back on clears an
emptied table, cooldowns can be muted away, a legacy "ignore amplifiers" record
reads back as three ticks off, and an unwatched source reads off with its
availability explaining why.

(cherry picked from commit c7f2bd966b3c36a0f3250e7fc64e2c72b8fa95b5)
Krathe: "a placed PI icon should show as PI Helper - PI Icon / Icon / Icons,
right now only the last actually shows."

Helper effects are named "PI Helper" and told apart by their type BADGE -- which
was the fix for the old "PI Helper - Big cooldown" on every row, and is still
right for Border, Square and the rest, each unique on the signal. The badge
stopped being enough the day both icons could exist: it says "Icon" for the one
pinned to Power Infusion and "Icon" for the one showing their cooldown, so the
pair added two changes ago arrives as two rows reading identically. That is
exactly the "somebody deletes the wrong one" case the label's own note warns
about, now reachable.

pihLabel takes the record as well as the signal, and appends a name for the two
icons only -- where it disambiguates, and nowhere else, so no row prints the word
its badge is already showing. ("Icons", the group, is stored data and was renamed
in the previous commit.)

staticSpellID's presence is the art, as everywhere else -- there is no second
field recording the choice. A frame-level cfg carries no `type`, so it can never
reach that branch; only placed instances do.

Unit-tested standalone: pinned and dynamic icons, square and bar falling through
to the badge's word, a frame-level cfg with no type field, a nil cfg, the infused
signal untouched, and an unknown signal still returning nil.

(cherry picked from commit 2190b62267370631ae2823334845f831fb5ecdae)
Krathe: "I could hear the sound trigger but did not see the border or PI icon at
all."

The two consumers of the helper's cooldown list do NOT resolve it the same way:

  · the SOUND registrations resolve fresh on every arm (Engine.lua's
    pihResolvedMap calls R:ResolveSelection directly);
  · the VISUALS go through DF:ResolveADFilterRef, which MEMOISES the resolved
    map and only clears when DF.auraLayoutVersion moves.

So a rewrite of `includes` with no version bump leaves the border and the icon
matching an older spell set while the cue plays off the current one -- a trigger
that makes a noise and draws nothing, with no visible cause.

P.PIH_SetAmplifier already invalidated, through pihRefresh. The paths that did
not are the ones nobody clicks: the schema sweep -- which writes these for the
first time on every upgrading profile, in the same pass that strips the 51
copied ids back out -- and the Triggers panel build.

INVALIDATED ONLY ON A REAL CHANGE. This runs on every panel build and every
sweep, so an unconditional bump would re-resolve every filter reference in the
addon each time the tab is opened. The comparison is set-equality on both halves,
which is also what makes the common case (rebuilding the same table) free.

This is a gap introduced with the references yesterday and is worth closing on
its own merits. Whether it is the whole of what Krathe saw is NOT established:
the sound path also checks the per-unit gate at ARM time while the visuals check
it at RENDER time, so a gate that closed after the cue was armed produces the
same symptom by a different route. That one needs the answer to "was your own
Power Infusion on cooldown at that moment", which only he can supply.

Unit-tested standalone: nil<->set transitions invalidate, a preset or custom
added or removed invalidates, and rebuilding an identical table -- every panel
build, every sweep -- does not.

(cherry picked from commit edf80024ba5f4dc376920679710b8c04accc7ff4)
Four reports, four different theories from me, no measurement:

  · the border cleared and the PI icon did not;
  · the sound played and neither the border nor the icon drew;
  · the border and the Icons group drew, the PI icon did not, and the sound
    did not play.

Every one of those is a slot whose LAST PUSH disagrees with the gate, and no
field in the readout could show it. It already prints what the gate INTENDS
against what the chokepoint SAYS -- deliberately as two lines, because they are
allowed to differ -- and the sound registrations per frame. It could not see the
third state, which is where all three reports live.

AuraContainer.GetHelperSlotStatus answers it: how many helper slots exist, how
many would be handed the DEAD filter right now, how many are waiting on a
DEFERRED push, and how many are parked. Derived on every call, never stored, so
it cannot drift from what the slots are doing -- the fault every "same config,
different outcome" hunt in this addon has come down to.

`pending` is the field that names a cause. SlotHandle:_applyHelperGate cannot
call a native tuning setter under lockdown, so it defers to
PLAYER_REGEN_ENABLED -- and Power Infusion is pressed in combat by definition. A
slot at pending > 0 while the gate reads OPEN is that deferral, visible for the
first time, and the line is marked bad in exactly that combination.

Reading it against "gate intends":
  OPEN + dark 0 + pending 0  -> the slots agree; the fault is elsewhere.
  OPEN + pending > 0         -> a deferred push not yet drained.
  OPEN + dark > 0            -> a per-unit exclusion (role, or the named player
                                list), not the gate.

Diagnostic only: nothing about rendering changes. Adding the detector before the
fix is the order this codebase's own rule asks for, and the last four rounds are
what happens without it.

(cherry picked from commit 07b006dcfae86db6f44e9b2c289c3fedafed7dc9)
…with the rest

Krathe: "the PI icon does clear when I use PI on them but it seems to take longer
than the border/icons and other indicators."

Not stuck -- QUEUED. SetAuraSlotCandidateFilters is a dirty-mark, not a repaint:
the container re-reads on its next processor pass, which is the unit's next
UNIT_AURA or the next OnUpdate while visible. So "stop matching" is a request the
engine honours WHEN IT NEXT LOOKS. The frame-level border is painted by DF on the
same frame, and the slot waits for traffic that may be a moment or a couple of
seconds away -- which is also why it read as intermittent.

_applyHelperGate now arms the processor after the push. UpdateAllAuras is itself
only a mark, so this collapses the wait to the next frame rather than making it
instant; there is no synchronous re-parse to reach for. It runs on the gate edge
-- twice per Power Infusion cycle, not per frame and not per aura event -- so the
cost is not measurable.

pcall(fn, self) rather than the closure form, per this file's own rule recorded
at applyGroupTuning's tail. SetHelperGate's note names this exact path as the
reason that rule exists.

☠ A COMMENT I NEARLY SHIPPED WAS FALSE. It claimed the combat deferral drains
back through this function. It does not: _replayTuning clears _cfPushed and calls
_pushFilter, which re-derives park vs live at drain time and pushes candidates
itself, never re-entering here. Checked before committing, and the note now says
what actually happens -- a gate edge deferred in combat drains with no arm of its
own and picks up whatever the combat-exit kick does.

_pushFilter is deliberately left alone. Arming there would cover that drain, and
also park, restore and the death latch -- a far wider blast radius than the
symptom this answers. If the lag is ever seen on those paths it should be its own
change with its own testing.

(cherry picked from commit ce70a12791ceac125f41390a9c3d4b094816fd07)
Reported from the alpha: helper borders and icons rendering for NON-PRIESTS.

There was no class gate on the resident side at all. Only the Options UI checked
-- DF.IsPIHelperAvailable hides the page and PoolDefs omits the pool tab, on both
layouts -- and hiding the controls does nothing about records that already exist.
A profile shared across an account, an imported preset, or a priest's own profile
opened on an alt all carry the marked records, and the factory renders what the
pool holds. It has never asked whose class it is, and it should not have to.

PIH_ApplySaved now refuses first, for anyone who is not a priest.

☠ DARK, NOT "NO HELPER". The existing `if not s` branch OPENS the gate --
"nothing is left to hide" -- which is right when the pool genuinely holds nothing
and exactly wrong here: a non-priest with marked records needs them SUPPRESSED,
and an open gate renders them. The two cases look alike and mean opposite things,
so this is its own branch rather than another condition on that one. Getting that
backwards would have shipped a fix that reproduced the bug.

PIH_SetEnabled(false) is the lever, not a new one: forced dark that nothing
reopens, the readiness ticker refused, the manual hold released, sound disarmed
through pihSet. Everything a class gate needs was already written and tested --
and it latches, so the watcher, the gate switch and the ticker all decline on
their own existing guards rather than needing new ones.

THE RECORDS ARE NOT TOUCHED. Deleting a priest's work because their alt logged in
would be destroying data over a display question, and the same profile has to
come back intact on the priest. Suppression only.

pihSyncWatcher takes the same test, so five events are not registered on every
alt of every priest who shares a profile for a feature they cannot enable. The
watcher's tick would have declined anyway; not registering is the honest version.

Read at call time rather than at load: UnitClass("player") is not dependable
before login, and this runs on login and on every profile switch. A character's
class cannot change, so there is nothing to re-check afterwards.

(cherry picked from commit 2807248efcc498c6275c31702b113d4f30c93681)
An option Krathe asked for, beside the cooldown gate on the Triggers tab -- same
kind of thing (a condition on whether the helper has anything to say at all) and
under the same heading, for the same reason that one is: a titled box around a
single tick is more chrome than either setting is worth.

Off by default and absent when off, so no existing profile changes and there is
no defaults entry to keep in step with the engine's own `== true` read.

pihShouldShow is now the one answer for every GLOBAL condition, as against
helperUnitExcluded which is the per-unit half. Every gate decision goes through
it: the readiness ticker, both switches, PLAYER_ENTERING_WORLD and the
cooldown-cleared branch. Leaving any of them on pihReadReady would have reopened
the gate out of combat the moment a cooldown ticked over.

☠ THE TWO CONDITIONS ARE INDEPENDENT, and the combat test runs FIRST. Someone who
has switched the cooldown gate off still means it when they say combat only, so
`not pihGateEnabled` can no longer short-circuit to "open" -- both the switch and
the PLAYER_ENTERING_WORLD guard had to stop treating it as the only condition.

☠☠ THE EVENT CARRIES THE TRUTH, NOT DF.playerInCombat. That flag is the house
source and is correct -- but Core.lua writes it from PLAYER_REGEN_DISABLED /
_ENABLED, the same two events this now watches, and handler order between two
frames is undefined. Reading it from inside our own handler can see the value
from BEFORE the transition and resolve the gate backwards. pihShouldShow takes an
optional override; the regen branch passes what the event itself means and every
other caller reads the flag, which by then has settled. (Never InCombatLockdown:
that is the addon-restriction state, not the player's.)

⚠ BOTH TRANSITIONS. The addon has a standing note that a combat-keyed gate needs
a refresh on entering AND leaving, and that leaving is the half people forget.
The branch sits ahead of the cooldown guards -- it was below them first, and
those return early when the cooldown gate is off, which would have silently
excluded exactly the users the independence above is for. Caught by reading the
placement back rather than trusting the comment I had just written.

Loaded before the gate setters on both paths (PIH_ApplySaved and the panel's
PIH_Apply), since both resolve through pihShouldShow; loading it after would
settle the gate from the old value and leave it wrong until the next transition.
Cleared on the reset branch with the rest, so a stale hold cannot outlive the
profile that set it.

Unit-tested standalone: the default changes nothing, combat-only hides out of
combat and shows in it, the cooldown gate still applies inside combat, the two
compose independently with the cooldown gate off, and a stale combat flag at
transition time resolves correctly in both directions.

(cherry picked from commit 2725640bc6690924b2926449406986786b9900a7)
Krathe: "it's not working in my raid but I could see it on people before my auto
layout kicked in."

Auto layout is a MASS RETARGET. A slot gated dark for unit A -- a tank excluded
by role, or someone off a named-player list -- migrates to unit B and keeps the
DEAD candidate filter it was handed for A. _cf() re-derives at READ time, but
nothing PUSHES after a retarget: the next push only comes on a gate edge, and
there is no reason for one. The verdict travelled with the container.

This is the exact class SetSlotOwnerUnit already re-seeds two other things for.
Death and visibility are re-asked from their registries there because "the new
unit may already be dead, and that edge will never fire again" -- role exclusion
is that sentence with a different noun. It was missed because the gate is DERIVED
rather than stored, which makes it look like it cannot go stale: the derivation
is fresh, the push is not.

Both retarget lanes get it:
  · SetSlotOwnerUnit re-pushes each helper slot's candidates (placed icons,
    squares, bars);
  · Handle:SetUnit re-tunes a helper group. config.unit is updated there, so
    recordCandidateFilters would DERIVE correctly -- but the bounce that follows
    re-parses against whatever map the container is still holding.

NARROWED TO OUR OWN CONFIGS in both, matching SetHelperGate's own walk:
SetAuraSlotCandidateFilters has no engine-side equality guard, so an unnarrowed
call would re-parse every placed indicator in the addon on every retarget -- and
a raid auto-layout change retargets the whole roster at once.

Lands on top of this morning's processor arm, so the re-derived filter is acted
on next frame rather than at the unit's next aura event -- which on a freshly
retargeted frame could be a long way off, and is the same "long-lived buff nobody
re-casts" hole the slot-retarget kick was written for.

(cherry picked from commit 7cf49acec32bf3973b5f133d452b235d2c5b6dcc)
Krathe's raid: the helper worked, then stopped when the auto layout kicked in at
20 people, and a reload did not bring it back. He spotted the cause himself --
the helper is not on the preset that layout switches to.

That part is configuration, not a bug: a raid auto-layout may point its own AD
PRESET at something other than the mode base, and his 21-30 layout uses a "Flex
21-30" preset while the helper lives in "Raid". The frames render from the
overlay (DF:ResolveAuraDesigner honours it) and it holds no helper, so nothing
draws. Nothing here changes that.

What IS a bug is that the engine could not tell. pihSettings read
GetModeBaseAuraDesigner -- the EDITOR's variant. Presets.lua says so on the
function itself: "the ACTIVE designer a mode resolves to right now ... Used by
LIVE consumers (SoundEngine, migrations) that must match what's on screen. The
EDITOR uses the GetModeBase* variants." The helper is a live consumer and was
reading the editor's answer, so with the overlay active it kept finding the
helper in the BASE preset and armed the gate, the roles and THE SOUND for a
preset that is not on screen.

⇒ "I could hear the sound trigger but did not see the border or PI icon at all"
is this, and so is a status readout reporting a healthy helper while nothing
renders. The feature could not diagnose itself because its two halves were
reading different presets -- which is most of why this took as many rounds as it
did.

It does not make his helper appear. What it fixes is the engine agreeing with the
screen: no helper on the active preset means silent, ungated, and a /df debug pi
that SAYS "no helper installed" instead of describing one nobody can see.

SoundEngine already used the live variant, which is why the sound followed the
frames while the gate did not.

(cherry picked from commit dc1c5f711215f7339d2a63310d4c390f4cc6cc13)
…e it

Krathe: "I've added a list of players and it was showing the effects on other
players not just them."

P.PIH_Apply pushed the roles, the gate, the feature switch, the sound and the
watcher -- and not the player list. Only Engine:PIH_ApplySaved sent it to the
container, and that runs on login and profile switch, so a list edited in the
panel did nothing at all until the next reload while the panel showed it filled
in. Mine, from the commit that added the feature.

The tell was there to be read: roles and players are the two halves of ONE
narrowing -- helperUnitExcluded reads both -- and only one of them was in this
function. A setting the panel stores but never pushes is the "lying control" this
feature has been cleaned of three times already; it came back through a path
nobody re-read.

Array in, map out, empty as nil -- the same conversion PIH_ApplySaved does, and
for the same reason: a present map means "these players and nobody else", so an
empty one would silence the helper for someone who just removed their last name.

AND THE READOUT COULD NOT SEE IT EITHER, which is why a missing push was
invisible. /df debug pi now prints "players watched" READ OFF THE CONTAINER, not
off the profile -- a field that repeated the setting would have agreed with the
panel and hidden the fault, exactly as "roles excluded" beside it reads the
container's own copy.

⚠ The second half of the report -- effects showing while Power Infusion was on
cooldown -- is NOT addressed here and is not explained by this. The gate is
global and this list is per-unit, so a missing list cannot hold the gate open.
Not guessing at it: the readout's "gate intends" / "chokepoint says" pair and the
"driven by" line answer it directly, and one of the states they name (a manual
hold from /df debug pi on, which suspends the watcher) would produce exactly that
symptom.

(cherry picked from commit 02f3463e5214b82e2e8300f8e1fa4ed0e80ad2d8)
…e log

Krathe's log answered it. 33 clean gate edges -- "DARK on cast" / "OPEN, cooldown
cleared" alternating across 72-98 containers -- so the watcher, the gate and the
broadcast were all working, while the placed icons carried on showing people
during his cooldown.

☠ THE COUNT WAS LYING. SlotHandle:_applyHelperGate deferred to
PLAYER_REGEN_ENABLED whenever InCombatLockdown() and returned TRUE, so
SetHelperGate counted a QUEUED slot as a pushed one. In a raid you are in combat
for the whole pull, so every edge on every placed slot queued and none of them
landed during the fight -- while the log read as perfectly healthy. That is "the
border DID go away but the PI icon did not", from the first report onwards: the
border is frame-level and painted by DF, the icon is a container slot.

ATTEMPT, THEN DEFER. The push is tried and the deferral is kept for an actual
failure, so a refusal is MEASURED rather than assumed. The precedent is in the
same walk: SetHelperGate's other half, applyGroupTuning, has always called native
tuning setters on this edge with no combat guard at all, in combat, twice per
Power Infusion cycle -- its own note says so. Two lanes of one broadcast cannot
both be right about whether that is allowed, and the guard was the inconsistency.
Pushed and deferred are now counted apart.

AND THE DETECTOR GOES IN THE LOG, WHERE IT SHOULD HAVE GONE THE FIRST TIME.
Krathe: "I've told you before I can't really run this debug commands in raid
combat, you need to add what you need to the debug console." He had, and I put it
behind /df debug pi twice anyway. One line per gate edge now carries pushed,
deferred, the four slot counts, every condition in force (enabled, gateEnabled,
combatOnly, inCombat, manual) and the size of the player list -- so a reload is
the whole reporting procedure. Guarded by DF:DebugActive so the slot walk that
builds it costs nothing with logging off.

☠ THE _ENV DIFF CAUGHT A BUG THE COMMENT WOULD NOT HAVE. pihGateEnabled is
declared BELOW pihSet, so logging it compiled to a nil GLOBAL read -- the line
would have reported "nil" forever while looking correct in the source. Hoisted to
the forward-declaration block; the assignment stays at its own site.

(cherry picked from commit f3ed12b0fb53117884f674184d6ce0ff16661880)
Krathe's log, after the combat fix: deferred=0 on every edge and "56 tot/56 dark"
on every DARK. So the slot lane is doing exactly what it should -- and the icons
are still on screen.

The one number that could not answer the next question was `pushed`. It counted
group HANDLES and placed SLOTS together, and the two lanes fail for entirely
different reasons: a handle is skipped when it is destroyed or its backend is
gone, a slot when it is parked or the push is refused. 72 pushed with icons still
showing, and no way to tell which 72.

Now reported apart:
  · handlesSkipped -- ours, carrying the mark, and NOT reachable. Those render
    whatever they last had and nothing in the broadcast can correct them, so they
    are counted rather than passed over in silence (the walk used to skip them
    with no record at all).
  · groups N tot/N live/N dark, from GetHelperHandleStatus -- the handle lane's
    equivalent of the slot line. The Cooldown Icons group IS a handle, so nothing
    in GetHelperSlotStatus could ever see it, which is precisely the blind spot
    in the screenshot: a row of three icons is what that group draws.

Diagnostic only; no rendering behaviour changes. What the next log will settle is
whether the group is being pushed and ignoring it, or is not in the walk at all.

(cherry picked from commit 8ec6c028e568d39ad440f107be0d435f92535919)
… map

Krathe's raid log, lane-split: on every DARK edge, 30 group handles and 30
placed slots, pushed=60, deferred=0, skipped=0, in combat -- and the helper
icons and borders carried on showing on his allowed players while Power
Infusion was on cooldown. The gate flipped, the broadcast reached every
container, the push succeeded, the filter did not blank the slot.

HELPER_GATE_DEAD_CF was { includeSpellIDs = { [1] = true } }: "match only spell
1", i.e. nothing. But includeSpellIDs is evaluated INSIDE
CanApplyIdentityCandidateFilters, and when the identity gate declines -- range,
visibility, a cinematic, every reason the latch work exists -- the include map
is skipped entirely and every helpful aura passes. This file documents that
fail-open at length (IDENTITY-GATE EXPOSURE), and its own classifier,
filterVulnerableToIdentityGate, has been returning TRUE for the dead filter all
along. A "dead" filter built on that lever is dead only while the gate happens
to be applied, which in a raid is intermittently -- and every report of this bug
was intermittent.

maxDuration = 0 is evaluated OUTSIDE that gate, in readable Lua
(DoesAuraPassCandidateFilters), and excludes every aura unconditionally. It is
SLOT_PARK_CF -- verified in game 2026-08-30 against a unit carrying eight live
buffs, rendering nothing -- and it is why parking a slot works when gating one
did not. The dead filter is that lever now.

A separate constant from SLOT_PARK_CF, deliberately: _cf() returns it by
identity and the value-tracked push compares against it, and a gated slot must
stay distinguishable from a parked one because the two are cleared by different
things.

One line, and it explains the whole history: the border (frame-level, painted by
DF, never through candidate filters) obeyed from the first report, and the
container-backed icons and groups obeyed only when the identity gate happened to
be applied at the moment of the push.

(cherry picked from commit d52f9dd1c22ceed97c5a332b065bb2ef2cd21cf9)
Krathe: "I might want to add my raid team to the list but turn off showing
only for those players in a pug group without having to add/remove them all
each time."

The list was doing two jobs. Names meant narrowing and no names meant
everyone, so the only way to stop narrowing for a night was to delete the
names and type them back afterwards -- the same fault as the enable tick that
used to delete records, a switch whose off position throws data away.

playersOn separates them. Absent means on, so every existing profile loads
exactly as it did: a profile with names was narrowing and still is, one
without was not and still is not. Only an explicit off is stored, which is
also why there is no defaults entry for a later migration to trip over.

An empty list still means everyone even with the switch on. "Watch nobody" is
not a state anyone asks for by emptying a box, and blanking the feature
silently is the worse failure -- the same reason the engine has always pushed
nil rather than an empty map.

Both push sites make the decision the same way. The panel's live push and the
engine's load push are two halves of one behaviour and have already drifted
apart once, when the live half did not exist at all and an edited list did
nothing until the next reload.

The header count now reports how many the box is narrowing to rather than how
many are saved, so a switched-off list shows none. A 5 beside a switch that is
off would be the header lying about the one thing it exists to report; the
names are one click away and the tick inside says why.

(cherry picked from commit c638e9e2c6ffb3070081daa61ba6b22ff637632d)
…lds y

Krathe, 2026-09-14: "I can't seem to tick 'only watch these players', it's
not doing anything."

With nil as the true arm, the and/or idiom collapses: `on and nil` is nil,
so `or y` wins on both inputs. P.PIH_SetPlayersOn stored false whether the
tick was being switched on or off, so the first click unticked it and no
click could tick it again. P.PIH_SetIconShowsAura had the same shape and
always pinned the Power Infusion icon, so "show the cooldown's own artwork"
never took. Both are explicit ifs now.

A sweep found the same idiom in six more places outside this feature; they
are reported separately rather than folded in here.

(cherry picked from commit 179082bfec498200334cf758022bf0a6a6c4f60f)
…s did

Sweep after the PI helper players tick (179082bf). Six more sites used the
idiom with nil as the true arm, and in every one the intended nil branch had
never once been taken:

  Cards.lua        the Other tab resolved card icons with the spec it is
                   meant to bypass
  Groups.lua (x2)  the Other pool previewed with the spec it must not use
  BindingEditor    macros showed the fallback line the comment says to hide
  AuraContainer    the assist verdict named every TRUSTED unit "cannot-assist"
                   in the idgate readout
  ElementAppearance  the alpha-host dump toned every line BAD, successes too
  Factory          `isBar and nil or defs` in the placed signature -- harmless
                   by its own note (both resolve identically on a bar), written
                   out so the expression says what it does

Each is now an explicit if, or the and/or with a non-nil true arm. No
global-name change in any file; line endings preserved (BindingEditor is LF
in the index and stays LF).

(cherry picked from commit 868e2e409b8b1708014d80b17f5dc8cd8b4841c0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant