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
  ...
This commit is contained in:
Adrien Maston
2026-03-17 15:27:29 +01:00
194 changed files with 2150 additions and 2313 deletions
@@ -0,0 +1,6 @@
json.(@export, :id, :status)
json.created_at @export.created_at.utc
if @export.completed? && @export.file.attached?
json.download_url rails_blob_url(@export.file, disposition: "attachment")
end
@@ -0,0 +1,3 @@
json.(@join_code, :code, :usage_count, :usage_limit)
json.url join_url(code: @join_code.code, script_name: Current.account.slug)
json.active !!@join_code.active?
-2
View File
@@ -21,5 +21,3 @@
<%= render "account/settings/cancellation" %>
</div>
</div>
<%= render "account/settings/subscription_panel" if Fizzy.saas? %>
@@ -0,0 +1,3 @@
json.(@account, :id, :name, :cards_count)
json.created_at @account.created_at.utc
json.auto_postpone_period_in_days @account.entropy.auto_postpone_period_in_days
+1
View File
@@ -1,6 +1,7 @@
json.cache! board do
json.(board, :id, :name, :all_access)
json.created_at board.created_at.utc
json.auto_postpone_period_in_days board.auto_postpone_period_in_days
json.url board_url(board)
json.creator board.creator, partial: "users/user", as: :user
@@ -0,0 +1 @@
json.array! @page.records, partial: "cards/card", as: :card
@@ -0,0 +1 @@
json.array! @page.records, partial: "cards/card", as: :card
@@ -0,0 +1 @@
json.array! @page.records, partial: "cards/card", as: :card
+1 -1
View File
@@ -12,7 +12,7 @@
</div>
<div class="cards__list hide-scrollbar" data-drag-drop-item-container>
<% if page.used? %>
<%= with_automatic_pagination "maybe", @page do %>
<%= with_automatic_pagination "maybe", page do %>
<%= render "cards/display/previews", cards: page.records, draggable: true %>
<% end %>
<% end %>
@@ -3,7 +3,7 @@
<% if @source_column %>
<%= turbo_stream.replace(dom_id(@source_column), partial: "boards/show/column", method: :morph, locals: { column: @source_column }) %>
<% elsif @was_in_stream %>
<%= turbo_stream.replace("the-stream", partial: "boards/show/stream", method: :morph, locals: { board: @card.board, page: @page }) %>
<%= turbo_stream.replace("maybe", partial: "boards/show/stream", method: :morph, locals: { board: @card.board, page: @page }) %>
<% end %>
<%= turbo_stream.replace([ @card, :card_container ], partial: "cards/container", method: :morph, locals: { card: @card.reload }) %>
@@ -3,7 +3,7 @@
<% if @card.column %>
<%= turbo_stream.replace(dom_id(@card.column), partial: "boards/show/column", method: :morph, locals: { column: @card.column }) %>
<% elsif @card.awaiting_triage? %>
<%= turbo_stream.replace("the-stream", partial: "boards/show/stream", method: :morph, locals: { board: @card.board, page: @page }) %>
<%= turbo_stream.replace("maybe", partial: "boards/show/stream", method: :morph, locals: { board: @card.board, page: @page }) %>
<% end %>
<%= turbo_stream.replace([ @card, :card_container ], partial: "cards/container", method: :morph, locals: { card: @card.reload }) %>
@@ -13,7 +13,5 @@
<span>Create and add another</span>
<% end %>
</div>
<%= render "cards/container/footer/saas/near_notice" if Fizzy.saas? %>
</div>
+1 -5
View File
@@ -26,9 +26,5 @@
</div>
<% end %>
<% if Fizzy.saas? %>
<%= render "cards/container/footer/saas/create", card: card %>
<% else %>
<%= render "cards/container/footer/create", card: card %>
<% end %>
<%= render "cards/container/footer/create", card: card %>
</section>
@@ -3,7 +3,7 @@
<% if @source_column %>
<%= turbo_stream.replace(dom_id(@source_column), partial: "boards/show/column", method: :morph, locals: { column: @source_column }) %>
<% elsif @was_in_stream %>
<%= turbo_stream.replace("the-stream", partial: "boards/show/stream", method: :morph, locals: { board: @card.board, page: @page }) %>
<%= turbo_stream.replace("maybe", partial: "boards/show/stream", method: :morph, locals: { board: @card.board, page: @page }) %>
<% end %>
<%= turbo_stream.replace([ @card, :card_container ], partial: "cards/container", method: :morph, locals: { card: @card.reload }) %>
@@ -0,0 +1 @@
json.array! @card.steps, partial: "cards/steps/step", as: :step
+3 -2
View File
@@ -6,8 +6,9 @@
<%= form_with model: model, url: url, data: { controller: "form" } do |form| %>
<%= render "entropy/knob",
form: form,
name: :auto_postpone_period,
knob_options: entropy_auto_close_options,
name: :auto_postpone_period_in_days,
current_value: model.auto_postpone_period_in_days,
knob_options: Entropy::AUTO_POSTPONE_PERIODS_IN_DAYS,
label: "Days until auto-close",
disabled: disabled %>
<% end %>
+2 -3
View File
@@ -1,6 +1,5 @@
<%
period = form.object.send(name)
current_index = knob_options.index(period.to_i / 1.day)
current_index = knob_options.index(current_value) || knob_options.index(Entropy::DEFAULT_AUTO_POSTPONE_PERIOD_IN_DAYS)
%>
<fieldset class="knob" data-controller="knob" data-knob-target="field" style="--knob-options: <%= knob_options.length %>; --knob-index: <%= current_index %>">
@@ -8,7 +7,7 @@
<% knob_options.each_with_index do |value, index| %>
<label class="knob__option" style="--i: <%= index %>">
<%= form.radio_button name,
value.days,
value,
data: {
action: "change->knob#optionChanged change->form#submit",
index: index,
-1
View File
@@ -17,7 +17,6 @@
<%= render "layouts/theme_preference" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "fizzy/saas", "data-turbo-track": "reload" if Fizzy.saas? %>
<%= javascript_importmap_tags %>
<%= tenanted_action_cable_meta_tag %>
@@ -0,0 +1,2 @@
json.(access_token, :id, :description, :permission)
json.created_at access_token.created_at.utc
@@ -0,0 +1 @@
json.array! @access_tokens, partial: "my/access_tokens/access_token", as: :access_token
+1 -1
View File
@@ -6,7 +6,7 @@
<%= tag.li class: "popup__item", data: { filter_target: "item", navigable_list_target: "item" } do %>
<%= icon_tag "logout", class: "popup__icon" %>
<%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %>
<%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", form: { data: { turbo: false, controller: "clear-offline-cache", action: "submit->clear-offline-cache#clearCache" } } do %>
<span>Sign out</span>
<% end %>
<% end %>
@@ -0,0 +1 @@
json.bundle_email_frequency Current.user.settings.bundle_email_frequency
-41
View File
@@ -1,41 +0,0 @@
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return
if (event.request.destination === 'document') {
event.respondWith(
fetch(event.request, { cache: 'no-cache' })
.catch(() => caches.match(event.request))
)
}
})
self.addEventListener("push", (event) => {
const data = event.data.json()
event.waitUntil(Promise.all([ showNotification(data), updateBadgeCount(data.options) ]))
})
async function showNotification({ title, options }) {
return self.registration.showNotification(title, options)
}
async function updateBadgeCount({ data: { badge } }) {
return self.navigator.setAppBadge?.(badge || 0)
}
self.addEventListener("notificationclick", (event) => {
event.notification.close()
const url = new URL(event.notification.data.url, self.location.origin).href
event.waitUntil(openURL(url))
})
async function openURL(url) {
const clients = await self.clients.matchAll({ type: "window" })
const focused = clients.find((client) => client.focused)
if (focused) {
await focused.navigate(url)
} else {
await self.clients.openWindow(url)
}
}
+97
View File
@@ -0,0 +1,97 @@
importScripts("<%= javascript_url("turbo-offline-umd.min") %>")
// Documents - top-level navigation requests only
// We need `cache: "no-cache"` here to work around
// an annoying Safari PWA bug that predates offline mode
TurboOffline.addRule({
match: (request) => request.destination === "document",
except: /\/(edit|pin|watch|new)$/,
handler: TurboOffline.handlers.networkFirst({
cacheName: "main",
maxAge: 60 * 60 * 24 * 3,
networkTimeout: 3,
fetchOptions: { cache: "no-cache" },
maxEntrySize: 1024 * 1024
})
})
TurboOffline.addRule({
match: /\/assets\/.+[-\.][0-9a-f]+\.(js|css|svg|png|jpg|webp|woff2?|ico)$/,
handler: TurboOffline.handlers.cacheFirst({
cacheName: "assets",
maxAge: 60 * 60 * 24 * 7,
maxEntrySize: 1024 * 1024
})
})
TurboOffline.addRule({
match: /\/(boards|cards|users)\//,
except: /\/(edit|pin|watch|new)$/,
handler: TurboOffline.handlers.networkFirst({
cacheName: "main",
maxAge: 60 * 60 * 24 * 3,
networkTimeout: 2,
maxEntrySize: 1024 * 1024
})
})
TurboOffline.addRule({
match: /\/rails\/active_storage\//,
handler: TurboOffline.handlers.networkFirst({
cacheName: "storage",
maxAge: 60 * 60 * 24 * 7,
networkTimeout: 2,
maxEntrySize: 2 * 1024 * 1024, // 2MB covers about 95% of all Fizzy blobs
maxEntries: 500,
fetchOptions: { mode: "cors" }
})
})
// Everything else
TurboOffline.addRule({
except: /\/(service-worker\.js|edit|pin|watch|new)$/,
handler: TurboOffline.handlers.networkFirst({
cacheName: "main",
maxAge: 60 * 60 * 24,
networkTimeout: 3,
maxEntrySize: 1024 * 1024
})
})
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim())
})
TurboOffline.start()
self.addEventListener("push", (event) => {
const data = event.data.json()
event.waitUntil(Promise.all([ showNotification(data), updateBadgeCount(data.options) ]))
})
async function showNotification({ title, options }) {
return self.registration.showNotification(title, options)
}
async function updateBadgeCount({ data: { badge } }) {
return self.navigator.setAppBadge?.(badge || 0)
}
self.addEventListener("notificationclick", (event) => {
event.notification.close()
const url = new URL(event.notification.data.url, self.location.origin).href
event.waitUntil(openURL(url))
})
async function openURL(url) {
const clients = await self.clients.matchAll({ type: "window" })
const focused = clients.find((client) => client.focused)
if (focused) {
await focused.navigate(url)
} else {
await self.clients.openWindow(url)
}
}
+1
View File
@@ -0,0 +1 @@
json.partial! "reactions/reaction", reaction: @reaction
+1
View File
@@ -0,0 +1 @@
json.array! @page.records, partial: "cards/card", as: :card
+1 -1
View File
@@ -8,7 +8,7 @@
</p>
</header>
<%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: "magic-link" } } do |form| %>
<%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: "magic-link clear-offline-cache", action: "submit->clear-offline-cache#clearCache" } } do |form| %>
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large txt-uppercase",
autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code],
+1 -1
View File
@@ -41,7 +41,7 @@
<% if Current.user == @user %>
<hr class="separator--horizontal full-width flex-item-grow margin-block-start-double" style="--border-color: var(--color-ink-light);" aria-hidden="true">
<%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %>
<%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", form: { data: { turbo: false, controller: "clear-offline-cache", action: "submit->clear-offline-cache#clearCache" } } do %>
<span>Sign out of Fizzy on this device</span>
<% end %>
<% end %>
@@ -0,0 +1,8 @@
json.cache! [ webhook, webhook.board ] do
json.(webhook, :id, :name, :active, :signing_secret, :subscribed_actions)
json.payload_url webhook.url
json.created_at webhook.created_at.utc
json.url board_webhook_url(webhook.board, webhook)
json.board webhook.board, partial: "boards/board", as: :board
end
+1
View File
@@ -0,0 +1 @@
json.array! @page.records, partial: "webhooks/webhook", as: :webhook
+1
View File
@@ -0,0 +1 @@
json.partial! "webhooks/webhook", webhook: @webhook