* Add saas:usage_report rake task and extract Subscription.paid scope
Add a rake task to generate a CSV usage report with per-account data:
Queenbee ID, sign up date, paid date, card count, storage used, and
last active date.
Extract the paid subscriptions query from Admin::StatsController into
an Account::Subscription.paid scope so both the controller and the
new rake task can share it.
* Add comped and account name columns to usage report
* Update saas/lib/tasks/fizzy/usage_report.rake
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Preload storage_total to avoid N+1 in usage report
* Batch last_active_at queries to avoid per-account aggregates
* Add tests for Account::Subscription.paid scope
* Update saas/app/models/account/subscription.rb
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Aggregate paid dates in SQL instead of loading all subscriptions
* Fix paid scope to derive plan keys from Plan.all instead of PLANS hash
* Move paid dates and comped lookups into per-batch queries
* Materialize batch IDs to avoid cross-database subquery
SaasRecord models live on a separate database (fizzy_saas) that doesn't
have the accounts table. Using batch.select(:id) generated a subquery
that ran on the saas database, causing a table-not-found error. Using
pluck(:id) materializes the IDs into an array instead.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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>
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.
Simplify the clear-offline-cache controller to use the new
Turbo.offline.clearCache() API instead of messaging the service
worker directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
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>
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>
The dependabot PR that bumped solid_queue and importmap-rails accidentally
dropped the platform variants for thruster 0.1.18 from Gemfile.saas.lock,
causing "Unsupported platform: x86_64-linux" boot failures on production.
Clear assignee's existing notifications in setup since Notifier now
uses create_or_find_by instead of create, and reload the association
to avoid caching after destroy_all.
Update fixture references after rebase: use `logo_assignment_kevin`
as base notification and `logo_mentioned_david` for mention tests.
Update source to `logo_published` in tests that need generic card events.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use ActionPushNative's new on_load hook to configure the database connection,
following the same pattern as Active Storage and Action Text:
ActiveSupport.on_load(:action_push_native_record) do
connects_to database: { writing: :saas, reading: :saas }
end
This allows ApplicationPushDevice to inherit directly from ActionPushNative::Device
without needing an intermediate abstract class.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use RESTful DELETE /devices/:id where :id can be token or database ID
- Remove redundant unregister collection route
- Remove old Users::DevicesController
- Return 404 when device not found instead of silently succeeding
- Return 422 for invalid platform via ActiveRecord validation
- Update action_push_native to main branch (includes validate: true on enum)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
web-console 4.2.1 overrides the now-renamed filter_proxies method in
ActionDispatch::RemoteIp::GetIp. The fix (rails/web-console#344) is on
main but not yet released.