Commit Graph

3336 Commits

Author SHA1 Message Date
Jay Ohms 096af59f8f Merge pull request #2714 from basecamp/mobile/application-bridge-components
Apply Hotwire Native bridge attributes to the `<body>` element from the user-agent
2026-03-17 12:26:23 -04:00
Jay Ohms f029fbfbeb Merge pull request #2683 from basecamp/mobile/bridge-button-slot
Mobile / Bridge button slot
2026-03-17 12:25:36 -04:00
Jay Ohms 2a19bb94a0 Merge branch 'main' into mobile/bridge-button-slot
* main: (33 commits)
  Remove payment/subscription system and card/storage limits
  Conditionally disable peer verification for ZIP streaming
  Prevent HTML injection through filenames
  Revert "Configure Lexxy to add extra spacing between block elements"
  Configure Lexxy to add extra spacing between block elements
  SaaS usage reporting (#2690)
  Update app/models/user/named.rb
  Prevent line breaks in the middle of familiar_name
  Tweak the layout
  Set the list of importable models based on the record sets in the manifest
  Validate polymorphic types against importable models allowlist
  Fix that the "maybe" stream wouldn't be replaced after triaging a card
  Use x_user_agent cookie for platform detection in Hotwire Native
  Add JSON response format to entropy endpoints (#2673)
  Fix path traversal vulnerability in ActiveStorage blob key import
  Enable CORS fetch mode for storage requests
  Revert CORS fetch mode for Active Storage
  Use CORS fetch mode for Active Storage to enable `maxEntrySize` checks
  Only start offline mode when signed in
  Exclude service worker and edit/pin/watch/new pages from main cache
  ...
2026-03-17 11:24:39 -04:00
Adrien Maston 780607dddc Merge branch 'main' into mobile/bridge-done-stamp
* main: (43 commits)
  Remove payment/subscription system and card/storage limits
  Conditionally disable peer verification for ZIP streaming
  Prevent HTML injection through filenames
  Revert "Configure Lexxy to add extra spacing between block elements"
  Configure Lexxy to add extra spacing between block elements
  SaaS usage reporting (#2690)
  Update app/models/user/named.rb
  Prevent line breaks in the middle of familiar_name
  Tweak the layout
  Set the list of importable models based on the record sets in the manifest
  Validate polymorphic types against importable models allowlist
  Fix that the "maybe" stream wouldn't be replaced after triaging a card
  Use x_user_agent cookie for platform detection in Hotwire Native
  Add JSON response format to entropy endpoints (#2673)
  Fix path traversal vulnerability in ActiveStorage blob key import
  Enable CORS fetch mode for storage requests
  Revert CORS fetch mode for Active Storage
  Use CORS fetch mode for Active Storage to enable `maxEntrySize` checks
  Only start offline mode when signed in
  Exclude service worker and edit/pin/watch/new pages from main cache
  ...
2026-03-17 15:27:29 +01:00
Jay Ohms 09e4bb13d3 Preset the data-bridge-platform and data-bridge-components on the body element from the user-agent. This prevents css flashes before the native bridge is registered in the app. 2026-03-17 09:59:45 -04:00
Jay Ohms 00b588fcef Don't display the "Mark done" bridge component button when the card has been marked as "Not Now" 2026-03-17 07:43:13 -04:00
Adrien Maston 2d1e201946 Add scope selector value 2026-03-17 11:57:59 +01:00
Adrien Maston 0b29a67e17 Change date and closedBy properties for a more generic description 2026-03-17 11:09:37 +01:00
Jorge Manrubia 964915bc66 Remove payment/subscription system and card/storage limits
Fizzy is now free. Remove the entire Stripe billing system,
subscription management, and card/storage limit enforcement.

Removes from saas/: Plan model, Account::Billing, Account::Subscription,
Account::Limited, Account::OverriddenLimits, Account::BillingWaiver,
all subscription/billing controllers and views, Stripe webhook handler,
card creation/publishing limit enforcement, admin account override UI,
usage report rake task, and all related tests.

Removes from main app: Fizzy.saas? guards for subscription panel,
SaaS card footer override, near-limit notices, and saas.css stylesheet.

Adds migration to drop billing tables from the SaaS database.

Non-billing SaaS features (push notifications, signup, authorization,
telemetry, console1984/audits1984) are preserved.
2026-03-17 09:22:04 +01:00
Jeffrey Hardy 0f5216f433 Fix that the "maybe" stream wouldn't be replaced after triaging a card
The element was originally named "the-stream" and later renamed to "maybe",
but the original references weren't updated.

Refs:
- https://github.com/basecamp/fizzy/pull/2211
- https://github.com/basecamp/fizzy/commit/b3cfae35
2026-03-10 14:50:32 -04:00
Rob Zolkos 8c2318e267 Add JSON response format to entropy endpoints (#2673)
* Add JSON response format to entropy endpoints

Add account settings JSON endpoint returning account info including auto_postpone_period. Add auto_postpone_period to board JSON responses. Add JSON update support to both account and board entropy controllers.

* Add auto_postpone_period to all board response examples in API docs

* Use auto_postpone_period_in_days in JSON responses and API docs

* Use valid period values in permission tests
2026-03-10 11:31:00 -04:00
Adrien Maston 1d523a98f9 Use it on the home's "Add board" button 2026-03-10 10:53:15 +01:00
Rosa Gutierrez 49a86c7cbf Enable CORS fetch mode for storage requests
The load balancer now returns a specific `Access-Control-Allow-Origin`
instead of a wildcard, so credentials can be included by default.
This enables `maxEntrySize` enforcement for storage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 1855490f80 Revert CORS fetch mode for Active Storage
CORS mode doesn't work with redirect-based Active Storage URLs
when the storage bucket uses wildcard Access-Control-Allow-Origin.
Relying on maxEntries alone until specific origin CORS is available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 67d629f722 Use CORS fetch mode for Active Storage to enable maxEntrySize checks
Otherwise, for resources like images loaded via <img> tags, the browser
sets `mode: "no-cors"` (as these aren't CORS requests), so the service
worker gets an opaque response even though the server sends CORS
headers.

We could upgrade all no-cors requests to cors mode, sending a `mode:
"cors"` request to a server that doesn't send CORS headers will fail
entirely: the `fetch` call throws a `TypeError` (network error), and the
browser blocks the response. So the resource wouldn't load at all, not
even as opaque. We wouldn't be able to cache it at all.

By opting in via `fetchOptions`, we can do it only for rules where we
know the server will send CORS headers.
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 0482670fec Exclude service worker and edit/pin/watch/new pages from main cache
We don't need the service worker to cache itself, or pages that won't
work offline anyway.
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 1c2de6ca79 Clear offline cache on sign-in as well as sign-out
Rename logout controller to clear-offline-cache and attach it to the
magic link verification form so the service worker cache is cleared
when a different user signs in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 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
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
Adrien Maston 7974629e20 Rename attribute 2026-03-09 16:42:59 +01:00
Adrien Maston 078597f132 Add bridge component 2026-03-09 15:40:45 +01: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
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
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
Adrien Maston c606a9f40c Remove fixed panel size 2026-03-03 10:59:23 +01:00
Adrien Maston 8fced550d8 Refactor 2026-03-03 10:31:08 +01:00