- Rails only applies the last callback when `after_create_commit` and
`after_update_commit` reference the same method name [1]:
> However, if you use the `after_create_commit` and the
`after_update_commit` callback with the same method name, it will only
allow the last callback defined to take effect, as they both internally
alias to `after_commit` which overrides previously defined callbacks
with the same method name.
- Push notifications were never sent when a notification was first
created — only when the source was updated
- Replaced the two callbacks with a single `after_save_commit`, which
fires on both create and update, with the
`source_id_previously_changed?` guard (true for both new records and
source changes)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
[1] https://guides.rubyonrails.org/active_record_callbacks.html#aliases-for-after-commit
Revert "Add bridged share button to board and card permas"
This reverts commit 069e165b43.
Revert "Move helper to bridge helper file"
This reverts commit 1ee37f046c.
The stemmer was concatenating tokens across hyphens (e.g. "BC3-IOS-1D8B"
→ "bc3ios1d8b") while the query sanitizer split on them, causing MySQL
MATCH AGAINST to never find the indexed token. Replace non-word chars
with spaces instead of stripping them so indexing and querying tokenize
consistently.
Requires search:reindex after deploy to rebuild existing search records.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ZipKit::FileReader raises MissingEOCD for truncated, corrupted, or
non-zip files. This exception is a direct StandardError subclass, not
a subclass of InvalidStructure or ReadError, so it escaped as an
unhandled job failure instead of being caught and surfaced to the user.
Broaden the rescue in ZipFile::Reader#initialize to catch ReadError
(parent of InvalidStructure), MissingEOCD, and UnsupportedFeature.
Rails now handles the insecure context case natively in
verified_via_header_only? — when Sec-Fetch-Site is missing and the
request is plain HTTP without force_ssl, the request is allowed.
Remove the now-redundant allowed_insecure_context_request? method and
update the test to set ActionDispatch::Http::URL.secure_protocol
alongside Rails.configuration.force_ssl so the upstream check works
correctly in the test environment.
Follow-up to #2550 which added a created_at index and increased cleanup
frequency. The delete itself was still unbatched and vulnerable whenever
a backlog accumulates (first run after deploy, clock skew, etc).
Delete in small batches with a pause between each to let other
transactions through, following the SolidQueue pattern.
When concurrent NotifyRecipientsJobs process an Event and a Mention for
the same user+card, Rails' dirty tracking can skip writing source_type
in the UPDATE if it hasn't changed from the stale in-memory value,
leaving source_type and source_id mismatched (e.g. source_type='Event'
with a Mention's source_id, resulting in a nil source).
Force source_type to always be included in the UPDATE via
source_type_will_change! to prevent this.
Here's a sample timeline of this race condition happening in the real
world (with simplified IDs):
Two `NotifyRecipientsJob` involving notification `03fklpu`, same card,
same user, same comment — enqueued within 50ms of each other:
1. Both jobs load notification `03fklpu` — it has source_type='Mention'
(from a previous job)
2. `EventNotifier` writes at 21:22:36.051: ```sql SET
source_type='Event', source_id=<event_id>, unread_count=1 ```
3. `source_type` included because it changed ('Mention' → 'Event')
4. `MentionNotifier` writes at 21:22:36.057 (~6ms later): ```sql SET
source_id=<mention_id>, unread_count=1 ```
5. No `source_type`! It was `'Mention'` when loaded and `'Mention'` is
what it's setting → not dirty → skipped
6. Final DB state: `source_type='Event'` (from step 2, untouched),
`source_id=<mention_id>` (from step 3)
`Notification.source` now does `Event.find(<mention_id>) → nil.`
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The convert_gids_to_sgids method resolved GIDs globally and minted
valid SGIDs without verifying the record belongs to the importing
account. This adds the same account_id check that the export path
already has, plus RecordNotFound handling for invalid GIDs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>