From 68906321e8e8f32cd7ac957af61b8721bbf3b83f Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Fri, 30 May 2025 13:11:36 -0500 Subject: [PATCH 1/5] Card shadow tweaks --- app/assets/stylesheets/_global.css | 12 +++++------- app/assets/stylesheets/card-columns.css | 17 ++++++++++++++++- app/assets/stylesheets/cards.css | 21 +++++---------------- app/views/cards/display/_preview.html.erb | 2 +- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 5b2a966af..6861d229e 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -23,12 +23,10 @@ --border: 1px solid var(--color-ink-lighter); /* Shadows */ - --shadow: 0 0 0 1px oklch(var(--lch-black) / 0.02), - 0 .2em 1.6em -0.8em oklch(var(--lch-black) / 0.2), - 0 .4em 2.4em -1em oklch(var(--lch-black) / 0.3), - 0 .4em .8em -1.2em oklch(var(--lch-black) / 0.4), - 0 .8em 1.2em -1.6em oklch(var(--lch-black) / 0.5), - 0 1.2em 1.6em -2em oklch(var(--lch-black) / 0.6); + --shadow: 0 0 0 1px oklch(var(--lch-black) / 5%), + 0 0.2em 0.2em oklch(var(--lch-black) / 5%), + 0 0.4em 0.4em oklch(var(--lch-black) / 5%), + 0 0.8em 0.8em oklch(var(--lch-black) / 5%); /* Components */ --btn-size: 2.65em; @@ -186,7 +184,7 @@ --color-code-token__punctuation: oklch(var(--lch-ink-dark)); --color-code-token__selector: oklch(var(--lch-green-dark)); --color-code-token__variable: oklch(var(--lch-red-dark)); - + @media (prefers-color-scheme: dark) { --lch-canvas: 20% 0.0195 232.58; diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 108e7fd19..692efdf5d 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -101,7 +101,6 @@ --text-small: 1.1em; background-color: var(--color-canvas); - box-shadow: 0 0 0 1px var(--color-ink-lighter); line-height: 1.2; .avatar { @@ -122,6 +121,22 @@ display: none; } + .card__collection { + background-color: var(--color-canvas); + color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + padding: 0 0 0 var(--inline-space); + } + + .card__collection-name { + border-inline-start: 1px solid color-mix(in srgb, var(--color-ink) 33%, var(--color-canvas)); + color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + } + + .card__header { + color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + margin: 0 0 calc(-0.5 * var(--card-padding-inline)) 0; + } + .card__title { margin-block-start: 0.1em; min-block-size: 0; diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 53315b8a6..bb74bc503 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -25,6 +25,10 @@ .avatar { --avatar-size: 2.75em; } + + @media (prefers-color-scheme: dark) { + box-shadow: 0 0 0 1px var(--color-ink-lighter); + } } /* Children @@ -69,11 +73,6 @@ margin-block-start: calc(-1 * var(--card-padding-block)); margin-inline-start: calc(-1 * var(--card-padding-inline)); min-inline-size: 0; - - .cards--considering & { - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); - margin: 0 0 calc(-0.5 * var(--card-padding-inline)) 0; - } } .card__collection { @@ -83,12 +82,6 @@ font-weight: 800; padding: var(--block-space-half) var(--inline-space) var(--block-space-half) var(--inline-space-double); text-transform: uppercase; - - .cards--considering & { - background-color: var(--color-canvas); - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); - padding: 0 0 0 var(--inline-space); - } } .card__id { @@ -107,11 +100,6 @@ display: inline-flex; margin-inline-start: var(--inline-space-half); padding-inline-start: calc(var(--inline-space) * 0.75); - - .cards--considering & { - border-inline-start: 1px solid color-mix(in srgb, var(--color-ink) 33%, var(--color-canvas)); - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); - } } .card__tags { @@ -211,6 +199,7 @@ box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-link) 20%, var(--color-canvas)); color: var(--btn-color); font-weight: 700; + transition: none; } .card__move-button { diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb index 4bdccb6bb..eadeadaba 100644 --- a/app/views/cards/display/_preview.html.erb +++ b/app/views/cards/display/_preview.html.erb @@ -6,7 +6,7 @@ <%= card.id %> <%= link_to card.collection.name, cards_path(collection_ids: [ card.collection ]), - class: "card__collection-name txt-uppercase overflow-ellipsis" %> + class: "card__collection-name overflow-ellipsis" %> <%= render "cards/display/preview/tags", card: card %> From febab0758aa0253a2c25c5e94d07875170b7216d Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 28 May 2025 12:56:17 -0400 Subject: [PATCH 2/5] Drop the unnecessary initializer creating storage paths --- config/initializers/storage_paths.rb | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 config/initializers/storage_paths.rb diff --git a/config/initializers/storage_paths.rb b/config/initializers/storage_paths.rb deleted file mode 100644 index 497a6d466..000000000 --- a/config/initializers/storage_paths.rb +++ /dev/null @@ -1,5 +0,0 @@ -Rails.application.config.after_initialize do - %w[ db files ].each do |dir| - Rails.root.join("storage", dir).mkpath - end -end From bd44d0558fbfdfed42e12d0b3196f34f215dd93e Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 28 May 2025 14:45:11 -0400 Subject: [PATCH 3/5] Delete old multitenancy migration script --- script/migrate-to-multi-tenant | 271 --------------------------------- 1 file changed, 271 deletions(-) delete mode 100755 script/migrate-to-multi-tenant diff --git a/script/migrate-to-multi-tenant b/script/migrate-to-multi-tenant deleted file mode 100755 index 79db5c0e0..000000000 --- a/script/migrate-to-multi-tenant +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env ruby - -require_relative "../config/environment" - -class EnactTenanting - CURRENT_DOMAIN = "https://fizzy.37signals.com" - TENANTED_DOMAIN = Rails.env.local? ? "http://%{tenant}.fizzy.localhost:3006" : "https://%{tenant}.fizzy.37signals.com" - - PROJECT_MAPPING = { - 693169841 => "37s", # Writebook improvements - 693169850 => "37s", # Fizzy: Issues - 693169853 => "37s", # Fizzy: 💡 IDEAS - 693169858 => "37s", # Know It All: 💡 IDEAS - 693169859 => "37s", # Cycle 1: BC4 Client access for templates - 693169860 => "37s", # Know It All: Issues - 693169862 => "37s", # Active Record Tenanting - 693169863 => "37s", # Cycle 1: BC4 Hilltop View - 693169865 => "37s", # [BC iOS] Search refactor - 693169867 => "37s", # File icons refactor - 693169870 => "37s", # HEY Preload Replies - 693169872 => "37s", # [BC4 Android] Search Refactor - 693169873 => "37s", # BC4 - 693169874 => "37s", # [HEY] ContentFilter pipeline rewrite - 693169875 => "37s", # [Hey Calendar Android] Search - 693169876 => "37s", # [HEY Calendar iOS] Search - 693169877 => "37s", # [BC4] Turbo for comment and answer forms - - 693169842 => "dev", # Foobar - 693169843 => "dev", # Test - 693169851 => "dev", # Kevin - 693169861 => "dev", # Testing space - 693169869 => "dev", # Flower ideas - 693169871 => "dev", # Mike's TODOs - - 693169856 => "qa" # QA Exploration - } - - TENANTS = PROJECT_MAPPING.values.uniq - - attr_reader :creation_counter, :update_counter - - def initialize - @creation_counter = {} - @update_counter = {} - end - - def up - setup - destroy_tenants - - safety_check - - copy_accounts_et_al - copy_buckets_et_al - copy_bubbles_et_al - copy_filters - copy_active_storage - - cross_check - - update_action_text_urls - - pp [ "updated:", update_counter ] - - hardlink_active_storage - end - - def setup - ApplicationRecord.connects_to shards: { untenanted: { reading: :primary_original } } - end - - def with_original_db(&block) - ApplicationRecord.connected_to(role: :reading, shard: :untenanted, &block) - end - - def safety_check - buckets = with_original_db { Bucket.all.to_a } - - unless buckets.map(&:id).sort == PROJECT_MAPPING.keys.sort - unknown_buckets = buckets.map(&:id) - PROJECT_MAPPING.keys - missing_buckets = PROJECT_MAPPING.keys - buckets.map(&:id) - - raise "Surprising buckets. unknown #{unknown_buckets.inspect}, missing #{missing_buckets.inspect}" - end - end - - def copy_accounts_et_al - account = with_original_db { Account.first } - users = with_original_db { User.all.to_a } - workflows = with_original_db { account.workflows.to_a } - workflow_stages = with_original_db { workflows.flat_map(&:stages) } - tags = with_original_db { account.tags.to_a } - - TENANTS.each do |tenant| - ApplicationRecord.with_tenant(tenant) do - Account.create! name: tenant, id: account.id - - upsert_all users - upsert_all workflows - upsert_all workflow_stages - upsert_all tags - end - end - end - - def copy_buckets_et_al - PROJECT_MAPPING.each do |bucket_id, tenant| - bucket = with_original_db { Bucket.where(id: bucket_id).first } - accesses = with_original_db { bucket.accesses.to_a } - subscriptions = with_original_db { bucket.subscriptions.to_a } - - ApplicationRecord.with_tenant(tenant) do - upsert_all bucket - upsert_all accesses - upsert_all subscriptions - end - end - end - - def copy_bubbles_et_al - PROJECT_MAPPING.each do |bucket_id, tenant| - bubbles = with_original_db { Bucket.find(bucket_id).bubbles.to_a } - assignments = with_original_db { bubbles.flat_map(&:assignments) } - pops = with_original_db { bubbles.filter_map(&:pop) } - notifications = with_original_db { bubbles.flat_map(&:notifications) } - events = with_original_db { bubbles.flat_map(&:events) } - taggings = with_original_db { bubbles.flat_map(&:taggings) } - watches = with_original_db { bubbles.flat_map(&:watches) } - messages = with_original_db { bubbles.flat_map(&:messages) } - messageables = with_original_db { messages.filter_map(&:messageable) } - comments = messageables.select { _1.is_a?(Comment) } - markdowns = with_original_db { comments.map(&:markdown_body) } - reactions = with_original_db { comments.flat_map(&:reactions) } - - ApplicationRecord.with_tenant(tenant) do - upsert_all bubbles - upsert_all assignments - upsert_all pops - upsert_all taggings - upsert_all watches - messageables.group_by(&:class).each do |klass, klass_messageables| - upsert_all klass_messageables - end - upsert_all markdowns - upsert_all reactions - upsert_all messages - upsert_all events - upsert_all notifications - end - end - end - - def copy_filters - creation_counter["Filter"] = 0 - TENANTS.each do |tenant| - buckets = ApplicationRecord.with_tenant(tenant) { Bucket.all.to_a } - - filters = with_original_db do - Filter.all.filter_map do |filter| # hah no pun intended - next unless filter.buckets.empty? || buckets.any? { |b| filter.buckets.include?(b) } - - filter.attributes.merge(filter.as_params) - end - end - - ApplicationRecord.with_tenant(tenant) do - filters.each do |attr| - Filter.create! attr - end - end - creation_counter["Filter"] += filters.length - end - end - - def copy_active_storage - attachments = with_original_db { ActiveStorage::Attachment.all.to_a } - blobs = with_original_db { ActiveStorage::Blob.all.to_a } - - TENANTS.each do |tenant| - ApplicationRecord.with_tenant(tenant) do - upsert_all blobs - upsert_all attachments - end - end - end - - def destroy_tenants - TENANTS.each do |tenant| - ApplicationRecord.destroy_tenant(tenant) - end - FileUtils.rm_rf "storage/tenants" - end - - def cross_check - pp [ "created:", creation_counter ] - - with_original_db do - assert_count User, User.count * TENANTS.length - assert_count Workflow, Workflow.count * TENANTS.length - assert_count Workflow::Stage, Workflow::Stage.count * TENANTS.length - assert_count Tag, Tag.count * TENANTS.length - - assert_count Bucket, Bucket.count - assert_count Access, Access.count - assert_count Subscription, Subscription.count - - assert_count Bubble, Bubble.count - assert_count Assignment, Assignment.count - assert_count Pop, Pop.count - assert_count EventSummary, EventSummary.count - assert_count Comment, Comment.count - assert_count Reaction, Reaction.count - assert_count Message, Message.count - assert_count Tagging, Tagging.count - assert_count Watch, Watch.count - - assert_count Event, Event.count - assert_count Notification, Notification.count - - assert_count ActiveStorage::Attachment, ActiveStorage::Attachment.count * TENANTS.length - assert_count ActiveStorage::Blob, ActiveStorage::Blob.count * TENANTS.length - - raise "Filter count is off" unless creation_counter["Filter"] >= Filter.count - end - end - - def update_action_text_urls - raise "No more markdown" - end - - def hardlink_active_storage - files = Dir.glob("storage/files/*/*/*").map { _1.split("/")[2..].join("/") } - - TENANTS.each do |tenant| - ApplicationRecord.with_tenant(tenant) do - destdir = ActiveStorage::Blob.service.root - - files.each do |file| - FileUtils.mkdir_p File.join(destdir, File.dirname(file)) - FileUtils.ln File.join("storage/files", file), File.join(destdir, file) - end - end - end - end - - def upsert_all(originals) - originals = Array(originals) - return if originals.empty? - - klass = originals.first.class - - result = klass.upsert_all(originals.collect(&:attributes)) - raise "Error upserting" unless result.rows.length == originals.length - - creation_counter[klass.name] ||= 0 - creation_counter[klass.name] += originals.length - - nil - end - - def assert_count(klass, expected) - actual = creation_counter[klass.name] - unless actual == expected - raise "#{klass} count is off: expected #{expected}, got #{actual}" - end - end -end - -EnactTenanting.new.up - -exit 0 From a17b024681abd0e3cea2f0ed730369c12f775266 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Fri, 30 May 2025 14:56:58 -0400 Subject: [PATCH 4/5] Update AR::Tenanted to have tenanted blob keys Also, a script to migrate blobs (on disk and in the database) to the new convention. ref: https://37s.fizzy.37signals.com/collections/7/cards/246 --- Gemfile | 1 + Gemfile.lock | 22 +++++++++++++++++++++- script/migrate-disk-service-blobs.rb | 28 ++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100755 script/migrate-disk-service-blobs.rb diff --git a/Gemfile b/Gemfile index 8c1219952..457f0790b 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,7 @@ gem "jbuilder" gem "actiontext-lexical", bc: "actiontext-lexical" gem "image_processing", "~> 1.14" gem "platform_agent" +gem "aws-sdk-s3", require: false # Telemetry and logging gem "sentry-ruby" diff --git a/Gemfile.lock b/Gemfile.lock index 5fcb8b1ef..8afd7ccc9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,7 @@ GIT GIT remote: https://github.com/basecamp/active_record-tenanted - revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b + revision: f444578d6e59a1bdd65eda0ff765c9b5dac73398 specs: active_record-tenanted (0.1.0) activerecord (>= 8.1.alpha) @@ -144,6 +144,24 @@ GEM addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.3) + aws-eventstream (1.3.2) + aws-partitions (1.1108.0) + aws-sdk-core (3.224.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.101.0) + aws-sdk-core (~> 3, >= 3.216.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.188.0) + aws-sdk-core (~> 3, >= 3.224.1) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.11.0) + aws-eventstream (~> 1, >= 1.0.2) base64 (0.2.0) bcrypt (3.1.20) bcrypt_pbkdf (1.1.1) @@ -225,6 +243,7 @@ GEM jbuilder (2.13.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) + jmespath (1.6.2) json (2.11.3) kamal (2.6.1) activesupport (>= 7.0) @@ -470,6 +489,7 @@ PLATFORMS DEPENDENCIES actiontext-lexical! active_record-tenanted! + aws-sdk-s3 bcrypt (~> 3.1.7) bootsnap brakeman diff --git a/script/migrate-disk-service-blobs.rb b/script/migrate-disk-service-blobs.rb new file mode 100755 index 000000000..540787239 --- /dev/null +++ b/script/migrate-disk-service-blobs.rb @@ -0,0 +1,28 @@ +#! /usr/bin/env ruby + +require_relative "../config/environment" + +ApplicationRecord.with_each_tenant do |tenant| + puts "\n## #{tenant}" + report = { updated: 0, skipped: 0 } + + ActiveStorage::Blob.find_each do |blob| + if blob.key.start_with?("#{tenant}/") + report[:skipped] += 1 + else + blob.update_column :key, "#{tenant}/#{blob.key}" + report[:updated] += 1 + end + end + pp report + + disk_service = ActiveStorage::Blob.services.fetch(:local) + new_root = File.join(disk_service.root, tenant) + old_root = File.join("storage", "tenants", Rails.env, tenant, "files") + + FileUtils.mkdir_p(new_root, verbose: true) unless File.directory?(new_root) + + Dir.glob(File.join(old_root, "??")).each_slice(20) do |blob_dirs| + FileUtils.mv(blob_dirs, new_root, verbose: true) + end +end From 75832857e3ba09ee5935353047bd195cd181f5a7 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sat, 31 May 2025 18:16:06 +0200 Subject: [PATCH 5/5] update lexical --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8afd7ccc9..8d216f67a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/actiontext-lexical - revision: 9328297709d532fcb4cb5e47cfbddefda34d7c32 + revision: 7f327ddd60f4902ac60bb426495e85b31b6f0ad2 specs: actiontext-lexical (0.1.0) rails (>= 8.0.2)