Merge branch 'main' into mobile/bridge-components

* main:
  Add rollback script to convert relative URLs back to absolute
  Add a script to migrate existing rich texts to relative URLs
  Use relative URLs where possible across all the app
  Use relative URLs for avatars and rich text attachments
This commit is contained in:
Jay Ohms
2026-01-14 09:39:08 -05:00
13 changed files with 284 additions and 14 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ class Users::AvatarsController < ApplicationController
if @user.system?
redirect_to view_context.image_path("system_user.png")
elsif @user.avatar.attached?
redirect_to rails_blob_url(@user.avatar_thumbnail, disposition: "inline")
redirect_to rails_blob_path(@user.avatar_thumbnail, disposition: "inline")
elsif stale? @user, cache_control: cache_control
render_initials
end
+1 -1
View File
@@ -42,6 +42,6 @@ module AvatarsHelper
end
def avatar_image_tag(user, **options)
image_tag user_avatar_url(user, script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, **options
image_tag user_avatar_path(user, script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, **options
end
end
@@ -12,14 +12,14 @@
height: height %>
<% elsif blob.audio? %>
<audio controls="true" width="100%" preload="metadata">
<source src="<%= rails_blob_url(blob) %>" type="<%= blob.content_type %>">
<source src="<%= rails_blob_path(blob) %>" type="<%= blob.content_type %>">
</audio>
<% elsif blob.variable? %>
<%= link_to url_for(blob.variant(variant)), data: { lightbox_target: "image", lightbox_caption_value: blob.filename.to_s } do %>
<%= image_tag url_for(blob.variant(variant)), width: width, height: height %>
<%= link_to rails_representation_path(blob.variant(variant)), data: { lightbox_target: "image", lightbox_caption_value: blob.filename.to_s } do %>
<%= image_tag rails_representation_path(blob.variant(variant)), width: width, height: height %>
<% end %>
<% elsif blob.previewable? %>
<%= image_tag url_for(blob.preview(variant)), width: width, height: height %>
<%= image_tag rails_representation_path(blob.preview(variant)), width: width, height: height %>
<% else %>
<span class="attachment__icon"><%= blob.filename.extension&.downcase.presence || "unknown" %></span>
<% end %>
@@ -3,9 +3,9 @@
<% height = attachment.metadata["height"] %>
<% if attachment.previewable? %>
<%= image_tag url_for(attachment.preview(variant)), class: "attachment attachment--image", width: width, height: height %>
<%= image_tag rails_representation_path(attachment.preview(variant)), class: "attachment attachment--image", width: width, height: height %>
<% elsif attachment.variable? %>
<%= image_tag url_for(attachment.variant(variant)), class: "attachment attachment--image", width: width, height: height %>
<%= image_tag rails_representation_path(attachment.variant(variant)), class: "attachment attachment--image", width: width, height: height %>
<% else %>
<div class="attachment attachment--file attachment--<%= attachment.filename.extension -%>">
<span class="attachment__icon"><%= attachment.filename.extension&.downcase.presence || "unknown" %></span>
+1 -1
View File
@@ -3,7 +3,7 @@
<%= collapsible_nav_section "Accounts" do %>
<%# Bust cache 1 Dec 2025 %>
<% accounts.each do |account| %>
<%= filter_place_menu_item landing_url(script_name: account.slug), account.name, "marker", current: account == Current.account, turbo: false %>
<%= filter_place_menu_item landing_path(script_name: account.slug), account.name, "marker", current: account == Current.account, turbo: false %>
<% end %>
<% end %>
<% end %>
+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_url(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", data: { turbo: false } do %>
<span>Sign out</span>
<% end %>
<% end %>
+1 -1
View File
@@ -3,6 +3,6 @@
#<%= card.number %> <%= card.title %>
</template>
<template type="editor">
<%= link_to "##{card.number} #{card.title}", card_url(card) %>
<%= link_to "##{card.number} #{card.title}", card_path(card) %>
</template>
</lexxy-prompt-item>
+1 -1
View File
@@ -13,7 +13,7 @@
<% @accounts.each do |account| %>
<li class="popup__item txt-medium">
<%= icon_tag "marker", class: "popup__icon" %>
<%= link_to landing_url(script_name: account.slug), class: "btn overflow-ellipsis fill-transparent popup__btn" do %>
<%= link_to landing_path(script_name: account.slug), class: "btn overflow-ellipsis fill-transparent popup__btn" do %>
<strong><%= account.name %></strong>
<% end %>
</li>
+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_url(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", data: { turbo: false } do %>
<span>Sign out of Fizzy on this device</span>
<% end %>
<% end %>
+1 -1
View File
@@ -1,4 +1,4 @@
<%= @event.description_for(Current.user).to_plain_text %>
<% if @event.eventable %>
<%= link_to "↗︎", polymorphic_url(@event.eventable) %>
<%= link_to "↗︎", polymorphic_url(@event.eventable, only_path: true) %>
<% end %>
+11
View File
@@ -8,6 +8,17 @@ ActiveSupport.on_load(:active_storage_blob) do
end
end
ActiveSupport.on_load(:action_text_content) do
# Install our extensions after ActionText::Engine's
ActiveSupport.on_load(:active_storage_blob) do
# Ensure all <action-text-attachment>s have a "url" attribute that's a relative
# path (for portability across host name changes, beta environments, etc).
def to_rich_text_attributes(*)
super.merge url: Rails.application.routes.url_helpers.polymorphic_url(self, only_path: true)
end
end
end
# Don't configure replica connections for ActiveStorage::Record.
# When ActiveStorage uses `connects_to`, it creates a separate connection pool
# from ApplicationRecord. This causes after_commit callbacks to fire in
@@ -0,0 +1,132 @@
#!/usr/bin/env ruby
# Convert absolute attachment URLs in rich text content to relative paths.
# This fixes URLs that were stored with full hostnames (e.g., https://app.fizzy.do/...)
# making them portable across beta environments and host changes.
#
# MUST BE RUN AFTER `decrypt!` when using ActiveRecord Encryption
#
# Run locally:
# bin/rails runner script/migrations/convert-absolute-attachment-urls-to-relative.rb --help
#
# Run via Kamal:
# kamal app exec -d <stage> -p --reuse "bin/rails runner script/migrations/convert-absolute-attachment-urls-to-relative.rb --help"
#
# Safe to re-run: won't modify already-relative URLs
class ConvertAbsoluteAttachmentUrlsToRelative
# Match absolute URLs pointing to Active Storage routes, keeping the account slug
ABSOLUTE_URL_PATTERN = %r{https?://[^/]+(/\d+/rails/active_storage/[^"']+)}
attr_reader :account, :dry_run
def initialize(account_id: nil, dry_run: false)
@account = Account.find_by(external_account_id: account_id)
@dry_run = dry_run
end
def run
puts "Converting absolute attachment URLs to relative paths"
puts dry_run ? "DRY RUN MODE - no changes will be saved" : "LIVE MODE - changes will be saved"
puts account ? "Only account: #{account.external_account_id} - #{account.name}" : "For **ALL ACCOUNTS**"
puts "\nPress ENTER to continue running or CTRL-C to bail..."
gets
puts "\nRunning..."
# Suppress SQL logs
Rails.event.debug_mode = false
seconds = Benchmark.realtime do
suppressing_turbo_broadcasts do
convert_urls
end
end
puts "\n\n"
puts "Finished in %.2f seconds." % seconds
end
private
def suppressing_turbo_broadcasts
Board.suppressing_turbo_broadcasts do
Card.suppressing_turbo_broadcasts do
yield
end
end
end
def convert_urls
scanned = 0
fixed = 0
urls_converted = 0
action_texts_scope.find_each do |rich_text|
scanned += 1
body = rich_text.body
edited = false
conversions = 0
body.send(:attachment_nodes).each do |node|
url = node["url"]
next unless url
if url.match?(ABSOLUTE_URL_PATTERN)
node["url"] = url.gsub(ABSOLUTE_URL_PATTERN, '\1')
edited = true
conversions += 1
end
end
if edited
record = rich_text.record
puts " - modifying #{record.class.name} #{record.to_param} (account: #{record.account&.external_account_id}) - #{conversions} URL(s)"
unless dry_run
rich_text.update! body: body.fragment.to_html
end
fixed += 1
urls_converted += conversions
end
end
puts "\n\nConversion complete!"
puts " Rich texts examined: #{scanned}"
puts " Rich texts modified: #{fixed}"
puts " URLs converted: #{urls_converted}"
end
def action_texts_scope
# Only examine rich texts that have embedded attachments
scope = ActionText::RichText.joins(:embeds_attachments)
scope = scope.where(account: account) if account
scope
end
end
require "optparse"
options = { account_id: nil, dry_run: true }
OptionParser.new do |opts|
opts.banner = "Usage: bin/rails runner #{__FILE__} [options]"
opts.on("-a", "--account ACCOUNT_ID", "Restrict to a specific account (external_account_id)") do |id|
options[:account_id] = id
end
opts.on("--[no-]dry-run", "Run in dry-run mode (default: --dry-run)") do |v|
options[:dry_run] = v
end
opts.on("-h", "--help", "Show this help message") do
puts opts
exit
end
end.parse!
ConvertAbsoluteAttachmentUrlsToRelative.new(**options).run
@@ -0,0 +1,127 @@
#!/usr/bin/env ruby
# Rollback script: Convert relative attachment URLs back to absolute URLs.
# Use this if you need to rollback the relative URL changes and want to
# convert rich texts created during the rollout back to absolute URLs.
#
# Run locally:
# bin/rails runner script/migrations/convert-relative-attachment-urls-to-absolute.rb --help
#
# Run via Kamal:
# kamal app exec -d <stage> -p --reuse "bin/rails runner script/migrations/convert-relative-attachment-urls-to-absolute.rb --help"
class ConvertRelativeAttachmentUrlsToAbsolute
# Match relative URLs pointing to Active Storage routes (with account slug)
RELATIVE_URL_PATTERN = %r{\A(/\d+/rails/active_storage/[^"']+)\z}
attr_reader :host, :since
def initialize(host:, since:)
@host = host
@since = since
end
def run
puts "Converting relative attachment URLs to absolute URLs"
puts "Host: #{host}"
puts "Processing rich texts created since: #{since}"
puts "\nPress ENTER to continue running or CTRL-C to bail..."
gets
puts "\nRunning..."
seconds = Benchmark.realtime do
suppressing_turbo_broadcasts do
convert_urls
end
end
puts "\n\n"
puts "Finished in %.2f seconds." % seconds
end
private
def suppressing_turbo_broadcasts
Board.suppressing_turbo_broadcasts do
Card.suppressing_turbo_broadcasts do
yield
end
end
end
def convert_urls
scanned = 0
fixed = 0
urls_converted = 0
action_texts_scope.find_each do |rich_text|
scanned += 1
body = rich_text.body
edited = false
conversions = 0
body.send(:attachment_nodes).each do |node|
url = node["url"]
next unless url
if url.match?(RELATIVE_URL_PATTERN)
node["url"] = "#{host}#{url}"
edited = true
conversions += 1
end
end
if edited
record = rich_text.record
puts " - modifying #{record.class.name} #{record.to_param} (account: #{record.account&.external_account_id}) - #{conversions} URL(s)"
rich_text.update! body: body.fragment.to_html
fixed += 1
urls_converted += conversions
end
end
puts "\n\nConversion complete!"
puts " Rich texts examined: #{scanned}"
puts " Rich texts modified: #{fixed}"
puts " URLs converted: #{urls_converted}"
end
def action_texts_scope
ActionText::RichText.joins(:embeds_attachments).where("action_text_rich_texts.created_at >= ?", since)
end
end
require "optparse"
require "time"
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: bin/rails runner #{__FILE__} [options]"
opts.on("--host HOST", "Host to prepend (e.g., https://app.fizzy.do)") do |host|
options[:host] = host
end
opts.on("--since TIME", "Process rich texts created since this time (ISO 8601 format)") do |time|
options[:since] = Time.parse(time)
end
opts.on("-h", "--help", "Show this help message") do
puts opts
exit
end
end.parse!
if options[:host].nil? || options[:since].nil?
puts "Error: --host and --since are required"
puts "Example: bin/rails runner #{__FILE__} --host https://app.fizzy.do --since 2026-01-14T10:00:00Z"
exit 1
end
ConvertRelativeAttachmentUrlsToAbsolute.new(**options).run