Commit Graph

1738 Commits

Author SHA1 Message Date
Alp Keser 610cf7a190 Merge pull request #2398 from basecamp/notifications-tray-api
Add JSON response support for `notifications/tray`
2026-02-18 13:19:59 +03:00
Stanko Krtalić 4b2586769b Merge pull request #2556 from basecamp/fix-notifications-staying-unread-on-card-visit
Reinstate card readings
2026-02-17 14:15:24 +01:00
Stanko K.R. 9d4db14fc3 Reinstate card readings 2026-02-17 13:57:24 +01:00
Jeremy Daer a253c31dee Batch webhook delivery cleanup to prevent DB lock contention (#2555)
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.
2026-02-16 15:31:10 -08:00
Rosa Gutierrez 95114b1d67 Fix race condition between concurrent Event and Mention notifier jobs
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>
2026-02-16 16:50:46 +01:00
Alp Keser a48c88cb5d Stabilize notifications tray test ordering via fixture updated_at timestamps 2026-02-16 18:33:32 +03:00
Alp Keser bcc0f93bb9 Preload notification card associations used by JSON responses 2026-02-16 17:58:33 +03:00
Rosa Gutierrez b742963f7c Add account ownership check to import GID resolution
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>
2026-02-16 11:49:00 +01:00
Mike Dalessio dc492ef17e Merge pull request #2540 from basecamp/fix-quotes
Fix double-escaped HTML entities in basecamp and campfire webhook payloads
2026-02-13 09:26:00 -05:00
Mike Dalessio 0c10622672 Fix double-escaped HTML entities in webhook payloads
`Event::Description#to_plain_text` now returns an html-safe string to
prevent double-escaping when the ERB template renders the basecamp
and campfire payloads.

ref: https://3.basecamp.com/2914079/buckets/27/card_tables/cards/9574495379
2026-02-13 03:25:59 -05:00
Stanko K.R. ae3d277868 Fix race condition on notification creation 2026-02-13 09:14:24 +01:00
Alp Keser abcbb74ac8 Make notification ordering deterministic. 2026-02-12 15:32:10 +03:00
Stanko K.R. 36ee253a1a Stack notifications everywhere
We had client-side notification stacking in the tray since launch, but now we want to stack notifications in the notifications page, in API responses and in email bundles.
2026-02-12 10:29:50 +01:00
Stanko K.R. 37d7f5c569 Give more detail about why the import failed 2026-02-11 13:47:21 +01:00
Mike Dalessio d611b2cc6c Move comment events when card moves to a new board (#2486)
When a card moves between boards, comment events were staying on the
old board. Now they move with the card.
2026-02-04 15:04:28 -05:00
Mike Dalessio 7f473eda66 Validate ActionText associations during import check phase
ActionTextRichTextRecordSet now calls check_associations_dont_exist to
ensure imported ActionText records only reference records within the
same import, matching the behavior of the parent RecordSet class.
2026-02-03 13:46:28 -05:00
Stanko K.R. 1c1a73c57c Check if the file exists before adding it
ZipKit creates a placeholder record when add_file is called, this record is then filled with the content that's passxed to write. But if the blob's file doesn't exist then an error is rased and the placeholder is never filled. This results in a ZIP that has a place in its catalog for the blob, but no contant for it, which makes it invalid.
2026-02-03 15:01:10 +01:00
Stanko K.R. ffce51a42f Ignore faulty ZIP files 2026-02-03 14:04:54 +01:00
Stanko K.R. a4bec6e407 Ignore missing attachments 2026-02-03 13:51:22 +01:00
Stanko K.R. b497aa07e9 Fix crash due to missing ActiveStorage URL options 2026-02-02 21:49:06 +01:00
Stanko K.R. e587cb6a2b Capture backtrace of nested errors 2026-02-02 21:37:37 +01:00
Stanko K.R. 5eb15ebf4d Remove debug code 2026-02-02 21:33:48 +01:00
Stanko K.R. e0c3e09aa9 Add more debug info 2026-02-02 21:17:43 +01:00
Stanko K.R. d0c6253716 Pass all service options 2026-02-02 20:06:45 +01:00
Stanko K.R. 88d84b3eaa Prevent duplicate files in user data export 2026-02-02 19:58:57 +01:00
Stanko K.R. 4b7bbda222 Write in binmode 2026-02-02 19:46:20 +01:00
Stanko K.R. 4b70c1cc3b Replace custom IO object with a transport manager IO 2026-02-02 19:31:18 +01:00
Stanko K.R. 77d9331d90 Add missing method to IO object 2026-02-02 19:06:37 +01:00
Stanko K.R. 5c929238b7 Rename jobs 2026-02-02 18:22:07 +01:00
Stanko K.R. b06d95a20d Delete accounts for failed imports 2026-02-02 16:24:10 +01:00
Stanko K.R. 539cd638e2 Check that associations don't exist
Without this you can change a belongs to to some other account's record and get access to it
2026-02-02 16:03:50 +01:00
Stanko K.R. dca67c59ca Bust the cache after import
Without this exporting, deleting and then re-imporing an account would reuse the old cache entries since the IDs are the same, but those cache entries will likely have the old account ID and would break
2026-02-02 15:48:49 +01:00
Stanko K.R. 1bc1b68ddf Explicitly use the default storage service 2026-02-02 15:22:16 +01:00
Stanko K.R. 60c63271b9 Fix ActiveStorage writing to disk 2026-02-02 15:16:37 +01:00
Stanko K.R. ac7d0308ac Add size to remote io 2026-02-02 14:09:04 +01:00
Stanko K.R. a70bbf472b Fix crash on account destruction 2026-02-02 14:08:52 +01:00
Stanko K.R. edb676cd2c Make remote IOs rewindable
This is required for active storage to work
2026-02-02 13:52:14 +01:00
Stanko K.R. d7dcf64310 Remove model validation 2026-02-02 13:38:34 +01:00
Stanko K.R. 54a7ecea98 Fail the import if the check fails 2026-02-02 13:33:06 +01:00
Stanko K.R. 2dbbeffdcf Fix SSL issues when reading remote ZIP files
This is caused by our self-signed PureStorage certs. Once that's taken care of this commit can be reverted.
2026-02-02 13:16:59 +01:00
Stanko K.R. ccbf571be4 Fix crash on commentable check 2026-02-02 12:51:46 +01:00
Stanko K.R. de69d2e6fe Add import cleanup 2026-02-02 12:36:48 +01:00
Stanko K.R. 5bbd7f633d Process everything in chunks
The old implementation loaded files into memory to provide an IO interface. This has the obvious downside of loading any file included in the import, e.g. a 10GB video file, into memory. ZipKit has no native IO object for reading but it provides all the necessary methods to implement one.
2026-02-02 12:36:48 +01:00
Stanko K.R. dd13a3b87c Style 2026-02-02 12:36:48 +01:00
Stanko K.R. 774a44f800 Tweak email copy 2026-02-02 12:36:48 +01:00
Stanko K.R. a791fe1919 Hide importing accounts 2026-02-02 12:36:48 +01:00
Stanko K.R. 27c73ff42b Rename validate to check
Validate is an ActiveModel method, overriding it could have unintended side-effects so I opted to rename it
2026-02-02 12:36:48 +01:00
Stanko K.R. 0240ffca34 Fix import validation failure
During import, card return nil since it doesn't exist yet
2026-02-02 12:36:48 +01:00
Stanko K.R. b659e326e1 Name the export ZIPs differently 2026-02-02 12:36:48 +01:00
Stanko K.R. 992f15066b Replace RubyZip with ZipKit
ZipKit can read and write files directly from S3 which makes both imports and exports way more efficient in terms of disk usage.
2026-02-02 12:36:48 +01:00