Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion PROJECT_INTENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ Dabble Me was built as a **direct replacement for OhLife** — a much-loved emai

- There's a dedicated `OhLife` import flow ([entries/import/ohlife](app/controllers/import_controller.rb)) including image-archive upload.
- There's an SEO landing page at `/ohlife-alternative` ([welcome_controller.rb](app/controllers/welcome_controller.rb)).
- Imported entries are tagged with `Inspiration.category = "OhLife"` so they're identifiable forever.
- There's a Day One migration page at `/day-one-alternative` (import + email journaling), kept separate from the MCP comparison at `/dabble-me-vs-day-one-ai-journaling`.
- Imported entries are tagged with `Inspiration.category = "OhLife"` (and similarly Day One / Ahhlife / Trailmix) so they're identifiable forever.

If you're modifying behavior around imports, scheduling, or the email reply format, **do not break OhLife parity unless explicitly asked** — that audience is part of the product's reason for existing.

Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,24 @@ rake

The Admin emails are accounts that have access to the Admin Dropdown in the navbar (lock icon) that give you details into the number of entries and users in the system.

### Inspirations and OhLife Importer
### Inspirations and Importers

If you want random bits of inspiration, you can load up different quotes in the Inspiration table to be shown above the New Posts page and at the bottom of emails. If you plan on using OhLife, the system will tag imported posts with ```inspiration_id``` of 1 - so create the first Inspiration with a category name of "OhLife".
If you want random bits of inspiration, you can load up different quotes in the Inspiration table to be shown above the New Posts page and at the bottom of emails. Import sources are tagged via Inspiration categories — seed OhLife / Ahhlife / Day One / Trailmix rows (see `db/seeds.rb`), or the Day One importer will create its category on first use.

```ruby
Inspiration.create(category: 'OhLife', body: 'Imported from OhLife')
Inspiration.create(category: 'Day One', body: 'Imported from Day One')
```

PRO users can import from OhLife (text + photo ZIP), Day One (JSON ZIP export), Ahhlife (JSON paste), and Trailmix.life (JSON upload) at `/entries/import`.

=====

**Current features:**

* Read past entries by month/year
* Create new entries with simple formatting
* OhLife Importer
* Importers: OhLife, Day One, Ahhlife, Trailmix.life
* Email: Reply-to-post new entries on days of the week you choose (with random past entries embedded)
* Associate 1 image to a specific entry
* Search with basic analytics around posting
Expand Down
5 changes: 4 additions & 1 deletion app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,8 @@ strong {
}
}

.j-copy-to-clipboard {
.j-copy-to-clipboard,
.email-post-cta {
background: $olive;
color: white;
border: none;
Expand All @@ -389,6 +390,8 @@ strong {
}
&:hover {
background: darken($olive, 10%);
color: white;
text-decoration: none;
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions app/controllers/import_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
before_action :authenticate_user!
SPLIT_AT_DATE_REGEX = /[12]{1}[90]{1}[0-9]{2}\-[0-1]{1}[0-9]{1}\-[0-3]{1}[0-9]{1}/

def update
if current_user.is_free?
flash[:alert] = "<a href='#{subscribe_path}' class='alert-link'>Subscribe to PRO</a> to import entries.".html_safe
redirect_to import_path and return
end

if params[:type]&.downcase == "ahhlife"

Check notice on line 13 in app/controllers/import_controller.rb

View check run for this annotation

codefactor.io / CodeFactor

app/controllers/import_controller.rb#L13

Convert `if-elsif` to `case-when`. (Style/CaseLikeIf)
import_ahhlife_entries(params[:entry][:text])
elsif params[:type]&.downcase == "trailmix"
enqueue_trailmix_import
elsif params[:type]&.downcase == "day_one"
enqueue_day_one_import
else
import_ohlife_entries(params[:entry][:text])
end
Expand Down Expand Up @@ -66,6 +68,21 @@
redirect_to import_path(type: "trailmix")
end

def enqueue_day_one_import
stored_path = ImportUploadStore.store!(
uploaded_file: params[:zip_file],
user_key: current_user.user_key,
kind: "day_one"
)
ImportDayOneJob.perform_later(current_user.id, stored_path)
flash[:notice] = "Day One import has started. You will receive an email when it is finished."
redirect_to entries_path
rescue ImportUploadStore::Error => e
FileUtils.rm_f(params[:zip_file]&.tempfile&.path) if params[:zip_file]
flash[:alert] = e.message
redirect_to import_path(type: "day_one")
end

def import_ohlife_entries(data)
errors = []
user = current_user
Expand Down
4 changes: 4 additions & 0 deletions app/controllers/welcome_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ def day_one_ai_journaling
# Comparison page for people choosing an MCP-enabled journal.
end

def day_one_alternative
# SEO landing page for Day One users considering email journaling + import.
end

def best_journaling_apps_with_mcp
# Guide to vendor-supported journaling apps with MCP.
end
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def self.faqs
},
{
q: "Can I import from OhLife or other services?",
a: "Yes! PRO members can visit the <a href='#{Rails.application.routes.url_helpers.import_path}' class='text-accent hover:text-primary underline'>Importer</a> to import entries from OhLife, Ahhlife, and Trailmix.life."
a: "Yes! PRO members can visit the <a href='#{Rails.application.routes.url_helpers.import_path}' class='text-accent hover:text-primary underline'>Importer</a> to import entries from OhLife, Day One, Ahhlife, and Trailmix.life."
},
{
q: "Can I get a refund?",
Expand Down
54 changes: 54 additions & 0 deletions app/jobs/import_day_one_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

class ImportDayOneJob < ActiveJob::Base
queue_as :default

def perform(user_id, stored_path)
@user = User.find(user_id)
result = DayOneImporter.new(user: @user, zip_path: stored_path).import!
deliver_completion_email(result)
rescue StandardError => e
Sentry.capture_exception(e, extra: { user_id: user_id, stored_path: stored_path })
deliver_failure_email(e)
ensure
ImportUploadStore.cleanup!(stored_path)
end

private

def deliver_completion_email(result)
parts = []
parts << "Finished importing #{ActionController::Base.helpers.pluralize(result.imported, 'entry')} from Day One."
parts << "You can view them at #{::Rails.application.routes.url_helpers.entries_url}."

if result.skipped.present?
Sentry.capture_message("Day One import skipped entries", level: :info, extra: { skipped: result.skipped, user_id: @user.id })
parts << "Skipped: #{result.skipped.join('; ')}."
end

if result.errors.present?
Sentry.capture_message("Day One import errors", level: :info, extra: { errors: result.errors, user_id: @user.id })
parts << "Errors: #{result.errors.join('; ')}."
end

ActionMailer::Base.mail(
from: "Paul from Dabble Me <hello@#{ENV['MAIN_DOMAIN']}>",

Check notice on line 35 in app/jobs/import_day_one_job.rb

View check run for this annotation

codefactor.io / CodeFactor

app/jobs/import_day_one_job.rb#L35

Use `ENV.fetch('MAIN_DOMAIN', nil)` instead of `ENV['MAIN_DOMAIN']`. (Style/FetchEnvVar)
to: @user.email,
subject: "Import of Day One entries is complete",
content_type: "text/html",
body: parts.join(" ")
).deliver_later
end

def deliver_failure_email(error)
return unless @user

ActionMailer::Base.mail(
from: "Paul from Dabble Me <hello@#{ENV['MAIN_DOMAIN']}>",

Check notice on line 47 in app/jobs/import_day_one_job.rb

View check run for this annotation

codefactor.io / CodeFactor

app/jobs/import_day_one_job.rb#L47

Use `ENV.fetch('MAIN_DOMAIN', nil)` instead of `ENV['MAIN_DOMAIN']`. (Style/FetchEnvVar)
to: @user.email,
subject: "Day One import failed",
content_type: "text/html",
body: "Your Day One import could not be completed (#{error.message}). Please try again with a Day One JSON ZIP export, or reply to this email for help."
).deliver_later
end
end
6 changes: 5 additions & 1 deletion app/models/entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,11 @@ def relocate_image_on_date_change
end

def associate_inspiration
self.inspiration = nil unless self.inspiration.in? Inspiration.without_imports_or_email_or_tips
return if inspiration.blank?
# Keep import/source tags (OhLife, Day One, etc.) so importers can label origin.
return if inspiration.category.in?(Inspiration::IMPORT_CATEGORIES)

self.inspiration = nil unless inspiration.in?(Inspiration.without_imports_or_email_or_tips)
end

def strip_out_base64
Expand Down
6 changes: 4 additions & 2 deletions app/models/inspiration.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
class Inspiration < ActiveRecord::Base
IMPORT_CATEGORIES = ["OhLife", "Ahhlife", "Email", "Seed", "Trailmix", "Day One"].freeze

has_many :entries
scope :without_imports_or_email, -> { where("category NOT IN (?)", ['OhLife', 'Ahhlife', 'Email', 'Seed', 'Trailmix']) }
scope :without_imports_or_email, -> { where("category NOT IN (?)", IMPORT_CATEGORIES) }
scope :without_imports_or_email_or_tips, -> { without_imports_or_email.where("category != 'Tip'") }
scope :writing_prompts, -> { where(category: "Question") }

validates :category, presence: true
validates :body, presence: true

def inspired_by
if ["OhLife", "Email", "Ahhlife", "Seed", "Trailmix"].include? category
if IMPORT_CATEGORIES.include?(category)
"Source: #{category}"
elsif category == "Tip"
"Tip"
Expand Down
19 changes: 19 additions & 0 deletions app/services/collage_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,16 @@
# - 3 landscapes → hero + pair [1, 2] (near-square: 1 big on top, 2 below)
# - 3 portraits → row-of-three [3] (already near-square at adaptive height)
# For N > 9 we fall back to a square-ish auto grid.
def row_plan(images)
n = images.size

if n == 2
return [1, 1] if images.all? { |img| img.width > img.height }

Check notice on line 183 in app/services/collage_generator.rb

View check run for this annotation

codefactor.io / CodeFactor

app/services/collage_generator.rb#L183

Add empty line after guard clause. (Layout/EmptyLineAfterGuardClause)
return [2]
end

if n == 3
return [1, 2] if images.all? { |img| img.width > img.height }

Check notice on line 188 in app/services/collage_generator.rb

View check run for this annotation

codefactor.io / CodeFactor

app/services/collage_generator.rb#L188

Add empty line after guard clause. (Layout/EmptyLineAfterGuardClause)
return [3]
end

Expand Down Expand Up @@ -224,7 +224,7 @@
def scale_to_height(image, target_h)
iw = image.width
ih = image.height
raise Vips::Error, 'zero-sized collage source' if iw < 1 || ih < 1

Check notice on line 227 in app/services/collage_generator.rb

View check run for this annotation

codefactor.io / CodeFactor

app/services/collage_generator.rb#L227

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping. (Style/StringLiterals)

scaled = image.resize(target_h.to_f / ih)

Expand Down Expand Up @@ -280,10 +280,18 @@
# delay — this is the common S3 read-after-write race when a browser has
# only just finished PUTting the tmp object. Without this, we hand a
# half-baked JPEG to libvips and it errors mid-decode.
def fetch_bytes(url, redirects_left = MAX_REDIRECTS, truncation_retries_left = MAX_TRUNCATION_RETRIES)

Check notice on line 283 in app/services/collage_generator.rb

View check run for this annotation

codefactor.io / CodeFactor

app/services/collage_generator.rb#L283

Method has too many lines. [47/25] (Metrics/MethodLength)

Check notice on line 283 in app/services/collage_generator.rb

View check run for this annotation

codefactor.io / CodeFactor

app/services/collage_generator.rb#L283

Assignment Branch Condition size for `fetch_bytes` is too high. [<14, 44, 18> 49.56/40] (Metrics/AbcSize)
return nil if redirects_left.negative?
return nil if url.blank?

# Local filesystem paths (used by Day One ZIP import) bypass HTTP.
local_path = local_image_path(url)
if local_path
return File.binread(local_path) if File.file?(local_path)

return nil
end

# Malformed strings (e.g. internal sentinels like "mailgun_collage:…" that
# leaked out of the email processor) should be dropped silently rather than
# paged to Sentry — they're a caller-contract problem, not a runtime fault
Expand All @@ -291,7 +299,7 @@
uri = begin
URI.parse(url)
rescue URI::InvalidURIError
return nil

Check notice on line 302 in app/services/collage_generator.rb

View check run for this annotation

codefactor.io / CodeFactor

app/services/collage_generator.rb#L302

Do not `return` in `begin..end` blocks in assignment contexts. (Lint/NoReturnInBeginEndBlocks)
end
return nil unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)

Expand All @@ -312,7 +320,7 @@
when Net::HTTPSuccess
body = response.body
expected = response["content-length"]&.to_i
if expected && expected.positive? && body.to_s.bytesize != expected

Check notice on line 323 in app/services/collage_generator.rb

View check run for this annotation

codefactor.io / CodeFactor

app/services/collage_generator.rb#L323

Use safe navigation (`&.`) instead of checking if an object exists before calling the method. (Style/SafeNavigation)
if truncation_retries_left.positive?
sleep TRUNCATION_RETRY_DELAY
return fetch_bytes(url, redirects_left, truncation_retries_left - 1)
Expand All @@ -336,6 +344,17 @@
nil
end

Check notice on line 346 in app/services/collage_generator.rb

View check run for this annotation

codefactor.io / CodeFactor

app/services/collage_generator.rb#L283-L346

Complex Method
def local_image_path(url)
path = url.to_s
if path.start_with?("file://")
path = path.delete_prefix("file://")
end
return nil unless path.start_with?("/")
return nil if path.include?("\0")

path
end

def sanitize_url(url)
url.to_s.gsub(%r{//[^/@]+@}, "//***@")[0, 200]
end
Expand Down
Loading
Loading