diff --git a/.kamal/secrets.beta b/.kamal/secrets.beta
index ef220785d..60d911ced 100644
--- a/.kamal/secrets.beta
+++ b/.kamal/secrets.beta
@@ -1,6 +1,5 @@
-SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Beta/SECRET_KEY_BASE Beta/OPEN_AI_API_KEY)
+SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Beta/RAILS_MASTER_KEY)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
-SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS)
-OPEN_AI_API_KEY=$(kamal secrets extract OPEN_AI_API_KEY $SECRETS)
+RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
diff --git a/.kamal/secrets.dhh b/.kamal/secrets.dhh
index b427fe0c2..60d911ced 100644
--- a/.kamal/secrets.dhh
+++ b/.kamal/secrets.dhh
@@ -1,5 +1,5 @@
-SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Production/SECRET_KEY_BASE)
+SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Beta/RAILS_MASTER_KEY)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
-SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS)
+RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
diff --git a/.kamal/secrets.production b/.kamal/secrets.production
index 8ac9f1e7c..b6d71edd3 100644
--- a/.kamal/secrets.production
+++ b/.kamal/secrets.production
@@ -1,6 +1,5 @@
-SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Production/SECRET_KEY_BASE Production/OPEN_AI_API_KEY)
+SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Production/RAILS_MASTER_KEY)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
-SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS)
-OPEN_AI_API_KEY=$(kamal secrets extract OPEN_AI_API_KEY $SECRETS)
+RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
diff --git a/Gemfile b/Gemfile
index 8c1219952..1984dc71d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,7 +2,6 @@ source "https://rubygems.org"
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
gem "rails", github: "rails/rails", branch: "main"
-gem "active_record-tenanted", bc: "active_record-tenanted"
# Assets & front end
gem "importmap-rails"
@@ -11,6 +10,7 @@ gem "stimulus-rails"
gem "turbo-rails"
# Deployment and drivers
+gem "active_record-tenanted", bc: "active_record-tenanted"
gem "bootsnap", require: false
gem "kamal", require: false
gem "puma", ">= 5.0"
@@ -30,8 +30,10 @@ 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
+# Telemetry, logging, and operations
+gem "mission_control-jobs"
gem "sentry-ruby"
gem "sentry-rails"
gem "rails_structured_logging", bc: "rails-structured-logging"
diff --git a/Gemfile.lock b/Gemfile.lock
index 5fcb8b1ef..d761017df 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,13 +1,13 @@
GIT
remote: https://github.com/basecamp/actiontext-lexical
- revision: 9328297709d532fcb4cb5e47cfbddefda34d7c32
+ revision: 7f327ddd60f4902ac60bb426495e85b31b6f0ad2
specs:
actiontext-lexical (0.1.0)
rails (>= 8.0.2)
GIT
remote: https://github.com/basecamp/active_record-tenanted
- revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b
+ revision: f6fca189655ae6609d10ecad7cceed61592320c4
specs:
active_record-tenanted (0.1.0)
activerecord (>= 8.1.alpha)
@@ -17,28 +17,29 @@ GIT
GIT
remote: https://github.com/basecamp/rails-structured-logging
- revision: fc810cc48ac54a07722013149e104ceb8ec9a7e3
+ revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6
specs:
- rails_structured_logging (0.2.0)
+ rails_structured_logging (0.2.1)
json
rails (>= 6.0.0)
GIT
remote: https://github.com/crmne/ruby_llm.git
- revision: 8412d51e869f1ee8e5229831a9a0bb5e9b8f3847
+ revision: 74305703dcdcdcf8b2adefd8236184bcffc83910
specs:
- ruby_llm (1.2.0)
+ ruby_llm (1.3.0rc1)
base64
event_stream_parser (~> 1)
- faraday (~> 2)
- faraday-multipart (~> 1)
- faraday-net_http (~> 3)
- faraday-retry (~> 2)
+ faraday (>= 1.10.0)
+ faraday-multipart (>= 1)
+ faraday-net_http (>= 1)
+ faraday-retry (>= 1)
+ marcel (~> 1.0)
zeitwerk (~> 2)
GIT
remote: https://github.com/rails/rails.git
- revision: c1d7d56e831acf0aff5f3e27a6b384f543f358d0
+ revision: 0ac3fba6ee5f7d1680024a4925eee6069095487b
branch: main
specs:
actioncable (8.1.0.alpha)
@@ -144,11 +145,29 @@ GEM
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.3)
- base64 (0.2.0)
+ aws-eventstream (1.3.2)
+ aws-partitions (1.1109.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.3.0)
bcrypt (3.1.20)
bcrypt_pbkdf (1.1.1)
- benchmark (0.4.0)
- bigdecimal (3.1.9)
+ benchmark (0.4.1)
+ bigdecimal (3.2.1)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (7.0.2)
@@ -207,7 +226,7 @@ GEM
addressable (>= 2.5.0)
globalid (1.2.1)
activesupport (>= 6.1)
- hashdiff (1.1.2)
+ hashdiff (1.2.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
image_processing (1.14.0)
@@ -225,7 +244,8 @@ GEM
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
- json (2.11.3)
+ jmespath (1.6.2)
+ json (2.12.2)
kamal (2.6.1)
activesupport (>= 7.0)
base64 (~> 0.2)
@@ -237,7 +257,7 @@ GEM
sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3)
zeitwerk (>= 2.6.18, < 3.0)
- language_server-protocol (3.17.0.4)
+ language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.24.1)
@@ -256,6 +276,16 @@ GEM
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.25.5)
+ mission_control-jobs (1.0.2)
+ actioncable (>= 7.1)
+ actionpack (>= 7.1)
+ activejob (>= 7.1)
+ activerecord (>= 7.1)
+ importmap-rails (>= 1.2.1)
+ irb (~> 1.13)
+ railties (>= 7.1)
+ stimulus-rails
+ turbo-rails
msgpack (1.8.0)
multipart-post (2.4.1)
net-http (0.6.0)
@@ -304,7 +334,7 @@ GEM
psych (5.2.6)
date
stringio
- public_suffix (6.0.1)
+ public_suffix (6.0.2)
puma (6.6.0)
nio4r (~> 2.0)
raabro (1.4.0)
@@ -327,7 +357,7 @@ GEM
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rainbow (3.1.1)
- rake (13.2.1)
+ rake (13.3.0)
rb_sys (0.9.106)
rdoc (6.14.0)
erb
@@ -342,7 +372,7 @@ GEM
chunky_png (~> 1.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.0.0)
- rubocop (1.75.4)
+ rubocop (1.75.8)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -360,12 +390,12 @@ GEM
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
- rubocop-rails (2.31.0)
+ rubocop-rails (2.32.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
- rubocop-ast (>= 1.38.0, < 2.0)
+ rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rails-omakase (1.1.0)
rubocop (>= 1.72)
rubocop-performance (>= 1.24)
@@ -376,7 +406,7 @@ GEM
logger
rubyzip (2.4.1)
securerandom (0.4.1)
- selenium-webdriver (4.32.0)
+ selenium-webdriver (4.33.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -452,7 +482,7 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket (1.2.11)
- websocket-driver (0.7.7)
+ websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
@@ -470,6 +500,7 @@ PLATFORMS
DEPENDENCIES
actiontext-lexical!
active_record-tenanted!
+ aws-sdk-s3
bcrypt (~> 3.1.7)
bootsnap
brakeman
@@ -481,6 +512,7 @@ DEPENDENCIES
importmap-rails
jbuilder
kamal
+ mission_control-jobs
platform_agent
propshaft
puma (>= 5.0)
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/bubble.css b/app/assets/stylesheets/bubble.css
index 0b330b06a..43baf342b 100644
--- a/app/assets/stylesheets/bubble.css
+++ b/app/assets/stylesheets/bubble.css
@@ -52,12 +52,12 @@
}
}
-.bubble--closing {
+.bubble--considering {
inset-inline-start: calc(-1 * var(--bubble-gap));
translate: -100% -50%;
}
-.bubble--considering {
+.bubble--doing {
inset-inline-end: calc(-1 * var(--bubble-gap));
translate: 100% -50%;
}
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 2ad8423e6..92e540165 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 {
@@ -211,6 +210,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/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index a6f4cab39..3a240af09 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -11,7 +11,7 @@ class UploadsController < ApplicationController
def show
expires_in 5.minutes, public: true
- redirect_to @attachment.url
+ redirect_to @attachment.url, allow_other_host: true
end
private
diff --git a/app/helpers/html_helper.rb b/app/helpers/html_helper.rb
new file mode 100644
index 000000000..f61da26c1
--- /dev/null
+++ b/app/helpers/html_helper.rb
@@ -0,0 +1,46 @@
+module HtmlHelper
+ def format_html(html)
+ fragment = Nokogiri::HTML.fragment(html)
+
+ auto_link(fragment)
+
+ fragment.to_html.html_safe
+ end
+
+ private
+ EXCLUDED_ELEMENTS = %w[ a figcaption pre code ]
+ EMAIL_REGEXP = /\b[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\b/
+ URL_REGEXP = URI::DEFAULT_PARSER.make_regexp(%w[http https])
+
+ def auto_link(fragment)
+ fragment.traverse do |node|
+ next unless auto_linkable_node?(node)
+
+ content = node.text
+ linked_content = content.dup
+
+ auto_link_urls(linked_content)
+ auto_link_emails(linked_content)
+
+ if linked_content != content
+ node.replace(Nokogiri::HTML.fragment(linked_content))
+ end
+ end
+ end
+
+ def auto_linkable_node?(node)
+ node.text? && node.ancestors.none? { |ancestor| EXCLUDED_ELEMENTS.include?(ancestor.name) }
+ end
+
+ def auto_link_urls(linked_content)
+ linked_content.gsub!(URL_REGEXP) do |match|
+ %(#{match})
+ end
+ end
+
+ def auto_link_emails(text)
+ text.gsub!(EMAIL_REGEXP) do |match|
+ %(#{match})
+ end
+ end
+end
diff --git a/app/javascript/controllers/retarget_links_controller.js b/app/javascript/controllers/retarget_links_controller.js
new file mode 100644
index 000000000..f0187711e
--- /dev/null
+++ b/app/javascript/controllers/retarget_links_controller.js
@@ -0,0 +1,15 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ connect() {
+ this.element.querySelectorAll("a").forEach(this.#retargetLink.bind(this))
+ }
+
+ #retargetLink(link) {
+ link.target = this.#targetsSameDomain(link) ? "_top" : "_blank"
+ }
+
+ #targetsSameDomain(link) {
+ return link.href.startsWith(window.location.origin)
+ }
+}
diff --git a/app/models/card.rb b/app/models/card.rb
index ba26a18d7..656a51b12 100644
--- a/app/models/card.rb
+++ b/app/models/card.rb
@@ -1,7 +1,7 @@
class Card < ApplicationRecord
- include Assignable, Colored, Engageable, Eventable, Golden,
- Mentions, Pinnable, Closeable, Readable, Searchable, Staged,
- Statuses, Taggable, Watchable
+ include Assignable, Colored, Engageable, Entropy, Eventable,
+ Golden, Mentions, Pinnable, Closeable, Readable, Searchable,
+ Staged, Statuses, Taggable, Watchable
belongs_to :collection, touch: true
belongs_to :creator, class_name: "User", default: -> { Current.user }
diff --git a/app/models/card/closeable.rb b/app/models/card/closeable.rb
index 46c3a9696..afcdbc53d 100644
--- a/app/models/card/closeable.rb
+++ b/app/models/card/closeable.rb
@@ -1,8 +1,6 @@
module Card::Closeable
extend ActiveSupport::Concern
- AUTO_CLOSE_REMINDER_BEFORE = 7.days
-
included do
has_one :closure, dependent: :destroy
@@ -10,30 +8,6 @@ module Card::Closeable
scope :open, -> { where.missing(:closure) }
scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) }
- scope :in_auto_closing_collection, -> { joins(:collection).merge(Collection.auto_closing) }
- scope :due_to_be_closed, -> { considering.in_auto_closing_collection.where("last_active_at <= DATETIME('now', '-' || auto_close_period || ' seconds')") }
-
- delegate :auto_closing?, :auto_close_period, to: :collection
- end
-
- class_methods do
- def auto_close_all_due
- due_to_be_closed.find_each do |card|
- card.close(user: User.system, reason: "Closed")
- end
- end
- end
-
- def auto_close_at
- last_active_at + auto_close_period if auto_closing? && last_active_at
- end
-
- def days_until_close
- (auto_close_at.to_date - Date.current).to_i if auto_close_at
- end
-
- def closing_soon?
- considering? && auto_closing? && Time.current >= auto_close_at - AUTO_CLOSE_REMINDER_BEFORE
end
def closed?
diff --git a/app/models/card/engageable.rb b/app/models/card/engageable.rb
index aa5279d64..9db8e6a64 100644
--- a/app/models/card/engageable.rb
+++ b/app/models/card/engageable.rb
@@ -1,15 +1,11 @@
module Card::Engageable
extend ActiveSupport::Concern
- STAGNATED_AFTER = 30.days
- AUTO_RECONSIDER_REMINDER_BEFORE = 7.days
-
included do
has_one :engagement, dependent: :destroy, class_name: "Card::Engagement"
scope :considering, -> { published_or_drafted_by(Current.user).open.where.missing(:engagement) }
scope :doing, -> { published.open.joins(:engagement) }
- scope :stagnated, -> { doing.where(last_active_at: ..STAGNATED_AFTER.ago) }
scope :by_engagement_status, ->(status) do
case status.to_s
@@ -19,16 +15,6 @@ module Card::Engageable
end
end
- class_methods do
- def auto_reconsider_all_stagnated
- stagnated.find_each(&:reconsider)
- end
- end
-
- def auto_reconsider_at
- last_active_at + STAGNATED_AFTER if last_active_at
- end
-
def doing?
open? && published? && engagement.present?
end
@@ -37,6 +23,14 @@ module Card::Engageable
open? && published? && engagement.blank?
end
+ def engagement_status
+ if doing?
+ "doing"
+ elsif considering?
+ "considering"
+ end
+ end
+
def engage
unless doing?
transaction do
diff --git a/app/models/card/entropy.rb b/app/models/card/entropy.rb
new file mode 100644
index 000000000..b0eded7a3
--- /dev/null
+++ b/app/models/card/entropy.rb
@@ -0,0 +1,47 @@
+module Card::Entropy
+ extend ActiveSupport::Concern
+
+ AUTO_RECONSIDER_PERIOD = 30.days
+ ENTROPY_REMINDER_BEFORE = 7.days
+
+ included do
+ scope :in_auto_closing_collection, -> { joins(:collection).merge(Collection.auto_closing) }
+
+ scope :stagnated, -> { doing.where(last_active_at: ..AUTO_RECONSIDER_PERIOD.ago) }
+ scope :due_to_be_closed, -> { considering.in_auto_closing_collection.where("last_active_at <= DATETIME('now', '-' || auto_close_period || ' seconds')") }
+
+ delegate :auto_closing?, :auto_close_period, to: :collection
+ end
+
+ class_methods do
+ def auto_close_all_due
+ due_to_be_closed.find_each do |card|
+ card.close(user: User.system, reason: "Closed")
+ end
+ end
+
+ def auto_reconsider_all_stagnated
+ stagnated.find_each(&:reconsider)
+ end
+ end
+
+ def subject_to_entropy?
+ doing? || (auto_closing? && considering?)
+ end
+
+ def auto_close_at
+ last_active_at + auto_close_period if auto_closing? && last_active_at
+ end
+
+ def days_until_close
+ (auto_close_at.to_date - Date.current).to_i if auto_close_at
+ end
+
+ def auto_reconsider_at
+ last_active_at + AUTO_RECONSIDER_PERIOD if last_active_at
+ end
+
+ def days_until_reconsider
+ (auto_reconsider_at.to_date - Date.current).to_i if auto_reconsider_at
+ end
+end
diff --git a/app/views/cards/comments/_comment.html.erb b/app/views/cards/comments/_comment.html.erb
index a1e863bf9..b0eeaee14 100644
--- a/app/views/cards/comments/_comment.html.erb
+++ b/app/views/cards/comments/_comment.html.erb
@@ -28,7 +28,7 @@
<% end %>
-
+
<%= comment.body %>
diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb
index 1767fd854..0a45685e9 100644
--- a/app/views/cards/container/_title.html.erb
+++ b/app/views/cards/container/_title.html.erb
@@ -4,7 +4,7 @@
<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>
-
+
<%= card.description %>
diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb
index bc27a29f9..9ccdf3f97 100644
--- a/app/views/cards/display/_preview.html.erb
+++ b/app/views/cards/display/_preview.html.erb
@@ -29,12 +29,8 @@
<%= render "cards/display/common/background", card: card %>
- <% if card.auto_closing? %>
- <% if card.considering? %>
- <%= render "cards/display/preview/bubble", card: card, style: "closing" %>
- <% elsif card.doing? %>
- <%= render "cards/display/preview/bubble", card: card, style: "considering" %>
- <% end %>
+ <% if card.subject_to_entropy? %>
+ <%= render "cards/display/preview/bubble", card: card %>
<% end %>
<% end %>
<% end %>
diff --git a/app/views/cards/display/preview/_bubble.html.erb b/app/views/cards/display/preview/_bubble.html.erb
index 13faa9920..c05989755 100644
--- a/app/views/cards/display/preview/_bubble.html.erb
+++ b/app/views/cards/display/preview/_bubble.html.erb
@@ -1,6 +1,6 @@
-
<%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %>
-