Skip to content

ROCK-8700: Fix double check-in caused by redundant Add after AddOrUpdate - #264

Merged
stphnlee merged 1 commit into
masterfrom
sl-bugfix-double-attendance
Jul 13, 2026
Merged

ROCK-8700: Fix double check-in caused by redundant Add after AddOrUpdate#264
stphnlee merged 1 commit into
masterfrom
sl-bugfix-double-attendance

Conversation

@stphnlee

@stphnlee stphnlee commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Rock core's AttendanceService.AddOrUpdate returns the existing tracked attendance when one exists for the same person + occurrence. The extra attendanceService.Add() flipped that entity to Added, so EF inserted a duplicate row and dropped the update closing the old attendance, leaving two active attendances (double check-in).

  • SaveAttendance.cs: remove the redundant Add (AddOrUpdate already adds new records); existing records now update in place.
  • DataHelper.cs (RoomScanner): comment only. The identical-looking Add in CloneAttendance is intentional clone-by-readd and must not be removed.

NOTE: Do not merge in before 7/13

Rock core's AttendanceService.AddOrUpdate returns the existing tracked
attendance when one exists for the same person + occurrence. The extra
attendanceService.Add() flipped that entity to Added, so EF inserted a
duplicate row and dropped the update closing the old attendance, leaving
two active attendances (double check-in).

- SaveAttendance.cs: remove the redundant Add (AddOrUpdate already adds
  new records); existing records now update in place.
- DataHelper.cs (RoomScanner): comment only. The identical-looking Add in
  CloneAttendance is intentional clone-by-readd and must not be removed.
Copilot AI review requested due to automatic review settings July 9, 2026 18:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a double check-in (duplicate active Attendance rows) caused by calling AttendanceService.Add() on an entity returned by AttendanceService.AddOrUpdate(), which can flip an existing tracked entity to Added and trigger an unintended INSERT rather than an in-place update.

Changes:

  • Removed the redundant attendanceService.Add( attendance ) in the FamilyCheckin save workflow path so existing attendance rows update in place instead of being re-inserted.
  • Added an explanatory note in FamilyCheckin documenting why Add() must not be called after AddOrUpdate() for the same tracked entity.
  • Added an explanatory note in RoomScanner clarifying that the Add() inside CloneAttendance is intentionally used to force a clone-by-reinsert behavior in that specific flow.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
Plugins/org.secc.FamilyCheckin/Workflows/SaveAttendance.cs Removes redundant Add() after AddOrUpdate() to prevent duplicate inserts / double check-in.
Plugins/org.secc.RoomScanner/Utilities/DataHelper.cs Adds documentation explaining why Add() remains intentional in CloneAttendance.

@jrwake89 jrwake89 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, Stephen! I found the same bug when looking into this a bit.

@stphnlee
stphnlee merged commit 04cd79e into master Jul 13, 2026
1 check passed
stphnlee added a commit that referenced this pull request Jul 21, 2026
* ROCK-7057 Added workflow action to handle camp group updates

* ROCK-7057 Updated workflow action to allow dynamically setting the attribute key and value prior in the workflow

* ROCK-8322 Fixing issue with auto-input from iOS (#200)

* ROCK-8322 Fixing issue with auto-input from iOS

* ROCK-8322 Reduced timeout and add enter on submit for phone number

* ROCK-8322 Updated enter submission to format phone number

* ROCK-8322 Add aria announcement for screenreaders on auto submit

* ROCK-8193: Copy StartDate and CustomSchedule at PublishGroup creation, disable schedule fields when CustomSchedule is set

- Add StartDate (from Group.Schedule.EffectiveStartDate) and CustomSchedule (from Group.Schedule.FriendlyScheduleText) to the field copy-over when a new PublishGroup is created
- Disable Day of Week and Time of Day dropdowns on the form when CustomSchedule has a value, since ScheduleText prioritizes CustomSchedule

* ROCK-7057 Updated workflow action to handle diverse situations flexibly

* ROCK-8193: Populate MeetingLocation with full address when Location.Name is empty

Falls back to Location.ToString() (full street address) when the
location has no name set, fixing empty MeetingLocation on PublishGroups.

* ROCK-7057 Improved log message for finding additional groups on removal; added trim to groupnameprefix; updated logic for finding other matching placement groups to include groups attached to the template

* ROCK-8193: Schedule fields always pull from Group, update help text

- Schedule fields (Time of Day, Custom Schedule, Starts On) now always
  pull from Group.Schedule and are disabled on the PublishGroup form
- Day of Week also pulls from Group when weekly, but stays editable
  when the Group has a custom schedule (for filtering on group list page)
- CustomSchedule only populated at creation when WeeklyDayOfWeek is null
- Updated help text on Day of Week and Custom Schedule info icons

* ROCK-8318 Add logic to handle leader registrations where the groups are not linked directly to the registration instance

* ROCK-8318 Modified group and groupmember lookup logic for efficiency and trimmed workflow group attribute key

* ROCK-8356 Added suppress giving statement logic to Generate Statements
plugin

* ROCK-8328: Medication Manager Updates

Add bulk distribute, manage medications modal, and add medication sub-modal to the MedicationDispense block.

* ROCK-8397 Converted ServerHealth.ashx into a block to confirm that Rock is running before returning that the server is healthy

* Update Plugins/org.secc.SystemsMonitor/org_secc/SystemsMonitor/ServerHealth.ascx.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Plugins/org.secc.SystemsMonitor/org_secc/SystemsMonitor/ServerHealth.ascx.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Plugins/org.secc.SystemsMonitor/org_secc/SystemsMonitor/ServerHealth.ascx.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ROCK-8362: Sanitize CustomSchedule to prevent HTML and iCal content in publish groups

- Add FormatScheduleDates() to convert RDATE schedules into readable date lists
  (e.g., "Tuesdays at 6:30 PM: Aug 5, Aug 19, Sep 2...")
- Replace FriendlyScheduleText and iCalendarContent references with FormatScheduleDates
- Apply .SanitizeHtml() on save to strip any remaining HTML from CustomSchedule
- Add | StripHtml in GroupFinder Lava templates as defense-in-depth
- Fix NullReferenceException in OnInit when publishGroup is null

* ROCK-8362: Sanitize CustomSchedule to prevent HTML and iCal content in publish groups

- Add FormatScheduleDates() to convert RDATE schedules into readable date lists
  (e.g., "Tuesdays at 6:30 PM: Aug 5, Aug 19, Sep 2...")
- Replace FriendlyScheduleText and iCalendarContent references with FormatScheduleDates
- Apply .SanitizeHtml() on save to strip any remaining HTML from CustomSchedule
- Add | StripHtml in GroupFinder Lava templates as defense-in-depth
- Fix NullReferenceException in OnInit when publishGroup is null

* ROCK-8367: Remove SCC staff reference blocking and employer section from volunteer application

Remove the validation that blocks volunteer applications when a reference is a
Southeast Christian Church staff member. The Staff checkbox is kept on the form
for data collection but no longer prevents submission. Also removes the Current
Employer section (CurrentEmployer, PositionHeld, WorkPhone) from the PDF merge
and removes WorkPhone validation from personal information.

* ROCK-8362: Truncate long date lists in FormatScheduleDates

Show first 3 dates + last date for schedules with 5+ specific dates
to prevent overly long Custom Schedule text (e.g., "Tuesdays at 6:30 PM: Aug 5, Aug 19, Sep 2, ... Dec 2")

* ROCK-8422 Optimized job to reduce webfarm messages

* Update Plugins/org.secc.FamilyCheckin/Jobs/ResetGroupLocationSchedules.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ROCK-8373 Added null checks and better exception handling

* ROCK-8373 Added concurrent workers attribute and comma-separated giving IDs

* ROCK-8442 Improvements to Exporting Statements (#212)

* ROCK-8442 Improvements to Exporting Statements

* ROCK-8442 Removed ZIP

* Update Plugins/org.secc.Finance/org_secc/Finance/ContributionStatementList.ascx.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ROCK-8404 Removed custom email from group manager

* Update Plugins/org.secc.GroupManager/org_secc/GroupManager/LWYARoster.ascx.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Make From Email Address a required block attribute

Agent-Logs-Url: https://github.com/secc/RockPlugins/sessions/922a0f5c-d114-4800-b5da-146765e1c987

Co-authored-by: stphnlee <902855+stphnlee@users.noreply.github.com>

* ROCK-8214 Initial commit on CSV import block

* ROCK-8428 Fixed false positive critical error

* ROCK-8407 Add jobs to disable/restore communications for inactive people

Adds two Quartz jobs that manage email and SMS preferences based on Person
record status:

- DisableCommunicationsForInactivePeople: snapshots original EmailPreference
  and per-phone IsMessagingEnabled to a DefinedType, then sets EmailPreference
  to DoNotEmail and disables SMS on all phones. Includes a configurable
  lookback window and a Dry Run mode.

- RestoreCommunicationsForReactivatedPeople: for anyone now Active with a
  tracking entry, restores the snapshot and deletes the tracking entry. No
  lookback so nobody gets stranded if a run is missed.

Snapshots are stored as DefinedValues (Value=PersonId, Description=JSON) in
a new "Inactive Person Communication Overrides" DefinedType, which is
created operationally as part of the initial rollout.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* ROCK-8407 Track skipped count for benign continue branches

* ROCK-8214 Fixed bug in row processing

* ROCK-8214 Enhanced results page to show per row which updates were successful

* ROCK-8214 Saved column mapping to persist through postback

* Wrap per-iteration RockContext in using block

Addresses PR review feedback: ensures each per-person RockContext is
disposed immediately at the end of its loop iteration instead of
waiting on GC.

* ROCK-8214 Improved handling for duplicate names and improved query for faster performance

* ROCK-8214 Added base parent group to make mapping multiple columns easier

* ROCK-8214 Improved handling for duplicate records

* ROCK-8469 Added background processing

* ROCK-8469 Added efficiency and safety improvements

* ROCK-8469 Added efficiency and safety improvements

* ROCK-8469 Added efficiency and safety improvements

* ROCK-8488 Fall back to minor's mobile phone when parent phone is blank

More minors are providing their own mobile number rather than a parent's,
which left the Decision Analytics grid phone column blank. When IsMinor is
true and ParentPhone is null/whitespace, fall back to the minor's MobilePhone.

* ROCK-8469 Added efficiency and safety improvements

* ROCK-8469 Added efficiency and safety improvements

* ROCK-8488 Fall back to minor's mobile phone when parent phone is blank (#218)

More minors are providing their own mobile number rather than a parent's,
which left the Decision Analytics grid phone column blank. When IsMinor is
true and ParentPhone is null/whitespace, fall back to the minor's MobilePhone.

* ROCK-8469 Implementing comments from PR

* ROCK-8469 Implementing comments from PR

* ROCK-8469 Implementing comments from PR

* ROCK-8469 Implementing comments from PR

* ROCK-8469 Implementing comments from PR

* ROCK-8469 Implementing comments from PR

* ROCK-8488 Add profile-link icon column to Decision Analytics grid

Modal access alone is not discoverable for most users. Add a leftmost
icon column linking to /Person/{PersonId} so staff can navigate from
any row to the candidate's profile in one click. ExcludeFromExport
keeps Excel exports clean.

* ROCK-8488 Fix Parent Email modal binding for minors

The minor branch of LoadDetailModal was binding the Parent Email
literal to decision.Email (the candidate's own email) instead of
decision.ParentEmail. Modal now shows the parent's email under the
Parent Email label.

* ROCK-8488 Use ExcelExportBehavior instead of ExcludeFromExport

ExcludeFromExport is not the correct attribute name in Rock 13.7.
The supported attribute on RockTemplateField is ExcelExportBehavior
with values AlwaysInclude or NeverInclude. Using NeverInclude to
keep the profile-link icon column out of Excel exports.

* ROCK-8488 Stop profile-link click from bubbling to row-select handler

Without stopPropagation, clicking the profile icon both navigates to
/Person/{id} and fires gResults_RowSelected, which opens the row's
detail modal. Browser navigation usually wins the race, but slow
postbacks can flash the modal before navigation completes.

* ROCK-8507 Updating server health block to only monitor for maintenance flag

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* ROCK-8507 Removed vistigial panel elements

* ROCK-8501 Add MedicationActive flag and source-aware medication manager

Adds an Active/Inactive toggle for medications when the camp page reads
the Person master matrix (e.g. HSM Fall Retreat, MSM Discipleship Weekend,
RUCKUS), and restores full Edit/Delete/Add Medication on the snapshot
matrix for camps whose registration workflow copies medications to a
GroupMember-level matrix.

The MedicationActive boolean attribute is added to the medication matrix
template; all consumers filter inactive items consistently — the dispense
grid, the leader notification job, the family-facing medication summary
block, the check-in kiosk label printer, and the camp medication Excel
export stored procedure (via new migration 006).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ROCK-8499 Fix sermon share link to point to YouTube (#225)

* ROCK-8431 Restore missing connection workflow actions (#223)

* ROCK-8431 Restored SetConnectionRequestGroup and SetConnectionAttributeValue workflow actions

Recovered from secc/Rock@26f714c979 (originally authored by Mark Lee, Mar 2018).
The classes were lost during the Rock 13 migration to hotfix-1.13.7 in 2022,
leaving EntityType rows 1616/1617 with empty AssemblyName fields and ~170
workflow references silently failing for the past ~3.5 years.

Namespace stays Rock.Workflow.Action so the existing EntityType.Name records
match. Rock auto-populates AssemblyName via MEF discovery on startup, so no
plugin migration is required.

* ROCK-8431 Use non-obsolete GetWorkflowAttributeValue spelling

The recovered code called the [Obsolete] GetWorklowAttributeValue (typo,
deprecated since Rock 1.11). Swapped to the correctly-spelled
GetWorkflowAttributeValue. Behavior is identical -- the obsolete method
is a 1:1 shim that internally calls the correct one -- but this removes
the CS0618 build warning and survives eventual removal of the shim.

* Typo

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* ROCK-8431 Address code review: cleanup + implement Group Status

Resolves the 8 Copilot review comments on PR #223:
- Fixed "shoud" -> "should" copyright header typo in both files
- Removed unused usings (System.Linq, Rock.Security, Rock.Web.Cache)
- Removed redundant `?? Guid.NewGuid()` on already-null-checked guids
- Hoisted GetMergeFields(action) out of the per-entry loop in
  SetConnectionAttributeValue (computed once, reused)
- Implemented the previously-dead "Group Status" dropdown: it now sets
  ConnectionRequest.AssignedGroupMemberStatus. Also corrected the
  dropdown list source from 3^Pending to 2^Pending so values map
  directly to the GroupMemberStatus enum (Inactive=0, Active=1,
  Pending=2). The field was never read before, so no existing
  workflow's stored value was meaningful.

Group Status is a deliberate behavior change (chosen over removing the
field) to make the action sustainable rather than carry a misleading
no-op config knob.

---------

Co-authored-by: Stephen Lee <slee@secc.org>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Rock 8398/multi file field type (#228)

* Changing workflow csproj file to use robocopy

* ROCK-8398 Adding multi file field type & upload functionality

* Typo fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Typo fix for pull request finding #2

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Robocopy fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Replace robocopy with xcopy in post-build events (#231)

Reverting the use of robocopy to fix build pipeline

* ROCK-8575: Revert Medication Manager to Person-only reads

Roll back the GroupMember snapshot direction in the Medication Dispense
grid. Nothing populates GroupMember.Medications in prod, which was
causing ~6,300 campers to render blank in the grid. Every other
medication consumer (camp report stored proc, kiosk labels, MyEvents
family info block, leader notifications) already reads Person.Medications.

- Strip the qrySnapshot LINQ branch and groupMemberMatrixAttributeIds.
  Grid reads exclusively from Person.Medications.
- Manage Medications modal opens against the Person master; Edit and Add
  write to the master, Toggle Active flips MedicationActive on the master.
- Remove Delete entirely (column, handler, defense guards). Staff use the
  Active toggle to deactivate instead.
- Drop dead source-aware plumbing: SourcePerson/SourceSnapshot constants,
  IsSnapshotMatrix method, hfManageMedsSource hidden field, badge literal,
  source parameter on the configure helper.

UX add: inactive-but-distributed-today rows stay visible, grayed out
with an "Inactive" badge and Dispense disabled, so same-day audit trail
is preserved. Filter is per-(person, medicine, schedule) so distributing
breakfast and then deactivating the med doesn't leak lunch and dinner
rows into the grid.

Perf: index today's distribution notes into a HashSet keyed by
(PersonId | MatrixItemId | ScheduleGuid) for O(1) lookup during the
inactive filter, avoiding a per-medicine linear scan on heavy-load days.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Output encoding for ChangeManager fields to address stored XSS vulnerability

* Encoding outputs of person info for CheckinMonitor blocks to address xss vulnerabilities

* Encoding outputs for CMS & Communications plugins to address xss vulnerabilities

* ROCK-8632 Added job to re-encrypt encrypted data (#234)

* ROCK-8632 Added job to re-encrypt encrypted data

* Added standard copyright blob

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* ROCK-8637 Fix IDOR / broken access control findings (rows 6-10)

Address the IDOR / broken-access-control batch from the security review:

- PublicProfileEdit (row 8): add server-side ownership gate before save;
  reject tampered hfPersonId/ddlGroup that don't belong to CurrentPerson's
  families.
- LinkListEditUsers (row 9): re-resolve the editors group from the content
  channel item and re-run IsAuthorized(EDIT) on every mutating postback
  instead of trusting the tamperable hfSecurityGroupId hidden field
  (privilege-escalation fix).
- GroupDetailLava (row 10): add object-level authorization in OnLoad so an
  authenticated user cannot swap GroupId to render an arbitrary group's
  member PII; matches the sibling-block pattern.

Row 6 (GroupManagerBlock base) is covered by the row 10 block-level fix; row 7
(FinancialTransactions REST) is staff-only/401-gated and accepted as internal
risk per policy. Open-redirect/XSS findings are tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8638 Require authentication on Imaging REST endpoints

Add [Authenticate, Secured] to the api/imaging/test and
api/imaging/generateimage actions. GenerateImage rendered
caller-supplied HTML into a PNG with no auth, exposing an
unauthenticated endpoint and resource-abuse/SSRF vector.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ROCK-8638 Encode Purchasing outputs to address stored XSS

RequisitionDetail: HTML-encode the user-entered Title and
Status.Value before assigning to labels, preserving the appended
<small> markup.

PODetail: HTML-encode VendorName, street address, and city/state/zip.
Build the vendor web-address anchor from an encoded link text and an
attribute-encoded href restricted to http/https, with rel=noopener.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ROCK-8638 Encode ChangeManager outputs to address stored XSS

Encode the user-controlled Name and requestor FullName in the header
block, and the requestor/approver comment literals.

In FormatValues, track whether a value column holds trusted Rock HTML
(attribute ValueFormatted or the photo thumbnail) and HTML-encode the
plain-text NewValue/OldValue and Property/Comment only when it does
not, leaving the trusted-HTML branches and the grid's HtmlEncode=false
columns intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ROCK-8638 Adding HTML encoding to remaining RockLiterals in Purchasing plugin

* ROCK-8615: Plugin documentation — pilot READMEs (format review) (#233)

* ROCK-8615: Add pilot plugin README docs (QRManager, PastoralCare, Workflow)

Pilot of the per-plugin documentation format for ROCK-8615: one README per
plugin covering Overview, Project Info, Project Layout, Components,
Dependencies, Migrations, Observations (security/improvements), and Making
Changes. Three representative plugins (small/medium/large) for format review
before documenting the remaining plugins.

* ROCK-8615: Upgrade Workflow README to deep-technical tier (format sample)

Demonstrates the deeper documentation tier alongside the standard-tier
QRManager and PastoralCare pilots: action execution data-flow diagram,
ActionComponent contract/conventions, per-action config-attribute reference
for the most-used actions, edge cases, and an extending guide. Workflow is a
deep-tier plugin under the agreed tiered approach (deep where it pays;
standard for the rest).

* ROCK-8615: Document 8 more plugins (PersonMatch, Jobs, FamilyCheckin, Finance, Communication, Connection, Rest, OAuth)

Standard-tier READMEs for PersonMatch, Jobs, FamilyCheckin, Finance,
Communication, Connection; deep-tier READMEs (data-flow diagram, edge
cases, config-attribute reference, extending guide) for Rest and OAuth.
Follows the tiered format established in the pilot READMEs.

* ROCK-8615: Document remaining 44 plugins + add top-level Plugins/README.md index

Adds a README for every remaining org.secc.* plugin (42 standard tier,
2 newly deep: none added here beyond the 5 payment/security deep tier —
PayFlowPro, PayPalExpress, PayPalReporting, Authentication, Security) and
a top-level Plugins/README.md catalog grouping all 55 plugins by category
with tier + one-line description. Every README was generated from source
and adversarially verified against the code (Migrations and Purchasing
verified by hand after their automated verifiers timed out).

* ROCK-8657 Updated action to get person and group from the passed grou… (#237)

* ROCK-8657 Updated action to get person and group from the passed groupmember entity

* ROCK-8657 Log warning on attribute resolve failure before entity fallback

Address PR review on the placement-group sync action:
- Log a warning when a configured Person/Placement Group workflow attribute fails to resolve, before falling back to the triggering GroupMember entity, so genuine misconfigurations stay debuggable.
- Use AsNoTracking on the Person entity fallback to match the attribute-resolution path and avoid tracking the trigger graph.

* ROCK-8638 Fixing double encoding of ship to information

* ROCK-8637 Fix IDOR / broken access control + PIN logic-flaw findings

- ManageCommunicationLists: resolve the anonymous person via GetByImpersonationToken
  instead of a guessable GUID-suffix match (Guid.EndsWith) — closes an anonymous
  account-takeover IDOR (email/phone overwrite -> password-reset hijack).
- SuperCheckin: add the missing return so a security-role member can no longer have a
  PIN UserLogin minted (privilege escalation via fall-through).
- RequisitionDetail: add object-level UserCanViewRequisition() gating LoadRequisition/
  LoadSummary/LoadItems + refresh handlers, plus CreatedBy/CurrentPerson null-safety
  — closes a horizontal IDOR that let any authenticated user view any requisition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8637 Note in code that the ?p= path has no live generator

Document inline that the subscribe link is a bare /s (no token), verified in code and
the Rock DB, so switching to GetByImpersonationToken breaks no existing link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8637 Tighten security-fix comments for brevity

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8637 Pull ManageCommunicationLists out of this PR

The GetByImpersonationToken change is NOT code-only: live SMS workflows generate the
guessable ?p= links it would reject — WorkflowType 250 (* Communication List Signup,
secc.org/s/{keyword}?p=, last sent today). Shipping it alone would break SMS sign-up
completion. Tracking as a coordinated code+config follow-up (block + re-tokenize the
WT 250 SMS link, deployed together). WorkflowType 361 (/connect/) is a sibling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8637 ManageCommunicationLists: require impersonation token for anonymous resolution

Replace the guessable GUID-suffix match (Guid.EndsWith(param), length>=12) with
GetByImpersonationToken. The old code used the tail of a person's GUID as a bearer
credential; since a GUID is not secret (sent in plaintext SMS links, provider logs,
other Rock URLs), a leaked/forwarded suffix let an anonymous caller resolve that person
and overwrite their email/phone (account takeover via password reset).

COORDINATED with a config change (deploy together): WorkflowType 250
'* Communication List Signup' action 5380 builds secc.org/s/{keyword}?p={Guid|Slice:24,12}
and must instead emit a token via PersonTokenCreate. WorkflowType 361 '/connect/' is a sibling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8650 Sync OnePoint search icon into repo

Mirror the live-on-prod SECC2019Portal header change that swaps the
plain-text "Search" trigger for a Font Awesome magnifying-glass icon
styled to match the news bell. Already applied and tested on prod via
Rock File Manager; this commit only brings the repo back in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8654 Cap event promo image height on event detail page

The event detail page (SECC2024 theme) renders the promo image as a
full-width <img> with no height constraint, so tall/landscape photos
fill the viewport and push event copy below the fold on desktop.

Add a max-height: 45dvh + overflow: hidden cap on
.event-photo-container .event-photo. Scoped to the bare selector the
live DOM actually uses (no .event-details wrapper exists on the detail
page, so the pre-existing .event-details-scoped rule never matched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* ROCK-8654 Scope event photo cap to the event-detail block; drop dvh

Scope the cap to .event-photo-cap (the CSS Class on the page-specific
Calendar Event Item Lava block 7325) so it only affects the event detail
page, not the shared .event-photo-container markup on the retired group
detail template. Use a single max-height: 45vh (dropped the dvh variant).

Requires the event-photo-cap CSS Class set on the block in each environment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8654 Shorten comment

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8672: Remove vulnerable FFmpeg workflow action and xFFmpeg.NET dependency (#244)

The FFmpeg workflow action resolved the admin-configured Command attribute
through Lava (with {{file}} / {{outputPath}} merge fields) and passed the
result straight to the FFmpeg engine (argument/command injection), and called
Directory.CreateDirectory on an unsanitized OutputPath (path traversal).

Per the approved plan (JIRA ROCK-8672 #29957), this removes the action
outright rather than hardening it: only one inactive workflow referenced it
and no other code depends on it.

- Delete Plugins/org.secc.Workflow/Media/FFmpeg.cs
- org.secc.Workflow.csproj: remove the FFmpeg.NET reference, the
  Media\FFmpeg.cs compile include, and the FFmpeg.NET.dll post-build xcopy
- packages.config: remove xFFmpeg.NET 3.4.0
- README.md: remove the FFmpeg action docs and the "Security (review)" note
  that described this (now-fixed) vulnerability as a live finding

No database changes: the orphaned EntityType / WorkflowActionType rows are
left in place as inert metadata (the type can no longer be resolved, so it
cannot execute).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8675: send Messaging API function key via x-functions-key header (#241)

* ROCK-8675: send Messaging API function key via x-functions-key header

Move the decrypted Messaging API function key out of the ?code= query
string and into the x-functions-key request header across all
MessagingClient methods, so the secret no longer travels in request URLs
(captured in Function access logs, App Insights, and proxies). Behavior
is otherwise unchanged. Also fixes an Accept-header typo in
GetPhoneNumber (applicaiton/json -> application/json).

* ROCK-8675: update README security note to reflect x-functions-key header migration

---------

Co-authored-by: Stephen Lee <slee@secc.org>

* ROCK-8654 Vertically center the capped event photo

Add display:flex + justify-content/align-items center so the 45vh-capped
event promo image is cropped from its center (trimming top and bottom
equally) instead of clipping only the bottom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8637 Clarify incrementUsage:false comment (address Copilot review)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Remove unused org.secc.GroupTrackerDemo plugin

Dead code: three anonymous api/GroupTracker/* REST endpoints (IDOR write + PII read)
with no post-build deploy step, not in RockWeb/bin, and no source/content/registration
references. The live 'Group Tracker' tool is a separate block (GroupMemberTracker.ascx).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8665: Fix note duplicating in PersonalGroupInformationPanel view (#240)

* ROCK-8665: Fix note duplicating in PersonalGroupInformationPanel view

Move the group member Note rendering out of the per-attribute foreach in
ShowView() so it renders once per member instead of once per attribute.

* Encode HTML in group member notes

* ROCK-8673: add in-memory rolling-window OTP request rate limiter (#242)

* ROCK-8673: add in-memory rolling-window OTP request rate limiter

Enforce a per-person/number cap on OTP code requests (default 5 per 15 min,
both configurable) before code regeneration, so regenerating a code no longer
resets the 5-guess brute-force budget. Implemented in-memory via the existing
SMSRecords helper; existing send-flood limiter and per-code guess budget unchanged.

* Removing configured 0's edge case for limits

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fixed indentation mistake

* Reduce max code requests from 5 to 3

Also added explanatory comment on per-server max request cap

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* ROCK-8625: Add optional per-instance Lava Waiver Text to Volunteer Signup Form

* ROCK-8676 Updated instructions for documentation on Copilot review

* ROCK-8647: render MultiFile attribute media inline (native video/image)

Replace the plain-link display of the MultiFileFieldType with server-side
markup that embeds <video>/<img> for media files, keeping the download
link directly beneath each. Type detection uses BinaryFile.MimeType with a
filename-extension fallback. The condensed branch (grids/summaries) still
returns the 'N files' count, and FormatValueAsText still degrades to
filenames via the retained anchors. This removes the need for the
downstream page-level <script> that previously enhanced the links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ROCK-8647: address Copilot review feedback on PR #249

- Order display blocks via a Guid-keyed dictionary iterated over fileGuids
  (matrix order) instead of OrderBy(IndexOf(...)); O(n), one materialization.
- Add aria-label, inner fallback text, and playsinline to the inline <video>
  for accessibility and unsupported-browser handling.
- Add loading="lazy" decoding="async" to inline <img> thumbnails.
- Document the new inline-media display behavior in the org.secc.Attributes
  README and add a 'Last updated' line per the repo documentation guideline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ROCK-8681 Reconcile Group App attendance confirm with kiosk check-in

PostGroupAttendance matched existing attendance by Occurrence.LocationId,
resolving it from group.GroupLocations.FirstOrDefault() when the caller sent no
locationId. Student small groups have no GroupLocation, so that resolved to
null and the lookup searched for a null-location occurrence -- never matching
the kiosk's room-specific occurrence. The confirm then created a duplicate row
on a separate null-location occurrence instead of flipping the check-in row.

Match the existing attendance on (GroupId, PersonAliasId, OccurrenceDate),
ignoring location (keeping the optional scheduleId filter), and prefer a
kiosk-origin occurrence (AttendanceCodeId set) when more than one matches. Only
create a new DidAttend=true row when none exists. Also return NotFound instead
of a 500 when groupMemberId is invalid, and read groupMember.PersonId directly
to avoid a null Person navigation in the query.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8647: address review — move media to FormatValueAsHtml; don't embed SVG

Per review: keep FormatValue link-only so exports/Lava merge fields don't
leak markup, and render <video>/<img> in FormatValueAsHtml (matches Rock's
MediaElementFieldType convention; the 6-arg entity overload delegates to
the 4-arg one, covering the ChangeManager block). Exclude SVG from inline
rendering (guarding both .svg and image/svg+xml) to avoid inline-script
XSS via GetFile.ashx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ROCK-8647: embed multi-file media in FormatValue (+AsHtml); keep exports clean via text path

Render <video>/<img> from both FormatValue and FormatValueAsHtml, mirroring
Rock core (ImageFieldType embeds via FormatValue; MediaElement/MediaWatch
override both). This restores inline media on Workflow Entry forms and Lava
output, which resolve attribute display through FormatValue rather than
FormatValueAsHtml.

The plan called for a FormatValueAsText override to keep exports/plain-text
merge markup-free. That override point does not exist in this Rock v13 base
(verified against the bundled Rock.dll: FormatValueAsText is absent;
GetTextValue/GetCondensedTextValue are its value-agnostic successors). So the
clean text channel is implemented via GetTextValue (comma-separated filename
list) and GetCondensedTextValue ("N files") instead. Condensed FormatValue
also returns "N files". SVG remains link-only (inline-script XSS guard).

Verified: full org.secc.Attributes assembly compiles clean (Roslyn,
langversion latest) against the in-repo reference assemblies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ROCK-8696: Remove Magick.NET/ImageMagick from org.secc.Workflow

The sole consumer was the ImageMontage workflow action, whose only referring workflow (#514 "Generate Trick Play Images") is dormant/inactive (last run 2024-03-10). Removes the vulnerable dependency and its dead code.

- Delete Media/ImageMontage.cs (only "using ImageMagick;" consumer in the repo)
- Remove both Magick.NET <Reference> blocks, the <Compile> entry, and the Magick* xcopy line from org.secc.Workflow.csproj (other PostBuildEvent xcopy lines kept)
- Remove both Magick entries from packages.config
- Remove Magick/ImageMontage references from README.md

No DB change; the orphaned EntityType + workflow #514 action-type rows are intentionally left in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8752: Filter inactive Defined Values out of VolunteerSignupWizard

VolunteerSignupWizard enumerated a configured Defined Type's DefinedValues
with no IsActive filter, so retired values still rendered (e.g. the Food Pack
wizard's 3 retired Oct-2023 slots in DefinedType 339 "2026 | Food Pack Times").

Added an IsActive filter at all four value-surfacing sites: the GetTree
fallback and saved-sub-values branches (public signup), the admin config
checkboxes, and the admin totals grid + column filter. Saved sub-values are
resolved via DefinedValueCache.Get with a null-guard so a deleted/retired GUID
in stored config is silently ignored. The DefinedType picker is intentionally
untouched (no inactive Defined Types exist site-wide).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8696: fix stale image-montage example in README flowchart

The workflow-engine mermaid diagram still cited 'create image montage' as an example action after the ImageMontage action was removed. Swap to a surviving example (remove a binary file).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8710: Move parent fields below waiver, relabel submit button

Change 1 - Volunteer Signup Form (Connections) block: render the editable
Form-key attribute controls (parent info) below the Waiver Text instead of
inline with the per-role display controls. Adds a phFormAttributes
PlaceHolder outside the repeater (after lWaiverText, before .actions) and
retargets both the write path (AddEditControls) and the read path
(GetEditValues) to it. Display-only non-URL attributes still render into the
per-role phAttributes.

Change 2 - primary submit button now reads "I Agree & Connect". The label is
set at runtime (was from the ConnectButtonText block setting); hardcoded per
stakeholder direction so every instance shows the waiver-acknowledgment
wording. The secondary "Connect and Add Another" button is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* ROCK-8710: Note in README that Connect Button Text no longer applies to primary button

The primary submit button label is hardcoded to "I Agree & Connect" in code
(ROCK-8710), so the "Connect Button Text" block setting is a no-op for it.
Documented on that setting's README row so configurators aren't misled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* ROCK-8710: Back out button hardcode + README note, keep layout move only

The primary submit label is already handled by the existing "Connect Button
Text" block setting, which is set to "I Agree & Connect" on the live food-pack
block instance (block 6192). Hardcoding btnConnect.Text overrode that working
per-instance setting for every copy of the block, so it is reverted here.

Restores to origin/master:
- .ascx.cs btnConnect.Text = GetAttributeValue( "ConnectButtonText" )
- .ascx primary button Text="Connect"
- README "Connect Button Text" row (removed the no-op note)

Remaining change vs master is layout-only: the phFormAttributes placeholder
(renders Form-key edit controls below the Waiver Text) plus the retargeted
AddEditControls/GetEditValues calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* GroupApp: enforce minor communication policy in Communicate endpoint

- Add IsMinor to GroupAppGroupMember DTO (set in GetGroupMembers)
- Individual emails to minors: CC parent/guardian emails via
  Communication.CCEmails; reject with 400 if none on record
- Policy: communications to minors must include another adult

* Address review: cross-group guard, leader-gate IsMinor, README

- Communicate: 404 when GroupMemberId doesn't belong to {groupId}
- IsMinor only returned to leaders, consistent with other PII fields
- Document Communicate contract change + minor policy in README

* Remove stray tool markup from org.secc.Attributes README

* ROCK-8702 Add leader import support to Camp Placement Import (#256)

* ROCK-8702 Add leader import support to Camp Placement Import

Adds a Campers/Leaders toggle to the column mapping step. Leader imports
place people using the group type role marked IsLeader instead of the
default role, update existing non-leader members to the leader role, and
error when a target group type has no leader role. Preview validates
leader roles before processing.

* ROCK-8471 improved leader upgrade path

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* docs: document camp leader import behavior

* fix camp placement import labels for leader mode

* ROCK-8702 Reactivated dead archived groupmember path

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* Sl bugfix camp import leaders (#261)

* ROCK-8702 Add leader import support to Camp Placement Import

Adds a Campers/Leaders toggle to the column mapping step. Leader imports
place people using the group type role marked IsLeader instead of the
default role, update existing non-leader members to the leader role, and
error when a target group type has no leader role. Preview validates
leader roles before processing.

* ROCK-8471 improved leader upgrade path

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* docs: document camp leader import behavior

* fix camp placement import labels for leader mode

* ROCK-8702 Reactivated dead archived groupmember path

* ROCK-8702 Fixed bug in back button; added next button to file upload

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* ROCK-8756: Dev SMS capture transport (Papercut for SMS) (#258)

* ROCK-8756: add org.secc.SmsCapture project with CapturedSms entity and migration

New plugin project for the DEV-only SMS capture transport. Kept in its own
assembly (not org.secc.Communication) so the DLL never has to ship to prod.
CommunicationId/CommunicationRecipientId stored without FK constraints so
communication cleanup jobs are never blocked by capture rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ROCK-8756: add SMS Capture transport component

Mirrors the v13 Twilio transport structure (both Send overloads, per-recipient
RockContext cadence, recipient status/History side effects) but writes a
capture row instead of calling the Twilio API. Recipients advance to Delivered
with a 'Captured by SMS Capture transport' status note. Sync-only on purpose:
MediumComponent falls back to the sync Send overloads for non-IAsyncTransport
components on every pipeline path. No Twilio SDK, no HttpClient, no outbound
calls. Enable Logging and Max Captured Messages component attributes; oldest
rows beyond the cap are trimmed after each send.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ROCK-8756: add SMS Capture Inbox block

Papercut-style grid over the capture table: date range/to number/body filters,
detail modal with full pre-wrapped body and attachment links, per-row delete,
and a confirmed Clear All action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ROCK-8756: document SMS Capture wiring and post-clone checklist

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ROCK-8756: reload component attributes per send so cap changes take effect

Component.GetAttributeValue reads the in-memory AttributeValues snapshot
loaded when the MEF singleton was constructed at app start. Editing Max
Captured Messages in the admin UI only updates the singleton on the node
that served the edit, so the node running the Send Communications job kept
reading the stale default (5000) and never trimmed. Reload attributes at
the start of both Send overloads (one cheap lookup per send call), and log
the trim result so the cap is observable in the Rock log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* ROCK-8756: guard against null recipient on requery (Copilot review)

If the recipient row vanished between GetNextPending and the per-recipient
requery, the NRE in the try would be followed by a second NRE in the catch
(recipient.Status assignment), aborting the send loop and stranding the
remaining recipients as Pending. Return early instead. Pattern inherited
from core Twilio transport, which has the same latent issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ROCK-8756: trim cap enforcement in SQL instead of EF (Copilot review)

Materializing excess rows pulled every over-cap nvarchar(max) body into
memory and deleted row-by-row; a ROW_NUMBER CTE delete does it in one
statement with no materialization. ExecuteSqlCommand's affected-row count
feeds the existing trim log line. Verified against LocalDB: 150 rows with
cap 100 deletes the 50 oldest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ROCK-8756: make inbox date filter end date inclusive

DateRangePicker is date-only and DateRange.FromDelimitedValues parses the
end date to midnight, so filtering CreatedDateTime < End excluded the
entire last selected day — a range ending today hid everything captured
today, which is the tool's primary workflow. Add a day and keep the
exclusive comparison.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Minor check: use AgeClassification == Child to catch no-DOB minors

Age.HasValue dropped child-role members without a birthdate; Rock
classifies them Child regardless. Businesses stay Unknown (classifier
skips them), so == Child never misfires on them. Applies to both the
Communicate guard and the roster IsMinor/parent-info logic.

* ROCK-8696: address review — README drift, one-time stale-DLL cleanup

- README Overview: "~29" -> "~27" actions, drop "media processing" (27
  ActionComponent exports remain; the sole surviving Media action,
  BinaryFileRemove, deletes files rather than processing media).
- README: add a "Last updated" marker per the org.secc.Attributes
  convention.
- csproj PostBuildEvent: sweep stale Magick.NET/FFmpeg.NET DLLs out of
  RockWeb/bin. xcopy never deletes, so the vulnerable binaries this
  ticket and ROCK-8672 removed from source stay loaded on any
  environment that built the prior code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* ROCK-8752: Address Copilot review — null guards + reuse filtered list

- LoadSettings: materialize the active-values list once and reuse it for
  both the filter dropdown and the grid rows (single predicate, no drift);
  guard an unresolved defined type by skipping the partition.
- SetUpDefinedTypeDynamicControls: return early when no defined type is
  selected ("Select One" passes Guid.Empty and DefinedTypeCache.Get
  returns null).
- GetTree fallback: treat an unresolved defined type as zero values
  instead of throwing on a misconfigured partition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8706: narrow Attendance by occurrence before the member/employee OR in LoadHosts

LoadHosts() built hostsQry by GroupJoin-ing Attendance to the guest-count
subquery first, then applying the occurrence/DidAttend/EndDateTime/
member-or-employee/note Where clauses against the joined projection. EF6
generated a plan that ran the member-ConnectionStatus-OR-employee-subquery
predicate over the full ~9.4M-row Attendance table before narrowing by
occurrence, producing the reported 10-15s hang.

Move those five Where clauses onto attendanceService.Queryable() directly,
before the GroupJoin, so occurrence narrows the row set first. The GroupJoin/
DefaultIfEmpty guest-count join is unchanged (a correlated subquery there
re-runs the ValueAsPersonId scalar UDF per row and was measured at 58s).

DEV-verified on 2026-01-05 sample data: old and new shapes return identical
row sets (458/458, GuestSum 1159=1159). New shape measured at 310ms;
reproducing the pre-fix full-table-scan shape measured 16,454ms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* ROCK-8752: Remove unreachable null guard in LoadSettings

The guard added in 371bffe3 duplicated a pre-existing check at the top of
the DefinedType partition block (L469-472), which already breaks out of
the partition loop when the type doesn't resolve — so the new guard could
never execute, and its comment misdescribed the actual behavior. The
materialized definedValues list (Copilot's actual ask) is kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8696: revert PostBuildEvent stale-DLL cleanup — doesn't reach prod

The Rock13 Azure DevOps release pipeline deploys via IIS Web App Deploy
with RemoveAdditionalFilesFlag=false (additive-only sync), and the build
runs on a disposable Microsoft-hosted agent. A del in the csproj's
PostBuildEvent only ever cleaned that throwaway agent's checkout — it
had no path to the actual Dev/Datably/Production bin folders, which
keep whatever a prior release deployed regardless of what's in the new
build. Dropping it; the stale Magick.NET/FFmpeg.NET DLLs need a manual
delete on each environment (see JIRA for steps).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* ROCK-8710: Clear phFormAttributes before re-add + guard Form-key controls to first role (fixes Add Another duplication)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8706: add AsNoTracking() to LoadHosts attendance query

The host query in LoadHosts() projects into SportsAndFitnessHost for
display and never saves changes, matching its three sibling queries in
the same method (attributeValueQry, hostsGuests, employees) which all
use AsNoTracking(). Adding it here avoids change-tracker overhead on the
result set, in line with the ROCK-8706 performance goal. (Copilot review
note on PR #262.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8706: decouple guest counts + batch occurrence lookup in LoadHosts

Fix B (guest-count tail): the guest-count aggregation was folded into the
attendance query via GroupJoin, which EF6 rewrote as a per-attendance-row
correlated subquery over the ValueAsPersonId scalar UDF — tail-spiking to
28.7s in Query Store. Run it as its own query materialized into a
Dictionary<int,int> (~87ms on DEV) and stitch GuestCount onto each host
after the attendance query materializes.

Fix A (occurrence N+1): the active-occurrence lookup made one
AttendanceOccurrenceService.Get() round trip per active (group, location,
schedule) triple. That Get(DateTime,int?,int?,int?) overload is read-only
(Queryable().FirstOrDefault(), no create/save) and
IX_GroupId_LocationID_ScheduleID_Date is UNIQUE on
(GroupId, LocationId, ScheduleId, OccurrenceDate), so the loop collapses to
a single query (three IN-lists) intersected against the exact triple set in
memory — provably identical result, one round trip instead of N.

Behavior-preserving: same attendance filters, same ordering, same per-host
guest counts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8706: decouple the employee lookup in LoadHosts (Fix C)

The member/employee OR left "employees.Contains(PersonId)" as an IQueryable
subquery, which EF6 compiled into a correlated EXISTS re-evaluated per
attendance row. AttributeValue.Value is nvarchar(max) (not indexable), so
each evaluation seeked attribute 740 and residual-filtered the string over
~11k rows -> ~2M row reads, the single largest operator in the Query Store
plan (subtree cost 2.42 of 3.01). The scalar UDF in the guest-count subquery
also forced the whole plan single-threaded (TSQLUserDefinedFunctionsNot-
Parallelizable) and the optimizer timed out.

Materialize the ~676 employee person ids ONCE into a list so the OR becomes a
constant IN-list evaluated a single time. Combined with Fix B (guest-count
decoupling, which removes the UDF and unlocks parallelism) the remaining
attendance query is a clean occurrence seek -> person lookup.

Behavior-preserving: same employee id set (same AttributeId=740 + Value
predicate), null EntityIds filtered (a null could never match a non-null
PersonId), OR unchanged -> identical host rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8790: Age-gate specialty roles — evaluate Group Requirements at signup

In VolunteerSignupFormConnections.btnConnect_Click, after the assigned
group + role are known for each ConnectionRoleRequest and before
rockContext.SaveChanges(), evaluate the role's Group Requirements for the
person via Group.PersonMeetsGroupRequirements(). If any requirement is
NotMet, surface the requirement's negative label inline (lResponseMessage),
keep the signup panel visible, and return before SaveChanges() so no
ConnectionRequest or downstream membership is created for an ineligible
volunteer. The person prospect record already exists (committed earlier)
and is intentionally left in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8790: address verifier review — add-time filter, fail-closed, use entered DOB

Verifier fixes on the signup Group Requirement gate:
- Fix 1: only block on requirements Rock enforces at add-time
  (GroupRequirement.MustMeetRequirementToAddMember && RequirementCheckType
  != Manual), so a Manual/background-check requirement no longer
  blanket-blocks every new signup.
- Fix 2: rockContext.SaveChanges() before evaluation so a matched existing
  person's form-entered birthday (not their stale on-file DOB) is what the
  requirement evaluates.
- Fix 3: fail closed — block on MeetsGroupRequirement.NotMet OR Error, not
  just NotMet.
- Fix 4: wrap PersonMeetsGroupRequirements in try/catch; on throw, log via
  ExceptionLogService and block with a generic message (fail closed) instead
  of surfacing an unhandled error page.
- Fix 5: render the block message as alert-danger (red) instead of
  alert-warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8790: prefer on-file birthday for age check, never overwrite from form

Stakeholder-approved change to the signup birthday rule:
- A matched person's existing on-file birthday is now PRESERVED (never
  overwritten from the signup form); the age check evaluates it as-is.
- Only fill BirthDate from the form (bpBirthdate) when the person has none
  on file — brand-new person, or matched person whose BirthDate is null —
  as a fallback so the age check has a value.
- Guard changed from 'SelectedDate != person.BirthDate' to
  '!person.BirthDate.HasValue'; the pre-evaluation SaveChanges persists a
  newly-filled fallback birthday so PersonMeetsGroupRequirements reads it.

Shared-block behavior change: applies to ALL signups through this block
(Meal Packers, etc.), not just specialty. Accepted tradeoff (stakeholder):
the gate trusts the on-file birthday even if it is wrong.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8790: make signup requirement gate opt-in per requirement type

This block is shared across ~30+ signup instances, so an unconditional
gate would wrongly hard-block opportunities that only intend to pass their
Group Requirements through onto the connection request.

- New [TextField] block setting 'Signup-Blocking Requirement Types'
  (key SignupBlockingRequirementTypes, order 15), declared in the same
  style as the existing UrlKeys/FormKeys settings.
- Parse the setting (split on comma, Trim, drop blanks). Empty => skip the
  gate entirely, so every other instance is unaffected by default and its
  requirements still pass through onto the connection request.
- Gate now blocks only when GroupRequirementType.Name is in the configured
  allowlist (case-insensitive) AND RequirementCheckType != Manual AND
  (NotMet OR Error). Dropped the MustMeetRequirementToAddMember filter — the
  explicit name allowlist is the intent control now; kept the non-Manual
  guard so a mistakenly-listed Manual type can't blanket-block new signups.

Specialty instance must set the value to 'Over 18 Years Old, Over 14 Years
Old' (config/recipe, not code). Name-match means renaming a requirement type
requires updating the setting — same tradeoff as UrlKeys/FormKeys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8790: move block-gate message above submit buttons and personalize it

UX tweaks to the signup requirement gate (message location + wording only;
gate logic, birthday rule, setting, and commit-before-evaluate unchanged):

- .ascx: add <asp:Literal ID="lRequirementBlockMessage"> inside pnlSignup,
  just above the actions div, so the error shows directly above the Connect
  buttons instead of at the top of the block.
- Render the block message on lRequirementBlockMessage instead of
  lResponseMessage (top literal stays reserved for the normal connect
  response flow).
- Personalize: "[NickName] isn't eligible for the [Role] role — [reason]."
  Name = person.NickName (fallback FirstName); role resolved via
  GroupTypeRoleService (fallback "this role"); reason = requirement type's
  Negative Label (fallback Name). Multiple failing requirements lead with
  "... isn't eligible for the [Role] role:" then a <br/>-joined list.
- Fail-closed evaluation-error message stays generic and renders in the same
  spot: "We couldn't verify [Name]'s eligibility for this role...".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8706: fix CS8059 — RockWeb runtime-compiles this .ascx.cs as C# 6

The Fix B guest-count stitch loop used an inline out-variable declaration
(`out int guestCount`), a C# 7 feature. RockWeb compiles plugin .ascx.cs
code-behind at runtime under C# 6, which rejects it (CS8059: "Feature 'out
variable declaration' is not available in C# 6"), so the block failed to
load. Declare guestCount before the loop and pass `out guestCount`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8706: hand occurrence IN-lists to EF as List, not HashSet

Fix A built activeGroupIds/activeLocationIds/activeScheduleIds as HashSet<int>
and used .Contains() inside the EF6 IQueryable. EF6 translates List<T>.Contains
to SQL IN but does NOT reliably translate HashSet<T>.Contains (throws
NotSupportedException at query execution — a runtime page-load failure, not a
compile error). Materialize the three to List<int> before the query (dedup is
still done by the HashSet build). Consistent with the List used for employeeIds
in Fix C. The activeTriples HashSet is unaffected — it's used after ToList()
(in-memory LINQ), not translated to SQL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8790: HTML-encode name/role/label in the eligibility-block message

Copilot follow-up: the eligibility-block message builds an alert-danger HTML
string with data-derived values interpolated in. HTML-encode each so a name,
role name, or requirement label containing &, ', or < can't break the markup:
- registrant name (person.NickName / FirstName) — covers both the not-met
  message and the fail-closed catch message,
- the resolved role name,
- each requirement label (NegativeLabel / Name) in the blocking-messages list.

Uses Rock's null-safe .EncodeHtml() string extension (in scope via using
Rock;, already used elsewhere in this file); no using System.Web added.
Literal markup in the format strings (div wrapper, <br />) is left un-encoded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8790: revert form DOB on block, clear stale message at handler top, note nothing submitted

Fix A (DOB re-entry trap): a birthday filled from the form this request was
committed by the pre-loop SaveChanges, so on a blocked submit a typo'd birth
year stuck (!person.BirthDate.HasValue was false on resubmit). Track whether
we filled BirthDate from the form this request (new-person SetBirthDate path;
matched-person fallback fill) and, on any blocking exit (not-met and the
fail-closed catch), revert it to null so a corrected resubmit re-reads the
form value. Revert runs on a SEPARATE RockContext (RevertFormBirthDate) so it
commits only the birthday clear — never the ConnectionRequests added earlier
in a multi-role loop on the main context, keeping a blocked submit
all-or-nothing. An existing on-file birthday is never reverted.

Fix B: clear lRequirementBlockMessage (Text/Visible) at the very top of
btnConnect_Click so a stale block message can't linger via ViewState on
'Connect and Add Another'. Done at the handler top, not in the gate (the gate
is skipped entirely when the setting is blank).

Fix C: append 'No signup was submitted.' to the single-, multi-requirement,
and fail-closed messages so it's clear the whole submit was cancelled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ROCK-8700: Fix double check-in caused by redundant Add after AddOrUpdate (#264)

Rock core's AttendanceService.AddOrUpdate returns the existing tracked
attendance when one exists for the same person + occurrence. The extra
attendanceService.Add() flipped that entity to Added, so EF inserted a
duplicate row and dropped the update closing the old attendance, leaving
two active attendances (double check-in).

- SaveAttendance.cs: remove the redundant Add (AddOrUpdate already adds
  new records); existing records now update in place.
- DataHelper.cs (RoomScanner): comment only. The identical-looking Add in
  CloneAttendance is intentional clone-by-readd and must not be removed.

* ROCK-8801 Fix Mobile Check In Bug (#266)

* ROCK-8801 Rework MCRValidForKiosk to validate against persisted attendance data

QuickCheckin crashed with a NullReferenceException in MCRValidForKiosk when a
mobile-reserved room was closed (its GroupLocationSchedule detached) between the
reservation and the family's arrival at the kiosk. The occurrence could no longer
be resolved from OccurrenceCache, so occurrence.GroupTypeId threw and took down the
whole kiosk page.

Validate the reservation directly from the persisted Attendance -> Occurrence ->
Group records instead of OccurrenceCache. This matches the completion path
(btnCompleteMCRActual_Click), which already works off the attendance records and
never touches the cache, so a closed room no longer blocks or crashes check-in and
the family completes their original reservation without losing their spot.

* ROCK-8801 Add missing System.Data.Entity using for AsNoTracking

* ROCK-8801 Address review feedback on MCRValidForKiosk

- Collapse validation to a single SQL-side Any() query and skip the
  DB round-trip entirely when the kiosk has no group types configured
- Trim the rationale comment to the essentials
- Document mobile-reservation validation behavior and the room-closure
  edge case in the FamilyCheckin README

* Fix README Last updated formatting to match repo convention

* Update formatting

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* ROCK-8856 Event Pass: group registrations for the logged-in viewer (#267)

* ROCK-8856 Event Pass: add Include Family Registrations option

When enabled, the block expands the pass to include registrants from
other registrations in the same registration instance made by members
of the registrar's family, deduped by person. Defaults to off so
existing event passes are unchanged. Supports camps where each child
is registered individually but families expect one swipeable pass.

* ROCK-8856 Event Pass: hide carousel arrows when only one pass

* ROCK-8856 Event Pass: key registration grouping to the logged-in viewer

Replaces the registrar-family expansion with a viewer-keyed model:
anonymous visitors see exactly what the Registration/Registrant guid
names, and a logged-in viewer also sees registrants from other
registrations in the same registration instance made by the viewer or
their family. This removes the access-widening where one registrant
guid could unlock other people's pass QR codes (person-level check-in
credentials), plus:

- waitlist filter applies before anchoring, so a waitlist-only link
  stays "Pass Not Found" instead of rendering family passes
- registrants without a person alias are excluded (previously crashed
  the page in the per-registrant loop / DataBind)
- null-registrar registrations no longer throw during anchor
  materialization (Registration.PersonAliasId is nullable)
- a ?Registrant= deep link orders that registrant first, so the
  carousel opens on the requested pass
- one pass per person via order-preserving DistinctBy
- per-registrant PersonSearchKey N+1 replaced with one batched query
  with a deterministic lowest-Id winner
- block setting renamed to "Include Registrar's Registrations"
  (IncludeRegistrarRegistrations); environments that deployed the
  earlier branch build should delete the orphaned
  IncludeFamilyRegistrations attribute and re-enable the new setting

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ROCK-8856 Event Pass: collapse registrar expansion into one parameterized query

Review cleanup on PR #267:
- Single registrant query replaces the anchor/family/main round trips;
  eligibility filters (person alias, waitlist) now exist in one place.
- Family membership and anchor checks compose as EXISTS/subqueries instead
  of literal IN lists, so every request reuses one cached SQL plan.
- Anchor rows outran…
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.

3 participants