e24124b6d483d2aa8e44629581ed1369d6db7c74
A parent opening their notification settings saw every category in the system - Bosch, Inbox, Inventory, Oncall, Signage, Ticketing, Tools - and an ALWAYS SENT list including "Impersonation attempts locked out", "Notice of Employment posted" and "New school account". None of it could ever reach them. The registry was a static catalogue with no per-user filter and the page rendered 100% of it to 100% of users. Permission answers "may you administer this". It never answered "might this be about you". Categories now declare an audience - everyone / staff / student / family - resolved RECORD-AWARE via the shell's App\Support\Audience, so on-call staff count as staff without naming a role and a stale role with no record does not. ONE RESOLVER, NOT TWO. NotificationAudience delegates cohortsFor/matches/ familyRoles to the shell behind a class_exists guard, with a legacy path so this ship-dark beta still degrades rather than fataling on an older shell. The local FAMILY_ROLES_WITHOUT_ADMISSIONS = ['parent'] fallback is DELETED - it had already drifted, and on a shell without admissions it would have silently stopped counting applicant/open-house/homeschool as family. FILTERED IN selfServe(), NOT THE CONTROLLER. ChannelFloor counts the same list the page shows to enforce "you cannot switch off your last channel". Filtering in the controller instead would have let a parent be REFUSED A SAVE because of a Bosch category they cannot see. A test pins the two lists as the same set for parent, staff and student, with a non-empty guard so the comparison cannot pass vacuously. THE THROW COULD NOT DO ITS JOB. Registering without an audience throws outside production and falls back to staff (fail-closed) inside it - but every snap-in wraps register() in a bare catch(\Throwable) and most register in a LOOP, so a missing argument is swallowed AND silently drops every remaining category for that app. Mutation M8 proves it: deleting one audience argument loses all six task categories with nothing logged. EXPECTED_CATEGORY_COUNT = 43 is the only thing that catches it, because a per-app try/catch guarantees this never surfaces as an error. Classification corrected three of my proposed defaults against the evidence: ticketing.replied is staff+student, not family - a parent cannot raise a ticket (submitter_id is hardcoded to Auth::id(), there is no inbound-email path, and canLogin() bars parents). system.school_account_created is staff+student - its own description says "a new staff member OR STUDENT". forms.* is everyone, because forms go to staff self-service AND to families. 13 mutations, 12 red. M11 SURVIVED and is reported rather than papered over: the explicit unknown-cohort loop in normalise() is genuinely redundant, since the trailing array_filter also drops a typo. M11b removes both and goes red, so the behaviour is pinned by something. Super-admin: no bypass in the send path, deliberately - a staff-targeted notice must not reach the diagnostic super accounts. A bypass exists ONLY in catalogue visibility, because impersonation_lockout is addressed to administrators rather than to a staff record, so a super would otherwise be hidden mail they do get. It sits in the shared filter so the floor stays in lockstep. Also: the digest registers with coalesce: false. It goes out through Notify like anything else, so the 5-minute coalescer could hold it and fold it into a summary-of-summaries. A digest is already a batch. Co-Authored-By: Claude <noreply@anthropic.com>
dashboard-ticketing
Description
Languages
PHP
100%