* Add self-service account deletion
* Disable access to cancelled accounts & implement Stripe interactions
* Add tests
* Fix failing tests
* Remove cancelled accounts from lists
* Fix incorrect redirect
* Use _path instead of _url for consistency
* Don't track how far the inicieration job got
We still want the step tracking so that the job can be interrupted. But, since the scope is idempotent, we don't need to track how far it got.
* Specify the exact time when the data will be deleted
* Fix crash due to unadvancable cursor
* Rename up_for_incineration to due_for_incineration
* Regenrate the schema
* Fix incorrect path check
* Migrate the SQLite schema
* Only show the cancel button on cancellable accounts
* Check that a subscirption method exists before calling it
* Ignore job failures due to missing records when an account gets deleted
* Skip sending notifications on cancelled accounts
* Use collbacks to integrate
* Add a blank line between queue_as and discard_on
* Break checks into methods
* Inline methods
* Rename Account::IncinerateJob
* Run migrations
* Migrate SQLite
- Split each CJK character into individual tokens for FTS5 indexing
- Add stem_content callback to SQLite adapter to pre-process content
- Stem search queries before matching against FTS5 index
- Update stemmer tests to reflect character-splitting behavior
This enables CJK search on SQLite installations where the FTS5
tokenizer cannot natively segment CJK text.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The search functionality was silently dropping CJK characters because:
1. Query sanitization used `\w` which only matches ASCII word characters
2. Stemmer split by whitespace, which doesn't work for CJK languages
3. Highlighter used `\b` word boundaries that don't apply to CJK
This commit fixes all three issues:
- Query: Use `\p{L}\p{N}` (Unicode letters/numbers) instead of `\w`
- Stemmer: Preserve CJK characters as-is without stemming, since CJK
languages don't have stemming rules like English
- Highlighter: Skip word boundary matching for CJK terms
Also extracts `CJK_PATTERN` to `Search` module to avoid duplication.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Use the more idiomatic ActiveRecord ids method instead of pluck(:id)
across controllers and models. The ids method is more readable and
explicitly conveys the intent to retrieve primary key values.
Changes:
- BoardsController#edit: Use @board.users.ids
- Board::Storage: Use cards.ids, Comment.where().ids, and ActionText::RichText.where().ids
- User::Accessor: Use account.boards.all_access.ids
This change improves code clarity while maintaining the same functionality.
The SSRF filter checked ipv4_mapped? but not ipv4_compat?, allowing
addresses like ::169.254.169.254 to bypass the link-local check and
reach cloud metadata endpoints.
Changes:
- Add ipv4_compat? check to block deprecated IPv4-compatible format
- Rename private_address? to blocked_address? (more accurate - method
blocks more than just RFC 1918 private ranges)
- Add IPv6 test coverage for both mapped and compat formats
Both ipv4_mapped and ipv4_compat formats are blocked entirely as
defense-in-depth: DNS never returns these formats, so they only
appear in attack scenarios.
HackerOne: #3481701
Avatars are purposely accessible without authentication
(5e3b5b6d7c) because they can be in public
collections. Trying to restrict this by checking whether they're in fact
present in some public collection is rather expensive, so let's keep
them public.
- Update `Card::Closeable` to destroy associated "not now" state during close.
- Add new tests to verify closure behavior for different column types, including "not now".
* Storage: harden reconcile for concurrent writes and fix board transfer
Reconcile now uses two-cursor approach: captures cursor before and after
the storage scan, aborting if they differ (entries added during scan).
Job retries 3x with 1-minute waits and limits concurrency to 1 per owner.
Board transfer now correctly moves storage for card description embeds
and comment embeds, not just direct attachments. Uses batched queries
to handle cards with thousands of comments efficiently.
Also fixes N+1 queries in attachment grouping via lookup maps.
* Storage: fix per-attachment reconcile and enforce no blob reuse
The storage ledger tracks per-attachment (not per-blob) as a business
abstraction for quotas. This fixes reconcile to match that model and
adds enforcement to prevent blob reuse in tracked contexts.
* Reconcile now uses joins(:blob).sum() for per-attachment counting
* New validation prevents reusing blobs across tracked attachments
* Race-safe storage_total creation with create_or_find_by
* Job efficiency: skip find_by when object already available
* Backfill script checks per-attachment, not just per-blob
As the tests for it could lead to confusion where it seems drafted cards
are not accessible to someone with access to the board and with the
direct drafted card URL.
Addresses an issue where User#familiar_name assumed `name` was always
present, potentially raising an exception during view rendering. Now
User validates name presence, and User#familiar_name handles blank
strings without error, in case any existing invalid records exist.
* main:
Use decimals for ordered lists
Update cached fragments
Revert "Merge pull request #1865 from basecamp/public-avatar-caching"
Show only public cards on public boards
Swap order of avatar links
Limit length of full name during signup
Make layout bulletproof
Serve own avatar from its own endpoint
If we don't validate for length, then signups that overflow the database
columns will unnecessarily create and cancel a tenant. Adding a
validation means we can avoid this.
* main: (119 commits)
Bust comment view cache
Update lexxy to bring fixes from https://github.com/basecamp/lexxy/releases/tag/v0.1.24.beta
Fix unexpected remove empty line from README
Lightbox uses Stimulus target callbacks instead of data-action
Add test coverage for the image lightbox
Add tests for tenancy middleware and timezone cookie
Refactor: Simplify TimeWindowParser using Rails convenience methods
SMTP: support SMTPS on port 465 (#2132)
Bump fizzy-saas to retain fewer docker images (#2134)
Add test coverage for with_golden_first scope (#2130)
Refactor: improve query scope composition with merge syntax (#2131)
Validate avatar sizes
Introduce Vips configuration
Tailscale serve support (#2126)
Update tests with final method naming: record! -> record
CSP config to allow Minio in development
Update fizzy-saas to get employee restriction in staging
Drop staff restriction in beta and staging
Unused
Use new FIZZY_GH_TOKEN with limited access
...
Replace all occurrences of reverse_merge with with_defaults across the codebase.
The with_defaults method provides clearer intent and better readability when
setting default values for hash parameters.
Changes:
- app/helpers/columns_helper.rb: Update column_frame_tag method
- app/models/user/email_address_changeable.rb: Update generate_email_address_change_token method
- app/models/account.rb: Update create_with_owner method
- app/controllers/concerns/filter_scoped.rb: Update filter_params method
This refactoring maintains the same functionality while improving code clarity.
- Replace `beginning_of_day..end_of_day` with `all_day`
- Replace `beginning_of_week..end_of_week` with `all_week`
- Replace `beginning_of_month..end_of_month` with `all_month`
- Replace `beginning_of_year..end_of_year` with `all_year`
These changes improve code readability by using idiomatic Rails
methods that accomplish the same thing in a more concise and
expressive way.
* Refactor: improve query scope composition with merge syntax
Replace manual WHERE clause concatenation with Rails' merge method
for more elegant and maintainable scope composition across Card,
Comment, and Filter models. This approach better follows Rails
conventions and improves code readability.
* Extend scope composition improvements to Card::Closeable
Apply the same nested hash syntax pattern to closures table references
in order and where clauses.
* Remove unnecessary outer braces from where clause
---------
Co-authored-by: Jeremy Daer <jeremy@37signals.com>
Replace SQL string syntax with Rails range syntax for date filtering
in the ActivitySpike::Detector. This improves code readability and
follows Rails idioms.
Changed from:
.where("created_at >= ?", recent_period.seconds.ago)
To:
.where(created_at: recent_period.seconds.ago..)
This modernizes the codebase while maintaining the same functionality.
Simplifies the last_event method in ActivitySpike::Detector by using
the more idiomatic Rails pattern .order(:created_at).last instead of
.order(created_at: :desc).first. Both generate the same SQL query but
.last is more readable and conventional in Rails codebases.
* Allow Card#last_updated_at to be set
This is useful when doing an import from another system. I'm currently
working on a script to import our Github issues into Fizzy.
This is discussed in
https://github.com/basecamp/fizzy/pull/2056#discussion_r2609560246
* Add nil fallback and expand test coverage for last_active_at
Adds a safety fallback to Time.current if created_at is unexpectedly nil
during card creation.
Test coverage to verify:
* last_active_at defaults to created_at when not provided
* last_active_at can be updated via API on existing cards
* import workflow where last_active_at is restored after comments
* publishing doesn't overwrite explicit last_active_at values
---------
Co-authored-by: Jeremy Daer <jeremy@37signals.com>