Commit Graph

6629 Commits

Author SHA1 Message Date
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
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
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
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 98d019d47b Make it a fitted pill shape on iOS 2026-03-04 15:36:07 +01: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 c41e3277c8 Merge branch 'main' into fix-blurred-nav-menu-in-safari 2026-03-03 09:52:12 -06: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
Adrien Maston f6b9e765b8 Merge pull request #2619 from basecamp/style-devices-page
Style devices page
2026-03-03 12:20:09 +01:00
Adrien Maston 8ce5a3c1b5 Merge pull request #2648 from basecamp/mobile/fix-change-email-layout
Mobile / Fix change email layout
2026-03-03 11:39:14 +01:00
Adrien Maston 00dd28790b Merge pull request #2644 from basecamp/mobile/done-button-with-icon
Mobile / Done button with icon
2026-03-03 11:38:56 +01:00
Adrien Maston 0cdb4495f9 Merge pull request #2640 from basecamp/mobile/search-scrolling+clear-suggestion
Mobile / Search scrolling + clear button suggestion
2026-03-03 11:38:27 +01:00
Adrien Maston a041c5bbfb Merge pull request #2643 from basecamp/mobile/fix-tag-button-tap-target
Mobile / Fix tag button tap target
2026-03-03 11:34:35 +01:00
Adrien Maston 9798026e73 Merge branch 'mobile/search-scrolling' into mobile/search-scrolling+clear-suggestion
* mobile/search-scrolling:
  Mobile: improve search screen (#2636)
  Document has_attachments in API card responses (#2523)
  Change exceeding and nearing limits to account for the 1000th card (#2345)
  Include concrete example motivating this module file
  Simplify comment in skip-detached override
  Extract detached-blob guard into its own file
  Skip AnalyzeJob when blob has no attachments
  Allow notice to take more inline space
  Fix small documentation typos
  Display the save form button on Board Settings page as a bridge button
  billing.rb: destroy -> destroy!
  Strip whitespace from webhook URLs
  Add test for webhook URL with trailing whitespace
2026-03-03 11:33:00 +01:00
Jorge Manrubia 7f0ac98902 Merge pull request #2637 from basecamp/mobile/board-settings
Mobile: display the save form button on Board Settings page as a bridge button
2026-03-03 11:23:31 +01:00
Jay Ohms 2e6d06afa7 Mobile: improve search screen (#2636)
* Disable search results nested scrolling behavior for mobile apps

* Display the native search input clear button in the native apps
2026-03-03 11:22:28 +01:00
Adrien Maston c606a9f40c Remove fixed panel size 2026-03-03 10:59:23 +01:00
Adrien Maston 757f990b85 Merge pull request #2641 from basecamp/mobile/fix-notice-size
Mobile / Fix notice size
2026-03-03 10:32:45 +01:00
Adrien Maston 8fced550d8 Refactor 2026-03-03 10:31:08 +01:00
Jorge Manrubia cae7990876 Strip whitespace from webhook URL's
Strip whitespace from webhook URL's
2026-03-03 10:06:31 +01:00
Jay Ohms 1e18ddcad5 Merge branch 'mobile/search-scrolling' into mobile/search-scrolling+clear-suggestion
* mobile/search-scrolling:
  Extract event particulars helper method
  Reapply "Push event payload body updates"
  Bump z-index
  Bump z-index
  dep: Fix up Gemfile.saas.lock
2026-03-02 11:53:55 -05:00
Jay Ohms 71e295fb22 Merge branch 'main' into mobile/search-scrolling
* main:
  Extract event particulars helper method
  Reapply "Push event payload body updates"
  Bump z-index
  Bump z-index
  dep: Fix up Gemfile.saas.lock
2026-03-02 11:53:28 -05:00
Adrien Maston bedd2ff8ac Add check icon 2026-03-02 16:56:24 +01:00
Adrien Maston f4c35a1ca4 Merge pull request #2642 from basecamp/mobile/fix-missing-draft-button
Mobile / Fix missing draft button
2026-03-02 16:28:18 +01:00
Adrien Maston 1cb556ab2e Merge pull request #2639 from basecamp/mobile/fix-new-column-dialog
Mobile / Fix new column dialog
2026-03-02 16:27:40 +01:00
Jirka Hutárek f22afa7b56 Extract event particulars helper method 2026-03-02 16:15:28 +01:00
Jirka Hutárek 7e05794114 Reapply "Push event payload body updates"
This reverts commit 6e9de6a017.
2026-03-02 16:15:28 +01:00
Adrien Maston 911d114eb5 Add tap target button utility 2026-03-02 15:46:34 +01:00
Adrien Maston 85ba8a63d5 Bump z-index 2026-03-02 15:07:26 +01:00
Adrien Maston ef980a0d48 Allow notice to take more inline space 2026-03-02 14:50:58 +01:00
Adrien Maston 6a4db9af4d Show the custom clear button
... instead of the browser's
2026-03-02 14:22:28 +01:00