Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save anon987654321/1bb6c29c1cf7de3081d713f9a8ce6eb1 to your computer and use it in GitHub Desktop.

Select an option

Save anon987654321/1bb6c29c1cf7de3081d713f9a8ce6eb1 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby -E UTF-8
# Unified patch for DEPLOY/ – applies shared engine, basic fixes, feed, map, Solidus marketplace, and Reddit scraper
require "fileutils"
require "yaml"
ROOT = Dir.pwd
puts "📦 Applying unified patch from #{ROOT}"
# ------------------------------------------------------------------------------
# 1. Create shared engine (rails/shared)
# ------------------------------------------------------------------------------
shared_root = "shared"
shared_lib = "#{shared_root}/lib/shared"
shared_engine_rb = "#{shared_lib}/engine.rb"
shared_gemspec = "#{shared_root}/shared.gemspec"
shared_gemfile = "#{shared_root}/Gemfile"
FileUtils.mkdir_p("#{shared_root}/app/controllers/concerns")
FileUtils.mkdir_p("#{shared_root}/app/views/layouts")
FileUtils.mkdir_p("#{shared_root}/app/assets/stylesheets")
FileUtils.mkdir_p("#{shared_root}/config/initializers")
FileUtils.mkdir_p(shared_lib)
# shared.gemspec
unless File.exist?(shared_gemspec)
File.write(shared_gemspec, <<~'RUBY')
require_relative "lib/shared/version"
Gem::Specification.new do |spec|
spec.name = "shared"
spec.version = "0.1.0"
spec.authors = ["Brgen Core"]
spec.email = ["core@brgen.no"]
spec.summary = "Shared engine for Brgen apps"
spec.description = "Authentication, pagination, flash partials, base styles"
spec.license = "MIT"
spec.files = Dir["{app,config,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
spec.add_dependency "rails", "~> 8.1.2"
spec.add_dependency "pagy"
spec.add_dependency "importmap-rails"
spec.add_dependency "stimulus-rails"
spec.add_dependency "turbo-rails"
spec.add_dependency "propshaft"
end
RUBY
puts " ✅ Created #{shared_gemspec}"
end
# shared/Gemfile
unless File.exist?(shared_gemfile)
File.write(shared_gemfile, <<~'RUBY')
source "https://rubygems.org"
gemspec
gem "rails", "~> 8.1.2"
gem "pagy", "~> 9.3"
gem "importmap-rails"
gem "stimulus-rails"
gem "turbo-rails"
gem "propshaft"
group :development, :test do
gem "debug"
gem "brakeman"
gem "rubocop-rails-omakase"
end
RUBY
puts " ✅ Created #{shared_gemfile}"
end
# shared/lib/shared/engine.rb
unless File.exist?(shared_engine_rb)
FileUtils.mkdir_p(File.dirname(shared_engine_rb))
File.write(shared_engine_rb, <<~'RUBY')
module Shared
class Engine < ::Rails::Engine
isolate_namespace Shared
initializer "shared.assets.precompile" do |app|
app.config.assets.precompile += %w[shared_manifest.js]
end
end
end
RUBY
puts " ✅ Created #{shared_engine_rb}"
end
# shared/lib/shared/version.rb
version_rb = "#{shared_lib}/version.rb"
unless File.exist?(version_rb)
File.write(version_rb, <<~'RUBY')
module Shared
VERSION = "0.1.0"
end
RUBY
puts " ✅ Created #{version_rb}"
end
# shared/app/controllers/concerns/authentication.rb
auth_concern = "#{shared_root}/app/controllers/concerns/authentication.rb"
unless File.exist?(auth_concern)
File.write(auth_concern, <<~'RUBY')
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_authentication
helper_method :authenticated?
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
end
private
def authenticated?
resume_session
end
def require_authentication
resume_session || request_authentication
end
def resume_session
Current.session ||= find_session_by_cookie
end
def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
end
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_path
end
def after_authentication_url
session.delete(:return_to_after_authenticating) || root_url
end
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
end
end
def terminate_session
Current.session.destroy
cookies.delete(:session_id)
end
end
RUBY
puts " ✅ Created #{auth_concern}"
end
# shared/app/views/layouts/_flash.html.erb
flash_partial = "#{shared_root}/app/views/layouts/_flash.html.erb"
unless File.exist?(flash_partial)
FileUtils.mkdir_p(File.dirname(flash_partial))
File.write(flash_partial, <<~'ERB')
<% if flash.any? %>
<div class="flash-container" data-controller="flash">
<% flash.each do |type, msg| %>
<div class="flash flash--<%= type %>" role="alert">
<%= msg %>
<button type="button" class="flash__close" data-action="click->flash#dismiss">&times;</button>
</div>
<% end %>
</div>
<% end %>
ERB
puts " ✅ Created #{flash_partial}"
end
# shared/app/views/layouts/_errors.html.erb
errors_partial = "#{shared_root}/app/views/layouts/_errors.html.erb"
unless File.exist?(errors_partial)
File.write(errors_partial, <<~'ERB')
<% if object.errors.any? %>
<div class="errors">
<h2><%= pluralize(object.errors.count, "error") %> prohibited this record from being saved:</h2>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
ERB
puts " ✅ Created #{errors_partial}"
end
# shared/config/initializers/pagy.rb
pagy_init = "#{shared_root}/config/initializers/pagy.rb"
unless File.exist?(pagy_init)
File.write(pagy_init, <<~'RUBY')
require "pagy/extras/overflow"
Pagy::DEFAULT[:items] = 25
Pagy::DEFAULT[:overflow] = :last_page
Pagy::DEFAULT[:size] = [1, 2, 2, 1]
RUBY
puts " ✅ Created #{pagy_init}"
end
# ------------------------------------------------------------------------------
# Helper to update Gemfile and application files for each app
# ------------------------------------------------------------------------------
apps = %w[amber baibl blognet brgen bsdports hjerterom]
apps.each do |app|
app_root = "rails/#{app}"
next unless Dir.exist?(app_root)
# ----- Gemfile: add shared gem, faker, bcrypt, etc. -----
gemfile = "#{app_root}/Gemfile"
if File.exist?(gemfile)
content = File.read(gemfile, encoding: "UTF-8")
unless content.include?('gem "shared", path: "../../shared"')
content.sub!(/^gem "rails".*$/, "\\0\ngem \"shared\", path: \"../../shared\"")
File.write(gemfile, content, encoding: "UTF-8")
puts " ✅ Added shared gem to #{gemfile}"
end
unless content.include?('gem "faker"')
content.sub!(/group :development, :test do/, "\\0\n gem \"faker\"")
File.write(gemfile, content, encoding: "UTF-8")
puts " ✅ Added faker to #{gemfile}"
end
if app == "baibl" && !content.include?('gem "bcrypt"')
content.sub!(/^gem "rails".*$/, "\\0\ngem \"bcrypt\", \"~> 3.1.7\"")
File.write(gemfile, content, encoding: "UTF-8")
puts " ✅ Added bcrypt to #{gemfile}"
end
if app == "brgen"
unless content.include?('gem "solidus"')
content.sub!(/group :development, :test do/, "\\0\n gem \"solidus\", \"~> 4.5\"\n gem \"solidus_starter_frontend\"")
File.write(gemfile, content, encoding: "UTF-8")
puts " ✅ Added solidus to #{gemfile}"
end
unless content.include?('gem "geocoder"')
content.sub!(/^gem "rails".*$/, "\\0\ngem \"geocoder\"")
File.write(gemfile, content, encoding: "UTF-8")
puts " ✅ Added geocoder to #{gemfile}"
end
end
if app == "hjerterom" && !content.include?('gem "geocoder"')
content.sub!(/^gem "rails".*$/, "\\0\ngem \"geocoder\"")
File.write(gemfile, content, encoding: "UTF-8")
puts " ✅ Added geocoder to #{gemfile}"
end
end
# ----- Delete local authentication.rb -----
auth_local = "#{app_root}/app/controllers/concerns/authentication.rb"
if File.exist?(auth_local)
File.delete(auth_local)
puts " ✅ Deleted #{auth_local}"
end
# ----- Delete local pagy initializer -----
pagy_local = "#{app_root}/config/initializers/pagy.rb"
if File.exist?(pagy_local)
File.delete(pagy_local)
puts " ✅ Deleted #{pagy_local}"
end
# ----- Update ApplicationController to include Shared helpers -----
app_controller = "#{app_root}/app/controllers/application_controller.rb"
if File.exist?(app_controller)
content = File.read(app_controller, encoding: "UTF-8")
unless content.include?("helper Shared::Engine.helpers")
content.sub!(/class ApplicationController.*\n/, "\\0 helper Shared::Engine.helpers\n")
File.write(app_controller, content, encoding: "UTF-8")
puts " ✅ Updated #{app_controller}"
end
if app == "brgen" && !content.include?("def current_ability")
content << "\n def current_ability\n @current_ability ||= Spree::Ability.new(current_user)\n end\n"
File.write(app_controller, content, encoding: "UTF-8")
puts " ✅ Added current_ability to #{app_controller}"
end
end
# ----- Update layout to use shared flash partial -----
layout = "#{app_root}/app/views/layouts/application.html.erb"
if File.exist?(layout)
content = File.read(layout, encoding: "UTF-8")
if content.include?('render "shared/flash"')
content.gsub!('render "shared/flash"', 'render "layouts/flash"')
File.write(layout, content, encoding: "UTF-8")
puts " ✅ Updated flash partial in #{layout}"
elsif !content.include?('render "layouts/flash"')
content.sub!(/<body>.*\n/, "\\0 <%= render \"layouts/flash\" %>\n")
File.write(layout, content, encoding: "UTF-8")
puts " ✅ Added flash partial to #{layout}"
end
end
# ----- App-specific fixes -----
if app == "baibl"
# Add has_secure_password to User model
user_model = "#{app_root}/app/models/user.rb"
if File.exist?(user_model)
content = File.read(user_model, encoding: "UTF-8")
unless content.include?("has_secure_password")
content.sub!(/class User < ApplicationRecord/, "\\0\n has_secure_password")
File.write(user_model, content, encoding: "UTF-8")
puts " ✅ Added has_secure_password to #{user_model}"
end
end
# Enhance seeds (add Genesis)
seeds = "#{app_root}/db/seeds.rb"
if File.exist?(seeds)
content = File.read(seeds, encoding: "UTF-8")
unless content.include?("Genesis")
content << <<~'RUBY'
genesis = Book.find_or_create_by!(name: "Genesis", abbreviation: "Gen", testament: "Old", order_index: 1)
50.times do |chapter_num|
chapter = genesis.chapters.find_or_create_by!(number: chapter_num + 1)
20.times do |verse_num|
chapter.verses.find_or_create_by!(number: verse_num + 1, content: "Verse #{verse_num + 1} of chapter #{chapter_num + 1} of Genesis. (Replace with real text.)")
end
end
RUBY
File.write(seeds, content, encoding: "UTF-8")
puts " ✅ Enhanced seeds for baibl"
end
end
end
if app == "blognet"
# Add Follow model
follow_model = "#{app_root}/app/models/follow.rb"
unless File.exist?(follow_model)
File.write(follow_model, <<~'RUBY')
class Follow < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, uniqueness: { scope: :followed_id }
validate :no_self_follow
private def no_self_follow = errors.add(:followed, "can't follow yourself") if follower_id == followed_id
end
RUBY
puts " ✅ Created #{follow_model}"
end
# Add UsersController
users_controller = "#{app_root}/app/controllers/users_controller.rb"
unless File.exist?(users_controller)
File.write(users_controller, <<~'RUBY')
class UsersController < ApplicationController
allow_unauthenticated_access only: [:show]
def show
@user = User.find(params[:id])
@posts = @user.posts.published.order(published_at: :desc).limit(10)
end
end
RUBY
puts " ✅ Created #{users_controller}"
end
# Update User model with follow associations
user_model = "#{app_root}/app/models/user.rb"
if File.exist?(user_model)
content = File.read(user_model, encoding: "UTF-8")
unless content.include?("has_many :active_follows")
content.sub!(/has_many :posts.*\n/, "\\0 has_many :active_follows, class_name: \"Follow\", foreign_key: \"follower_id\", dependent: :destroy\n has_many :following, through: :active_follows, source: :followed\n has_many :passive_follows, class_name: \"Follow\", foreign_key: \"followed_id\", dependent: :destroy\n has_many :followers, through: :passive_follows, source: :follower\n")
File.write(user_model, content, encoding: "UTF-8")
puts " ✅ Added follow associations to User in blognet"
end
end
end
if app == "brgen"
# ----- Add swipe controller -----
swipe_js = "#{app_root}/app/javascript/controllers/swipe_controller.js"
unless File.exist?(swipe_js)
File.write(swipe_js, <<~'JS')
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["card"]
connect() {
this.startX = 0; this.currentX = 0; this.isDragging = false
this.card = this.cardTargets[0]
this.element.addEventListener("touchstart", this.onTouchStart)
this.element.addEventListener("touchmove", this.onTouchMove)
this.element.addEventListener("touchend", this.onTouchEnd)
}
onTouchStart = (e) => { this.startX = e.touches[0].clientX; this.isDragging = true }
onTouchMove = (e) => {
if (!this.isDragging) return
this.currentX = e.touches[0].clientX
const diff = this.currentX - this.startX
this.card.style.transform = `translateX(${diff}px) rotate(${diff * 0.02}deg)`
this.card.style.opacity = 1 - Math.abs(diff) / 500
}
onTouchEnd = () => {
const diff = this.currentX - this.startX
if (Math.abs(diff) > 100) this[diff > 0 ? "right" : "left"]()
else { this.card.style.transform = ""; this.card.style.opacity = 1 }
this.isDragging = false
}
right() { this.submitVote("like") }
left() { this.submitVote("dislike") }
submitVote(type) {
const userId = this.card.dataset.userId
fetch(`/dating/${type}s`, { method: "POST", headers: { "X-CSRF-Token": document.querySelector("[name=csrf-token]").content }, body: new URLSearchParams({ user_id: userId }) })
.then(() => this.nextCard())
}
nextCard() { this.card.remove(); this.card = this.cardTargets[0] }
}
JS
puts " ✅ Created #{swipe_js}"
end
# ----- Add post_form controller for Facebook feed -----
post_form_js = "#{app_root}/app/javascript/controllers/post_form.js"
unless File.exist?(post_form_js)
File.write(post_form_js, <<~'JS')
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["content", "anonymousCheckbox"]
clear() {
this.contentTarget.value = ""
if (this.hasAnonymousCheckboxTarget) {
this.anonymousCheckboxTarget.checked = false
}
}
}
JS
puts " ✅ Created #{post_form_js}"
end
# ----- Add Cart model -----
cart_model = "#{app_root}/app/models/cart.rb"
unless File.exist?(cart_model)
File.write(cart_model, <<~'RUBY')
class Cart < ApplicationRecord
belongs_to :user
has_many :cart_items, dependent: :destroy
has_many :listings, through: :cart_items
validates :status, inclusion: { in: %w[active abandoned] }
def total_cents
cart_items.sum { |ci| ci.quantity * ci.listing.price_cents }
end
def to_order!
order = Marketplace::Order.create!(buyer: user, status: "pending", total_cents: total_cents)
cart_items.each do |ci|
order.line_items.create!(purchasable: ci.listing, quantity: ci.quantity, unit_price_cents: ci.listing.price_cents)
end
update!(status: "abandoned")
order
end
end
RUBY
puts " ✅ Created #{cart_model}"
end
cart_item_model = "#{app_root}/app/models/cart_item.rb"
unless File.exist?(cart_item_model)
File.write(cart_item_model, <<~'RUBY')
class CartItem < ApplicationRecord
belongs_to :cart
belongs_to :listing, class_name: "Marketplace::Listing"
validates :quantity, numericality: { greater_than: 0 }
delegate :price_cents, to: :listing, prefix: true
end
RUBY
puts " ✅ Created #{cart_item_model}"
end
# ----- Add cart show view -----
cart_view_dir = "#{app_root}/app/views/marketplace/cart"
cart_view = "#{cart_view_dir}/show.html.erb"
unless File.exist?(cart_view)
FileUtils.mkdir_p(cart_view_dir)
File.write(cart_view, <<~'ERB')
<h1>Shopping Cart</h1>
<% if @cart.cart_items.any? %>
<table>
<% @cart.cart_items.each do |item| %>
<tr>
<td><%= item.listing.title %></td>
<td><%= item.quantity %></td>
<td><%= number_to_currency(item.listing.price_cents / 100.0) %></td>
</tr>
<% end %>
</table>
<p>Total: <%= number_to_currency(@cart.total_cents / 100.0) %></p>
<%= button_to "Checkout", checkout_marketplace_cart_path, method: :post %>
<% else %><p>Your cart is empty.</p><% end %>
ERB
puts " ✅ Created #{cart_view}"
end
# ----- Update home view with Facebook feed -----
home_view = "#{app_root}/app/views/home/index.html.erb"
if File.exist?(home_view)
new_content = <<~'ERB'
<div class="container mx-auto px-4 py-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2">
<div class="bg-white rounded-lg shadow mb-6 p-4" data-controller="post-form">
<%= form_with model: Post.new, url: posts_path, data: { turbo: true } do |f| %>
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<% if authenticated? && Current.user.avatar.attached? %>
<%= image_tag Current.user.avatar, class: "h-10 w-10 rounded-full" %>
<% else %>
<div class="h-10 w-10 rounded-full bg-gray-300"></div>
<% end %>
</div>
<div class="flex-1">
<%= f.text_area :content, placeholder: "What's on your mind?", rows: 2, class: "w-full border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-blue-500", data: { post_form_target: "content" } %>
<div class="flex items-center justify-between mt-2">
<div class="flex items-center space-x-4">
<%= f.collection_select :community_id, Community.all, :id, :name, { include_blank: "Share to…" }, class: "text-sm border rounded px-2 py-1" %>
<label class="inline-flex items-center text-sm">
<%= f.check_box :anonymous, data: { post_form_target: "anonymousCheckbox" } %>
<span class="ml-2">Post anonymously</span>
</label>
</div>
<%= f.submit "Post", class: "bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600" %>
</div>
</div>
</div>
<% end %>
</div>
<div id="feed">
<%= render @posts %>
</div>
</div>
<div class="lg:col-span-1">
<div class="bg-white rounded-lg shadow p-4 sticky top-6">
<h3 class="font-bold text-lg mb-3">Communities</h3>
<ul class="space-y-2">
<% @communities.each do |c| %>
<li><%= link_to c.name, community_path(c), class: "text-blue-600 hover:underline" %></li>
<% end %>
</ul>
<% if authenticated? %>
<%= link_to "+ Create community", new_community_path, class: "mt-3 inline-block text-sm text-blue-600" %>
<% end %>
</div>
</div>
</div>
</div>
ERB
File.write(home_view, new_content, encoding: "UTF-8")
puts " ✅ Updated home view for brgen feed"
end
# ----- Add post partial with anonymous support -----
post_partial = "#{app_root}/app/views/posts/_post.html.erb"
if File.exist?(post_partial)
content = File.read(post_partial, encoding: "UTF-8")
unless content.include?("Anonymous")
content.sub!(/<header>/, '<header class="flex items-center gap-2">')
content.sub!(/<header.*>/, "\\0\n <% if post.anonymous? %>\n <span class=\"text-gray-500 text-sm\">Anonymous</span>\n <% else %>\n <%= link_to post.user.username, user_path(post.user), class: \"font-medium\" %>\n <% end %>")
File.write(post_partial, content, encoding: "UTF-8")
puts " ✅ Added anonymous support to post partial"
end
end
# ----- Add create.turbo_stream.erb for live feed update -----
turbo_stream_view = "#{app_root}/app/views/posts/create.turbo_stream.erb"
unless File.exist?(turbo_stream_view)
File.write(turbo_stream_view, <<~'ERB')
<%= turbo_stream.prepend "feed", partial: "posts/post", locals: { post: @post } %>
<%= turbo_stream.update "post_form", "" do %>
<%= javascript_tag "document.querySelector('[data-controller=\"post-form\"]').dispatchEvent(new CustomEvent('reset'))" %>
<% end %>
ERB
puts " ✅ Created turbo stream for posts"
end
# ----- Update routes to mount Solidus and remove old marketplace -----
routes = "#{app_root}/config/routes.rb"
if File.exist?(routes)
content = File.read(routes, encoding: "UTF-8")
# Remove old marketplace constraints block if present
content.gsub!(/constraints\(subdomain: MARKETPLACE_SUBDOMAINS\) do.*?end/m, "")
# Add Solidus mount if not present
unless content.include?("mount Spree::Core::Engine")
content.sub!(/^end$/, " mount Spree::Core::Engine, at: '/marketplace', as: 'spree'\nend")
File.write(routes, content, encoding: "UTF-8")
puts " ✅ Updated routes with Solidus mount"
end
end
# ----- Add Solidus initializers -----
spree_init = "#{app_root}/config/initializers/spree.rb"
unless File.exist?(spree_init)
File.write(spree_init, <<~'RUBY')
Spree.load_defaults '4.5'
Spree.config do |config|
config.currency = "NOK"
config.shipping_instructions = true
config.address_requires_state = false
config.require_master_price = false
config.admin_interface_v2 = true
end
Spree::Backend::Config.configure do |config|
config.locale = 'en'
end
Spree.user_class = "User"
Spree::Auth::Config.set(:signout_redirect_url, "/")
Spree::Auth::Config.set(:registration_redirect_path, "/marketplace")
Spree::Auth::Config.set(:login_redirect_path, "/marketplace")
RUBY
puts " ✅ Created #{spree_init}"
end
auth_init = "#{app_root}/config/initializers/solidus_auth.rb"
unless File.exist?(auth_init)
File.write(auth_init, <<~'RUBY')
Devise.setup do |config|
config.parent_controller = 'ApplicationController'
config.scoped_views = true
end
Rails.application.config.to_prepare do
Devise.mappings[:spree_user] = Devise.mappings[:user]
Spree::User = User
end
Spree::Order.class_eval do
def guest_token
session[:guest_token] if session
end
def associate_user!(user, override_email = true)
self.user = user
self.email = user.email if override_email
save!
end
end
RUBY
puts " ✅ Created #{auth_init}"
end
# ----- Add Reddit scraper rake task -----
scraper_task = "#{app_root}/lib/tasks/reddit_scraper.rake"
unless File.exist?(scraper_task)
FileUtils.mkdir_p("#{app_root}/lib/tasks")
File.write(scraper_task, <<~'RUBY')
namespace :reddit do
desc "Scrape r/bergen posts using Reddit's JSON API and save as Solidus products"
task scrape_bergen: :environment do
require "open-uri"
require "json"
subreddit = "bergen"
url = "https://www.reddit.com/r/#{subreddit}/new.json?limit=50"
puts "🌐 Fetching r/#{subreddit}..."
response = URI.open(url, "User-Agent" => "Brgen/1.0 (Rails 8 Scraper)").read
data = JSON.parse(response)
posts = data["data"]["children"]
created = 0
posts.each do |child|
post = child["data"]
next if Spree::Product.exists?(meta_keywords: "reddit_#{post['id']}")
product = Spree::Product.new(
name: post["title"].truncate(255),
description: post["selftext"].presence || "No description.",
price: (post["score"] / 10.0).round(2),
available_on: Time.at(post["created_utc"]),
meta_keywords: "reddit_#{post['id']}",
shipping_required: false,
taxon_ids: [Spree::Taxon.first_or_create!(name: "Community Posts", taxonomy: Spree::Taxonomy.first_or_create!(name: "Categories")).id]
)
product.master.sku = "RDDT-#{post['id']}"
product.master.cost_price = 0.0
if product.save
created += 1
puts "✅ Created product: #{post['title']}"
else
puts "❌ Failed: #{product.errors.full_messages.join(', ')}"
end
end
puts "🎉 Done. Created #{created} products."
end
end
RUBY
puts " ✅ Created #{scraper_task}"
end
# ----- Delete obsolete marketplace custom files -----
obsolete_files = [
"#{app_root}/app/controllers/marketplace/cart_controller.rb",
"#{app_root}/app/models/marketplace/listing.rb",
"#{app_root}/app/models/marketplace/order.rb",
"#{app_root}/app/views/marketplace/listings/index.html.erb"
]
obsolete_files.each do |f|
if File.exist?(f)
File.delete(f)
puts " ✅ Deleted obsolete #{f}"
end
end
end
if app == "bsdports"
# Add ports import rake task
ports_rake = "#{app_root}/lib/tasks/ports.rake"
unless File.exist?(ports_rake)
FileUtils.mkdir_p("#{app_root}/lib/tasks")
File.write(ports_rake, <<~'RUBY')
namespace :ports do
desc "Import OpenBSD ports from official mirror"
task import: :environment do
require "open-uri"
require "tmpdir"
url = "https://cdn.openbsd.org/pub/OpenBSD/ports.tar.gz"
Dir.mktmpdir do |dir|
tgz = File.join(dir, "ports.tar.gz")
puts "Downloading #{url}..."
URI.open(url) { |f| File.write(tgz, f.read) }
puts "Extracting..."
system("tar xzf #{tgz} -C #{dir}") or raise "Extraction failed"
ports_dir = File.join(dir, "ports")
Dir.glob("#{ports_dir}/*/*/Makefile") do |makefile|
category = makefile.split("/")[-3]
name = makefile.split("/")[-2]
comment = File.readlines(makefile).grep(/^COMMENT=/).first.to_s.gsub(/^COMMENT=\s*/, "").strip
Port.find_or_create_by!(name: name, category: category) do |p|
p.comment = comment
p.version = "unknown"
p.pkgpath = "#{category}/#{name}"
end
end
puts "Imported #{Port.count} ports."
end
end
end
RUBY
puts " ✅ Created #{ports_rake}"
end
end
if app == "hjerterom"
# ----- Add map controller for Mapbox -----
map_js = "#{app_root}/app/javascript/controllers/map_controller.js"
unless File.exist?(map_js)
File.write(map_js, <<~'JS')
import { Controller } from "@hotwired/stimulus"
import mapboxgl from "mapbox-gl"
export default class extends Controller {
static values = { accessToken: String, markers: Array, centerLat: { type: Number, default: 60.3913 }, centerLng: { type: Number, default: 5.3221 }, zoom: { type: Number, default: 12 } }
connect() {
mapboxgl.accessToken = this.accessTokenValue
this.map = new mapboxgl.Map({ container: this.element, style: "mapbox://styles/mapbox/streets-v12", center: [this.centerLngValue, this.centerLatValue], zoom: this.zoomValue })
this.map.on("load", () => this.addMarkers())
}
addMarkers() {
this.markersValue.forEach(markerData => {
if (!markerData.lat || !markerData.lng) return
const popup = new mapboxgl.Popup({ offset: 25 }).setHTML(`<div class="p-2"><strong>${this.escapeHtml(markerData.title)}</strong><br><span class="text-sm text-gray-600">${markerData.type}</span><br><a href="${markerData.url}" class="text-blue-600 text-sm mt-1 inline-block">View details →</a></div>`)
new mapboxgl.Marker().setLngLat([markerData.lng, markerData.lat]).setPopup(popup).addTo(this.map)
})
}
escapeHtml(str) { return str.replace(/[&<>]/g, m => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[m])) }
disconnect() { if (this.map) this.map.remove() }
}
JS
puts " ✅ Created #{map_js}"
end
# ----- Update home view to use map -----
home_view = "#{app_root}/app/views/home/index.html.erb"
if File.exist?(home_view)
new_home = <<~'ERB'
<div class="container mx-auto px-4 py-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2">
<div class="bg-white rounded-lg shadow overflow-hidden" style="height: 70vh;">
<div data-controller="map"
data-map-access-token-value="<%= ENV['MAPBOX_ACCESS_TOKEN'] %>"
data-map-markers-value="<%= @markers.to_json %>"
data-map-center-lat-value="60.3913"
data-map-center-lng-value="5.3221"
data-map-zoom-value="12"
class="w-full h-full"></div>
</div>
</div>
<div class="lg:col-span-1 space-y-6">
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-bold text-lg mb-2">How to help</h3>
<p class="text-gray-600 mb-4">Find food or resources near you. Click on a marker for details.</p>
<%= link_to "List food", new_food_listing_path, class: "block w-full text-center bg-green-600 text-white py-2 rounded-lg mb-2" if authenticated? %>
<%= link_to "Add resource", new_resource_path, class: "block w-full text-center bg-blue-600 text-white py-2 rounded-lg" if authenticated? %>
</div>
<div class="bg-white rounded-lg shadow p-4">
<h3 class="font-bold text-lg mb-2">Crisis support</h3>
<ul class="space-y-2">
<% Crisis.where(available_24h: true).limit(5).each do |c| %>
<li><strong><%= c.title %></strong><br><%= c.phone %></li>
<% end %>
</ul>
</div>
</div>
</div>
</div>
ERB
File.write(home_view, new_home, encoding: "UTF-8")
puts " ✅ Updated home view with map for hjerterom"
end
# ----- Add geocoder initializer -----
geocoder_init = "#{app_root}/config/initializers/geocoder.rb"
unless File.exist?(geocoder_init)
File.write(geocoder_init, <<~'RUBY')
Geocoder.configure(
lookup: :nominatim,
http_headers: { "User-Agent" => "Hjerterom App (contact@hjerterom.no)" },
timeout: 5
)
RUBY
puts " ✅ Created #{geocoder_init}"
end
# ----- Update importmap to include mapbox-gl -----
importmap = "#{app_root}/config/importmap.rb"
if File.exist?(importmap)
content = File.read(importmap, encoding: "UTF-8")
unless content.include?("mapbox-gl")
content.sub!(/pin "application"/, "pin \"application\"\npin \"mapbox-gl\", to: \"https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.js\"\npin \"mapbox-gl.css\", to: \"https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.css\"")
File.write(importmap, content, encoding: "UTF-8")
puts " ✅ Added mapbox-gl to importmap"
end
end
# ----- Precompile mapbox css -----
assets_init = "#{app_root}/config/initializers/assets.rb"
if File.exist?(assets_init)
content = File.read(assets_init, encoding: "UTF-8")
unless content.include?("mapbox-gl.css")
content << "\nRails.application.config.assets.precompile += %w( mapbox-gl.css )\n"
File.write(assets_init, content, encoding: "UTF-8")
puts " ✅ Added mapbox-gl.css to precompile"
end
end
end
end
# ------------------------------------------------------------------------------
# 3. Add global seed scraper (LLM-assisted) rake task for brgen
# ------------------------------------------------------------------------------
seed_scraper = "rails/brgen/lib/tasks/seed_scraper.rake"
unless File.exist?(seed_scraper)
FileUtils.mkdir_p(File.dirname(seed_scraper))
File.write(seed_scraper, <<~'RUBY')
namespace :seed do
desc "Scrape real data using Ferrum + LLM and insert into DB (brgen only)"
task scrape: :environment do
unless defined?(Brgen) || Rails.root.basename.to_s == "brgen"
puts "This task is intended for brgen app only."
next
end
require_relative "../../app/services/scrape"
url = ENV["SEED_URL"] || "https://news.ycombinator.com"
schema = ["title", "url", "score", "author"]
hint = "Extract the top 30 posts. Return JSON with items array."
puts "Scraping #{url}..."
result = Scrape.call(url, schema: schema, hint: hint)
if result["items"].is_a?(Array)
result["items"].each do |item|
Post.create!(
title: item["title"],
content: "Source: #{item['url']}\nScore: #{item['score']}",
anonymous: true,
user: User.guest.first || User.create!(email_address: "scraper@example.com", password: "scraper123", guest: true)
)
end
puts "Created #{result['items'].size} posts from scraper."
else
puts "No items found or invalid response."
end
end
end
RUBY
puts " ✅ Created global seed scraper rake task"
end
# ------------------------------------------------------------------------------
# 4. Final instructions
# ------------------------------------------------------------------------------
puts <<~MSG
✅ Patch applied successfully!
Next steps:
1. Run `bundle install` in each app (rails/{amber,baibl,blognet,brgen,bsdports,hjerterom})
2. Run `bin/rails solidus:install` inside brgen (answer Y to auth, choose starter frontend, skip payment)
3. Set environment variable MAPBOX_ACCESS_TOKEN for hjerterom
4. Set OPENROUTER_API_KEY if using LLM scraper
5. Run migrations: `bin/rails db:migrate` in each app
6. Seed data: `RAILS_ENV=development SEED_REAL=true bin/rails db:seed` (optional)
7. Test Reddit scraper: `cd rails/brgen && bin/rails reddit:scrape_bergen`
Your apps now have shared engine, Facebook feed, map frontpage, Solidus marketplace, and r/bergen scraper.
MSG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment