Commit Graph

9698 Commits

Author SHA1 Message Date
Rosa Gutierrez 089fcf94f8 Preload resources after service worker installation
When the service worker is registered for the first time, resources loaded
before it becomes active won't go through the service worker. These resources
may be served from the browser's HTTP cache on subsequent requests, bypassing
the service worker cache entirely.

The new `preload` option accepts a regex pattern. On first visit (when no
controller exists), it waits for the service worker to take control, then
sends a message with URLs from `performance.getEntriesByType("resource")`
that match the pattern. The service worker fetches and caches these resources.
2026-03-10 10:20:54 +01:00
Rosa Gutierrez b93808235c Limit cached entry sizes
1MB and no limit for number of entries except for attachments, where we
limit individual entries to 2MB and total of entries to 500. The number
is based on the following percentiles for Active Storage blobs:

```
p50: 97.1044921875 KB
p75: 236.9140625 KB
p90: 917.7548828125 KB
```
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 9b264483d1 Bump turbo-rails for cache size limits 2026-03-10 10:20:54 +01:00
Rosa Gutierrez 32134be882 Bump turbo-rails for network-first strategy fix on network error 2026-03-10 10:20:54 +01:00
Rosa Gutierrez 90d5a351f0 Bump turbo-rails for Range requests support
Cached video and audio files.
2026-03-10 10:20:54 +01:00
Rosa Gutierrez cf8d92afea Enable offline caching for all web users
Previously, offline caching was conditionally enabled only for Hotwire
Native apps. This removes that restriction and enables offline support
for all users, including PWAs and regular browsers.

This Uses the new `fetchOptions` support in `TurboOffline` handlers
to pass `cache: "no-cache"` for document fetches, which we need
to work around a quite annoying Safari PWA bug
(see https://github.com/basecamp/fizzy/pull/1014).

Also, simplify a bit the cache names and remove the `misc` one.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez a949f1e3ae Clear offline cache when logging out
Adds a logout Stimulus controller that sends a message to the service
worker to clear all cached content when the user logs out. This ensures
that cached data from one user isn't accessible after logout.

The implementation:
- Adds logout_controller.js that posts { action: "clearCache" } to the
  service worker via postMessage
- Updates logout buttons to use the controller on form submission

Also fixes data-turbo placement: moved from button to form element where
it actually takes effect for disabling Turbo Drive form submissions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 975968b50f Only enable offline caching for Hotwire Native apps
This change conditionally renders the TurboOffline caching rules based on
whether the request comes from a Hotwire Native app. Web browsers get a
minimal service worker that only handles push notifications and a simple
document fetch fallback.

Why separate behavior for native vs web?
-----------------------------------------
We want offline caching for Hotwire Native apps (where users expect
app-like offline behavior) but not for regular web browsers.

Why use ERB conditional rendering?
----------------------------------
We explored several approaches to detect Hotwire Native requests in the
service worker:

1. User-Agent detection in fetch handler: Would be ideal, but Android
   WebViews override the User-Agent header with the system default when
   requests are intercepted by service workers, stripping the custom
   "Hotwire Native" identifier.

2. Custom header (X-Hotwire-Native) from native apps: Would require
   intercepting ALL requests at the native level using both
   WebViewClient.shouldInterceptRequest() and ServiceWorkerClient.
   Complex to implement and still incomplete coverage for all request
   types (navigation, resources loaded by HTML).

3. In-memory flag with IndexedDB persistence: Service workers can be
   terminated when idle and restart with fresh state. Reading from
   IndexedDB is async, but the decision to call respondWith() in a
   fetch handler must be synchronous.

4. Separate service worker URLs: Same theoretical churn problem as ERB
   rendering, with more complexity.

Why ERB conditional rendering works in practice
-----------------------------------------------
The main concern with conditional ERB rendering was "churn" — the service
worker constantly updating as different client types fetch it. However,
this only happens when web and native share storage on the same device.

In practice, this is rare because:
- Android native apps use isolated WebView storage
- iOS doesn't support service workers in WebViews (yet)
- Web browsers have completely separate storage

So each context gets its own stable service worker without churn.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 9fc724572e Use new except option for offline rules 2026-03-10 10:20:54 +01:00
Rosa Gutierrez ada4688c4e Take a first stab at offline mode support 2026-03-10 10:20:54 +01:00
Rosa Gutierrez 5d084a46ce Switch to turbo-rails version with offline mode support 2026-03-10 10:20:54 +01:00
Jeremy Daer d33aa8b757 Wrap params coverage nits (#2681)
* Add missing assert_response to signup API test

* Add JSON response format to joins#create
2026-03-09 21:53:43 -07:00
Jeremy Daer a52b6f1c87 Add explicit wrap_parameters to all controllers (#2680)
* Add explicit wrap_parameters to controllers for flat JSON API support

Virtual attributes (has_rich_text, has_one_attached, delegated setters,
ActiveModel attrs) are not in Model.attribute_names, so wrap_parameters
auto-detection silently drops them from flat JSON requests. Add explicit
include: lists matching each controller's permitted params.

* Convert short-form wrap_parameters to explicit include: lists

Defense-in-depth: these controllers only have real-column params today,
so auto-detection works, but explicit lists prevent future regressions
if virtual attributes are added.

* Add flat JSON param tests for all wrap_parameters controllers

17 new tests covering every controller with wrap_parameters, verifying
that flat (unwrapped) JSON payloads are correctly wrapped and processed.
Focuses on virtual attributes that would be silently dropped without
explicit include: lists.
2026-03-09 21:47:05 -07:00
Jeremy Daer 71a1ee7031 Wrap comment params for flat JSON payloads (#2679)
* Wrap comment params for flat JSON payloads

The SDK sends flat JSON ({"body": "..."}) but Rails wrap_parameters
auto-detection misses :body since it's a virtual ActionText attribute,
not in Comment.attribute_names. Explicitly declare wrap_parameters so
params.expect(comment: [...]) works with both wrapped and flat payloads.

* Test comment create and update with flat JSON params
2026-03-09 17:02:08 -07:00
Rob Zolkos 23ac76555b Validate and normalize auto-postpone period to days (#2672)
* Validate and normalize auto-postpone period to days

- Add Entropy::AUTO_POSTPONE_PERIODS and validate auto_postpone_period against the allowed set
- Introduce auto_postpone_period_in_days for forms and entropy update endpoints
- Fall back to index 0 in knob partial when current value isn't in options
- Remove entropy_auto_close_options helper in favor of model constant
- Update tests to use allowed period values

* Use auto_postpone_period_in_days for JSON entropy updates

The JSON API was accepting auto_postpone_period (seconds) which
bypassed the days normalization and validation. Switch to
auto_postpone_period_in_days consistently and add explicit
wrap_parameters so flat JSON params are wrapped correctly for
the virtual attribute.

* Default to account or 30-day fallback when knob value is invalid

* Address PR review feedback for entropy validation

- Fall back to first knob option (index 0) when persisted value isn't in
  the allowed set, preventing nil index errors for legacy values
- Use integer division (1.day.to_i) for consistent day calculations

* Default to account entropy period instead of first option for knob fallback

* Test that default auto-postpone period is in the allowed periods

* Return 422 instead of 500 for invalid entropy auto-postpone values

Rescue ActiveRecord::RecordInvalid in entropy controllers so invalid
auto_postpone_period_in_days values return 422 Unprocessable Entity
instead of raising a 500.

* Fix NoMethodError when board entropy falls back to account default

Use container.account.entropy.auto_postpone_period_in_days instead of
container.account.auto_postpone_period_in_days since Account doesn't
delegate that method.

* Test board entropy fallback to account default for invalid periods
2026-03-09 17:37:56 -04:00
Rob Zolkos 0435db30ba Add JSON response format to webhooks (#2671)
* Add JSON response format to webhooks

Support JSON responses for all webhook endpoints: index, show, create,
update, destroy, and activation. Adds jbuilder views, updates controllers
to use respond_to, and documents the API in docs/API.md.

* Address PR review feedback

- Use distinct board ID in API docs webhook example
- Use render "webhooks/show" in activations controller for consistency

* Address PR review feedback from flavorjones

- Include webhook.board in cache key to avoid stale board data
- Use _url instead of _path for location header consistency

* Fix webhook create test to match Location header format

Use board_webhook_url instead of board_webhook_path since the controller
returns a full URL in the Location header.

* Fix board ID in Location header example in API docs

Use distinct board ID in the Location URL to match the JSON body example.
2026-03-09 17:33:30 -04:00
Mike Dalessio b6ea558de8 Move TrackTrueClientIp middleware into saas engine (#2677)
The True-Client-IP header is set by Cloudflare and is only trustworthy
when behind a Cloudflare proxy. In non-Cloudflare deployments, this
header is attacker-controlled and can be used to spoof IP addresses.

Moving the middleware into the saas engine ensures it only loads for our
Cloudflare-fronted production deployment, not for self-hosted OSS
instances.

GHSA-cpch-9qg2-x8fq
2026-03-09 15:14:17 -04:00
Jeremy Daer 723c8180b4 Add JSON response format for full SDK/CLI coverage (#2675)
* Add JSON response format to void-response controller actions

Add respond_to blocks with JSON format to 14 controllers that
previously only rendered HTML or Turbo Stream responses. JSON
callers get head :no_content (or :created/:ok for push
subscriptions, distinguishing new vs existing).

Covers: board involvements, board/account entropies, column
reordering, card readings, card publishing, user roles, user
avatars, account settings, notification settings, join codes
(with 422 error branch), and push subscriptions.

* Add steps index endpoint with JSON view

Add index action to Cards::StepsController so SDK/CLI callers
can list all steps for a card. Reuses the existing _step.json
partial.

* Add JSON views for paginated card list endpoints

Add show.json.jbuilder for stream, not-now, and closed column
endpoints. No controller changes needed — set_page_and_extract_
portion_from and fresh_when already work format-agnostically.

* Add JSON search endpoint with distinct-card pagination

JSON search paginates distinct Card records (via the existing
Card.mentioning scope with .distinct) rather than Search::Record
objects. This ensures page boundaries, Link headers, and counts
reflect unique cards — no short pages from post-pagination dedup.

ID-match searches return a uniform single-element Card[] array
through the same pagination path.

* Add JSON views for settings and configuration endpoints

Add show.json.jbuilder for account settings (name), join codes
(code, usage_count, usage_limit, url, active), and notification
settings (bundle_email_frequency). Tests for show and update
were added in the void-response commit.

* Add JSON response format to data exports

Create returns 201 with export id, status, and created_at so
callers can poll. Show returns any-status exports for JSON (not
just completed) with a download_url when ready, or 404 for
missing/other-user exports.

* Extend access token JSON API

Add index.json and _access_token.json partial for listing tokens.
Add JSON format to destroy. Include id and created_at in the
create response alongside the existing token/description/permission
fields.

* Fix ambiguous precedence warning in export JSON view

* Assert X-Total-Count pagination header in streams JSON test

* Address review feedback

- Guard steps index against HTML requests (redirect to card)
- Normalize created_at to UTC in access token and export JSON
- Add deterministic ordering (.latest) to search JSON pagination

* Return 406 for HTML requests to steps index instead of redirect

* Add wrap_parameters for flat SDK JSON payload compatibility

Five controllers infer the wrong wrapper key from their class
name, so flat JSON bodies like {"role":"admin"} fail with 400.
Add explicit wrap_parameters declarations:

- Users::RolesController → :user
- Notifications::SettingsController → :user_settings
- Account::JoinCodesController → :account_join_code
- Account::SettingsController → :account
- Boards::EntropiesController → :board

Add integration tests that send flat (unwrapped) JSON payloads
for all seven param-bearing endpoints to prove SDK compatibility.

* Address PR review feedback

- Use 201 Created (not 204) for all create actions: publishes, readings,
  left/right positions, push subscriptions
- Simplify push subscription to always return :created (no 200/201 variance)
- Remove superfluous respond_to block from steps#index
- Merge exports#show into single respond_to block
- Move export download_url conditional out of inline args
- Ensure join code active is explicitly boolean
- Remove extra parens from search controller assignment
- Add blank lines between format blocks for readability

* Return JSON bodies on create actions

Create actions for boards, cards, columns, comments, and steps now
render the resource as JSON (via their show views) instead of returning
empty 201 responses with only a Location header. SDKs expect a JSON
body they can deserialize into the created resource.

* Return JSON bodies on reaction create actions

Card and comment reaction creates now render the reaction as JSON
instead of returning empty 201 responses, consistent with the other
resource-creating endpoints.

* Allow access tokens API without account scope

Access tokens are identity-level (not account-scoped), so the
controller needs to work with bearer token auth and no account slug
in the URL. Skip require_account so the bearer token auth path
can proceed.

Also fix involvement test to send params in JSON body (not query
string) to match real SDK usage.
2026-03-09 11:39:49 -07:00
Rob Zolkos a896b0a9e6 Touch board when publication is created or destroyed (#2674)
Publishing or unpublishing a board didn't bust the `json.cache! board`
fragment cache because `Board::Publication` didn't touch its parent
board. The board's `updated_at` stayed stale, so subsequent GET requests
continued serving cached JSON without `public_url`.
2026-03-09 13:59:57 -04:00
dependabot[bot] 9585d13f5f Bump actions/attest-build-provenance from 3.2.0 to 4.1.0 (#2668)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3.2.0 to 4.1.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v3.2.0...v4.1.0)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 08:58:45 -07:00
dependabot[bot] 17112fa0ee Bump brakeman from 8.0.2 to 8.0.4 in the development-dependencies group (#2669)
* Bump brakeman from 8.0.2 to 8.0.4 in the development-dependencies group

Bumps the development-dependencies group with 1 update: [brakeman](https://github.com/presidentbeef/brakeman).


Updates `brakeman` from 8.0.2 to 8.0.4
- [Release notes](https://github.com/presidentbeef/brakeman/releases)
- [Changelog](https://github.com/presidentbeef/brakeman/blob/main/CHANGES.md)
- [Commits](https://github.com/presidentbeef/brakeman/compare/v8.0.2...v8.0.4)

---
updated-dependencies:
- dependency-name: brakeman
  dependency-version: 8.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* Sync Gemfile.saas.lock

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 08:58:36 -07:00
Rob Zolkos 854c0b85bc Update magic link sign-up email copy to be surface-agnostic (#2670)
Remove "on the Fizzy sign-up page" from the verification code email since users can now sign up via CLI, not just the web.
2026-03-06 16:47:15 -05:00
Rob Zolkos e47f2d93eb Add JSON response format to board publication toggle (#2663) 2026-03-05 20:48:03 -05:00
Mike Dalessio e27c0c5fa1 Filter draft cards from public closed column (#2667)
The public closed column was showing draft cards because it queried
`@board.cards.closed` without a `.published` filter. This adds the
missing `.published` scope to match the pattern used elsewhere in the
public controllers.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:31:41 -05:00
Rob Zolkos 8ee2d7d65f Add JSON response format to access token creation (#2662)
* Add JSON response format to access token creation

Allows programmatic clients (e.g., CLI) to create access tokens
via the API by returning the token, description, and permission
as JSON instead of redirecting to the HTML show page.

* Add JSON response format to access token creation

Allows programmatic clients (e.g., CLI) to create access tokens
via the API by returning the token, description, and permission
as JSON instead of redirecting to the HTML show page.

* Style

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-03-05 13:48:51 -04:00
Adrien Maston 734549a4cf Merge pull request #2657 from basecamp/mobile/fix-clipped-column-menu
Mobile / Fix clipped column menu
2026-03-05 17:39:21 +01:00
Adrien Maston deb31ddc40 Merge pull request #2655 from basecamp/mobile/fix-search
Mobile / Fix search
2026-03-04 17:15:05 +01:00
Adrien Maston 62d4bab5a8 Merge pull request #2660 from basecamp/mobile/tweak-activity-headers
Mobile / Tweak activity headers
2026-03-04 17:03:30 +01:00
Adrien Maston 646b552293 Merge pull request #2656 from basecamp/mobile/fix-change-email-confirmation
Mobile / Fix change email confirmation layout
2026-03-04 15:38:09 +01:00
Adrien Maston 98d019d47b Make it a fitted pill shape on iOS 2026-03-04 15:36:07 +01:00
Rosa Gutierrez e217c7286e Clean up now unused /devices routes
Follow-up to #2652
2026-03-04 11:10:44 +00:00
Adrien Maston 920502b590 Scroll column into view when opening its menu 2026-03-04 11:31:45 +01:00
Adrien Maston 9c26e834b7 Don't try to orient column menus 2026-03-04 11:30:42 +01:00
Adrien Maston 8a2a93563d Remove fixed panel size 2026-03-04 10:51:29 +01:00
Adrien Maston 57249fb9b7 Target results frame and advance action 2026-03-04 10:29:07 +01:00
Andy Smith fc95ddca05 Update the import link language 2026-03-03 16:31:50 -06:00
Andy Smith 5f919b312c Merge pull request #2560 from nqst/fix-blurred-nav-menu-in-safari
Fix font/icon rendering in the nav menu in Safari
2026-03-03 09:56:47 -06:00
Andy Smith c41e3277c8 Merge branch 'main' into fix-blurred-nav-menu-in-safari 2026-03-03 09:52:12 -06:00
Mike Dalessio 4496a203e6 Quash the new Rails warnings about premature class loading (#2653) 2026-03-03 10:44:51 -05:00
Jason Zimdars 613dfa5cee Merge pull request #2557 from nqst/cancellation-section-adjustments
Cancellation section adjustments
2026-03-03 09:38:29 -06:00
Andy Smith f8815fd914 Merge pull request #2603 from nqst/mobile-nav-menu-no-autofocus
Mobile → Jump menu: Remove input autofocus
2026-03-03 09:37:10 -06:00
Rosa Gutierrez 53df0e405b Move devices endpoint from /devices to /my/devices
Keep legacy /devices routes for backwards compatibility with mobile
apps not yet updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:30:41 +01:00
Kevin McConnell f6ed923e75 Merge pull request #2651 from basecamp/update-thruster-0.1.18
Update Thruster to v0.1.18
2026-03-03 13:41:23 +00:00
Kevin McConnell 6e3ee266ec Update Thruster to v0.1.18 2026-03-03 13:30:55 +00:00
Jorge Manrubia 5b15ae91fa Merge pull request #2650 from basecamp/update-deps
Bump solid_queue to 1.3.2 and importmap-rails to 2.2.3
2026-03-03 14:09:42 +01:00
Jorge Manrubia 4c2b6626e7 Revert "Bump thruster from 0.1.17 to 0.1.18"
This reverts commit a0dcc26cda.
2026-03-03 14:06:13 +01:00
Jorge Manrubia 7ef343e83d Revert "Sync Gemfile.saas.lock"
This reverts commit 2fef068ad0.
2026-03-03 14:06:13 +01:00
Jorge Manrubia 4853536ced Revert "Fix Gemfile.saas.lock: add missing thruster 0.1.18 platform variants"
This reverts commit a515f1f007.
2026-03-03 14:06:04 +01:00
Jorge Manrubia a515f1f007 Fix Gemfile.saas.lock: add missing thruster 0.1.18 platform variants
The saas.lock sync only bumped the version number but never added the
platform-specific entries that were present for 0.1.17.
2026-03-03 14:01:50 +01:00
Jorge Manrubia 8ae619891c Bump solid_queue from 1.3.1 to 1.3.2 and importmap-rails from 2.2.2 to 2.2.3 2026-03-03 14:01:36 +01:00