Skip to content

Instantly share code, notes, and snippets.

@anon987654321
Created May 11, 2026 12:05
Show Gist options
  • Select an option

  • Save anon987654321/74aa76a52dddee35d887fc77f1b4401b to your computer and use it in GitHub Desktop.

Select an option

Save anon987654321/74aa76a52dddee35d887fc77f1b4401b to your computer and use it in GitHub Desktop.
DEPLOY 2026-05-11

DEPLOY Snapshot — 2026-05-11T12:05:45Z

Tree

openbsd/
openbsd/files/
openbsd/files/rc.d/
rails/
rails/__shared/
rails/__shared/layouts/
rails/amber/
rails/amber/app/
rails/amber/app/app/
rails/amber/app/app/assets/
rails/amber/app/app/assets/builds/
rails/amber/app/app/assets/images/
rails/amber/app/app/assets/stylesheets/
rails/amber/app/app/channels/
rails/amber/app/app/channels/application_cable/
rails/amber/app/app/controllers/
rails/amber/app/app/controllers/concerns/
rails/amber/app/app/helpers/
rails/amber/app/app/javascript/
rails/amber/app/app/javascript/controllers/
rails/amber/app/app/jobs/
rails/amber/app/app/mailers/
rails/amber/app/app/models/
rails/amber/app/app/models/concerns/
rails/amber/app/app/services/
rails/amber/app/app/views/
rails/amber/app/app/views/ai/
rails/amber/app/app/views/home/
rails/amber/app/app/views/items/
rails/amber/app/app/views/layouts/
rails/amber/app/app/views/outfits/
rails/amber/app/app/views/passwords/
rails/amber/app/app/views/passwords_mailer/
rails/amber/app/app/views/planned_outfits/
rails/amber/app/app/views/posts/
rails/amber/app/app/views/pwa/
rails/amber/app/app/views/registrations/
rails/amber/app/app/views/sessions/
rails/amber/app/app/views/shared/
rails/amber/app/app/views/users/
rails/amber/app/bin/
rails/amber/app/config/
rails/amber/app/config/environments/
rails/amber/app/config/initializers/
rails/amber/app/config/locales/
rails/amber/app/db/
rails/amber/app/db/migrate/
rails/amber/app/lib/
rails/amber/app/lib/tasks/
rails/amber/app/public/
rails/amber/app/script/
rails/amber/app/storage/
rails/baibl/
rails/baibl/app/
rails/baibl/app/app/
rails/baibl/app/app/assets/
rails/baibl/app/app/assets/images/
rails/baibl/app/app/assets/stylesheets/
rails/baibl/app/app/controllers/
rails/baibl/app/app/controllers/concerns/
rails/baibl/app/app/helpers/
rails/baibl/app/app/javascript/
rails/baibl/app/app/javascript/controllers/
rails/baibl/app/app/jobs/
rails/baibl/app/app/mailers/
rails/baibl/app/app/models/
rails/baibl/app/app/models/concerns/
rails/baibl/app/app/views/
rails/baibl/app/app/views/bookmarks/
rails/baibl/app/app/views/highlights/
rails/baibl/app/app/views/layouts/
rails/baibl/app/app/views/pwa/
rails/baibl/app/app/views/scriptures/
rails/baibl/app/bin/
rails/baibl/app/config/
rails/baibl/app/config/environments/
rails/baibl/app/config/initializers/
rails/baibl/app/config/locales/
rails/baibl/app/db/
rails/baibl/app/db/migrate/
rails/baibl/app/lib/
rails/baibl/app/lib/tasks/
rails/baibl/app/public/
rails/baibl/app/script/
rails/baibl/app/storage/
rails/blognet/
rails/blognet/app/
rails/blognet/app/app/
rails/blognet/app/app/assets/
rails/blognet/app/app/assets/images/
rails/blognet/app/app/assets/stylesheets/
rails/blognet/app/app/channels/
rails/blognet/app/app/channels/application_cable/
rails/blognet/app/app/controllers/
rails/blognet/app/app/controllers/concerns/
rails/blognet/app/app/helpers/
rails/blognet/app/app/javascript/
rails/blognet/app/app/javascript/controllers/
rails/blognet/app/app/jobs/
rails/blognet/app/app/mailers/
rails/blognet/app/app/models/
rails/blognet/app/app/models/concerns/
rails/blognet/app/app/views/
rails/blognet/app/app/views/active_storage/
rails/blognet/app/app/views/active_storage/blobs/
rails/blognet/app/app/views/blogs/
rails/blognet/app/app/views/comments/
rails/blognet/app/app/views/layouts/
rails/blognet/app/app/views/layouts/action_text/
rails/blognet/app/app/views/layouts/action_text/contents/
rails/blognet/app/app/views/passwords/
rails/blognet/app/app/views/passwords_mailer/
rails/blognet/app/app/views/posts/
rails/blognet/app/app/views/pwa/
rails/blognet/app/app/views/sessions/
rails/blognet/app/bin/
rails/blognet/app/config/
rails/blognet/app/config/environments/
rails/blognet/app/config/initializers/
rails/blognet/app/config/locales/
rails/blognet/app/db/
rails/blognet/app/db/migrate/
rails/blognet/app/lib/
rails/blognet/app/lib/tasks/
rails/blognet/app/public/
rails/blognet/app/script/
rails/blognet/app/storage/
rails/brgen/
rails/brgen/app/
rails/brgen/app/app/
rails/brgen/app/app/assets/
rails/brgen/app/app/assets/images/
rails/brgen/app/app/assets/stylesheets/
rails/brgen/app/app/channels/
rails/brgen/app/app/channels/application_cable/
rails/brgen/app/app/controllers/
rails/brgen/app/app/controllers/concerns/
rails/brgen/app/app/controllers/dating/
rails/brgen/app/app/controllers/marketplace/
rails/brgen/app/app/controllers/playlist/
rails/brgen/app/app/controllers/takeaway/
rails/brgen/app/app/controllers/tv/
rails/brgen/app/app/helpers/
rails/brgen/app/app/javascript/
rails/brgen/app/app/javascript/controllers/
rails/brgen/app/app/jobs/
rails/brgen/app/app/mailers/
rails/brgen/app/app/models/
rails/brgen/app/app/models/concerns/
rails/brgen/app/app/models/dating/
rails/brgen/app/app/models/marketplace/
rails/brgen/app/app/models/playlist/
rails/brgen/app/app/models/takeaway/
rails/brgen/app/app/models/tv/
rails/brgen/app/app/services/
rails/brgen/app/app/views/
rails/brgen/app/app/views/comments/
rails/brgen/app/app/views/communities/
rails/brgen/app/app/views/conversations/
rails/brgen/app/app/views/dating/
rails/brgen/app/app/views/dating/home/
rails/brgen/app/app/views/dating/matches/
rails/brgen/app/app/views/dating/profiles/
rails/brgen/app/app/views/home/
rails/brgen/app/app/views/layouts/
rails/brgen/app/app/views/marketplace/
rails/brgen/app/app/views/marketplace/categories/
rails/brgen/app/app/views/marketplace/listings/
rails/brgen/app/app/views/messages/
rails/brgen/app/app/views/passwords/
rails/brgen/app/app/views/passwords_mailer/
rails/brgen/app/app/views/playlist/
rails/brgen/app/app/views/playlist/playlists/
rails/brgen/app/app/views/playlist/tracks/
rails/brgen/app/app/views/posts/
rails/brgen/app/app/views/pwa/
rails/brgen/app/app/views/sessions/
rails/brgen/app/app/views/shared/
rails/brgen/app/app/views/takeaway/
rails/brgen/app/app/views/takeaway/menu_items/
rails/brgen/app/app/views/takeaway/orders/
rails/brgen/app/app/views/takeaway/restaurants/
rails/brgen/app/app/views/tv/
rails/brgen/app/app/views/tv/channels/
rails/brgen/app/app/views/tv/home/
rails/brgen/app/app/views/tv/videos/
rails/brgen/app/app/views/typing_indicators/
rails/brgen/app/app/views/votes/
rails/brgen/app/bin/
rails/brgen/app/config/
rails/brgen/app/config/environments/
rails/brgen/app/config/initializers/
rails/brgen/app/config/locales/
rails/brgen/app/db/
rails/brgen/app/db/migrate/
rails/brgen/app/lib/
rails/brgen/app/lib/tasks/
rails/brgen/app/public/
rails/brgen/app/script/
rails/brgen/app/storage/
rails/brgen/app/test/
rails/brgen/app/test/controllers/
rails/brgen/app/test/fixtures/
rails/brgen/app/test/fixtures/files/
rails/brgen/app/test/helpers/
rails/brgen/app/test/integration/
rails/brgen/app/test/models/
rails/brgen/subapps/
rails/brgen/subapps/dating/
rails/brgen/subapps/marketplace/
rails/brgen/subapps/playlist/
rails/brgen/subapps/takeaway/
rails/brgen/subapps/tv/
rails/bsdports/
rails/bsdports/app/
rails/bsdports/app/app/
rails/bsdports/app/app/assets/
rails/bsdports/app/app/assets/images/
rails/bsdports/app/app/assets/stylesheets/
rails/bsdports/app/app/controllers/
rails/bsdports/app/app/controllers/concerns/
rails/bsdports/app/app/helpers/
rails/bsdports/app/app/javascript/
rails/bsdports/app/app/javascript/controllers/
rails/bsdports/app/app/jobs/
rails/bsdports/app/app/mailers/
rails/bsdports/app/app/models/
rails/bsdports/app/app/models/concerns/
rails/bsdports/app/app/views/
rails/bsdports/app/app/views/categories/
rails/bsdports/app/app/views/comments/
rails/bsdports/app/app/views/layouts/
rails/bsdports/app/app/views/ports/
rails/bsdports/app/app/views/pwa/
rails/bsdports/app/bin/
rails/bsdports/app/config/
rails/bsdports/app/config/environments/
rails/bsdports/app/config/initializers/
rails/bsdports/app/config/locales/
rails/bsdports/app/db/
rails/bsdports/app/db/migrate/
rails/bsdports/app/lib/
rails/bsdports/app/lib/tasks/
rails/bsdports/app/public/
rails/bsdports/app/script/
rails/bsdports/app/storage/
rails/hjerterom/
rails/hjerterom/app/
rails/hjerterom/app/app/
rails/hjerterom/app/app/assets/
rails/hjerterom/app/app/assets/images/
rails/hjerterom/app/app/assets/stylesheets/
rails/hjerterom/app/app/controllers/
rails/hjerterom/app/app/controllers/concerns/
rails/hjerterom/app/app/helpers/
rails/hjerterom/app/app/javascript/
rails/hjerterom/app/app/javascript/controllers/
rails/hjerterom/app/app/jobs/
rails/hjerterom/app/app/mailers/
rails/hjerterom/app/app/models/
rails/hjerterom/app/app/models/concerns/
rails/hjerterom/app/app/views/
rails/hjerterom/app/app/views/community/
rails/hjerterom/app/app/views/food_listings/
rails/hjerterom/app/app/views/home/
rails/hjerterom/app/app/views/layouts/
rails/hjerterom/app/app/views/pwa/
rails/hjerterom/app/app/views/resources/
rails/hjerterom/app/bin/
rails/hjerterom/app/config/
rails/hjerterom/app/config/environments/
rails/hjerterom/app/config/initializers/
rails/hjerterom/app/config/locales/
rails/hjerterom/app/db/
rails/hjerterom/app/db/migrate/
rails/hjerterom/app/lib/
rails/hjerterom/app/lib/tasks/
rails/hjerterom/app/public/
rails/hjerterom/app/script/
rails/hjerterom/app/storage/
README.md
openbsd/README.md
openbsd/files/httpd.conf
openbsd/files/pf.stage1.conf
openbsd/files/pf.stage2.conf
openbsd/files/renew-certs.sh
openbsd/files/smtpd.conf
openbsd/openbsd.sh
postpro.rb
rails/@shared_functions.sh
rails/README.md
rails/__shared/@active_storage_and_imageprocessing.sh
rails/__shared/@ai.sh
rails/__shared/@airbnb_features.sh
rails/__shared/@common.sh
rails/__shared/@devise.sh
rails/__shared/@features_base.sh
rails/__shared/@instant_messaging.sh
rails/__shared/@live_cam_streaming.sh
rails/__shared/@live_streaming.sh
rails/__shared/@messenger_features.sh
rails/__shared/@postgresql.sh
rails/__shared/@posts.sh
rails/__shared/@pwa.sh
rails/__shared/@rails_new.sh
rails/__shared/@reddit_features.sh
rails/__shared/@redis.sh
rails/__shared/@twitter_features.sh
rails/__shared/@yarn.sh
rails/__shared/layouts/_flash.html.erb
rails/__shared/layouts/_footer.html.erb
rails/__shared/layouts/_meta.html.erb
rails/__shared/layouts/_nav.html.erb
rails/__shared/layouts/application.html.erb
rails/__shared/layouts/visualizer.js
rails/amber/@shared_functions.sh
rails/amber/README.md
rails/amber/amber.sh
rails/amber/app/Dockerfile
rails/amber/app/Gemfile
rails/amber/app/README.md
rails/amber/app/Rakefile
rails/amber/app/app/channels/application_cable/connection.rb
rails/amber/app/app/controllers/ai_controller.rb
rails/amber/app/app/controllers/application_controller.rb
rails/amber/app/app/controllers/concerns/authentication.rb
rails/amber/app/app/controllers/follows_controller.rb
rails/amber/app/app/controllers/home_controller.rb
rails/amber/app/app/controllers/items_controller.rb
rails/amber/app/app/controllers/outfits_controller.rb
rails/amber/app/app/controllers/passwords_controller.rb
rails/amber/app/app/controllers/planned_outfits_controller.rb
rails/amber/app/app/controllers/posts_controller.rb
rails/amber/app/app/controllers/registrations_controller.rb
rails/amber/app/app/controllers/sessions_controller.rb
rails/amber/app/app/controllers/users_controller.rb
rails/amber/app/app/helpers/application_helper.rb
rails/amber/app/app/javascript/application.js
rails/amber/app/app/javascript/controllers/animated_number_controller.js
rails/amber/app/app/javascript/controllers/application.js
rails/amber/app/app/javascript/controllers/auto_submit_controller.js
rails/amber/app/app/javascript/controllers/character_counter_controller.js
rails/amber/app/app/javascript/controllers/clipboard_controller.js
rails/amber/app/app/javascript/controllers/dialog_controller.js
rails/amber/app/app/javascript/controllers/dropdown_controller.js
rails/amber/app/app/javascript/controllers/filter_controller.js
rails/amber/app/app/javascript/controllers/hello_controller.js
rails/amber/app/app/javascript/controllers/index.js
rails/amber/app/app/javascript/controllers/notification_controller.js
rails/amber/app/app/javascript/controllers/sortable_controller.js
rails/amber/app/app/javascript/controllers/textarea_autogrow_controller.js
rails/amber/app/app/javascript/controllers/timeago_controller.js
rails/amber/app/app/jobs/application_job.rb
rails/amber/app/app/mailers/application_mailer.rb
rails/amber/app/app/mailers/passwords_mailer.rb
rails/amber/app/app/models/application_record.rb
rails/amber/app/app/models/current.rb
rails/amber/app/app/models/follow.rb
rails/amber/app/app/models/item.rb
rails/amber/app/app/models/outfit.rb
rails/amber/app/app/models/outfit_item.rb
rails/amber/app/app/models/planned_outfit.rb
rails/amber/app/app/models/post.rb
rails/amber/app/app/models/session.rb
rails/amber/app/app/models/user.rb
rails/amber/app/app/services/wardrobe_ai_service.rb
rails/amber/app/app/services/weather_service.rb
rails/amber/app/app/views/ai/_analysis.html.erb
rails/amber/app/app/views/ai/_item_tags.html.erb
rails/amber/app/app/views/ai/capsule.html.erb
rails/amber/app/app/views/ai/color_palette.html.erb
rails/amber/app/app/views/ai/declutter_guide.html.erb
rails/amber/app/app/views/ai/mood_board.html.erb
rails/amber/app/app/views/ai/occasion_map.html.erb
rails/amber/app/app/views/ai/search.html.erb
rails/amber/app/app/views/ai/suggest_outfits.html.erb
rails/amber/app/app/views/home/index.html.erb
rails/amber/app/app/views/items/_form.html.erb
rails/amber/app/app/views/items/_item.html.erb
rails/amber/app/app/views/items/edit.html.erb
rails/amber/app/app/views/items/index.html.erb
rails/amber/app/app/views/items/new.html.erb
rails/amber/app/app/views/items/show.html.erb
rails/amber/app/app/views/layouts/application.html.erb
rails/amber/app/app/views/layouts/mailer.html.erb
rails/amber/app/app/views/layouts/mailer.text.erb
rails/amber/app/app/views/outfits/_form.html.erb
rails/amber/app/app/views/outfits/_outfit.html.erb
rails/amber/app/app/views/outfits/edit.html.erb
rails/amber/app/app/views/outfits/index.html.erb
rails/amber/app/app/views/outfits/new.html.erb
rails/amber/app/app/views/outfits/show.html.erb
rails/amber/app/app/views/passwords/edit.html.erb
rails/amber/app/app/views/passwords/new.html.erb
rails/amber/app/app/views/passwords_mailer/reset.html.erb
rails/amber/app/app/views/passwords_mailer/reset.text.erb
rails/amber/app/app/views/planned_outfits/index.html.erb
rails/amber/app/app/views/posts/_post.html.erb
rails/amber/app/app/views/posts/feed.html.erb
rails/amber/app/app/views/posts/index.html.erb
rails/amber/app/app/views/posts/new.html.erb
rails/amber/app/app/views/posts/show.html.erb
rails/amber/app/app/views/pwa/manifest.json.erb
rails/amber/app/app/views/pwa/service-worker.js
rails/amber/app/app/views/registrations/new.html.erb
rails/amber/app/app/views/sessions/new.html.erb
rails/amber/app/app/views/shared/_errors.html.erb
rails/amber/app/app/views/shared/_flash.html.erb
rails/amber/app/app/views/shared/_pagination.html.erb
rails/amber/app/app/views/users/show.html.erb
rails/amber/app/config/application.rb
rails/amber/app/config/boot.rb
rails/amber/app/config/bundler-audit.yml
rails/amber/app/config/cable.yml
rails/amber/app/config/cache.yml
rails/amber/app/config/ci.rb
rails/amber/app/config/database.yml
rails/amber/app/config/deploy.yml
rails/amber/app/config/environment.rb
rails/amber/app/config/environments/development.rb
rails/amber/app/config/environments/production.rb
rails/amber/app/config/environments/test.rb
rails/amber/app/config/falcon.rb
rails/amber/app/config/importmap.rb
rails/amber/app/config/initializers/assets.rb
rails/amber/app/config/initializers/content_security_policy.rb
rails/amber/app/config/initializers/filter_parameter_logging.rb
rails/amber/app/config/initializers/inflections.rb
rails/amber/app/config/initializers/pagy.rb
rails/amber/app/config/initializers/requires.rb
rails/amber/app/config/locales/en.yml
rails/amber/app/config/puma.rb
rails/amber/app/config/queue.yml
rails/amber/app/config/recurring.yml
rails/amber/app/config/routes.rb
rails/amber/app/config/storage.yml
rails/amber/app/db/cable_schema.rb
rails/amber/app/db/cache_schema.rb
rails/amber/app/db/migrate/20260504180350_create_users.rb
rails/amber/app/db/migrate/20260504180352_create_sessions.rb
rails/amber/app/db/migrate/20260504180357_create_active_storage_tables.active_storage.rb
rails/amber/app/db/migrate/20260504180401_create_items.rb
rails/amber/app/db/migrate/20260504180405_create_outfit_items.rb
rails/amber/app/db/migrate/20260504180406_create_planned_outfits.rb
rails/amber/app/db/migrate/20260504180410_add_extended_fields_to_items.rb
rails/amber/app/db/migrate/20260504205505_create_outfits.rb
rails/amber/app/db/migrate/20260504211952_create_follows.rb
rails/amber/app/db/migrate/20260504212306_create_posts.rb
rails/amber/app/db/queue_schema.rb
rails/amber/app/db/schema.rb
rails/amber/app/db/seeds.rb
rails/amber/app/public/robots.txt
rails/baibl/README.md
rails/baibl/app/Dockerfile
rails/baibl/app/Gemfile
rails/baibl/app/README.md
rails/baibl/app/Rakefile
rails/baibl/app/app/controllers/application_controller.rb
rails/baibl/app/app/controllers/bookmarks_controller.rb
rails/baibl/app/app/controllers/concerns/authentication.rb
rails/baibl/app/app/controllers/highlights_controller.rb
rails/baibl/app/app/controllers/passwords_controller.rb
rails/baibl/app/app/controllers/scriptures_controller.rb
rails/baibl/app/app/controllers/sessions_controller.rb
rails/baibl/app/app/helpers/application_helper.rb
rails/baibl/app/app/javascript/application.js
rails/baibl/app/app/javascript/controllers/animated_number_controller.js
rails/baibl/app/app/javascript/controllers/application.js
rails/baibl/app/app/javascript/controllers/auto_submit_controller.js
rails/baibl/app/app/javascript/controllers/character_counter_controller.js
rails/baibl/app/app/javascript/controllers/clipboard_controller.js
rails/baibl/app/app/javascript/controllers/dialog_controller.js
rails/baibl/app/app/javascript/controllers/dropdown_controller.js
rails/baibl/app/app/javascript/controllers/hello_controller.js
rails/baibl/app/app/javascript/controllers/index.js
rails/baibl/app/app/javascript/controllers/notification_controller.js
rails/baibl/app/app/javascript/controllers/sortable_controller.js
rails/baibl/app/app/javascript/controllers/textarea_autogrow_controller.js
rails/baibl/app/app/javascript/controllers/timeago_controller.js
rails/baibl/app/app/jobs/application_job.rb
rails/baibl/app/app/mailers/application_mailer.rb
rails/baibl/app/app/models/application_record.rb
rails/baibl/app/app/models/book.rb
rails/baibl/app/app/models/bookmark.rb
rails/baibl/app/app/models/chapter.rb
rails/baibl/app/app/models/current.rb
rails/baibl/app/app/models/highlight.rb
rails/baibl/app/app/models/reading_plan.rb
rails/baibl/app/app/models/reading_plan_day.rb
rails/baibl/app/app/models/session.rb
rails/baibl/app/app/models/user.rb
rails/baibl/app/app/models/verse.rb
rails/baibl/app/app/views/bookmarks/index.html.erb
rails/baibl/app/app/views/highlights/create.turbo_stream.erb
rails/baibl/app/app/views/highlights/destroy.turbo_stream.erb
rails/baibl/app/app/views/layouts/application.html.erb
rails/baibl/app/app/views/layouts/mailer.html.erb
rails/baibl/app/app/views/layouts/mailer.text.erb
rails/baibl/app/app/views/pwa/manifest.json.erb
rails/baibl/app/app/views/pwa/service-worker.js
rails/baibl/app/app/views/scriptures/book.html.erb
rails/baibl/app/app/views/scriptures/chapter.html.erb
rails/baibl/app/app/views/scriptures/index.html.erb
rails/baibl/app/app/views/scriptures/search.html.erb
rails/baibl/app/config/application.rb
rails/baibl/app/config/boot.rb
rails/baibl/app/config/bundler-audit.yml
rails/baibl/app/config/cable.yml
rails/baibl/app/config/ci.rb
rails/baibl/app/config/database.yml
rails/baibl/app/config/deploy.yml
rails/baibl/app/config/environment.rb
rails/baibl/app/config/environments/development.rb
rails/baibl/app/config/environments/production.rb
rails/baibl/app/config/environments/test.rb
rails/baibl/app/config/importmap.rb
rails/baibl/app/config/initializers/assets.rb
rails/baibl/app/config/initializers/content_security_policy.rb
rails/baibl/app/config/initializers/filter_parameter_logging.rb
rails/baibl/app/config/initializers/inflections.rb
rails/baibl/app/config/locales/en.yml
rails/baibl/app/config/puma.rb
rails/baibl/app/config/routes.rb
rails/baibl/app/config/storage.yml
rails/baibl/app/db/migrate/20260501020807_create_users.rb
rails/baibl/app/db/migrate/20260501020818_create_sessions.rb
rails/baibl/app/db/migrate/20260507120001_create_books.rb
rails/baibl/app/db/migrate/20260507120002_create_chapters.rb
rails/baibl/app/db/migrate/20260507120003_create_verses.rb
rails/baibl/app/db/migrate/20260507120004_create_highlights.rb
rails/baibl/app/db/migrate/20260507120005_create_bookmarks.rb
rails/baibl/app/db/migrate/20260507120006_create_reading_plans.rb
rails/baibl/app/db/migrate/20260507120007_create_reading_plan_days.rb
rails/baibl/app/db/seeds.rb
rails/baibl/app/public/robots.txt
rails/baibl/baibl.sh
rails/blognet/README.md
rails/blognet/app/Dockerfile
rails/blognet/app/Gemfile
rails/blognet/app/README.md
rails/blognet/app/Rakefile
rails/blognet/app/app/channels/application_cable/connection.rb
rails/blognet/app/app/controllers/application_controller.rb
rails/blognet/app/app/controllers/blogs_controller.rb
rails/blognet/app/app/controllers/comments_controller.rb
rails/blognet/app/app/controllers/concerns/authentication.rb
rails/blognet/app/app/controllers/passwords_controller.rb
rails/blognet/app/app/controllers/posts_controller.rb
rails/blognet/app/app/controllers/sessions_controller.rb
rails/blognet/app/app/helpers/application_helper.rb
rails/blognet/app/app/javascript/application.js
rails/blognet/app/app/javascript/controllers/animated_number_controller.js
rails/blognet/app/app/javascript/controllers/application.js
rails/blognet/app/app/javascript/controllers/auto_submit_controller.js
rails/blognet/app/app/javascript/controllers/character_counter_controller.js
rails/blognet/app/app/javascript/controllers/clipboard_controller.js
rails/blognet/app/app/javascript/controllers/dialog_controller.js
rails/blognet/app/app/javascript/controllers/dropdown_controller.js
rails/blognet/app/app/javascript/controllers/hello_controller.js
rails/blognet/app/app/javascript/controllers/index.js
rails/blognet/app/app/javascript/controllers/notification_controller.js
rails/blognet/app/app/javascript/controllers/sortable_controller.js
rails/blognet/app/app/javascript/controllers/textarea_autogrow_controller.js
rails/blognet/app/app/javascript/controllers/timeago_controller.js
rails/blognet/app/app/jobs/application_job.rb
rails/blognet/app/app/mailers/application_mailer.rb
rails/blognet/app/app/mailers/passwords_mailer.rb
rails/blognet/app/app/models/application_record.rb
rails/blognet/app/app/models/blog.rb
rails/blognet/app/app/models/categorization.rb
rails/blognet/app/app/models/category.rb
rails/blognet/app/app/models/comment.rb
rails/blognet/app/app/models/current.rb
rails/blognet/app/app/models/post.rb
rails/blognet/app/app/models/session.rb
rails/blognet/app/app/models/tag.rb
rails/blognet/app/app/models/tagging.rb
rails/blognet/app/app/models/user.rb
rails/blognet/app/app/views/active_storage/blobs/_blob.html.erb
rails/blognet/app/app/views/blogs/_form.html.erb
rails/blognet/app/app/views/blogs/edit.html.erb
rails/blognet/app/app/views/blogs/index.html.erb
rails/blognet/app/app/views/blogs/new.html.erb
rails/blognet/app/app/views/blogs/show.html.erb
rails/blognet/app/app/views/comments/_comment.html.erb
rails/blognet/app/app/views/layouts/action_text/contents/_content.html.erb
rails/blognet/app/app/views/layouts/application.html.erb
rails/blognet/app/app/views/layouts/mailer.html.erb
rails/blognet/app/app/views/layouts/mailer.text.erb
rails/blognet/app/app/views/passwords/edit.html.erb
rails/blognet/app/app/views/passwords/new.html.erb
rails/blognet/app/app/views/passwords_mailer/reset.html.erb
rails/blognet/app/app/views/passwords_mailer/reset.text.erb
rails/blognet/app/app/views/posts/_form.html.erb
rails/blognet/app/app/views/posts/edit.html.erb
rails/blognet/app/app/views/posts/new.html.erb
rails/blognet/app/app/views/posts/show.html.erb
rails/blognet/app/app/views/pwa/manifest.json.erb
rails/blognet/app/app/views/pwa/service-worker.js
rails/blognet/app/app/views/sessions/new.html.erb
rails/blognet/app/config/application.rb
rails/blognet/app/config/boot.rb
rails/blognet/app/config/bundler-audit.yml
rails/blognet/app/config/cable.yml
rails/blognet/app/config/cache.yml
rails/blognet/app/config/ci.rb
rails/blognet/app/config/database.yml
rails/blognet/app/config/deploy.yml
rails/blognet/app/config/environment.rb
rails/blognet/app/config/environments/development.rb
rails/blognet/app/config/environments/production.rb
rails/blognet/app/config/environments/test.rb
rails/blognet/app/config/importmap.rb
rails/blognet/app/config/initializers/assets.rb
rails/blognet/app/config/initializers/content_security_policy.rb
rails/blognet/app/config/initializers/filter_parameter_logging.rb
rails/blognet/app/config/initializers/inflections.rb
rails/blognet/app/config/locales/en.yml
rails/blognet/app/config/puma.rb
rails/blognet/app/config/queue.yml
rails/blognet/app/config/recurring.yml
rails/blognet/app/config/routes.rb
rails/blognet/app/config/storage.yml
rails/blognet/app/db/cable_schema.rb
rails/blognet/app/db/cache_schema.rb
rails/blognet/app/db/migrate/20260501020807_create_users.rb
rails/blognet/app/db/migrate/20260501020818_create_sessions.rb
rails/blognet/app/db/migrate/20260501020848_create_active_storage_tables.active_storage.rb
rails/blognet/app/db/migrate/20260501020920_create_action_text_tables.action_text.rb
rails/blognet/app/db/migrate/20260507120001_create_blogs.rb
rails/blognet/app/db/migrate/20260507120002_create_posts.rb
rails/blognet/app/db/migrate/20260507120003_create_categories.rb
rails/blognet/app/db/migrate/20260507120004_create_categorizations.rb
rails/blognet/app/db/migrate/20260507120005_create_comments.rb
rails/blognet/app/db/migrate/20260507120006_create_tags.rb
rails/blognet/app/db/migrate/20260507120007_create_taggings.rb
rails/blognet/app/db/queue_schema.rb
rails/blognet/app/db/schema.rb
rails/blognet/app/db/seeds.rb
rails/blognet/app/public/robots.txt
rails/blognet/blognet.sh
rails/blognet/blognet_test.sh
rails/brgen/README.md
rails/brgen/README_takeaway.md
rails/brgen/README_tv.md
rails/brgen/app/Dockerfile
rails/brgen/app/Gemfile
rails/brgen/app/README.md
rails/brgen/app/Rakefile
rails/brgen/app/app/channels/application_cable/channel.rb
rails/brgen/app/app/channels/application_cable/connection.rb
rails/brgen/app/app/controllers/application_controller.rb
rails/brgen/app/app/controllers/comments_controller.rb
rails/brgen/app/app/controllers/communities_controller.rb
rails/brgen/app/app/controllers/concerns/authentication.rb
rails/brgen/app/app/controllers/conversations_controller.rb
rails/brgen/app/app/controllers/dating/base_controller.rb
rails/brgen/app/app/controllers/dating/dislikes_controller.rb
rails/brgen/app/app/controllers/dating/home_controller.rb
rails/brgen/app/app/controllers/dating/likes_controller.rb
rails/brgen/app/app/controllers/dating/matches_controller.rb
rails/brgen/app/app/controllers/dating/profiles_controller.rb
rails/brgen/app/app/controllers/follows_controller.rb
rails/brgen/app/app/controllers/home_controller.rb
rails/brgen/app/app/controllers/marketplace/base_controller.rb
rails/brgen/app/app/controllers/marketplace/categories_controller.rb
rails/brgen/app/app/controllers/marketplace/listings_controller.rb
rails/brgen/app/app/controllers/marketplace/orders_controller.rb
rails/brgen/app/app/controllers/messages_controller.rb
rails/brgen/app/app/controllers/passwords_controller.rb
rails/brgen/app/app/controllers/playlist/base_controller.rb
rails/brgen/app/app/controllers/playlist/listens_controller.rb
rails/brgen/app/app/controllers/playlist/playlists_controller.rb
rails/brgen/app/app/controllers/playlist/tracks_controller.rb
rails/brgen/app/app/controllers/playlist_controller.rb
rails/brgen/app/app/controllers/posts_controller.rb
rails/brgen/app/app/controllers/sessions_controller.rb
rails/brgen/app/app/controllers/takeaway/base_controller.rb
rails/brgen/app/app/controllers/takeaway/menu_items_controller.rb
rails/brgen/app/app/controllers/takeaway/orders_controller.rb
rails/brgen/app/app/controllers/takeaway/restaurants_controller.rb
rails/brgen/app/app/controllers/tv/base_controller.rb
rails/brgen/app/app/controllers/tv/channels_controller.rb
rails/brgen/app/app/controllers/tv/home_controller.rb
rails/brgen/app/app/controllers/tv/videos_controller.rb
rails/brgen/app/app/controllers/typing_indicators_controller.rb
rails/brgen/app/app/controllers/votes_controller.rb
rails/brgen/app/app/helpers/application_helper.rb
rails/brgen/app/app/javascript/application.js
rails/brgen/app/app/javascript/controllers/animated_number_controller.js
rails/brgen/app/app/javascript/controllers/application.js
rails/brgen/app/app/javascript/controllers/auto_submit_controller.js
rails/brgen/app/app/javascript/controllers/character_counter_controller.js
rails/brgen/app/app/javascript/controllers/clipboard_controller.js
rails/brgen/app/app/javascript/controllers/dialog_controller.js
rails/brgen/app/app/javascript/controllers/dropdown_controller.js
rails/brgen/app/app/javascript/controllers/hello_controller.js
rails/brgen/app/app/javascript/controllers/index.js
rails/brgen/app/app/javascript/controllers/notification_controller.js
rails/brgen/app/app/javascript/controllers/sortable_controller.js
rails/brgen/app/app/javascript/controllers/textarea_autogrow_controller.js
rails/brgen/app/app/javascript/controllers/timeago_controller.js
rails/brgen/app/app/javascript/controllers/typing_controller.js
rails/brgen/app/app/javascript/controllers/typing_input_controller.js
rails/brgen/app/app/jobs/application_job.rb
rails/brgen/app/app/mailers/application_mailer.rb
rails/brgen/app/app/mailers/passwords_mailer.rb
rails/brgen/app/app/models/application_record.rb
rails/brgen/app/app/models/comment.rb
rails/brgen/app/app/models/community.rb
rails/brgen/app/app/models/concerns/commentable.rb
rails/brgen/app/app/models/concerns/mentionable.rb
rails/brgen/app/app/models/concerns/taggable.rb
rails/brgen/app/app/models/concerns/votable.rb
rails/brgen/app/app/models/conversation.rb
rails/brgen/app/app/models/conversation_participant.rb
rails/brgen/app/app/models/current.rb
rails/brgen/app/app/models/dating.rb
rails/brgen/app/app/models/dating/dislike.rb
rails/brgen/app/app/models/dating/like.rb
rails/brgen/app/app/models/dating/match.rb
rails/brgen/app/app/models/dating/profile.rb
rails/brgen/app/app/models/follow.rb
rails/brgen/app/app/models/hashtag.rb
rails/brgen/app/app/models/marketplace.rb
rails/brgen/app/app/models/marketplace/category.rb
rails/brgen/app/app/models/marketplace/listing.rb
rails/brgen/app/app/models/marketplace/order.rb
rails/brgen/app/app/models/mention.rb
rails/brgen/app/app/models/message.rb
rails/brgen/app/app/models/message_receipt.rb
rails/brgen/app/app/models/playlist.rb
rails/brgen/app/app/models/playlist/listen.rb
rails/brgen/app/app/models/playlist/playlist.rb
rails/brgen/app/app/models/playlist/playlist_track.rb
rails/brgen/app/app/models/playlist/track.rb
rails/brgen/app/app/models/post.rb
rails/brgen/app/app/models/reaction.rb
rails/brgen/app/app/models/session.rb
rails/brgen/app/app/models/stream.rb
rails/brgen/app/app/models/tagging.rb
rails/brgen/app/app/models/takeaway.rb
rails/brgen/app/app/models/takeaway/menu_item.rb
rails/brgen/app/app/models/takeaway/order.rb
rails/brgen/app/app/models/takeaway/order_item.rb
rails/brgen/app/app/models/takeaway/restaurant.rb
rails/brgen/app/app/models/tv.rb
rails/brgen/app/app/models/tv/broadcast.rb
rails/brgen/app/app/models/tv/channel.rb
rails/brgen/app/app/models/tv/subscription.rb
rails/brgen/app/app/models/tv/video.rb
rails/brgen/app/app/models/tv/view_event.rb
rails/brgen/app/app/models/typing_indicator.rb
rails/brgen/app/app/models/user.rb
rails/brgen/app/app/models/vote.rb
rails/brgen/app/app/services/scrape.rb
rails/brgen/app/app/views/comments/_comment.html.erb
rails/brgen/app/app/views/communities/index.html.erb
rails/brgen/app/app/views/communities/new.html.erb
rails/brgen/app/app/views/communities/show.html.erb
rails/brgen/app/app/views/conversations/index.html.erb
rails/brgen/app/app/views/conversations/show.html.erb
rails/brgen/app/app/views/dating/home/index.html.erb
rails/brgen/app/app/views/dating/matches/index.html.erb
rails/brgen/app/app/views/dating/profiles/edit.html.erb
rails/brgen/app/app/views/dating/profiles/new.html.erb
rails/brgen/app/app/views/dating/profiles/show.html.erb
rails/brgen/app/app/views/home/index.html.erb
rails/brgen/app/app/views/layouts/application.html.erb
rails/brgen/app/app/views/layouts/mailer.html.erb
rails/brgen/app/app/views/layouts/mailer.text.erb
rails/brgen/app/app/views/marketplace/categories/show.html.erb
rails/brgen/app/app/views/marketplace/listings/edit.html.erb
rails/brgen/app/app/views/marketplace/listings/index.html.erb
rails/brgen/app/app/views/marketplace/listings/new.html.erb
rails/brgen/app/app/views/marketplace/listings/show.html.erb
rails/brgen/app/app/views/messages/_message.html.erb
rails/brgen/app/app/views/messages/create.turbo_stream.erb
rails/brgen/app/app/views/messages/new.html.erb
rails/brgen/app/app/views/passwords/edit.html.erb
rails/brgen/app/app/views/passwords/new.html.erb
rails/brgen/app/app/views/passwords_mailer/reset.html.erb
rails/brgen/app/app/views/passwords_mailer/reset.text.erb
rails/brgen/app/app/views/playlist/index.html.erb
rails/brgen/app/app/views/playlist/playlists/edit.html.erb
rails/brgen/app/app/views/playlist/playlists/index.html.erb
rails/brgen/app/app/views/playlist/playlists/new.html.erb
rails/brgen/app/app/views/playlist/playlists/show.html.erb
rails/brgen/app/app/views/posts/_post.html.erb
rails/brgen/app/app/views/posts/index.html.erb
rails/brgen/app/app/views/posts/new.html.erb
rails/brgen/app/app/views/posts/show.html.erb
rails/brgen/app/app/views/pwa/manifest.json.erb
rails/brgen/app/app/views/pwa/service-worker.js
rails/brgen/app/app/views/sessions/new.html.erb
rails/brgen/app/app/views/shared/_vote.html.erb
rails/brgen/app/app/views/takeaway/orders/index.html.erb
rails/brgen/app/app/views/takeaway/orders/show.html.erb
rails/brgen/app/app/views/takeaway/restaurants/edit.html.erb
rails/brgen/app/app/views/takeaway/restaurants/index.html.erb
rails/brgen/app/app/views/takeaway/restaurants/new.html.erb
rails/brgen/app/app/views/takeaway/restaurants/show.html.erb
rails/brgen/app/app/views/tv/channels/edit.html.erb
rails/brgen/app/app/views/tv/channels/index.html.erb
rails/brgen/app/app/views/tv/channels/new.html.erb
rails/brgen/app/app/views/tv/channels/show.html.erb
rails/brgen/app/app/views/tv/home/index.html.erb
rails/brgen/app/app/views/tv/videos/_tv_video.html.erb
rails/brgen/app/app/views/tv/videos/new.html.erb
rails/brgen/app/app/views/tv/videos/show.html.erb
rails/brgen/app/app/views/typing_indicators/_indicator.html.erb
rails/brgen/app/app/views/votes/create.turbo_stream.erb
rails/brgen/app/config/application.rb
rails/brgen/app/config/boot.rb
rails/brgen/app/config/bundler-audit.yml
rails/brgen/app/config/cable.yml
rails/brgen/app/config/cache.yml
rails/brgen/app/config/ci.rb
rails/brgen/app/config/database.yml
rails/brgen/app/config/deploy.yml
rails/brgen/app/config/environment.rb
rails/brgen/app/config/environments/development.rb
rails/brgen/app/config/environments/production.rb
rails/brgen/app/config/environments/test.rb
rails/brgen/app/config/falcon.rb
rails/brgen/app/config/importmap.rb
rails/brgen/app/config/initializers/assets.rb
rails/brgen/app/config/initializers/content_security_policy.rb
rails/brgen/app/config/initializers/filter_parameter_logging.rb
rails/brgen/app/config/initializers/inflections.rb
rails/brgen/app/config/locales/en.yml
rails/brgen/app/config/puma.rb
rails/brgen/app/config/queue.yml
rails/brgen/app/config/recurring.yml
rails/brgen/app/config/routes.rb
rails/brgen/app/config/storage.yml
rails/brgen/app/db/cable_schema.rb
rails/brgen/app/db/cache_schema.rb
rails/brgen/app/db/migrate/20260311162114_create_users.rb
rails/brgen/app/db/migrate/20260311162121_create_sessions.rb
rails/brgen/app/db/migrate/20260311162206_create_communities.rb
rails/brgen/app/db/migrate/20260311162227_create_reactions.rb
rails/brgen/app/db/migrate/20260311162235_create_streams.rb
rails/brgen/app/db/migrate/20260311162345_create_posts.rb
rails/brgen/app/db/migrate/20260311162350_create_comments.rb
rails/brgen/app/db/migrate/20260311162355_add_fields_to_users.rb
rails/brgen/app/db/migrate/20260311163039_create_votes.rb
rails/brgen/app/db/migrate/20260311163634_create_follows.rb
rails/brgen/app/db/migrate/20260311163641_create_hashtags.rb
rails/brgen/app/db/migrate/20260311163648_create_taggings.rb
rails/brgen/app/db/migrate/20260311163655_create_mentions.rb
rails/brgen/app/db/migrate/20260311164112_create_conversations.rb
rails/brgen/app/db/migrate/20260311164119_create_conversation_participants.rb
rails/brgen/app/db/migrate/20260311164127_create_messages.rb
rails/brgen/app/db/migrate/20260311164134_create_message_receipts.rb
rails/brgen/app/db/migrate/20260311164141_create_typing_indicators.rb
rails/brgen/app/db/migrate/20260311165000_add_guest_to_users.rb
rails/brgen/app/db/migrate/20260311221744_add_user_description_to_communities.rb
rails/brgen/app/db/migrate/20260505002649_create_tv_channels.rb
rails/brgen/app/db/migrate/20260505002659_create_tv_videos.rb
rails/brgen/app/db/migrate/20260505002711_create_tv_broadcasts.rb
rails/brgen/app/db/migrate/20260505002719_create_tv_subscriptions.rb
rails/brgen/app/db/migrate/20260505002729_create_tv_view_events.rb
rails/brgen/app/db/migrate/20260505014447_create_dating_profiles.rb
rails/brgen/app/db/migrate/20260505014452_create_dating_likes.rb
rails/brgen/app/db/migrate/20260505014457_create_dating_dislikes.rb
rails/brgen/app/db/migrate/20260505014503_create_dating_matches.rb
rails/brgen/app/db/migrate/20260505015400_create_playlist_playlists.rb
rails/brgen/app/db/migrate/20260505015406_create_playlist_tracks.rb
rails/brgen/app/db/migrate/20260505015411_create_playlist_playlist_tracks.rb
rails/brgen/app/db/migrate/20260505015416_create_playlist_listens.rb
rails/brgen/app/db/migrate/20260505015440_create_takeaway_restaurants.rb
rails/brgen/app/db/migrate/20260505015446_create_takeaway_menu_items.rb
rails/brgen/app/db/migrate/20260505015451_create_takeaway_orders.rb
rails/brgen/app/db/migrate/20260505015456_create_takeaway_order_items.rb
rails/brgen/app/db/migrate/20260505015518_create_marketplace_categories.rb
rails/brgen/app/db/migrate/20260505015523_create_marketplace_listings.rb
rails/brgen/app/db/migrate/20260505015530_create_marketplace_orders.rb
rails/brgen/app/db/queue_schema.rb
rails/brgen/app/db/schema.rb
rails/brgen/app/db/seeds.rb
rails/brgen/app/public/robots.txt
rails/brgen/app/test/test_helper.rb
rails/brgen/brgen.sh
rails/brgen/subapps/dating/README.md
rails/brgen/subapps/marketplace/README.md
rails/brgen/subapps/playlist/README.md
rails/brgen/subapps/takeaway/README.md
rails/brgen/subapps/tv/README.md
rails/bsdports/README.md
rails/bsdports/app/Dockerfile
rails/bsdports/app/Gemfile
rails/bsdports/app/README.md
rails/bsdports/app/Rakefile
rails/bsdports/app/app/controllers/application_controller.rb
rails/bsdports/app/app/controllers/categories_controller.rb
rails/bsdports/app/app/controllers/comments_controller.rb
rails/bsdports/app/app/controllers/concerns/authentication.rb
rails/bsdports/app/app/controllers/passwords_controller.rb
rails/bsdports/app/app/controllers/ports_controller.rb
rails/bsdports/app/app/controllers/sessions_controller.rb
rails/bsdports/app/app/helpers/application_helper.rb
rails/bsdports/app/app/javascript/application.js
rails/bsdports/app/app/javascript/controllers/animated_number_controller.js
rails/bsdports/app/app/javascript/controllers/application.js
rails/bsdports/app/app/javascript/controllers/auto_submit_controller.js
rails/bsdports/app/app/javascript/controllers/character_counter_controller.js
rails/bsdports/app/app/javascript/controllers/clipboard_controller.js
rails/bsdports/app/app/javascript/controllers/dialog_controller.js
rails/bsdports/app/app/javascript/controllers/dropdown_controller.js
rails/bsdports/app/app/javascript/controllers/hello_controller.js
rails/bsdports/app/app/javascript/controllers/index.js
rails/bsdports/app/app/javascript/controllers/notification_controller.js
rails/bsdports/app/app/javascript/controllers/sortable_controller.js
rails/bsdports/app/app/javascript/controllers/textarea_autogrow_controller.js
rails/bsdports/app/app/javascript/controllers/timeago_controller.js
rails/bsdports/app/app/jobs/application_job.rb
rails/bsdports/app/app/mailers/application_mailer.rb
rails/bsdports/app/app/models/application_record.rb
rails/bsdports/app/app/models/category.rb
rails/bsdports/app/app/models/comment.rb
rails/bsdports/app/app/models/current.rb
rails/bsdports/app/app/models/dependency.rb
rails/bsdports/app/app/models/port.rb
rails/bsdports/app/app/models/port_update.rb
rails/bsdports/app/app/models/session.rb
rails/bsdports/app/app/models/user.rb
rails/bsdports/app/app/models/watch.rb
rails/bsdports/app/app/views/categories/index.html.erb
rails/bsdports/app/app/views/categories/show.html.erb
rails/bsdports/app/app/views/comments/_comment.html.erb
rails/bsdports/app/app/views/layouts/application.html.erb
rails/bsdports/app/app/views/layouts/mailer.html.erb
rails/bsdports/app/app/views/layouts/mailer.text.erb
rails/bsdports/app/app/views/ports/index.html.erb
rails/bsdports/app/app/views/ports/show.html.erb
rails/bsdports/app/app/views/pwa/manifest.json.erb
rails/bsdports/app/app/views/pwa/service-worker.js
rails/bsdports/app/config/application.rb
rails/bsdports/app/config/boot.rb
rails/bsdports/app/config/bundler-audit.yml
rails/bsdports/app/config/cable.yml
rails/bsdports/app/config/ci.rb
rails/bsdports/app/config/database.yml
rails/bsdports/app/config/deploy.yml
rails/bsdports/app/config/environment.rb
rails/bsdports/app/config/environments/development.rb
rails/bsdports/app/config/environments/production.rb
rails/bsdports/app/config/environments/test.rb
rails/bsdports/app/config/importmap.rb
rails/bsdports/app/config/initializers/assets.rb
rails/bsdports/app/config/initializers/content_security_policy.rb
rails/bsdports/app/config/initializers/filter_parameter_logging.rb
rails/bsdports/app/config/initializers/inflections.rb
rails/bsdports/app/config/locales/en.yml
rails/bsdports/app/config/puma.rb
rails/bsdports/app/config/routes.rb
rails/bsdports/app/config/storage.yml
rails/bsdports/app/db/migrate/20260501020807_create_users.rb
rails/bsdports/app/db/migrate/20260501020818_create_sessions.rb
rails/bsdports/app/db/migrate/20260507120001_create_categories.rb
rails/bsdports/app/db/migrate/20260507120002_create_ports.rb
rails/bsdports/app/db/migrate/20260507120003_create_dependencies.rb
rails/bsdports/app/db/migrate/20260507120004_create_port_updates.rb
rails/bsdports/app/db/migrate/20260507120005_create_watches.rb
rails/bsdports/app/db/migrate/20260507120006_create_comments.rb
rails/bsdports/app/db/seeds.rb
rails/bsdports/app/public/robots.txt
rails/bsdports/bsdports.sh
rails/bsdports/bsdports_test.sh
rails/check_ports.sh
rails/demo.sh
rails/hjerterom/README.md
rails/hjerterom/app/Dockerfile
rails/hjerterom/app/Gemfile
rails/hjerterom/app/README.md
rails/hjerterom/app/Rakefile
rails/hjerterom/app/app/controllers/application_controller.rb
rails/hjerterom/app/app/controllers/community_controller.rb
rails/hjerterom/app/app/controllers/concerns/authentication.rb
rails/hjerterom/app/app/controllers/food_listings_controller.rb
rails/hjerterom/app/app/controllers/food_requests_controller.rb
rails/hjerterom/app/app/controllers/home_controller.rb
rails/hjerterom/app/app/controllers/passwords_controller.rb
rails/hjerterom/app/app/controllers/resources_controller.rb
rails/hjerterom/app/app/controllers/sessions_controller.rb
rails/hjerterom/app/app/helpers/application_helper.rb
rails/hjerterom/app/app/javascript/application.js
rails/hjerterom/app/app/javascript/controllers/animated_number_controller.js
rails/hjerterom/app/app/javascript/controllers/application.js
rails/hjerterom/app/app/javascript/controllers/auto_submit_controller.js
rails/hjerterom/app/app/javascript/controllers/character_counter_controller.js
rails/hjerterom/app/app/javascript/controllers/clipboard_controller.js
rails/hjerterom/app/app/javascript/controllers/dialog_controller.js
rails/hjerterom/app/app/javascript/controllers/dropdown_controller.js
rails/hjerterom/app/app/javascript/controllers/hello_controller.js
rails/hjerterom/app/app/javascript/controllers/index.js
rails/hjerterom/app/app/javascript/controllers/notification_controller.js
rails/hjerterom/app/app/javascript/controllers/sortable_controller.js
rails/hjerterom/app/app/javascript/controllers/textarea_autogrow_controller.js
rails/hjerterom/app/app/javascript/controllers/timeago_controller.js
rails/hjerterom/app/app/jobs/application_job.rb
rails/hjerterom/app/app/mailers/application_mailer.rb
rails/hjerterom/app/app/models/application_record.rb
rails/hjerterom/app/app/models/category.rb
rails/hjerterom/app/app/models/comment.rb
rails/hjerterom/app/app/models/crisis.rb
rails/hjerterom/app/app/models/current.rb
rails/hjerterom/app/app/models/food_listing.rb
rails/hjerterom/app/app/models/food_request.rb
rails/hjerterom/app/app/models/post.rb
rails/hjerterom/app/app/models/resource.rb
rails/hjerterom/app/app/models/session.rb
rails/hjerterom/app/app/models/support_request.rb
rails/hjerterom/app/app/models/user.rb
rails/hjerterom/app/app/views/community/index.html.erb
rails/hjerterom/app/app/views/community/new.html.erb
rails/hjerterom/app/app/views/community/show.html.erb
rails/hjerterom/app/app/views/food_listings/_form.html.erb
rails/hjerterom/app/app/views/food_listings/edit.html.erb
rails/hjerterom/app/app/views/food_listings/index.html.erb
rails/hjerterom/app/app/views/food_listings/new.html.erb
rails/hjerterom/app/app/views/food_listings/show.html.erb
rails/hjerterom/app/app/views/home/index.html.erb
rails/hjerterom/app/app/views/layouts/application.html.erb
rails/hjerterom/app/app/views/layouts/mailer.html.erb
rails/hjerterom/app/app/views/layouts/mailer.text.erb
rails/hjerterom/app/app/views/pwa/manifest.json.erb
rails/hjerterom/app/app/views/pwa/service-worker.js
rails/hjerterom/app/app/views/resources/_form.html.erb
rails/hjerterom/app/app/views/resources/edit.html.erb
rails/hjerterom/app/app/views/resources/index.html.erb
rails/hjerterom/app/app/views/resources/new.html.erb
rails/hjerterom/app/app/views/resources/show.html.erb
rails/hjerterom/app/config/application.rb
rails/hjerterom/app/config/boot.rb
rails/hjerterom/app/config/bundler-audit.yml
rails/hjerterom/app/config/cable.yml
rails/hjerterom/app/config/ci.rb
rails/hjerterom/app/config/database.yml
rails/hjerterom/app/config/deploy.yml
rails/hjerterom/app/config/environment.rb
rails/hjerterom/app/config/environments/development.rb
rails/hjerterom/app/config/environments/production.rb
rails/hjerterom/app/config/environments/test.rb
rails/hjerterom/app/config/importmap.rb
rails/hjerterom/app/config/initializers/assets.rb
rails/hjerterom/app/config/initializers/content_security_policy.rb
rails/hjerterom/app/config/initializers/filter_parameter_logging.rb
rails/hjerterom/app/config/initializers/inflections.rb
rails/hjerterom/app/config/locales/en.yml
rails/hjerterom/app/config/puma.rb
rails/hjerterom/app/config/routes.rb
rails/hjerterom/app/config/storage.yml
rails/hjerterom/app/db/migrate/20260501020807_create_users.rb
rails/hjerterom/app/db/migrate/20260501020818_create_sessions.rb
rails/hjerterom/app/db/migrate/20260507120001_create_categories.rb
rails/hjerterom/app/db/migrate/20260507120002_create_resources.rb
rails/hjerterom/app/db/migrate/20260507120003_create_crises.rb
rails/hjerterom/app/db/migrate/20260507120004_create_food_listings.rb
rails/hjerterom/app/db/migrate/20260507120005_create_food_requests.rb
rails/hjerterom/app/db/migrate/20260507120006_create_posts.rb
rails/hjerterom/app/db/migrate/20260507120007_create_comments.rb
rails/hjerterom/app/db/migrate/20260507120008_create_support_requests.rb
rails/hjerterom/app/db/seeds.rb
rails/hjerterom/app/public/robots.txt
rails/hjerterom/hjerterom.sh
rails/modernize_zsh.sh
rails/rich_editor_system.sh
repligen.rb

README.md

# DEPLOY

Deploy scripts for all pub4 services on OpenBSD 7.8.

## Layout

DEPLOY/ openbsd/ Full VPS stack (pf, relayd, httpd, smtpd, nsd, masterweb) rails/ Rails app deploy scripts per project


## OpenBSD

Two-stage deploy — run from tmux:

```zsh
tmux new-session -d -s deploy "doas zsh DEPLOY/openbsd/openbsd.sh 2>&1 | tee /tmp/deploy.log"

Stage 1: DNS checks, TLS certs (acme-client), pkg_add. Stage 2: app installs, relayd config, rc.d services.

Resume interrupted run: doas zsh openbsd.sh --resume

Rails

Each subdirectory contains a deploy script for one app:

rails/
  amber/       amber.sh
  baibl/       baibl.sh
  blognet/     blognet.sh
  brgen/       brgen*.sh
  bsdports/    bsdports.sh
  hjerterom/   hjerterom.sh
  privcam/     privcam.sh
  __shared/    Common utilities and feature modules

## `openbsd/README.md`
```markdown
# OpenBSD Deploy

Full VPS stack deploy for OpenBSD 7.8 at 185.52.176.18.

## Run

```zsh
cd ~/pub4/DEPLOY/openbsd
tmux new-session -d -s deploy "doas zsh openbsd.sh 2>&1 | tee /tmp/deploy.log"
tmux attach -t deploy

Resume after interruption: doas zsh openbsd.sh --resume

What it deploys

Stage 1 (DNS + TLS + packages):

  • DNS validation for brgen.no, ai.brgen.no
  • acme-client TLS certificates
  • pkg_add: ruby, postgresql, redis, node

Stage 2 (services):

  • pf firewall rules (ports 22, 25, 80, 443, 3000, 4430, 8080–8086)
  • relayd TLS termination (443 → 53187, 4430 → 53187)
  • httpd static file server
  • smtpd mail server
  • nsd authoritative DNS
  • master rc.d service (127.0.0.1:53187)
  • Rails apps under /home/dev/rails/

Checks

After deploy:

doas rcctl check master
doas pfctl -s rules
curl -sk https://ai.brgen.no:4430/chat/metrics

## `openbsd/files/httpd.conf`
```text
# HTTP: ACME challenges + HTTP→HTTPS redirect (httpd.conf(5))
server "*" {
  listen on 0.0.0.0 port 80

  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }

  location * {
    block return 301 "https://$HTTP_HOST$REQUEST_URI"
  }
}

openbsd/files/pf.stage1.conf

# Minimal PF for DNS in Stage 1 (pf.conf(5))
ext_if="vio0"
brgen_ip="$BRGEN_IP"
hyp_ip="$HYP_IP"
set skip on lo
pass in on \$ext_if inet proto { tcp, udp } to \$brgen_ip port 53
pass out on \$ext_if inet proto udp to \$hyp_ip port 53

openbsd/files/pf.stage2.conf

# PF for DNS, HTTP/HTTPS, SSH, SMTP (pf.conf(5))
ext_if="vio0"
brgen_ip="$BRGEN_IP"
hyp_ip="$HYP_IP"
set skip on lo
set block-policy return
set loginterface \$ext_if
set reassemble yes
set limit { states 10000, frags 5000 }
block log all
scrub in all
table <bruteforce> persist
block quick from <bruteforce>
pass out quick on \$ext_if all
pass in on \$ext_if inet proto tcp to \$ext_if port 22 keep state \\
  (max-src-conn 15, max-src-conn-rate 5/3, overload <bruteforce> flush global)
pass in on \$ext_if inet proto { tcp, udp } to \$brgen_ip port 53 log
pass in on \$ext_if inet proto tcp to \$brgen_ip port { 22, 25, 80, 443, 3000, 8080, 8082, 8084, 8086 } log
pass out on \$ext_if inet proto tcp to any port 25

openbsd/files/renew-certs.sh

#!/bin/ksh
# Certificate renewal script
generate_tlsa_record() {
  typeset domain=$1
  typeset cert=/etc/ssl/$domain.fullchain.pem
  typeset zonefile=/var/nsd/zones/master/$domain.zone
  typeset zsk=/var/nsd/zones/master/K$domain.+013+zsk.key
  typeset ksk=/var/nsd/zones/master/K$domain.+013+ksk.key
  [[ ! -f $cert ]] && return 1
  typeset tlsa_record=$(openssl x509 -noout -pubkey -in "$cert" | \
    openssl pkey -pubin -outform der 2>/dev/null | \
    openssl dgst -sha256 2>/dev/null); tlsa_record=${tlsa_record##* }
  [[ -z $tlsa_record ]] && return 1
  typeset -a lines
  lines=("${(@f)$(<$zonefile)}")
  lines=("${(@)lines:#_443._tcp.$domain. IN TLSA*}")
  print -rl -- $lines > "$zonefile"
  print -r -- "_443._tcp.$domain. IN TLSA 3 1 1 $tlsa_record" >> "$zonefile"
  ldns-signzone -n -p -s $(dd if=/dev/random bs=16 count=1 2>/dev/null | sha1 -q) "$zonefile" "$zsk" "$ksk"
  nsd-control reload
}

ALL_DOMAINS=(
  brgen.no longyearbyn.no oshlo.no stvanger.no trmso.no trndheim.no
  reykjavk.is kbenhvn.dk gtebrg.se mlmoe.se stholm.se hlsinki.fi
  brmingham.uk cardff.uk edinbrgh.uk glasgw.uk lndon.uk lverpool.uk
  mnchester.uk amstrdam.nl rottrdam.nl utrcht.nl brssels.be zrich.ch
  lchtenstein.li frankfrt.de brdeaux.fr mrseille.fr mlan.it lisbon.pt
  wrsawa.pl gdnsk.pl austn.us chcago.us denvr.us dllas.us dnver.us
  dtroit.us houstn.us lsangeles.com mnnesota.com newyrk.us prtland.com
  wshingtondc.com pub.healthcare pub.attorney freehelp.legal
  bsdports.org bsddocs.org discordb.org foodielicio.us
  stacyspassion.com antibettingblog.com anticasinoblog.com
  antigamblingblog.com foball.no
)

for domain in ${ALL_DOMAINS[@]}; do
  if acme-client -v -f /etc/acme-client.conf "$domain"; then
    echo "Renewed: $domain"
    generate_tlsa_record "$domain"
  fi
done

/usr/sbin/rcctl reload relayd

openbsd/files/smtpd.conf

# OpenSMTPD for outbound email (smtpd.conf(5))
table aliases file:/etc/mail/aliases
pki mail.pub.attorney cert "/etc/ssl/smtp.crt"
pki mail.pub.attorney key "/etc/ssl/private/smtp.key"
listen on $BRGEN_IP port 25 tls pki mail.pub.attorney
action "outbound" relay
match from local for any action "outbound"

openbsd/openbsd.sh

#!/usr/bin/env zsh
# Configures OpenBSD 7.8 for NSD & DNSSEC, Ruby on Rails, PF firewall, and minimal OpenSMTPD.

# Usage: doas zsh openbsd.sh [--help | --resume]

#

# VERIFIED AGAINST: OpenBSD 7.8 manual pages (2026-01-06)

# - All configuration syntax validated against man.openbsd.org

# - smtpd.conf updated to OpenBSD 7.8 syntax (PKI-based TLS)

# - relayd.conf includes TLS keypair directives

# - pf.conf uses proper macro definitions

# - rc.d scripts follow proper rc.d(8) format

# - PostgreSQL and Redis removed (use SQLite or external DB)

# - Modern Zsh and OpenBSD security best practices applied

# - Inspired by structured thinking principles (unvalidated)

# - NOTE: pledge/unveil not applicable (C syscalls, not shell features)

# - Privilege control via doas(1), idempotent operations, atomic config writes

set +e  # Don't use errexit - handle errors explicitly
setopt no_unset nullglob local_traps

zmodload zsh/regex

# Temporary files tracking
typeset -a TMPFILES

# Trap handlers for cleanup and errors
cleanup() {

  typeset exit_code=$?

  for tmpfile in "${TMPFILES[@]}"; do

    [[ -n $tmpfile && -f $tmpfile ]] && rm -f "$tmpfile"

  done

  return $exit_code

}

error_handler() {
  typeset exit_code=$1

  typeset line_num=$2

  log ERROR "Script failed with exit code $exit_code at line $line_num"

  cleanup

  exit $exit_code

}

trap 'cleanup' EXIT
trap 'error_handler $? $LINENO' INT TERM
# ERR trap: log unexpected exits
trap 'log ERROR "Script exited unexpectedly at line $LINENO with status $?"' ERR

# Convenience wrappers matching task spec
log_info()  { log INFO "$@" }
log_error() { log ERROR "$@" }

# Step completion tracking
is_step_completed() {
  [[ -f "${STATE_FILE}.steps" ]] && [[ $(<"${STATE_FILE}.steps") == *"$1"* ]]
}
mark_step_completed() {
  print -r -- "$1" >> "${STATE_FILE}.steps"
}


# Backup function for data integrity
backup_directory() {

  typeset target_dir=$1

  typeset backup_name=${2:-${target_dir:t}}

  typeset backup_dir=/var/backups/openbsd_setup

  typeset timestamp=$EPOCHSECONDS

  typeset backup_file="$backup_dir/${backup_name}-${timestamp}.tar.gz"

  [[ ! -d $backup_dir ]] && mkdir -p "$backup_dir"
  if [[ -d $target_dir ]]; then
    log INFO "Backing up $target_dir to $backup_file"

    transaction_log "BACKUP" "$target_dir" "START"

    if tar -czf "$backup_file" -C "${target_dir:h}" "${target_dir:t}" 2>/dev/null; then

      transaction_log "BACKUP" "$target_dir" "SUCCESS" "$backup_file"

      log INFO "Backup created: $backup_file"

      # Keep only last 10 backups
      typeset -a _bfiles; _bfiles=("$backup_dir"/${backup_name}-*.tar.gz(N)); typeset backup_count=${#_bfiles}

      if (( backup_count > 10 )); then

        typeset -a _sorted_bfiles; _sorted_bfiles=("$backup_dir"/${backup_name}-*.tar.gz(NOm)); for _f in "${_sorted_bfiles[@]:10}"; do rm -f "$_f"; done

        log INFO "Pruned old backups, keeping last 10"

      fi

      echo "$backup_file"

      return 0

    else

      transaction_log "BACKUP" "$target_dir" "FAILURE"

      log ERROR "Backup failed for $target_dir"

      return 1

    fi

  else

    log WARN "Directory $target_dir does not exist, skipping backup"

    return 0

  fi

}

# Transaction logging for audit trail
transaction_log() {

  typeset operation=$1

  typeset target=$2

  typeset op_status=$3

  typeset metadata=${4:-}

  typeset logfile=/var/log/openbsd_transactions.log

  print -r -- "[$(date +'%Y-%m-%d %H:%M:%S')] [$operation] $target | Status: $op_status | $metadata" >> "$logfile"
}

# Logging function
log() {

  typeset level=$1

  shift

  print -r -- "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a /var/log/openbsd_setup.log >&2

}

# Template helpers — render files/* with $var expansion, install to dest
install_template() {
  typeset src=${SCRIPT_DIR}/$1 dst=$2
  [[ -f $src ]] || { log ERROR "Missing template: $src"; exit 1 }
  typeset content; content=$(<"$src")
  eval "cat > \"$dst\" <<INSTALL_TEMPLATE_EOF
$content
INSTALL_TEMPLATE_EOF"
}
append_template() {
  typeset src=${SCRIPT_DIR}/$1 dst=$2
  [[ -f $src ]] || { log ERROR "Missing template: $src"; exit 1 }
  typeset content; content=$(<"$src")
  eval "cat >> \"$dst\" <<APPEND_TEMPLATE_EOF
$content
APPEND_TEMPLATE_EOF"
}
install_static() {
  typeset src=${SCRIPT_DIR}/$1 dst=$2
  [[ -f $src ]] || { log ERROR "Missing file: $src"; exit 1 }
  cp "$src" "$dst"
}


# Configuration settings (constants per master.yml p04: explicit over implicit)
typeset -r BRGEN_IP="185.52.176.18"   # Primary server IP (updated for this VPS)

typeset -r HYP_IP="194.63.248.53"     # ns.hyp.net, external secondary

typeset -r LOCALHOST="127.0.0.1"      # Localhost constant

typeset -r EMAIL_ADDRESS="bergen@pub.attorney"  # Email address for OpenSMTPD

typeset -r STATE_FILE="./openbsd_setup_state"   # Runtime state file

SCRIPT_DIR=${0:a:h}

typeset -a PUBLIC_RESOLVERS=(8.8.8.8 1.1.1.1 9.9.9.9)  # Public DNS resolvers

typeset -A APP_PORTS              # Rails app port mappings

typeset -A FAILED_CERTS           # Failed certificate tracking

# Validate IP addresses with proper octet checking
validate_ip() {

  typeset ip=$1

  [[ $ip =~ ^([0-9]{1,3}.){3}[0-9]{1,3}$ ]] || return 1

  typeset IFS=.

  typeset -a octets

  octets=(${(s:.:)ip})

  for octet in $octets; do

    (( octet > 255 )) && return 1

  done

  return 0

}

validate_ip "$BRGEN_IP" || { log ERROR "Invalid BRGEN_IP: $BRGEN_IP"; exit 1; }
validate_ip "$HYP_IP" || { log ERROR "Invalid HYP_IP: $HYP_IP"; exit 1; }

# Rails applications
ALL_APPS=(

  brgen:brgen.no

  amber:amber.brgen.no

  bsdports:bsdports.org

  baibl:baibl.no

)

# Non-Rails services (name:subdomain.domain:port)
SERVICES=(
)

# Domain list for DNS
ALL_DOMAINS=(

  brgen.no:markedsplass,playlist,dating,tv,takeaway,maps,ai

  longyearbyn.no:markedsplass,playlist,dating,tv,takeaway,maps

  oshlo.no:markedsplass,playlist,dating,tv,takeaway,maps

  stvanger.no:markedsplass,playlist,dating,tv,takeaway,maps

  trmso.no:markedsplass,playlist,dating,tv,takeaway,maps

  trndheim.no:markedsplass,playlist,dating,tv,takeaway,maps

  reykjavk.is:markadur,playlist,dating,tv,takeaway,maps

  kbenhvn.dk:markedsplads,playlist,dating,tv,takeaway,maps

  gtebrg.se:marknadsplats,playlist,dating,tv,takeaway,maps

  mlmoe.se:marknadsplats,playlist,dating,tv,takeaway,maps

  stholm.se:marknadsplats,playlist,dating,tv,takeaway,maps

  hlsinki.fi:markkinapaikka,playlist,dating,tv,takeaway,maps

  brmingham.uk:marketplace,playlist,dating,tv,takeaway,maps

  cardff.uk:marketplace,playlist,dating,tv,takeaway,maps

  edinbrgh.uk:marketplace,playlist,dating,tv,takeaway,maps

  glasgw.uk:marketplace,playlist,dating,tv,takeaway,maps

  lndon.uk:marketplace,playlist,dating,tv,takeaway,maps

  lverpool.uk:marketplace,playlist,dating,tv,takeaway,maps

  mnchester.uk:marketplace,playlist,dating,tv,takeaway,maps

  amstrdam.nl:marktplaats,playlist,dating,tv,takeaway,maps

  rottrdam.nl:marktplaats,playlist,dating,tv,takeaway,maps

  utrcht.nl:marktplaats,playlist,dating,tv,takeaway,maps

  brssels.be:marche,playlist,dating,tv,takeaway,maps

  zrich.ch:marktplatz,playlist,dating,tv,takeaway,maps

  lchtenstein.li:marktplatz,playlist,dating,tv,takeaway,maps

  frankfrt.de:marktplatz,playlist,dating,tv,takeaway,maps

  brdeaux.fr:marche,playlist,dating,tv,takeaway,maps

  mrseille.fr:marche,playlist,dating,tv,takeaway,maps

  mlan.it:mercato,playlist,dating,tv,takeaway,maps

  lisbon.pt:mercado,playlist,dating,tv,takeaway,maps

  wrsawa.pl:marktplatz,playlist,dating,tv,takeaway,maps

  gdnsk.pl:marktplatz,playlist,dating,tv,takeaway,maps

  austn.us:marketplace,playlist,dating,tv,takeaway,maps

  chcago.us:marketplace,playlist,dating,tv,takeaway,maps

  denvr.us:marketplace,playlist,dating,tv,takeaway,maps

  dllas.us:marketplace,playlist,dating,tv,takeaway,maps

  dnver.us:marketplace,playlist,dating,tv,takeaway,maps

  dtroit.us:marketplace,playlist,dating,tv,takeaway,maps

  houstn.us:marketplace,playlist,dating,tv,takeaway,maps

  lsangeles.com:marketplace,playlist,dating,tv,takeaway,maps

  mnnesota.com:marketplace,playlist,dating,tv,takeaway,maps

  newyrk.us:marketplace,playlist,dating,tv,takeaway,maps

  prtland.com:marketplace,playlist,dating,tv,takeaway,maps

  wshingtondc.com:marketplace,playlist,dating,tv,takeaway,maps

  pub.healthcare

  pub.attorney

  freehelp.legal

  bsdports.org

  bsddocs.org

  discordb.org
  foodielicio.us

  stacyspassion.com

  antibettingblog.com

  anticasinoblog.com

  antigamblingblog.com

  foball.no

  amber.brgen.no

  baibl.no

)

# Zsh completion function
_openbsd_sh() {

  _arguments \

    '--help[Show usage information]' \

    '--resume[Resume with Stage 2]'

}

# Utility functions
generate_random_port() {
  # Generate random port (10000–60000), ensuring it’s free

  typeset port

  while :; do

    port=$((RANDOM % 50000 + 10000))

    typeset _netstat_out; _netstat_out=$(/usr/bin/netstat -an); [[ $_netstat_out != *".$port "* ]] && echo $port && break

  done
... 823 lines truncated (1223 total)

postpro.rb

#!/usr/bin/env ruby
# frozen_string_literal: true

require "logger"
require "json"
require "time"
require "fileutils"
require "rbconfig"

module PostproBootstrap
  LOG_PREFIX = "[postpro]".freeze

  def self.dmesg(msg)
    puts "#{LOG_PREFIX} #{msg}"
  end

  def self.startup_banner
    dmesg "boot ruby=#{RUBY_VERSION} os=#{RbConfig::CONFIG['host_os']}"
  end

  def self.ensure_gems
    { vips: ensure_vips, tty: ensure_tty_prompt }
  end

  def self.ensure_vips
    require "vips"
    true
  rescue LoadError
    dmesg "WARN ruby-vips missing, installing..."
    if system("gem install ruby-vips --no-document")
      require "vips"
      dmesg "OK ruby-vips installed"
      true
    else
      dmesg "WARN ruby-vips install failed, probing libvips"
      probe_and_install_libvips
      false
    end
  rescue StandardError => e
    dmesg "WARN ruby-vips unavailable: #{e.message}"
    false
  end

  def self.ensure_tty_prompt
    require "tty-prompt"
    true
  rescue LoadError
    dmesg "WARN tty-prompt missing, installing..."
    if system("gem install tty-prompt --no-document")
      require "tty-prompt"
      dmesg "OK tty-prompt installed"
      true
    else
      dmesg "WARN tty-prompt install failed"
      false
    end
  rescue StandardError => e
    dmesg "WARN tty-prompt unavailable: #{e.message}"
    false
  end

  def self.probe_and_install_libvips
    dmesg "probing libvips..."
    return true if system("pkg-config --exists vips")

    os = RbConfig::CONFIG["host_os"]
    case os
    when /darwin/
      if system("which brew > /dev/null 2>&1")
        dmesg "brew install vips"
        system("brew install vips")
      else
        dmesg "ERROR brew not found"
      end
    when /linux/
      install_cmd = if system("which apt > /dev/null 2>&1")
                      "sudo apt update && sudo apt install -y libvips-dev"
                    elsif system("which dnf > /dev/null 2>&1")
                      "sudo dnf install -y vips-devel"
                    elsif system("which yum > /dev/null 2>&1")
                      "sudo yum install -y vips-devel"
                    elsif system("which apk > /dev/null 2>&1")
                      "sudo apk add vips-dev"
                    elsif system("which pacman > /dev/null 2>&1")
                      "sudo pacman -S --noconfirm libvips"
                    end
      if install_cmd
        dmesg install_cmd
        system(install_cmd)
      else
        dmesg "ERROR unsupported package manager"
      end
    when /openbsd/
      if system("which pkg_add > /dev/null 2>&1")
        dmesg "pkg_add vips"
        system("doas pkg_add vips")
      else
        dmesg "ERROR pkg_add missing"
      end
    else
      dmesg "ERROR unsupported OS: #{os}"
    end

    if system("pkg-config --exists vips")
      dmesg "OK libvips installed"
      true
    else
      dmesg "ERROR libvips installation failed"
      false
    end
  end

  def self.load_camera_profiles(dir)
    return {} unless Dir.exist?(dir)

    profiles = {}
    Dir.glob(File.join(dir, "*.json")).each do |f|
      begin
        data = JSON.parse(File.read(f))
        vendor = data["vendor"]
        profiles[vendor] = data["profiles"] if vendor && data["profiles"]
      rescue StandardError => e
        dmesg "WARN profile #{File.basename(f)}: #{e.message}"
      end
    end
    dmesg "camera_profiles=#{profiles.keys.join(',')}"
    profiles
  end

  def self.load_master_config
    return {} unless File.exist?("master.json")

    begin
      raw = File.read("master.json")
      json = JSON.parse(raw.gsub(%r{^.*//.*$}, ""))
      json.dig("config", "multimedia", "postpro") || {}
    rescue StandardError => e
      dmesg "WARN master.json: #{e.message}"
      {}
    end
  end

  def self.run
    startup_banner
    gems = ensure_gems
    unless gems[:vips]
      dmesg "FATAL libvips missing"
      abort <<~MSG
        Postpro.rb requires libvips.
        Install manually:
          macOS: brew install vips
          Debian/Ubuntu: sudo apt install libvips-dev
          OpenBSD: doas pkg_add vips
      MSG
    end

    {
      gems: gems,
      camera_profiles: load_camera_profiles("multimedia/camera_profiles"),
      config: load_master_config
    }
  end
end

BOOTSTRAP = PostproBootstrap.run
LOGGER = Logger.new("postpro.log", "daily")
LOGGER.level = Logger::DEBUG
CLI_LOGGER = Logger.new($stdout)
CLI_LOGGER.level = Logger::INFO

PROMPT = if BOOTSTRAP[:gems][:tty]
           require "tty-prompt"
           TTY::Prompt.new
         end

require "vips" if BOOTSTRAP[:gems][:vips]

REPLIGEN_PRESENT = File.exist?("repligen.rb")
CAMERA_PROFILES = BOOTSTRAP[:camera_profiles]
CONFIG = BOOTSTRAP[:config]

STOCKS = {
  kodak_portra: { grain: 15, gamma: 0.65, rolloff: 0.88, lift: 0.05, matrix: [1.05, -0.02, -0.03, 0.02, 0.98, 0.00, 0.01, -0.05, 1.04] },
  kodak_vision3: { grain: 20, gamma: 0.65, rolloff: 0.85, lift: 0.08, matrix: [1.08, -0.05, -0.03, 0.03, 0.95, 0.02, 0.02, -0.08, 1.06] },
  fuji_velvia: { grain: 8, gamma: 0.75, rolloff: 0.92, lift: 0.03, matrix: [1.12, -0.08, -0.04, 0.05, 1.05, -0.02, 0.01, -0.12, 1.11] },
  tri_x: { grain: 25, gamma: 0.70, rolloff: 0.80, lift: 0.12, matrix: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] }
}.freeze

PRESETS = {
  portrait:   { fx: %w[skin_protect film_curve highlight_roll micro_contrast grain color_temp base_tint], stock: :kodak_portra, temp: 5200, intensity: 0.8 },
  landscape:  { fx: %w[film_curve color_separate highlight_roll micro_contrast grain vintage_lens], stock: :fuji_velvia, temp: 5800, intensity: 0.9 },
  street:     { fx: %w[film_curve shadow_lift micro_contrast vintage_lens grain], stock: :tri_x, temp: 5600, intensity: 1.0 },
  blockbuster:{ fx: %w[teal_orange grain bloom_pro highlight_roll micro_contrast], stock: :kodak_vision3, temp: 4800, intensity: 1.2 }
}.freeze

def safe_cast(img, fmt = "uchar")
  img.cast(fmt)
rescue StandardError => e
  LOGGER.error "Cast failed: #{e.message}"
  img
end

def rgb_bands(img, bands = 3)
  return img if img.bands == bands
  img.bands < bands ? img.bandjoin([img] * (bands - img.bands)) : img.extract_band(0, n: bands)
end

def load_image(path)
  return nil unless File.file?(path) && File.readable?(path)

  img = Vips::Image.new_from_file(path, access: :sequential)
  img = img.colourspace("srgb") if img.bands < 3
  rgb_bands(img)
rescue StandardError => e
  LOGGER.error "Load #{path}: #{e.message}"
  nil
end

def get_camera_profile(img)
  return nil if CAMERA_PROFILES.empty?
  make  = img.get("exif-ifd0-Make")&.strip&.downcase
  model = img.get("exif-ifd0-Model")&.strip&.downcase
  return nil unless make && model

  CAMERA_PROFILES.each { |_, p| return p[model] if p[model] }
  CAMERA_PROFILES.each { |brand, p| return p.values.first if make.include?(brand) || brand.include?(make) }
  nil
rescue StandardError => e
  LOGGER.debug "EXIF error: #{e.message}"
  nil
end

def apply_camera_profile(img, profile)
  return img unless profile && profile["color_matrix"]
  matrix = profile["color_matrix"]
  return img unless matrix.size == 9

  result = img.recomb([
    [matrix[0], matrix[1], matrix[2]],
    [matrix[3], matrix[4], matrix[5]],
    [matrix[6], matrix[7], matrix[8]]
  ])

  if profile["saturation"]
    hsv = result.colourspace("hsv")
    h, s, v = hsv.bandsplit
    s = s.linear([profile["saturation"]], [0])
    result = Vips::Image.bandjoin([h, s, v]).colourspace("srgb")
  end

  result = result.linear([1.0 + profile["vibrance"].to_f * 0.1], [0]) if profile["vibrance"]
  result = base_tint(result, profile["base_tint"], 0.1) if profile["base_tint"]
  safe_cast(result)
rescue StandardError => e
  LOGGER.error "Camera profile: #{e.message}"
  img
end

def color_temp(img, kelvin, intensity = 1.0)
  factor = kelvin / 5500.0
  r, g, b = if factor < 1.0
              [1.0, factor**0.5, factor**2]
            else
              [factor**-0.3, 1.0, 1.0 + (factor - 1.0) * 0.5]
            end
  safe_cast img.linear([
    1.0 + (r - 1.0) * intensity,
    1.0 + (g - 1.0) * intensity,
    1.0 + (b - 1.0) * intensity
  ], [0, 0, 0])
end

def skin_protect(img, intensity = 1.0)
  hsv = img.colourspace("hsv")
  h, s, _ = hsv.bandsplit
  mask = (h > 25.5) & (h < 63.75) & (s > 51) & (s < 153)
  protection = mask.cast("float") / 255.0 * (1.0 - intensity * 0.7)
  safe_cast img * (1.0 - protection) + img * protection
end

def film_curve(img, stock = :kodak_portra, intensity = 1.0)
  data = STOCKS[stock] || STOCKS[:kodak_portra]
  shadows = img.linear([1.0], [data[:lift] * 255 * intensity])
  highlights = shadows.pow(data[:gamma]).pow(data[:rolloff])
  safe_cast img * (1 - intensity) + highlights * intensity
end

def highlight_roll(img, threshold = 200, intensity = 1.0)
  mask = img > threshold
  rolled = threshold + (img - threshold) * 0.3 ** 0.7
  safe_cast img * (1 - intensity) + (mask.ifthenelse(rolled, img)) * intensity
end

def shadow_lift(img, lift = 0.15, preserve = true)
  gray = img.colourspace("grey16").cast("float") / 255.0
  mask = preserve ? ((1.0 - gray).pow(2.0)) * 0.8 : (1.0 - gray) * lift
  safe_cast img.linear([1.0, 1.0, 1.0], [mask * 255 * lift])
end

def micro_contrast(img, radius = 5, intensity = 0.3)
  blurred = img.gaussblur(radius)
  high_pass = img - blurred
  safe_cast img + high_pass * intensity
end

def color_separate(img, intensity = 0.6)
  r, g, b = img.bandssplit
  r = (r - g * 0.08 * intensity - b * 0.05 * intensity).max(0)
  g = (g - r * 0.06 * intensity - b * 0.10 * intensity).max(0)
  b = (b - r * 0.04 * intensity - g * 0.07 * intensity).max(0)
  safe_cast img * (1 - intensity) + Vips::Image.bandjoin([r, g, b]) * intensity
end

def grain(img, iso = 400, stock = :kodak_portra, intensity = 0.4)
  data = STOCKS[stock]
  sigma = data[:grain] * Math.sqrt(iso / 100.0) * intensity
  noise = Vips::Image.gaussnoise(img.width, img.height, sigma: sigma)
  bright = img.colourspace("grey16").cast("float") / 255.0
  strength = (1.2 - bright).max(0.3) * intensity
  safe_cast img + rgb_bands(noise * strength) * 0.25
end

def base_tint(img, color = [252, 248, 240], intensity = 0.08)
  overlay = Vips::Image.black(img.width, img.height, bands: 3) + color
  ov = overlay.cast("float") / 255.0
  im = img.cast("float") / 255.0
  blended = im.ifthenelse(ov < 0.5, 2 * im * ov, 1 - 2 * (1 - im) * (1 - ov)) * 255
  safe_cast img * (1 - intensity) + blended * intensity
end

def vintage_lens(img, type = "zeiss", intensity = 0.7)
  case type
  when "zeiss"
    micro_contrast(img, 3, 0.4 * intensity)
  when "leica"
    glow = img.gaussblur(20).linear([0.3 * intensity], [0])
    safe_cast img + glow
  when "helios"
    sharp = img.sharpen(mask: [[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
    safe_cast img * (1 - intensity * 0.3) + sharp * (intensity * 0.3)
  else
    img
  end
end

def teal_orange(img, intensity = 1.0)
  r, g, b = skin_protect(img, 0.8).bandsplit
  r = r.linear([1 + 0.25 * intensity], [8 * intensity])
  g = g.linear([1 - 0.08 * intensity], [0])
  b = b.linear([1 + 0.35 * intensity], [0])
  safe_cast Vips::Image.bandjoin([r, g, b])
end

def bloom_pro(img, intensity = 1.0)
  bright = img.linear([2.0 * intensity], [0])
  combined = (bright.gaussblur(8 * intensity) + bright.gaussblur(16 * intensity) * 0.5) * 0.2
  safe_cast img + combined
end

def preset(img, name)
  cfg = PRESETS[name.to_sym]
  return img unless cfg

  result = img
  cfg[:fx].each do |fx|
    result = case fx
             when "skin_protect"   then skin_protect(result, cfg[:intensity])
             when "film_curve"     then film_curve(result, cfg[:stock], cfg[:intensity])
             when "highlight_roll" then highlight_roll(result, 200, cfg[:intensity] * 0.7)
             when "shadow_lift"    then shadow_lift(result, 0.2, false)
             when "micro_contrast" then micro_contrast(result, 6, cfg[:intensity] * 0.4)
             when "grain"          then grain(result, 400, cfg[:stock], cfg[:intensity] * 0.4)
             when "color_temp"     then color_temp(result, cfg[:temp], cfg[:intensity] * 0.6)
             when "base_tint"      then base_tint(result, [255, 250, 245], 0.08)
             when "color_separate" then color_separate(result, cfg[:intensity] * 0.6)
             when "vintage_lens"   then vintage_lens(result, "zeiss", cfg[:intensity] * 0.8)
             when "teal_orange"    then teal_orange(result, cfg[:intensity])
             when "bloom_pro"      then bloom_pro(result, cfg[:intensity])
             else result
             end
  end
  result
end

def grain_basic(img, intensity)
  noise = Vips::Image.gaussnoise(img.width, img.height, sigma: 25 * intensity)
  safe_cast img + rgb_bands(noise) * 0.2
end

def leaks_basic(img, intensity)
  overlay = Vips::Image.black(img.width, img.height, bands: 3)
  rand(2..5).times do
    x = rand(img.width)
    y = rand(img.height)
    radius = img.width / rand(2..4)
    color = [255 * intensity, 180 * intensity, 80 * intensity]
    overlay = overlay.draw_circle(color, x, y, radius, fill: true)
  end
  safe_cast img + overlay.gaussblur(15 * intensity) * 0.3
end
... 221 lines truncated (621 total)

rails/@shared_functions.sh

#!/usr/bin/env zsh
# @shared_functions.sh — shared helpers for DEPLOY/rails/* scripts
# Source this file; do not execute directly.
# Requires: zsh, ruby34, bundle, rails, doas
set -euo pipefail

PATH="${PATH:-/usr/local/bin:/usr/bin:/bin}"

if command -v doas >/dev/null 2>&1; then
  _PRIV=doas
else
  _PRIV=sudo
fi

: "${APP_PORT:=3000}"

log()      { print -P "%F{cyan}==>%f $*"; }
log_ok()   { print -P "%F{green}ok%f $*"; }
log_warn() { print -P "%F{yellow}WARN%f $*" >&2; }
log_err()  { print -P "%F{red}ERR%f $*" >&2; }

need_cmd() {
  for cmd in "$@"; do
    command -v "$cmd" >/dev/null 2>&1 || { log_err "Required: $cmd"; exit 1; }
    log_ok "$cmd found"
  done
}

already_done() {
  local sentinel=$1
  [[ -f $sentinel ]] && { log_warn "Already set up ($sentinel exists). Skipping."; return 0; }
  return 1
}

create_rails_app() {
  local app_dir=$1
  local app_name=${app_dir:t:h}
  mkdir -p "${app_dir:h}"
  if [[ ! -f "${app_dir}/config/application.rb" ]]; then
    log "Creating Rails 8 app at $app_dir"
    rails new "$app_dir" \
      --database=sqlite3 \
      --asset-pipeline=propshaft \
      --javascript=importmap \
      --skip-git \
      --skip-test \
      --skip-bundle
    # Bootstrap gems from amber to avoid OOM on fresh bundle install
    local bundle_home="/home/${app_dir:h:t}/.bundle"
    if [[ ! -d "${bundle_home}/gems" ]]; then
      log "Bootstrapping gems from amber"
      mkdir -p "${bundle_home}"
      cp -r /home/amber/.bundle/gems "${bundle_home}/"
      cp -r /home/amber/.bundle/cache "${bundle_home}/" 2>/dev/null || true
    fi
    mkdir -p "${app_dir}/.bundle"
    print "---\nBUNDLE_PATH: \"${bundle_home}/gems\"" > "${app_dir}/.bundle/config"
    cp /home/amber/app/Gemfile.lock "${app_dir}/Gemfile.lock"
  fi
  cd "$app_dir"
  log_ok "Working in: $app_dir"
}

add_gem() {
  local gem=$1 ver=${2:-}
  if ! grep -q "\"${gem}\"" Gemfile 2>/dev/null; then
    if [[ -n $ver ]]; then
      print "gem \"${gem}\", \"${ver}\"" >> Gemfile
    else
      print "gem \"${gem}\"" >> Gemfile
    fi
    log_ok "gem ${gem} added"
  else
    log_ok "gem ${gem} already present"
  fi
}

bundle_install() {
  bundle check 2>/dev/null && { log_ok "bundle ok (no install needed)"; return 0; }
  log "bundle install"
  bundle install --jobs=2 2>&1 | tail -5
  log_ok "bundle install done"
}

add_gem_group() {
  local groups=$1; shift
  local -a gems=("$@")
  if ! grep -q "gem \"${gems[1]}\"" Gemfile 2>/dev/null; then
    {
      print "group :${groups//,/, :} do"
      for g in "${gems[@]}"; do print "  gem \"$g\""; done
      print "end"
    } >> Gemfile
  fi
}

install_solid_stack() {
  log "Installing Solid Cache / Queue / Cable"
  add_gem solid_cache
  add_gem solid_queue
  add_gem solid_cable
  bin/rails solid_cache:install 2>/dev/null || true
  bin/rails solid_queue:install 2>/dev/null || true
  bin/rails solid_cable:install 2>/dev/null || true
  log_ok "Solid stack installed"
}

install_auth() {
  if [[ ! -f app/models/session.rb ]]; then
    log "Generating Rails 8 authentication"
    bin/rails generate authentication
    bin/rails db:migrate
  else
    log_ok "Authentication already generated"
  fi
}

install_active_storage() {
  if [[ -z $(print db/migrate/*create_active_storage*(N)) ]]; then
    log "Installing Active Storage"
    bin/rails active_storage:install
    bin/rails db:migrate
  else
    log_ok "Active Storage already installed"
  fi
}

install_action_text() {
  if [[ -z $(print db/migrate/*create_action_text*(N)) ]]; then
    log "Installing Action Text"
    bin/rails action_text:install
    bin/rails db:migrate
  else
    log_ok "Action Text already installed"
  fi
}

db_setup() {
  log "Setting up database"
  RAILS_ENV=production bin/rails db:create db:migrate
  log_ok "Database ready"
}

db_migrate() {
  RAILS_ENV=${RAILS_ENV:-production} bin/rails db:migrate
  log_ok "Migrations complete"
}

configure_production() {
  local cfg=config/environments/production.rb
  grep -q 'force_ssl' "$cfg" || print '  config.force_ssl = true' >> "$cfg"
  grep -q 'solid_cache' "$cfg" || print '  config.cache_store = :solid_cache_store' >> "$cfg"
  log_ok "Production config updated"
}

install_security_tools() {
  add_gem_group "development,test" brakeman rubocop-rails-omakase
  log_ok "Security tools added"
}

install_dartsass() {
  add_gem dartsass-rails
  bin/rails dartsass:install 2>/dev/null || true
  log_ok "Dart Sass installed"
}

write_base_scss() {
  mkdir -p app/assets/stylesheets
  rm -f app/assets/stylesheets/application.css
  cat > app/assets/stylesheets/application.scss << 'SCSS'
// ==================== VARIABLES ====================
:root {
  // Colors
  --color-black: #000;
  --color-white: #fff;
  --color-extra-light-grey: #f0f0f0;

  // Spacing
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;

  // Typography
  --font-size-base: 14px;
  --line-height-base: 1.5;
}

// ==================== RESET & BASE ====================
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html,
body {
  height: 100%;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
  font-size: var(--font-size-base);
  line-height: var(--line-height-base);
  color: var(--color-black);
  background-color: var(--color-white);
  display: flex;
  flex-direction: column;
}

img { max-width: 100%; display: block; }

a {
  color: #4285f4;
  text-decoration: none;
  cursor: pointer;

  &:hover { text-decoration: underline; }
  &:focus { outline: 2px solid #4285f4; outline-offset: 2px; }
}

// ==================== NAV ====================
nav {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--color-extra-light-grey);

  a { color: inherit; }
  a:hover { text-decoration: underline; }
  .brand { font-weight: 700; margin-right: auto; }
}

// ==================== MAIN ====================
main {
  flex: 1;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
  padding: var(--space-md);
}

// ==================== FLASH ====================
.flash {
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--color-extra-light-grey);

  &--error, &--alert { color: #c00; }
  &--notice { color: #060; }
}

// ==================== RESPONSIVE ====================
@media (max-width: 768px) {
  .header {
    flex-direction: column;
    gap: var(--space-md);
    padding: var(--space-sm);

    &__tabs {
      gap: var(--space-sm);
      flex-wrap: wrap;
      justify-content: center;
    }
  }
}

@media (max-width: 480px) {
  html, body { font-size: 12px; }

  .header__tabs { gap: var(--space-xs); }
  .header__tab { padding: var(--space-xs) var(--space-sm); font-size: 0.9em; }
}
SCSS
  log_ok "application.scss written"
}

write_base_css() { write_base_scss; }
write_layout()     { write_full_layout "$@"; }

install_rcd() {
  local svc=$1 app_dir=$2 port=$3 user=$4
  local rcd="/etc/rc.d/${svc}"
  [[ -f $rcd ]] && { log_ok "rc.d/${svc} already exists"; return 0; }
  local secret
  secret=$(ruby34 -e 'require "securerandom"; print SecureRandom.hex(64)')
  $_PRIV tee "$rcd" > /dev/null << EOS
#!/bin/ksh
daemon="/usr/local/bin/bundle"
daemon_flags="exec env RAILS_ENV=production SECRET_KEY_BASE=${secret} HOME=/home/${user} falcon serve --bind http://127.0.0.1:${port}"
daemon_user="${user}"
daemon_execdir="${app_dir}"
daemon_timeout="60"
. /etc/rc.d/rc.subr
pexp="ruby.*${port}"
rc_bg=YES
rc_reload=NO
rc_cmd \$1
EOS
  $_PRIV chmod 755 "$rcd"
  $_PRIV rcctl enable "$svc"
  # App must be owned by the service user so it can write storage/log/tmp
  $_PRIV chown -R "${user}:${user}" "${app_dir}"
  log_ok "rc.d/${svc} installed (falcon on :${port})"
}

relayd_add_relay() {
  local host=$1 port=$2
  local table="${host%%.*}"
  local conf=/etc/relayd.conf
  grep -q "table <${table}>" "$conf" 2>/dev/null && { log_ok "relayd <${table}> exists"; return 0; }
  $_PRIV tee -a "$conf" > /dev/null << EOS

table <${table}> { 127.0.0.1 }
relay "${table}_http" {
  listen on 0.0.0.0 port 80
  forward to <${table}> port ${port} check tcp
}
EOS
  log_ok "relayd table <${table}> -> :${port} added"
}

write_falcon_config() {
  local port=${1:-3000}
  add_gem falcon
  cat > config/falcon.rb << FALCON
#!/usr/bin/env -S falcon host
# frozen_string_literal: true

load :rack, :supervisor

hostname = File.basename(__dir__)
port = ENV.fetch("PORT", ${port}).to_i

rack hostname do
  endpoint Async::HTTP::Endpoint.parse("http://0.0.0.0:\#{port}")
end
FALCON
  log_ok "Falcon config written (:${port})"
}

install_thruster() {
  add_gem thruster
  log_ok "Thruster added"
}

# ── Stimulus + Importmap ────────────────────────────────────────────────────

setup_stimulus() {
  log "Setting up Stimulus"
  bin/importmap pin @hotwired/stimulus --download 2>/dev/null || true
  mkdir -p app/javascript/controllers
  cat > app/javascript/controllers/application.js << 'JS'
import { Application } from "@hotwired/stimulus"
const application = Application.start()
application.debug = false
window.Stimulus = application
export { application }
JS
  cat > app/javascript/controllers/index.js << 'JS'
import { application } from "./application"
// controllers are auto-imported via eagerLoadControllersFrom in application.js
// or listed here explicitly:
JS
  cat >> app/javascript/application.js << 'JS'

import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)
JS
  log_ok "Stimulus ready"
}

write_stimulus_controller() {
  local name=$1
  mkdir -p app/javascript/controllers
  cat > "app/javascript/controllers/${name}_controller.js"
  log_ok "Stimulus ${name}_controller.js written"
}

# ── Pagy ───────────────────────────────────────────────────────────────────

setup_pagy() {
  add_gem pagy
  # Pagy 9+ (v43+): no initializer needed; Backend is now Pagy::Method
  ruby34 -e "
    src = File.read('app/controllers/application_controller.rb')
    unless src.include?('Pagy::Method')
      src.sub!(/class ApplicationController.*\n/, \"\\\\0  include Pagy::Method\n\")
      File.write('app/controllers/application_controller.rb', src)
    end
  "
  log_ok "Pagy configured"
}

# ── Shared partials ─────────────────────────────────────────────────────────

write_shared_partials() {
  mkdir -p app/views/shared
  cat > app/views/shared/_flash.html.erb << 'ERB'
<% flash.each do |type, msg| %>
  <div class="flash flash--<%= type %>"><%= msg %></div>
... 424 lines truncated (824 total)

rails/README.md

rails/
├─ amber/               # Amber‑related deployment scripts
│   └─ amber.sh
├─ baibl/               # Baibl service scripts
│   └─ baibl.sh
├─ blognet/             # Blognet deployment utilities
│   └─ blognet.sh
├─ brgen/               # Brgen family of scripts (brgen*.sh)
│   └─ brgen*.sh
├─ bsdports/            # BSD‑Ports integration scripts
│   └─ bsdports.sh
├─ hjerterom/           # Hjerterom service scripts
│   └─ hjerterom.sh
├─ privcam/             # PrivCam deployment helpers
│   └─ privcam.sh
├─ __shared/            # Shared resources used by all scripts
│   ├─ @common.sh                # Core utilities (e.g., `get_app_port`, feature loading)
│   ├─ @*_features.sh            # Feature modules (messaging, reddit, airbnb, …)
│   ├─ layouts/*                 # Reusable partials & static assets
│   └─ @shared_functions.sh      # Logging, environment handling, common helpers
├─ __common_patterns.css        # Global CSS patterns shared across deployments
├─ check_ports.sh                # Validate service ports against `master.json`
├─ modernize_zsh.sh              # Migrate legacy Zsh patterns to the new style
├─ voting_system.sh              # Scripts for deploying the voting subsystem
└─ rich_editor_system.sh         # Tools for installing and configuring the rich‑text editor

rails/__shared/@active_storage_and_imageprocessing.sh

#!/usr/bin/env zsh

bin/rails active_storage:install
bin/rails generate migration add_avatar_to_users avatar:attachment
bin/rails db:migrate

cat <<EOF > app/models/user.rb
class User < ApplicationRecord
  has_one_attached :avatar
end
EOF

yarn add @rails/activestorage image_processing
bundle add image_processing

cat <<EOF > app/controllers/users_controller.rb
class UsersController < ApplicationController
  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      redirect_to @user, notice: "User was successfully updated."
    else
      render :edit
    end
  end

  private

  def user_params
    params.require(:user).permit(:avatar)
  end
end
EOF

rails/__shared/@ai.sh

cd "$BASE_DIR"

doas pkg_add llvm

bundle add langchainrb
bundle add langchainrb_rails
bundle add weaviate-ruby
bundle add replicate-ruby
bundle add replicate-rails

bundle install

rails/__shared/@airbnb_features.sh

#!/usr/bin/env zsh
# __shared/@airbnb_features.sh — Airbnb-style rental models for Rails 8
# Source from app installers. Do not execute directly.
set -euo pipefail

setup_airbnb_models() {
  log "Setting up Airbnb rental models"

  generate_model Listing \
    title:string description:text \
    price_per_night:decimal max_guests:integer \
    location:string latitude:float longitude:float \
    user:references

  generate_model Booking \
    listing:references user:references \
    check_in:date check_out:date \
    guests_count:integer total_price:decimal \
    status:string

  generate_model Review \
    listing:references user:references \
    rating:integer content:text \
    cleanliness:integer accuracy:integer \
    communication:integer location:integer value:integer

  generate_model Availability \
    listing:references date:date available:boolean price_override:decimal

  generate_model Amenity name:string category:string icon:string

  generate_model ListingAmenity listing:references amenity:references

  log_ok "Airbnb models ready"
}

write_airbnb_model_logic() {
  cat > app/models/listing.rb << 'RUBY'
class Listing < ApplicationRecord
  belongs_to :user
  has_many :bookings, dependent: :destroy
  has_many :reviews, dependent: :destroy
  has_many :availabilities, dependent: :destroy
  has_many :listing_amenities, dependent: :destroy
  has_many :amenities, through: :listing_amenities
  has_many_attached :photos

  validates :title, :price_per_night, :max_guests, :location, presence: true
  validates :price_per_night, numericality: { greater_than: 0 }
  validates :max_guests, numericality: { only_integer: true, greater_than: 0 }

  scope :available_between, ->(check_in, check_out) {
    where.not(id: Booking.confirmed.select(:listing_id)
      .where("check_in < ? AND check_out > ?", check_out, check_in))
  }

  def average_rating
    reviews.average(:rating)&.round(1) || 0
  end

  def available_on?(date)
    availabilities.find_by(date: date)&.available != false
  end
end
RUBY

  cat > app/models/booking.rb << 'RUBY'
class Booking < ApplicationRecord
  belongs_to :listing
  belongs_to :user

  STATUSES = %w[pending confirmed cancelled completed].freeze

  validates :check_in, :check_out, :guests_count, :total_price, :status, presence: true
  validates :guests_count, numericality: { only_integer: true, greater_than: 0 }
  validates :total_price, numericality: { greater_than_or_equal_to: 0 }
  validates :status, inclusion: { in: STATUSES }
  validate :check_out_after_check_in
  validate :guests_within_capacity

  scope :confirmed, -> { where(status: "confirmed") }
  scope :upcoming, -> { where("check_in >= ?", Date.today).order(:check_in) }

  before_validation :calculate_total_price, on: :create

  private

  def check_out_after_check_in
    return if check_in.blank? || check_out.blank?
    errors.add(:check_out, "must be after check-in") if check_out <= check_in
  end

  def guests_within_capacity
    return unless listing && guests_count
    if guests_count > listing.max_guests
      errors.add(:guests_count, "exceeds listing capacity")
    end
  end

  def calculate_total_price
    return unless listing && check_in && check_out
    nights = (check_out - check_in).to_i
    self.total_price = nights * listing.price_per_night
  end
end
RUBY

  cat > app/models/review.rb << 'RUBY'
class Review < ApplicationRecord
  belongs_to :listing
  belongs_to :user

  RATING_FIELDS = %i[rating cleanliness accuracy communication location value].freeze

  validates :content, presence: true, length: { maximum: 2000 }
  RATING_FIELDS.each do |field|
    validates field, numericality: { in: 1..5 }, allow_nil: true
  end
  validates :rating, presence: true

  after_create_commit :broadcast_to_listing

  private

  def broadcast_to_listing
    broadcast_append_to listing, target: "reviews"
  end
end
RUBY
  log_ok "Airbnb model logic written"
}

rails/__shared/@common.sh

#!/usr/bin/env zsh
# __shared/@common.sh — common Rails 8 installer helpers
# Source from app installers. Do not execute directly.
set -euo pipefail

SCRIPT_DIR=${0:a:h}

log()  { print -P "%F{cyan}[%D{%H:%M:%S}]%f $*"; }
warn() { print -P "%F{yellow}WARN%f $*" >&2; }
err()  { print -P "%F{red}ERR%f $*" >&2; }

# Idempotent model generation: skip if model file exists
gen_model() {
  local name=$1; shift
  local path="app/models/${name:l}.rb"
  if [[ -f $path ]]; then
    log_ok "model $name already exists"
    return 0
  fi
  bin/rails generate model "$name" "$@" --no-test-framework
  bin/rails db:migrate
}

# Idempotent scaffold generation
gen_scaffold() {
  local name=$1; shift
  local path="app/models/${name:l}.rb"
  if [[ -f $path ]]; then
    log_ok "scaffold $name already exists"
    return 0
  fi
  bin/rails generate scaffold "$name" "$@" --no-test-framework
  bin/rails db:migrate
}

# Write file only if it does not exist
write_once() {
  local path=$1
  [[ -f $path ]] && { log_ok "$path exists"; return 0; }
  mkdir -p "${path:h}"
  cat > "$path"
  log_ok "wrote $path"
}

# Overwrite file unconditionally
write_file() {
  local path=$1
  mkdir -p "${path:h}"
  cat > "$path"
  log_ok "wrote $path"
}

rails/__shared/@devise.sh

cd "$BASE_DIR"

# -- SET UP DEVISE FOR USER AUTHENTICATION --

bundle add devise
bundle install

bin/rails generate devise:install
bin/rails generate devise User
bin/rails db:migrate

commit_to_git "Added Devise and hooked it up to User model."

# -- SET UP OMNIAUTH FOR USER AUTHENTICATION --

bundle add omniauth-openid-connect
bundle add omniauth-google-oauth2
bundle add omniauth-snapchat

bundle install

mkdir -p app/controllers/users

cat <<EOF > app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def vipps
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
      set_flash_message(:notice, :success, kind: "Vipps") if is_navigational_format?
    else
      session["devise.vipps_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url, alert: @user.errors.full_messages.join("\\n")
    end
  end

  def google_oauth2
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
      set_flash_message(:notice, :success, kind: "Google") if is_navigational_format?
    else
      session["devise.google_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url, alert: @user.errors.full_messages.join("\\n")
    end
  end

  def snapchat
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
      set_flash_message(:notice, :success, kind: "Snapchat") if is_navigational_format?
    else
      session["devise.snapchat_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url, alert: @user.errors.full_messages.join("\\n")
    end
  end
end
EOF

mkdir -p app/models

cat <<EOF > app/models/user.rb
class User < ApplicationRecord
  devise :omniauthable, omniauth_providers: %i[vipps google_oauth2 snapchat]

  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20]
      user.name = auth.info.name
    end
  end
end
EOF

commit_to_git "Set up OmniAuth for Vipps, Google, and Snapchat."

rails/__shared/@features_base.sh

#!/usr/bin/env zsh
# __shared/@features_base.sh — Rails 8 resource generation helpers
# Source from app installers. Do not execute directly.
set -euo pipefail

# Generate a model+controller+views (scaffold) if model absent
generate_resource() {
  local name=$1; shift
  local model_file="app/models/${name:l}.rb"
  if [[ -f $model_file ]]; then
    log_ok "resource $name already present"
    return 0
  fi
  log "Generating scaffold: $name $*"
  bin/rails generate scaffold "$name" "$@" --no-test-framework
  bin/rails db:migrate
  log_ok "resource $name done"
}

# Generate a plain model if absent
generate_model() {
  local name=$1; shift
  local model_file="app/models/${name:l}.rb"
  if [[ -f $model_file ]]; then
    log_ok "model $name already present"
    return 0
  fi
  log "Generating model: $name $*"
  bin/rails generate model "$name" "$@" --no-test-framework
  bin/rails db:migrate
  log_ok "model $name done"
}

# Write a Stimulus controller if absent
generate_stimulus() {
  local name=$1  # snake_case, e.g. posts_vote
  local path="app/javascript/controllers/${name}_controller.js"
  [[ -f $path ]] && { log_ok "stimulus $name exists"; return 0; }
  mkdir -p app/javascript/controllers
  cat > "$path" << JS
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {}
}
JS
  log_ok "Stimulus $name written"
}

# Append route block to routes.rb if pattern absent
ensure_route() {
  local pattern=$1
  local block=$2
  grep -q "$pattern" config/routes.rb 2>/dev/null && return 0
  # Insert before final 'end'
  ruby34 -i -e '
    lines = $stdin.readlines
    idx = lines.rindex { |l| l.strip == "end" }
    lines.insert(idx, ARGV[0] + "\n") if idx
    print lines.join
  ' "$block" config/routes.rb
  log_ok "route added: $pattern"
}

rails/__shared/@instant_messaging.sh

#!/usr/bin/env zsh

cd "$(dirname "$0")"

# Generate models, controllers, and views for instant messaging
bin/rails generate model Message sender:references recipient:references body:text read:boolean
bin/rails generate controller Messages create show index destroy
echo "resources :messages, only: [:create, :show, :index, :destroy]" >> config/routes.rb

# Update Message model
cat <<EOF > app/models/message.rb
class Message < ApplicationRecord
  belongs_to :sender, class_name: "User"
  belongs_to :recipient, class_name: "User"
  validates :body, presence: true
end
EOF

# Update MessagesController
cat <<EOF > app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  before_action :authenticate_user!

  def index
    @messages = Message.where(sender: current_user).or(Message.where(recipient: current_user))
  end

  def show
    @message = Message.find(params[:id])
    @message.update(read: true) if @message.recipient == current_user
  end

  def create
    @message = Message.new(message_params)
    @message.sender = current_user
    if @message.save
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_to messages_path, notice: t("message_sent") }
      end
    else
      render :new
    end
  end

  def destroy
    @message = Message.find(params[:id])
    @message.destroy
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to messages_path, notice: t("message_deleted") }
    end
  end

  private

  def message_params
    params.require(:message).permit(:recipient_id, :body)
  end
end
EOF

# Create views for messages
mkdir -p app/views/messages
cat <<EOF > app/views/messages/index.html.erb
<%= tag.h1 t("messages") %>
<%= tag.ul do %>
  <% @messages.each do |message| %>
    <%= tag.li do %>
      <%= link_to message.body.truncate(20), message %>
      <%= message.read ? t("read") : t("unread") %>
    <% end %>
  <% end %>
<% end %>
<%= turbo_stream_from "messages" %>
EOF

cat <<EOF > app/views/messages/show.html.erb
<%= tag.h1 t("message") %>
<p><strong><%= t("from") %>:</strong> <%= @message.sender.email %></p>
<p><strong><%= t("to") %>:</strong> <%= @message.recipient.email %></p>
<p><strong><%= t("body") %>:</strong> <%= @message.body %></p>
<p><strong><%= t("read") %>:</strong> <%= @message.read ? t("yes") : t("no") %></p>
<%= link_to t("back"), messages_path %>
EOF

cat <<EOF > app/views/messages/_form.html.erb
<%= form_with(model: @message, local: true) do |form| %>
  <%= form.label :recipient_id %>
  <%= form.collection_select :recipient_id, User.all, :id, :email, prompt: t("select_recipient") %>
  <%= form.label :body %>
  <%= form.text_area :body %>
  <%= form.submit %>
<% end %>
EOF

cat <<EOF > app/views/messages/new.html.erb
<%= tag.h1 t("new_message") %>
<%= render "form", message: @message %>
<%= link_to t("back"), messages_path %>
EOF

# Turbo Streams for creating and destroying messages
cat <<EOF > app/views/messages/create.turbo_stream.erb
<%= turbo_stream.append "messages" do %>
  <%= render @message %>
<% end %>
EOF

cat <<EOF > app/views/messages/destroy.turbo_stream.erb
<%= turbo_stream.remove dom_id(@message) %>
EOF

bin/rails db:migrate
commit_to_git "Set up instant messaging functionality"

rails/__shared/@live_cam_streaming.sh

#!/bin/zsh

# Add dependencies
yarn add video.js

# Generate models, controllers, and views for live streaming
bin/rails generate model Stream title:string description:text user:references
bin/rails generate controller Streams index show new create destroy

# Add routes for streams (append to routes.rb)
  echo "resources :streams, only: [:index, :show, :new, :create, :destroy]" >> config/routes.rb

# Create the Streams controller
cat <<EOF > app/controllers/streams_controller.rb
class StreamsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]
  before_action :set_stream, only: [:show, :destroy]

  def index
    @streams = Stream.all
  end

  def show
  end

  def new
    @stream = current_user.streams.build
  end

  def create
    @stream = current_user.streams.build(stream_params)
    if @stream.save
      redirect_to @stream, notice: "Stream created successfully"
    else
      render :new
    end
  end

  def destroy
    @stream.destroy
    redirect_to streams_path, notice: "Stream deleted successfully"
  end

  private

  def set_stream
    @stream = Stream.find(params[:id])
  end

  def stream_params
    params.require(:stream).permit(:title, :description)
  end
end
EOF

# Create the Stream model
cat <<EOF > app/models/stream.rb
class Stream < ApplicationRecord
  belongs_to :user
  validates :title, presence: true
end
EOF

# Create views for streams
mkdir -p app/views/streams
cat <<EOF > app/views/streams/index.html.erb
<%= tag.h1 "Streams" %>
<%= tag.ul do %>
  <% @streams.each do |stream| %>
    <%= tag.li do %>
      <%= link_to stream.title, stream %>
      <p><%= stream.description %></p>
    <% end %>
  <% end %>
<% end %>
EOF

cat <<EOF > app/views/streams/show.html.erb
<%= tag.h1 @stream.title %>
<p><%= @stream.description %></p>
<div id="live-stream" data-controller="stream" data-stream-id="<%= @stream.id %>">
  <video id="live-stream-video" controls autoplay></video>
</div>
<%= link_to "Back", streams_path %>
EOF

cat <<EOF > app/views/streams/_form.html.erb
<%= form_with(model: @stream, local: true) do |form| %>
  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>
  <div class="field">
    <%= form.label :description %>
    <%= form.text_area :description %>
  </div>
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
EOF

cat <<EOF > app/views/streams/new.html.erb
<%= tag.h1 "New Stream" %>
<%= render "form", stream: @stream %>
<%= link_to "Back", streams_path %>
EOF

cat <<EOF > app/views/streams/edit.html.erb
<%= tag.h1 "Edit Stream" %>
<%= render "form", stream: @stream %>
<%= link_to "Back", streams_path %>
EOF

# Create Stimulus controller for live streaming
mkdir -p app/javascript/controllers
cat <<EOF > app/javascript/controllers/stream_controller.js
import { Controller } from "stimulus"
import { createConsumer } from "@rails/actioncable"

export default class extends Controller {
  static targets = ["video"]

  connect() {
    this.channel = createConsumer().subscriptions.create(
      { channel: "StreamChannel", stream_id: this.data.get("id") },
      {
        received: data => this.#received(data)
      }
    )
  }

  #received(data) {
    if (data.action === "play") {
      this.videoTarget.src = data.url
    }
  }
}
EOF

# Create StreamChannel
cat <<EOF > app/channels/stream_channel.rb
class StreamChannel < ApplicationCable::Channel
  def subscribed
    stream_from "stream_\#{params[:stream_id]}"
  end
end
EOF

# Create broadcast job
cat <<EOF > app/jobs/stream_broadcast_job.rb
class StreamBroadcastJob < ApplicationJob
  queue_as :default

  def perform(stream, url)
    ActionCable.server.broadcast "stream_\#{stream.id}", action: "play", url: url
  end
end
EOF

# Run migrations
bin/rails db:migrate

commit_to_git "Set up live cam streaming for $APP"

rails/__shared/@live_streaming.sh

#!/usr/bin/env zsh

cd "$(dirname "$0")"

# Add dependencies
yarn add video.js @hotwired/turbo-rails stimulus

# Generate models, controllers, and views for live streaming
bin/rails generate model Stream title:string description:text user:references
bin/rails generate controller Streams index show new create destroy
echo "resources :streams, only: [:index, :show, :new, :create, :destroy]" >> config/routes.rb

# Update Stream model
cat <<EOF > app/models/stream.rb
class Stream < ApplicationRecord
  belongs_to :user
  validates :title, presence: true
end
EOF

# Update StreamsController
cat <<EOF > app/controllers/streams_controller.rb
class StreamsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]
  before_action :set_stream, only: [:show, :destroy]

  def index
    @streams = Stream.all
  end

  def show
  end

  def new
    @stream = current_user.streams.build
  end

  def create
    @stream = current_user.streams.build(stream_params)
    if @stream.save
      redirect_to @stream, notice: t("stream_created")
    else
      render :new
    end
  end

  def destroy
    @stream.destroy
    redirect_to streams_path, notice: t("stream_deleted")
  end

  private

  def set_stream
    @stream = Stream.find(params[:id])
  end

  def stream_params
    params.require(:stream).permit(:title, :description)
  end
end
EOF

# Create views for streams
mkdir -p app/views/streams
cat <<EOF > app/views/streams/index.html.erb
<%= tag.h1 t("streams") %>
<%= tag.ul do %>
  <% @streams.each do |stream| %>
    <%= tag.li do %>
      <%= link_to stream.title, stream %>
      <%= tag.p stream.description %>
    <% end %>
  <% end %>
<% end %>
<%= link_to t("new_stream"), new_stream_path %>
EOF

cat <<EOF > app/views/streams/show.html.erb
<%= tag.h1 @stream.title %>
<%= tag.p @stream.description %>
<%= link_to t("back"), streams_path %>
EOF

cat <<EOF > app/views/streams/_form.html.erb
<%= form_with(model: @stream, local: true) do |form| %>
  <%= form.label :title %>
  <%= form.text_field :title %>
  <%= form.label :description %>
  <%= form.text_area :description %>
  <%= form.submit %>
<% end %>
EOF

cat <<EOF > app/views/streams/new.html.erb
<%= tag.h1 t("new_stream") %>
<%= render "form", stream: @stream %>
<%= link_to t("back"), streams_path %>
EOF

bin/rails db:migrate
commit_to_git "Set up live streaming functionality"

rails/__shared/@messenger_features.sh

#!/usr/bin/env zsh
# __shared/@messenger_features.sh — Messenger-style chat models for Rails 8
# Source from app installers. Do not execute directly.
set -euo pipefail

setup_messenger_models() {
  log "Setting up messenger models"

  generate_model Conversation \
    name:string conversation_type:string \
    last_message_at:datetime

  generate_model ConversationParticipant \
    conversation:references user:references \
    last_read_at:datetime muted:boolean:'default[false]'

  generate_model Message \
    conversation:references user:references \
    content:text message_type:string \
    read_at:datetime edited_at:datetime \
    deleted_at:datetime

  log_ok "Messenger models ready"
}

write_messenger_model_logic() {
  cat > app/models/conversation.rb << 'RUBY'
class Conversation < ApplicationRecord
  has_many :conversation_participants, dependent: :destroy
  has_many :participants, through: :conversation_participants, source: :user
  has_many :messages, dependent: :destroy

  validates :conversation_type, inclusion: { in: %w[direct group] }

  scope :for_user, ->(user) {
    joins(:conversation_participants).where(conversation_participants: { user: user })
  }

  def self.direct_between(user1, user2)
    joins(:conversation_participants)
      .where(conversation_type: "direct")
      .where(conversation_participants: { user: user1 })
      .joins(:conversation_participants)
      .where(conversation_participants: { user: user2 })
      .first
  end

  def unread_count_for(user)
    participant = conversation_participants.find_by(user: user)
    return 0 unless participant
    messages.where("created_at > ?", participant.last_read_at || Time.at(0)).count
  end

  def mark_read_by!(user)
    conversation_participants.find_by(user: user)&.update!(last_read_at: Time.current)
  end
end
RUBY

  cat > app/models/message.rb << 'RUBY'
class Message < ApplicationRecord
  belongs_to :conversation
  belongs_to :user
  has_many_attached :attachments

  validates :content, presence: true, unless: :has_attachments?
  validates :message_type, inclusion: { in: %w[text image file voice] }

  default_scope -> { where(deleted_at: nil).order(:created_at) }

  after_create_commit :update_conversation_timestamp
  after_create_commit :broadcast_to_conversation

  def soft_delete!
    update!(deleted_at: Time.current, content: "Message deleted")
  end

  def edited?
    edited_at.present?
  end

  private

  def has_attachments?
    attachments.any?
  end

  def update_conversation_timestamp
    conversation.update_column(:last_message_at, Time.current)
  end

  def broadcast_to_conversation
    broadcast_append_to [conversation, "messages"],
      partial: "messages/message",
      locals: { message: self }
  end
end
RUBY

  cat > app/controllers/conversations_controller.rb << 'RUBY'
class ConversationsController < ApplicationController
  before_action :require_authentication

  def index
    @conversations = Conversation.for_user(Current.user)
      .includes(:participants, :messages)
      .order(last_message_at: :desc)
  end

  def show
    @conversation = Conversation.for_user(Current.user).find(params[:id])
    @conversation.mark_read_by!(Current.user)
    @messages = @conversation.messages.includes(:user).limit(50)
    @message = Message.new
  end

  def create
    recipient = User.find(params[:recipient_id])
    @conversation = Conversation.direct_between(Current.user, recipient) ||
      Conversation.create!(conversation_type: "direct")
    @conversation.participants << Current.user unless @conversation.participants.include?(Current.user)
    @conversation.participants << recipient unless @conversation.participants.include?(recipient)
    redirect_to @conversation
  end
end
RUBY

  cat > app/controllers/messages_controller.rb << 'RUBY'
class MessagesController < ApplicationController
  before_action :require_authentication
  before_action :set_conversation

  def create
    @message = @conversation.messages.build(message_params.merge(user: Current.user))
    if @message.save
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_to @conversation }
      end
    else
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    @message = @conversation.messages.find(params[:id])
    @message.soft_delete! if @message.user == Current.user
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @conversation }
    end
  end

  private

  def set_conversation
    @conversation = Conversation.for_user(Current.user).find(params[:conversation_id])
  end

  def message_params
    params.require(:message).permit(:content, :message_type, attachments: [])
  end
end
RUBY
  log_ok "Messenger logic written"
}

rails/__shared/@postgresql.sh

if ! command_exists psql; then
  echo "PostgreSQL is not installed. Installing..."
  doas pkg_add -U postgresql-server || { echo "Failed to install PostgreSQL."; exit 1; }
  doas rcctl enable postgresql
  doas rcctl start postgresql
fi

# Set up PostgreSQL roles and databases
createuser -s "${APP}" 2>/dev/null || echo "Role ${APP} already exists."
createdb "${APP}_development" 2>/dev/null || echo "Database ${APP}_development already exists."
createdb "${APP}_test" 2>/dev/null || echo "Database ${APP}_test already exists."
createdb "${APP}_production" 2>/dev/null || echo "Database ${APP}_production already exists."

  cat <<EOF > config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  username: ${APP}
  password: password
  host: localhost
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: ${APP}_development

test:
  <<: *default
  database: ${APP}_test

production:
  <<: *default
  database: ${APP}_production
EOF

echo "PostgreSQL setup complete."

rails/__shared/@posts.sh

cd "$BASE_DIR"

echo "Generating Posts, Communities, and Comments..."

bundle add friendly_id
bundle install

bin/rails generate model Community name:string description:text
bin/rails generate model Post title:string content:text user:references community:references
bin/rails generate model Comment content:text user:references post:references
bin/rails db:migrate

# Community model
cat <<EOF > app/models/community.rb
class Community < ApplicationRecord
  has_many :posts, class_name: "Post"

  validates :name, presence: true

  extend FriendlyId
  friendly_id :name, use: :slugged
end
EOF

# Post model
cat <<EOF > app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  belongs_to :community, class_name: "Community"

  has_many :comments, class_name: "Comment", dependent: :destroy
  has_many :post_visibilities
  has_many :visible_users, through: :post_visibilities, source: :user
  has_many :reactions, as: :reactable, dependent: :destroy

  validates :title, :content, presence: true

  extend FriendlyId
  friendly_id :title, use: :slugged

  after_create :set_expiry
  after_update_commit { broadcast_replace_to "posts" }

  def visible_to?(user)
    self.visible_users.include?(user)
  end

  def set_expiry
    ExpiryJob.set(wait_until: self.expiry_time).perform_later(self.id) if self.expiry_time.present?
  end
end
EOF

# Comment model
cat <<EOF > app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post, class_name: "Post"
  belongs_to :user

  validates :content, presence: true

  extend FriendlyId
  friendly_id :content, use: :slugged
end
EOF

# PostsController
cat <<EOF > app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!

  def create
    @post = current_user.posts.new(post_params)
    if @post.save
      params[:post][:visible_user_ids].each do |user_id|
        @post.post_visibilities.create(user_id: user_id)
      end
      @post.visible_users.each do |user|
        Notification.create(user: user, post: @post, message: "You have a new private post")
      end
      respond_to do |format|
        format.html { redirect_to @post, notice: t('posts.create.success') }
        format.turbo_stream
      end
    else
      render :new
    end
  end

  def update
    if @post.update(post_params)
      respond_to do |format|
        format.html { redirect_to main_community_post_path(@post.community, @post) }
        format.turbo_stream
      end
    else
      render :edit
    end
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end

  def post_params
    params.require(:post).permit(:title, :content, :community_id, :user_id, :expiry_time, visible_user_ids: [])
  end
end
EOF

# CommentsController
cat <<EOF > app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    @comment = Comment.new(comment_params)

    if @comment.save
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_to main_community_post_path(@comment.post.community, @comment.post) }
      end
    else
      render :new
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:content, :post_id, :user_id)
  end
end
EOF

# Turbo Stream Views
mkdir -p app/views/posts app/views/comments

cat <<EOF > app/views/posts/_post.html.erb
<%= turbo_frame_tag dom_id(post) do %>
  <div class="post" id="post_<%= post.id %>">
    <h2><%= link_to post.title, main_community_post_path(post.community, post) %></h2>
    <p><%= post.content %></p>
  </div>
<% end %>
EOF

cat <<EOF > app/views/comments/_comment.html.erb
<%= turbo_frame_tag dom_id(comment) do %>
  <div class="comment" id="comment_<%= comment.id %>">
    <p><%= comment.content %></p>
  </div>
<% end %>
EOF

cat <<EOF > app/views/posts/create.turbo_stream.erb
<%= turbo_stream.append "posts", partial: "posts/post", locals: { post: @post } %>
EOF

cat <<EOF > app/views/posts/update.turbo_stream.erb
<%= turbo_stream.replace @post, partial: "posts/post", locals: { post: @post } %>
EOF

cat <<EOF > app/views/comments/create.turbo_stream.erb
<%= turbo_stream.append "comments", partial: "comments/comment", locals: { comment: @comment } %>
EOF

# FriendlyId for SEO-friendly URLs
echo "Installing FriendlyId for SEO-friendly URLs..."

bundle add friendly_id
bundle install
bin/rails generate friendly_id
commit_to_git "Installed FriendlyId for SEO-friendly URLs."

cat <<EOF > app/models/user.rb
class User < ApplicationRecord
  extend FriendlyId
  friendly_id :username, use: :slugged
end
EOF

cat <<EOF > app/models/community.rb
class Community < ApplicationRecord
  has_many :posts, class_name: "Post"

  validates :name, presence: true

  extend FriendlyId
  friendly_id :name, use: :slugged
end
EOF

cat <<EOF > app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  belongs_to :community, class_name: "Community"

  has_many :comments, class_name: "Comment", dependent: :destroy
  has_many :post_visibilities
  has_many :visible_users, through: :post_visibilities, source: :user
  has_many :reactions, as: :reactable, dependent: :destroy

  validates :title, :content, presence: true

  extend FriendlyId
  friendly_id :title, use: :slugged
end
EOF

cat <<EOF > app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post, class_name: "Post"
  belongs_to :user

  validates :content, presence: true

  extend FriendlyId
  friendly_id :content, use: :slugged
end
EOF

commit_to_git "Set up FriendlyId for SEO-friendly URLs for User, Community, Post, and Comment models."

# I18n and Babosa for translation and transliteration
echo "Setting up I18n and Babosa for translation and transliteration..."
bundle add babosa

cat <<EOF > config/initializers/locale.rb
I18n.available_locales = [:en, :no]
I18n.default_locale = :en

require "babosa"
EOF

commit_to_git "Set up I18n and Babosa for translation and transliteration."

# Add Private Posts feature
echo "Adding private posts feature..."
cat <<EOF > app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :post_visibilities
  has_many :visible_users, through: :post_visibilities, source: :user
  has_many :comments, dependent: :destroy
  has_many :reactions, as: :reactable, dependent: :destroy

  validates :content, presence: true

  after_create :set_expiry
  after_update_commit { broadcast_replace_to "posts" }

  def visible_to?(user)
    self.visible_users.include?(user)
  end

  def set_expiry
    ExpiryJob.set(wait_until: self.expiry_time).perform_later(self.id) if self.expiry_time.present?
  end
end
EOF

cat <<EOF > app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!

  def create
    @post = current_user.posts.new(post_params)
    if @post.save
      params[:post][:visible_user_ids].each do |user_id|
        @post.post_visibilities.create(user_id: user_id)
      end
      @post.visible_users.each do |user|
        Notification.create(user: user, post: @post, message: "You have a new private post")
      end
      respond_to do |format|
        format.html { redirect_to @post, notice: t('posts.create.success') }
        format.turbo_stream
      end
    else
      render :new
    end
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end

  def post_params
    params.require(:post).permit(:content, :expiry_time, visible_user_ids: [])
  end
end
EOF

commit_to_git "Added private posts feature."

rails/__shared/@pwa.sh

cd "$BASE_DIR"

# Run the PWA generator
bin/rails generate pwa:install

# Stage changes and commit them
commit_to_git "Configured Rails to run as a Progressive Web App (PWA)"

rails/__shared/@rails_new.sh

cd "$BASE_DIR"

gem install bundler --user-install
gem install rails --user-install

bundle config set --local path "$HOME/.local"

rails33 new $APP --database=postgresql --javascript=esbuild --css=sass --assets=propshaft

cd $APP

git init
bundle install
yarn install

commit_to_git "Initial commit: Generate Rails app with PostgreSQL, Esbuild, SASS, and Propshaft."

rails/__shared/@reddit_features.sh

#!/usr/bin/env zsh
# __shared/@reddit_features.sh — Reddit-style voting and comments for Rails 8
# Source from app installers. Do not execute directly.
set -euo pipefail

setup_reddit_models() {
  log "Setting up Reddit-style vote+comment models"

  generate_model Vote \
    user:references votable:references{polymorphic}:index \
    value:integer

  generate_model Comment \
    user:references commentable:references{polymorphic}:index \
    parent_id:integer content:text \
    score:integer:'default[0]' \
    upvotes:integer:'default[0]' downvotes:integer:'default[0]'

  log_ok "Reddit models ready"
}

write_reddit_model_logic() {
  cat > app/models/vote.rb << 'RUBY'
class Vote < ApplicationRecord
  belongs_to :user
  belongs_to :votable, polymorphic: true

  validates :value, inclusion: { in: [1, -1] }
  validates :user_id, uniqueness: { scope: %i[votable_type votable_id] }

  after_save    :sync_votable_score
  after_destroy :sync_votable_score

  private

  def sync_votable_score
    votable.recalculate_score! if votable.respond_to?(:recalculate_score!)
  end
end
RUBY

  cat > app/models/concerns/votable.rb << 'RUBY'
module Votable
  extend ActiveSupport::Concern

  included do
    has_many :votes, as: :votable, dependent: :destroy
  end

  def upvote_by(user)
    cast_vote(user, 1)
  end

  def downvote_by(user)
    cast_vote(user, -1)
  end

  def vote_by(user)
    votes.find_by(user: user)
  end

  def recalculate_score!
    up   = votes.where(value: 1).count
    down = votes.where(value: -1).count
    update_columns(score: up - down, upvotes: up, downvotes: down)
  end

  private

  def cast_vote(user, value)
    existing = votes.find_by(user: user)
    if existing
      existing.value == value ? existing.destroy! : existing.update!(value: value)
    else
      votes.create!(user: user, value: value)
    end
    recalculate_score!
  end
end
RUBY

  cat > app/models/comment.rb << 'RUBY'
class Comment < ApplicationRecord
  include Votable

  belongs_to :user
  belongs_to :commentable, polymorphic: true
  belongs_to :parent, class_name: "Comment", optional: true
  has_many :replies, class_name: "Comment", foreign_key: :parent_id, dependent: :destroy

  validates :content, presence: true, length: { maximum: 10_000 }

  scope :roots,    -> { where(parent_id: nil) }
  scope :top,      -> { order(score: :desc) }
  scope :new_first,-> { order(created_at: :desc) }

  def depth
    parent ? parent.depth + 1 : 0
  end

  def tree
    [self] + replies.top.flat_map(&:tree)
  end
end
RUBY

  cat > app/models/concerns/commentable.rb << 'RUBY'
module Commentable
  extend ActiveSupport::Concern

  included do
    has_many :comments, as: :commentable, dependent: :destroy
  end

  def comment_count
    comments.count
  end
end
RUBY
  log_ok "Reddit model logic written"
}

write_vote_controller() {
  cat > app/controllers/votes_controller.rb << 'RUBY'
class VotesController < ApplicationController
  before_action :require_authentication

  VOTABLE_TYPES = %w[Post Comment].freeze

  def create
    votable = find_votable
    value   = params[:value].to_i
    raise ArgumentError, "invalid value" unless value.in?([-1, 1])
    votable.public_send(value == 1 ? :upvote_by : :downvote_by, Current.user)
    respond_to do |format|
      format.turbo_stream
      format.json { render json: { score: votable.score } }
    end
  end

  private

  def find_votable
    type = params[:votable_type]
    raise ArgumentError, "invalid type" unless VOTABLE_TYPES.include?(type)
    type.constantize.find(params[:votable_id])
  end
end
RUBY
  log_ok "Vote controller written"
}

rails/__shared/@redis.sh

cd "$BASE_DIR"

if ! command_exists redis-server; then
  echo "Redis is not installed. Installing..."
  doas pkg_add -U redis
  doas rcctl enable redis
  doas rcctl start redis
fi

commit_to_git "Configured Redis"

rails/__shared/@twitter_features.sh

#!/usr/bin/env zsh
# __shared/@twitter_features.sh — Twitter-style posts, follows, hashtags for Rails 8
# Source from app installers. Do not execute directly.
set -euo pipefail

setup_twitter_models() {
  log "Setting up Twitter-style models"

  generate_model Post \
    user:references content:string \
    likes_count:integer:'default[0]' \
    reposts_count:integer:'default[0]' \
    replies_count:integer:'default[0]' \
    in_reply_to_id:integer

  generate_model Follow \
    follower:references{User} following:references{User}

  generate_model Like \
    user:references likeable:references{polymorphic}:index

  generate_model Repost \
    user:references post:references quote:text

  generate_model Hashtag name:string:uniq posts_count:integer:'default[0]'

  generate_model Tagging \
    post:references hashtag:references

  generate_model Notification \
    user:references actor:references{User} \
    notifiable:references{polymorphic}:index \
    action:string read_at:datetime

  log_ok "Twitter models ready"
}

write_twitter_model_logic() {
  cat > app/models/post.rb << 'RUBY'
class Post < ApplicationRecord
  belongs_to :user
  belongs_to :reply_to, class_name: "Post", foreign_key: :in_reply_to_id, optional: true

  has_many :replies, class_name: "Post", foreign_key: :in_reply_to_id, dependent: :destroy
  has_many :likes, as: :likeable, dependent: :destroy
  has_many :reposts, dependent: :destroy
  has_many :taggings, dependent: :destroy
  has_many :hashtags, through: :taggings

  validates :content, presence: true, length: { maximum: 280 }

  after_create_commit :extract_hashtags
  after_create_commit :notify_mentions
  after_create_commit :broadcast_to_followers

  scope :chronological, -> { order(created_at: :desc) }
  scope :for_feed, ->(user) {
    where(user: user.following + [user])
      .where(in_reply_to_id: nil)
      .chronological
  }

  private

  def extract_hashtags
    tags = content.scan(/#([\w]+)/).flatten.uniq
    tags.each do |tag|
      h = Hashtag.find_or_create_by!(name: tag.downcase)
      taggings.find_or_create_by!(hashtag: h)
      h.increment!(:posts_count)
    end
  end

  def notify_mentions
    content.scan(/@(\w+)/).flatten.each do |handle|
      mentioned = User.find_by(username: handle)
      next unless mentioned && mentioned != user
      Notification.create!(
        user: mentioned, actor: user,
        notifiable: self, action: "mention"
      )
    end
  end

  def broadcast_to_followers
    user.followers.each do |follower|
      broadcast_prepend_to [follower, "feed"], partial: "posts/post", locals: { post: self }
    end
  end
end
RUBY

  cat > app/models/follow.rb << 'RUBY'
class Follow < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :following, class_name: "User"

  validates :follower_id, uniqueness: { scope: :following_id }
  validate :no_self_follow

  after_create_commit  :notify_following
  after_destroy :decrement_counts

  private

  def no_self_follow
    errors.add(:base, "cannot follow yourself") if follower_id == following_id
  end

  def notify_following
    Notification.create!(
      user: following, actor: follower,
      notifiable: self, action: "follow"
    )
  end

  def decrement_counts; end
end
RUBY

  cat > app/models/user.rb << 'RUBY'
class User < ApplicationRecord
  has_secure_password

  has_many :sessions, dependent: :destroy
  has_many :posts, dependent: :destroy
  has_many :likes, dependent: :destroy
  has_many :reposts, dependent: :destroy
  has_many :notifications, dependent: :destroy
  has_many :sent_follows,     class_name: "Follow", foreign_key: :follower_id,  dependent: :destroy
  has_many :received_follows, class_name: "Follow", foreign_key: :following_id, dependent: :destroy
  has_many :following, through: :sent_follows,    source: :following
  has_many :followers, through: :received_follows, source: :follower

  has_one_attached :avatar

  validates :email_address, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :username, presence: true, uniqueness: true, format: { with: /\A[a-z0-9_]+\z/ }, length: { maximum: 30 }

  normalizes :email_address, with: -> e { e.strip.downcase }

  def follow!(other)
    sent_follows.find_or_create_by!(following: other)
  end

  def unfollow!(other)
    sent_follows.find_by(following: other)&.destroy!
  end

  def following?(other)
    sent_follows.exists?(following: other)
  end
end
RUBY
  log_ok "Twitter model logic written"
}

write_twitter_controllers() {
  cat > app/controllers/posts_controller.rb << 'RUBY'
class PostsController < ApplicationController
  before_action :require_authentication, except: %i[index show]

  def index
    @posts = Post.includes(:user).for_feed(Current.user).limit(50)
  end

  def show
    @post    = Post.find(params[:id])
    @replies = @post.replies.includes(:user).chronological
    @reply   = Post.new(in_reply_to_id: @post.id)
  end

  def new
    @post = Post.new
  end

  def create
    @post = Current.user.posts.build(post_params)
    if @post.save
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_to root_path }
      end
    else
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    @post = Current.user.posts.find(params[:id])
    @post.destroy!
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to root_path }
    end
  end

  private

  def post_params
    params.require(:post).permit(:content, :in_reply_to_id)
  end
end
RUBY

  cat > app/controllers/follows_controller.rb << 'RUBY'
class FollowsController < ApplicationController
  before_action :require_authentication

  def create
    user = User.find(params[:user_id])
    Current.user.follow!(user)
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to user }
    end
  end

  def destroy
    user = User.find(params[:user_id])
    Current.user.unfollow!(user)
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to user }
    end
  end
end
RUBY

  cat > app/controllers/likes_controller.rb << 'RUBY'
class LikesController < ApplicationController
  before_action :require_authentication

  LIKEABLE_TYPES = %w[Post].freeze

  def create
    likeable = find_likeable
    unless Like.exists?(user: Current.user, likeable: likeable)
      Like.create!(user: Current.user, likeable: likeable)
      likeable.increment!(:likes_count)
    end
    respond_to do |format|
      format.turbo_stream
      format.json { render json: { count: likeable.likes_count } }
    end
  end

  def destroy
    likeable = find_likeable
    like = Like.find_by(user: Current.user, likeable: likeable)
    if like
      like.destroy!
      likeable.decrement!(:likes_count)
    end
    respond_to do |format|
      format.turbo_stream
      format.json { render json: { count: likeable.likes_count } }
    end
  end

  private

  def find_likeable
    type = params[:likeable_type]
    raise ArgumentError unless LIKEABLE_TYPES.include?(type)
    type.constantize.find(params[:likeable_id])
  end
end
RUBY
  log_ok "Twitter controllers written"
}

rails/__shared/@yarn.sh

#!/bin/zsh

if ! command_exists yarn; then
  echo "Yarn is not installed. Installing..."
  doas pkg_add -U node
  doas npm install yarn -g
fi

rails/__shared/layouts/_flash.html.erb

<%# Renders a flash message with a dismiss button – expects locals: flash_message, flash_type %>
<% return unless flash_message.present? %>

<div class="flash flash-<%= h flash_type %>"
     role="alert"
     aria-live="assertive"
     aria-atomic="true"
     data-controller="flash"
     data-action="click->flash#dismiss">
  <div class="flash__content" data-flash-target="content">
    <%= sanitize(
          flash_message,
          tags: %w[p b i u strong em a br],
          attributes: %w[href target]
        ) %>
  </div>
  <button type="button"
          class="flash-dismiss"
          aria-label="Dismiss alert"
          data-action="click->flash#dismiss">
    <span aria-hidden="true">&times;</span>
    <span class="visually-hidden">Dismiss this message</span>
  </button>
</div>

rails/__shared/layouts/_footer.html.erb

<%# frozen_string_literal: true %>
<%= tag.footer class: "site-footer", role: "contentinfo" do %>
  <div class="footer-content">
    <p class="footer-text">
      <%= t(
            "footer.copyright",
            year: Time.zone.now.year,
            app_name: ApplicationHelper.application_name
          ) %>
    </p>
    <nav aria-label="Footer links">
      <% (footer_links || []).each do |link| %>
        <%= link_to t(link[:translation_key]), link[:path],
                    class: "footer-link",
                    rel: "noopener" %>
      <% end %>
    </nav>
  </div>
<% end %>

rails/__shared/layouts/_meta.html.erb

<%= tag.meta charset: "utf-8" %>
<%= tag.meta name: "viewport", content: "width=device-width, initial-scale=1" %>

<%# Open Graph tags %>
<%= tag.meta property: "og:type", content: "website" %>
<%= tag.meta property: "og:image", content: (content_for?(:og_image) ? h(yield(:og_image)) : asset_path("default_og_image.png")) %>
<%= tag.meta property: "og:url", content: ERB::Util.u(request.original_url) %>
<%= tag.meta property: "og:description", content: (content_for?(:description) ? h(yield(:description)) : "My App Description") %>
<%= tag.meta property: "og:title", content: (content_for?(:title) ? h(yield(:title)) : "My App") %>

<%# Twitter Card tags – fall back to Open Graph values when not provided %>
<%= tag.meta name: "twitter:card", content: "summary_large_image" %>
<%= tag.meta name: "twitter:title", content: (content_for?(:title) ? h(yield(:title)) : "My App") %>
<%= tag.meta name: "twitter:description", content: (content_for?(:description) ? h(yield(:description)) : "My App Description") %>
<%= tag.meta name: "twitter:image", content: (content_for?(:og_image) ? h(yield(:og_image)) : asset_path("default_og_image.png")) %>

rails/__shared/layouts/_nav.html.erb

<%# frozen_string_literal: true %>
<a href="#main-content" class="skip-nav">Skip to main content</a>
<header class="site-header">
  <div class="container">
    <nav class="nav-main" aria-label="Main navigation" role="navigation">
      <div class="nav-brand">
        <%= link_to root_path, class: "logo-link" do %>
          <span class="logo"><%= @app_name.presence || "App" %></span>
        <% end %>
        <% if (tenant = ActsAsTenant.current_tenant&.name).present? %>
          <span class="tenant"><%= tenant %></span>
        <% end %>
      </div>

      <ul class="nav-links">
        <% if user_signed_in? %>
          <li>
            <span class="nav-user" aria-label="<%= current_user.email %>"><%= current_user.email %></span>
          </li>
          <li>
            <%= link_to t("navigation.sign_out"), destroy_user_session_path,
                        method: :delete,
                        class: "nav-link",
                        data: { turbo_method: :delete } %>
          </li>
        <% else %>
          <li>
            <%= link_to t("navigation.sign_in"), new_user_session_path, class: "nav-link" %>
          </li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

rails/__shared/layouts/application.html.erb

<!DOCTYPE html>
<html lang="<%= html_escape((I18n.locale.presence || I18n.default_locale || 'en').to_s) %>" dir="<%= html_escape(rtl_locale?(I18n.locale) ? 'rtl' : 'ltr') %>">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <% title = content_for?(:title) ? yield(:title) : (t('site.title', default: 'Default Site Title')) %>
    <%= render "shared/meta", title: title %>
    <%= yield :head if content_for?(:head) %>
  </head>
  <body data-controller="<%= html_escape(controller_name) %>"<% if defined?(body_class) && body_class.present? %> class="<%= html_escape(body_class) %>"<% end %>>
    <%= render "shared/skip_links" %>
    <%= render "shared/nav" %>

    <main id="main-content" class="site-main" role="main">
      <%= render "shared/flash" %>
      <%= yield %>
    </main>

    <%= render "shared/footer" %>
    <%= yield :scripts if content_for?(:scripts) %>
  </body>
</html>

rails/__shared/layouts/visualizer.js

    "use strict";

    const IN_SANDBOX=false;

    const FADE_MS=3500,START_FADE_IN=true,DPR=Math.min(2,window.devicePixelRatio||1),isLowEnd=(navigator.hardwareConcurrency&&navigator.hardwareConcurrency<=2)||(navigator.deviceMemory&&navigator.deviceMemory<=2);

    (()=>{const e=document.getElementById("uiDots");if(!e)return;const s=[0,1,2,3,2,1];let i=0;const t=()=>{e.textContent=".".repeat(s[i]);i=(i+1)%s.length};t();try{clearInterval(window.__RB_DOTS_IV)}catch{}window.__RB_DOTS_IV=setInterval(t,600)})();

    const motionScale=()=>typeof matchMedia==="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches?.35:1;

    class SimpleCarousel{constructor(e,i=2800){this.slides=Array.from(e.querySelectorAll(".carousel-slide"));this.i=0;this.n=this.slides.length;if(this.n>1)this.t=setInterval(()=>this.next(),i)}next(){this.slides[this.i].classList.remove("active");this.i=(this.i+1)%this.n;this.slides[this.i].classList.add("active")}}

    new SimpleCarousel(document.getElementById("cityCarousel"));

    const YOUTUBE_TRACKS=[

      {artist:"J Dilla",title:"Microphone Master",id:"9EGHwkDix78"},

      {artist:"J Dilla",title:"In Space",id:"vO2nWXCVt6o"},

      {artist:"J Dilla",title:"Timeless",id:"dbbfo9_7D8g"},

      {artist:"AFTA-1",title:"Due Time",id:"WC09qDzU9y4"},

      {artist:"Flying Lotus",title:"Massage Situation",id:"6oUx6wGCekM"},

      {artist:"Madlib",title:"Eye",id:"ScVz2mntmCE"},

      {artist:"Slum Village",title:"Players",id:"KsULjOCYdnY"},

      {artist:"Jay Electronica",title:"Exhibit A",id:"H3UIHZshNQ0"},

      {artist:"Slum Village",title:"La La (Instrumental)",id:"EYJxxHQ7sX0"},

      {artist:"Slum Village",title:"Get It Together",id:"t6T-Q6HMbEo"},

      {artist:"Slum Village",title:"Fantastic",id:"a3ISYWWYgz8"},

      {artist:"Flying Lotus",title:"me Yesterday//Corded",id:"8DgAhgmpXNA"},

      {artist:"Flying Lotus",title:"Camel",id:"fU9YRGLPDQ8"},

      {artist:"Flying Lotus",title:"Golden Diva",id:"iu4FVvR2QQs"},

      {artist:"Slum Village",title:"Worlds Full of Sadness",id:"MU3nfxsz2XA"},

      {artist:"A. Mochi & Takaaki Itoh",title:"Sarria's Mind",id:"gFKArkiz8vU"},

      {artist:"Samiyam",title:"Rounded",id:"oeaY2h_cKsg"},

      {artist:"Chase Swayze",title:"Traffic",id:"bH-30pDoQdo"},

      {artist:"Chase Swayze",title:"Underrated",id:"1jjFk2Vp5ok"},

      {artist:"Flying Lotus",title:"BTS Radio 2006",id:"6nWdggkulHk",start:1364}

    ];

    const loadYouTubeAPI=()=>{if(IN_SANDBOX||window.__YT_API_LOADED)return;window.__YT_API_LOADED=true;const s=document.createElement("script");s.src="https://www.youtube.com/iframe_api";s.async=true;document.head.appendChild(s)};

    // MP3 Playlist Detection and Parsing
    const detectMp3Playlist=async()=>{

      if(IN_SANDBOX)return null;

      let tracks=[];

      try{

        let r=await fetch("playlist.json");

        if(r.ok){

          const data=await r.json();

          if(Array.isArray(data)&&data.length>0)tracks=tracks.concat(data.map(t=>({...t,src:t.src})));

        }

      }catch{}

      try{

        let r=await fetch("playlist.m3u");

        if(r.ok){

          const text=await r.text();

          const m3uTracks=parseM3U(text);

          if(m3uTracks&&m3uTracks.length>0)tracks=tracks.concat(m3uTracks);

        }

      }catch{}

      try{

        let r=await fetch("index.json");

        if(r.ok){

          const data=await r.json();

          if(Array.isArray(data)){

            const mp3Files=data.filter(f=>typeof f==='string'&&f.toLowerCase().endsWith('.mp3'));

            tracks=tracks.concat(mp3Files.map(f=>{

              const name=f.replace(/\.mp3$/i,'').replace(/[-_]/g,' ');

              return{title:name,artist:'',src:f};

            }));

          }else if(data.files&&Array.isArray(data.files)){

            const mp3Files=data.files.filter(f=>typeof f==='string'&&f.toLowerCase().endsWith('.mp3'));

            tracks=tracks.concat(mp3Files.map(f=>{

              const name=f.replace(/\.mp3$/i,'').replace(/[-_]/g,' ');

              return{title:name,artist:'',src:f};

            }));

          }

        }

      }catch{}

      return tracks.length>0?tracks:null;

    };

    const parseM3U=(text)=>{
      const lines=text.split('\n').map(l=>l.trim()).filter(l=>l);

      const tracks=[];

      let current={};

      for(const line of lines){

        if(line.startsWith('#EXTINF:')){

          const info=line.substring(8);

          const parts=info.split(',');

          if(parts.length>=2){

            current.title=parts[1].trim();

            const match=parts[0].match(/(\d+)/);

            if(match)current.duration=parseInt(match[1]);

          }

        }else if(!line.startsWith('#')&&line){

          current.src=line;

          if(current.src)tracks.push({...current});

          current={};

        }

      }

      return tracks.length>0?tracks:null;

    };

    const YT_ORIGIN="https://www.youtube.com";

    const ytPost=(i,f,a=[])=>{if(IN_SANDBOX)return;try{if(!i||!i.contentWindow)return;i.contentWindow.postMessage({event:"command",func:f,args:a},YT_ORIGIN)}catch{try{i.contentWindow.postMessage({event:"command",func:f,args:a},"*")}catch{}}};

    class Mp3AudioEngine{

      constructor(tracks){

        this.started=false;this.muted=true;this.trackIndex=0;

        this.tracks=tracks.slice().sort(()=>Math.random()-.5);

        this.activeKey="a";this.inactiveKey="b";

        this.players={a:null,b:null};this._fadeIv=null;this._prefadeTimer=null;

        this.audioContext=null;this.analyser=null;this.dataArray=null;

        this.beatPhase=0;this.energyLevel=.5;this._lastBeat=0;this._beatEnv=0;

        this._initAudioElements();

      }

      _initAudioElements(){
        // Create two audio elements for crossfading

        this.players.a=new Audio();

        this.players.b=new Audio();

        this.players.a.crossOrigin="anonymous";

        this.players.b.crossOrigin="anonymous";

        this.players.a.preload="auto";

        this.players.b.preload="auto";

        this.players.a.volume=0;

        this.players.b.volume=0;

        // Setup Web Audio Context and Analyser
        try{

          this.audioContext=new(window.AudioContext||window.webkitAudioContext)();

          this.analyser=this.audioContext.createAnalyser();

          this.analyser.fftSize=512;

          this.analyser.smoothingTimeConstant=0.8;

          this.dataArray=new Uint8Array(this.analyser.frequencyBinCount);

          // Connect active player to analyser
          this._connectAnalyser();

        }catch{

          this.audioContext=null;

        }

        // Setup event listeners
        ['a','b'].forEach(k=>{

          const p=this.players[k];

          p.addEventListener('ended',()=>{

            if(k===this.activeKey)this.beginCrossfade({fast:true});

          });

          p.addEventListener('canplay',()=>{

            if(k===this.activeKey&&this.started){

              this._setupNextCrossfade(p);

            }

          });

          p.addEventListener('error',()=>{

            if(k===this.activeKey)this.beginCrossfade({fast:true});

          });

        });

      }

      _connectAnalyser(){
        if(!this.audioContext||!this.analyser)return;

        try{

          const activePlayer=this.players[this.activeKey];

          if(activePlayer&&!activePlayer._sourceNode){

            activePlayer._sourceNode=this.audioContext.createMediaElementSource(activePlayer);

            activePlayer._sourceNode.connect(this.analyser);

            this.analyser.connect(this.audioContext.destination);

          }

        }catch{}

      }

      _setupNextCrossfade(player){
        if(!player.duration)return;

        const fadeTime=Math.max(FADE_MS+1000,player.duration*1000-FADE_MS-500);

        clearTimeout(this._prefadeTimer);

        this._prefadeTimer=setTimeout(()=>this.beginCrossfade({}),fadeTime);

      }

      start(){
        this.started=true;this.updateUITrack();

        if(this.audioContext&&this.audioContext.state==='suspended'){

          this.audioContext.resume();

        }

        this._loadOn(this.activeKey,this.tracks[this.trackIndex],{fadeIn:START_FADE_IN});

      }

      _loadOn(k,t,{fadeIn}={fadeIn:true}){
        if(!k||!t||!this.players[k])return;

        const p=this.players[k];

        p.src=t.src;

        p.load();

        if(fadeIn){
          this._fadeVolumes({toKey:k,ms:FADE_MS});

        }else{

          p.volume=this.muted?0:1;

        }

        // Connect to analyser if this is the active player
        if(k===this.activeKey){

          this._connectAnalyser();

        }

        // Auto-play when ready
        p.addEventListener('canplay',()=>{

          if(!this.muted||fadeIn)p.play().catch(()=>{});

        },{once:true});

      }

      beginCrossfade({fast=false}={}){
        clearInterval(this._fadeIv);clearTimeout(this._prefadeTimer);

        const n=(this.trackIndex+1)%this.tracks.length,t=this.tracks[n];

        const f=this.activeKey,o=this.inactiveKey;

        this._loadOn(o,t,{fadeIn:false});

        setTimeout(()=>{

          this._fadeVolumes({fromKey:f,toKey:o,ms:fast?Math.min(1200,FADE_MS):FADE_MS});

          this.trackIndex=n;this.updateUITrack();

        },fast?200:500);

      }

      prev(){
        clearInterval(this._fadeIv);clearTimeout(this._prefadeTimer);

        const p=(this.trackIndex-1+this.tracks.length)%this.tracks.length,t=this.tracks[p];

        const f=this.activeKey,o=this.inactiveKey;

        this._loadOn(o,t,{fadeIn:false});

        setTimeout(()=>{

          this._fadeVolumes({fromKey:f,toKey:o,ms:FADE_MS});

          this.trackIndex=p;this.updateUITrack();

        },300);

      }

      next(){this.beginCrossfade({fast:false})}
      toggleMute(){
        this.muted=!this.muted;

        const p=this.players[this.activeKey];

        if(p){
... 551 lines truncated (951 total)

rails/amber/@shared_functions.sh

#!/usr/bin/env sh
# @shared_functions.sh — shared helpers for DEPLOY/rails/* scripts
# Source this file; do not execute directly.
#
# Conventions:
#   APP_DIR  — full path to the Rails app (caller must set, e.g. /home/brgen/app)
#   APP_PORT — TCP port Falcon listens on (default 3000)

: "${APP_PORT:=3000}"
readonly APP_PORT

# Ensure required environment variables are present
: "${APP_DIR:?APP_DIR must be set before sourcing}"

set -eu
set -o pipefail
IFS=$'\n\t'

# ── Logging ──────────────────────────────────────────────────────────────────

log()      { printf '%b\n' "$(printf '\033[36m==>\033[0m %s' "$*")"; }
log_ok()   { printf '%b\n' "$(printf '\033[32m✔\033[0m %s' "$*")"; }
log_warn() { printf '%b\n' "$(printf '\033[33mWARN\033[0m %s' "$*")" >&2; }
log_err()  { printf '%b\n' "$(printf '\033[31mERR\033[0m %s' "$*")" >&2; }

# ── Precondition checks ───────────────────────────────────────────────────────

command_exists() {
  cmd=$1
  if ! command -v "$cmd" >/dev/null 2>&1; then
    log_err "Required command not found: $cmd"
    exit 1
  fi
  log_ok "$cmd found"
}

check_app_exists() {
  sentinel=$1
  if [ -f "$sentinel" ]; then
    log_warn "Already set up ($sentinel exists). Skipping."
    return 0
  fi
  return 1
}

# ── App scaffolding ───────────────────────────────────────────────────────────

setup_full_app() {
  app_dir=$1
  mkdir -p "$(dirname "$app_dir")"

  if [ ! -f "$app_dir/config/application.rb" ]; then
    log "Creating Rails 8 app at $app_dir"
    rails new "$app_dir" \
      --database=sqlite3 \
      --skip-git \
      --asset-pipeline=propshaft \
      --javascript=importmap \
      --skip-test
  fi

  cd "$app_dir"

  # Ensure Falcon is the server adapter
  if ! grep -q '"falcon"' Gemfile; then
    printf '%s\n' 'gem "falcon"' >> Gemfile
    bundle install --quiet
  fi

  log_ok "Working in: $app_dir"
}

# ── Gem helpers ───────────────────────────────────────────────────────────────

install_gem() {
  gem=$1 version=${2:-}
  if ! grep -q "\"$gem\"" Gemfile 2>/dev/null; then
    if [ -n "$version" ]; then
      printf '%s\n' "gem \"$gem\", \"$version\"" >> Gemfile
    else
      printf '%s\n' "gem \"$gem\"" >> Gemfile
    fi
    bundle install --quiet
    log_ok "gem $gem installed"
  else
    log_ok "gem $gem already present"
  fi
}

# ── Database helpers ──────────────────────────────────────────────────────────

db_setup() {
  RAILS_ENV=production bin/rails db:create db:migrate 2>&1 |
    grep -E "Created|migrated|error" || :
  log_ok "database ready"
}

# ── relayd helpers ────────────────────────────────────────────────────────────

relayd_add_relay() {
  host=$1 port=$2
  table_name=${host%%.*}
  conf=/etc/relayd.conf

  if grep -q "table <${table_name}>" "$conf" 2>/dev/null; then
    return 0
  fi

  sudo sh -c "cat >> $conf <<'EOF'
table <${table_name}> { 127.0.0.1 }
EOF"
  log_ok "relayd table <${table_name}> → :${port} added (reload relayd to apply)"
}

# ── rc.d helpers ──────────────────────────────────────────────────────────────

install_rcd() {
  svc=$1 app_dir=$2 port=$3 user=$4
  rcd="/etc/rc.d/${svc}"

  [ -f "$rcd" ] && return 0

  sudo sh -c "cat > $rcd <<'EOF'
#!/bin/ksh
daemon_execdir='${app_dir}'
daemon='${app_dir}/bin/rails'
daemon_flags='server -b 0.0.0.0 -p ${port} -e production'
daemon_user='${user}'
. /etc/rc.d/rc.subr
rc_cmd \$1
EOF"

  sudo chmod 755 "$rcd"
  sudo rcctl enable "$svc"
  log_ok "rc.d/${svc} installed"
}

# ── Asset helpers ─────────────────────────────────────────────────────────────

generate_default_css() {
  mkdir -p app/assets/stylesheets
  cat > app/assets/stylesheets/application.css <<'CSS'
:root {
  --bg: #0a0a0a;
  --surface: #1a1a1a;
  --text: #e8eaed;
  --text-dim: #9aa0a6;
  --primary: #8ab4f8;
  --accent: #ff4500;
  --radius: 8px;
  --space: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; }
main { max-width: 1200px; margin: 0 auto; padding: calc(var(--space) * 2); }
a { color: var(--primary); text-decoration: none; }
.card { background: var(--surface); border-radius: var(--radius); padding: calc(var(--space) * 2); margin-bottom: calc(var(--space) * 2); }
@media (max-width: 768px) { main { padding: var(--space); } }
CSS
  log_ok "default CSS written"
}

generate_all_stimulus_controllers() {
  mkdir -p app/javascript/controllers
  cat > app/javascript/controllers/index.js <<'JS'
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)
JS
  log_ok "Stimulus controllers index written"
}

rails/amber/README.md

# amber

The world's largest social network for fashion. Rails 8 · PostgreSQL + pgvector · Redis · Falcon.

## Features

- **Import your wardrobe** — photograph each item; Amber removes backgrounds, upscales, relights, post-processes for professional shots of you (or AI fashion models) wearing them.
- **Mix & Match Magic** — four counter-rotating Stimulus carousels propose new outfit combinations, weighted by your evolving taste vector.
- **Closet Organization** — cleaning/storage tips drawn from architecture, interior design, and zen minimalism.
- **Wardrobe Analytics** — track usage, cost-per-wear, surface underutilized items.
- **Style Assistant** — daily outfit suggestions tuned to context and weather.
- **Shop Smarter** — surfaces newest/most-popular items from Net-a-porter et al.; supports your own affiliate links.

## Social

User profiles · activity feed · anonymous posting · public chatroom · live webcam streaming.

## Stack

CDN (Cloudflare) │ Load balancer (relayd) │ Falcon (Rails 8) │ ┌────┴──────────┐ │ │ PostgreSQL Redis

  • pgvector (Action Cable)

## Deploy

```zsh
doas zsh DEPLOY/rails/amber/amber.sh

## `rails/amber/amber.sh`
```bash
#!/usr/bin/env zsh
# amber.sh — deploys tracked Rails tree at app/ as %APP_NAME%
set -euo pipefail

APP_NAME=%APP_NAME%
APP_DIR=/home/${APP_NAME}/app
APP_PORT=61352
APP_DOMAIN=amber.brgen.no
SCRIPT_DIR=${0:a:h}
SRC_DIR=${SCRIPT_DIR}/app

. "${SCRIPT_DIR:h}/@shared_functions.sh"

need_cmd ruby34 bundle doas

[[ -d $SRC_DIR ]] || { log_err "missing source tree: $SRC_DIR"; exit 1 }

log "${APP_NAME} — deploying tracked tree → ${APP_DIR}"

id "$APP_NAME" >/dev/null 2>&1 || doas useradd -m -L daemon -s /bin/ksh "$APP_NAME"
doas mkdir -p "$APP_DIR"

doas cp -R "${SRC_DIR}/." "${APP_DIR}/"
doas chown -R "${APP_NAME}:${APP_NAME}" "$APP_DIR"

cd "$APP_DIR"

typeset bundle_home="/home/${APP_NAME}/.bundle"
if [[ ! -d ${bundle_home}/gems ]]; then
  log "Bootstrapping gems from amber"
  doas mkdir -p "$bundle_home"
  doas cp -R /home/amber/.bundle/gems "$bundle_home/"
  doas chown -R "${APP_NAME}:${APP_NAME}" "$bundle_home"
fi
print "---\nBUNDLE_PATH: \"${bundle_home}/gems\"" | doas tee "${APP_DIR}/.bundle/config" >/dev/null

doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bundle install --deployment --without development:test"
doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:create db:migrate"
[[ -f ${APP_DIR}/db/seeds.rb ]] && doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:seed" || true

install_rcd "$APP_NAME" "$APP_DIR" "$APP_PORT" "$APP_NAME"
[[ -n $APP_DOMAIN ]] && relayd_add_relay "$APP_DOMAIN" "$APP_PORT"

doas rcctl restart "$APP_NAME" || doas rcctl start "$APP_NAME"
log_ok "$APP_NAME live on :$APP_PORT"

rails/amber/app/Dockerfile

# syntax=docker/dockerfile:1
# check=error=true

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t app .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name app app

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.9
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
    ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment variables and enable jemalloc for reduced memory usage and latency.
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development" \
    LD_PRELOAD="/usr/local/lib/libjemalloc.so"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY vendor/* ./vendor/
COPY Gemfile Gemfile.lock ./

RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
    bundle exec bootsnap precompile -j 1 --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times.
# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
RUN bundle exec bootsnap precompile -j 1 app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile




# Final stage for app image
FROM base

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
USER 1000:1000

# Copy built artifacts: gems, application
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --chown=rails:rails --from=build /rails /rails

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]

rails/amber/app/Gemfile

source "https://rubygems.org"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.1.2"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Use sqlite3 as the database for Active Record
gem "sqlite3", ">= 2.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]

# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable
gem "solid_cache"
gem "solid_queue"
gem "solid_cable"

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
gem "kamal", require: false

# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
gem "thruster", require: false

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
gem "image_processing", "~> 1.2"

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

  # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues)
  gem "bundler-audit", require: false

  # Static analysis for security vulnerabilities [https://brakemanscanner.org/]
  gem "brakeman", require: false

  # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
  gem "rubocop-rails-omakase", require: false
end

group :development do
  # Use console on exceptions pages [https://github.com/rails/web-console]
  gem "web-console"
end
gem "pagy", "~> 9.3"
gem "ruby-openai"
gem "dartsass-rails"
gem "falcon"

rails/amber/app/README.md

# README

This README would normally document whatever steps are necessary to get the
application up and running.

Things you may want to cover:

* Ruby version

* System dependencies

* Configuration

* Database creation

* Database initialization

* How to run the test suite

* Services (job queues, cache servers, search engines, etc.)

* Deployment instructions

* ...

rails/amber/app/Rakefile

# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative "config/application"

Rails.application.load_tasks

rails/amber/app/app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      set_current_user || reject_unauthorized_connection
    end

    private
      def set_current_user
        if session = Session.find_by(id: cookies.signed[:session_id])
          self.current_user = session.user
        end
      end
  end
end

rails/amber/app/app/controllers/ai_controller.rb

class AiController < ApplicationController
  before_action :require_authentication

  def analyze_item
    item   = Current.user.items.find(params[:id])
    result = WardrobeAiService.new(Current.user).analyze_joy(item)
    item.update!(spark_joy: result["sparks_joy"]) if result["sparks_joy"].in?([true, false])
    respond_to do |format|
      format.turbo_stream { render turbo_stream: turbo_stream.replace("item_#{item.id}_analysis", partial: "ai/analysis", locals: { result: result, item: item }) }
      format.json { render json: result }
    end
  end

  def tag_item
    item   = Current.user.items.find(params[:id])
    result = WardrobeAiService.new(Current.user).enclothed_cognition_tag(item)
    item.update!(mood_effect: result["mood_effect"], life_phase: result["life_phase"])
    respond_to do |format|
      format.turbo_stream { render turbo_stream: turbo_stream.replace("item_#{item.id}_tags", partial: "ai/item_tags", locals: { item: item.reload, result: result }) }
      format.html { redirect_to item }
    end
  end

  def suggest_outfits
    @suggestions = WardrobeAiService.new(Current.user).suggest_outfits(
      occasion: params[:occasion], season: params[:season]
    )
  end

  def declutter_guide
    @candidates = WardrobeAiService.new(Current.user).declutter_candidates
  end

  def capsule
    @result = WardrobeAiService.new(Current.user).capsule_optimizer
  end

  def color_palette
    @result = WardrobeAiService.new(Current.user).color_palette_analysis
  end

  def search
    @query  = params[:q].to_s.strip
    if @query.present?
      result     = WardrobeAiService.new(Current.user).natural_language_search(@query)
      ids        = Array(result["item_ids"])
      @items     = Current.user.items.where(id: ids)
      @explanation = result["explanation"]
    else
      @items = Current.user.items.none
    end
  end

  def mood_board
    @description = params[:description].to_s.strip
    if @description.present?
      result   = WardrobeAiService.new(Current.user).mood_board_match(@description)
      ids      = Array(result["item_ids"])
      @items   = Current.user.items.where(id: ids)
      @outfit_name = result["outfit_name"]
      @reasoning   = result["description"]
    end
  end

  def occasion_map
    @coverage = Item::OCCASIONS.each_with_object({}) do |occ, h|
      h[occ] = Current.user.items.by_occasion(occ).to_a
    end
  end
end

rails/amber/app/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Authentication
  include Pagy::Backend
  allow_browser versions: :modern
end

rails/amber/app/app/controllers/concerns/authentication.rb

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

rails/amber/app/app/controllers/follows_controller.rb

class FollowsController < ApplicationController
  def create
    user = User.find(params[:user_id])
    Current.user.follows_as_follower.find_or_create_by!(followee: user) unless Current.user == user
    redirect_back fallback_location: user_path(user)
  end

  def destroy
    user = User.find(params[:user_id])
    Current.user.follows_as_follower.find_by(followee: user)&.destroy!
    redirect_back fallback_location: user_path(user)
  end
end

rails/amber/app/app/controllers/home_controller.rb

class HomeController < ApplicationController
  def index
    return unless authenticated?
    items = Current.user.items
    @items_count      = items.count
    @joy_count        = items.joy.count
    @never_worn_count = items.never_worn.count
    @worn_this_month  = items.where("updated_at > ?", 30.days.ago).where("times_worn > 0").count
    @utilization_rate = @items_count > 0 ? (@worn_this_month * 100.0 / @items_count).round : 0
    @worst_cpw        = items.where("price > 0 AND times_worn > 0")
                             .select { |i| i.cost_per_wear }
                             .sort_by { |i| -i.cost_per_wear }
                             .first(3)
    @aging_unworn     = items.aging_unworn.limit(4)
    @recent_items     = items.recent.limit(6)
    @planned_this_week = Current.user.planned_outfits.this_week.includes(:outfit)
    @weather          = WeatherService.today
  end
end

rails/amber/app/app/controllers/items_controller.rb

class ItemsController < ApplicationController
  before_action :require_authentication
  before_action :set_item, only: %i[show edit update destroy spark_joy declutter wear]
  before_action :authorize!, only: %i[edit update destroy spark_joy declutter wear]

  def index
    @pagy, @items = pagy(Current.user.items.recent)
  end

  def show; end

  def new
    @item = Current.user.items.build
  end

  def create
    @item = Current.user.items.build(item_params)
    @item.save ? redirect_to(@item, notice: "Item added") : render(:new, status: :unprocessable_entity)
  end

  def edit; end

  def update
    @item.update(item_params) ? redirect_to(@item, notice: "Updated") : render(:edit, status: :unprocessable_entity)
  end

  def destroy
    @item.destroy
    redirect_to items_path, notice: "Removed from wardrobe"
  end

  def spark_joy
    @item.update!(spark_joy: true)
    redirect_to items_path, notice: "This item sparks joy!"
  end

  def declutter
    @item.update!(spark_joy: false)
    redirect_to items_path, notice: "Marked for declutter"
  end

  def wear
    @item.wear!
    redirect_to @item, notice: "Worn today — +1"
  end

  private

  def set_item = @item = Item.find(params[:id])

  def authorize!
    redirect_to(items_path, alert: "Unauthorized") unless @item.user == Current.user
  end

  def item_params
    params.require(:item).permit(
      :title, :category, :color, :size, :material,
      :brand, :price, :times_worn, :purchase_date,
      :mood_effect, :life_phase, :occasion_tags, :season,
      photos: []
    )
  end
end

rails/amber/app/app/controllers/outfits_controller.rb

class OutfitsController < ApplicationController
  before_action :require_authentication
  before_action :set_outfit, only: %i[show edit update destroy like]
  before_action :authorize!, only: %i[edit update destroy]

  def index
    @pagy, @outfits = pagy(Current.user.outfits.order(created_at: :desc))
  end

  def show; end

  def new
    @outfit = Current.user.outfits.build
  end

  def create
    @outfit = Current.user.outfits.build(outfit_params)
    @outfit.save ? redirect_to(@outfit, notice: "Outfit created") : render(:new, status: :unprocessable_entity)
  end

  def edit; end

  def update
    @outfit.update(outfit_params) ? redirect_to(@outfit, notice: "Updated") : render(:edit, status: :unprocessable_entity)
  end

  def destroy
    @outfit.destroy
    redirect_to outfits_path, notice: "Outfit deleted"
  end

  def like
    @outfit.like!
    redirect_to @outfit
  end

  private

  def set_outfit = @outfit = Outfit.find(params[:id])

  def authorize!
    redirect_to(outfits_path, alert: "Unauthorized") unless @outfit.user == Current.user
  end

  def outfit_params
    params.require(:outfit).permit(:name, :description, :category, :season, :occasion)
  end
end

rails/amber/app/app/controllers/passwords_controller.rb

class PasswordsController < ApplicationController
  allow_unauthenticated_access
  before_action :set_user_by_token, only: %i[ edit update ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.find_by(email_address: params[:email_address])
      PasswordsMailer.reset(user).deliver_later
    end

    redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
  end

  def edit
  end

  def update
    if @user.update(params.permit(:password, :password_confirmation))
      @user.sessions.destroy_all
      redirect_to new_session_path, notice: "Password has been reset."
    else
      redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
    end
  end

  private
    def set_user_by_token
      @user = User.find_by_password_reset_token!(params[:token])
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
    end
end

rails/amber/app/app/controllers/planned_outfits_controller.rb

class PlannedOutfitsController < ApplicationController
  before_action :require_authentication

  def index
    @planned = Current.user.planned_outfits.upcoming.includes(:outfit)
    @outfits = Current.user.outfits.order(:name)
  end

  def create
    @plan = Current.user.planned_outfits.build(plan_params)
    @plan.save ? redirect_to(planned_outfits_path, notice: "Planned") : redirect_to(planned_outfits_path, alert: @plan.errors.full_messages.first)
  end

  def destroy
    Current.user.planned_outfits.find(params[:id]).destroy!
    redirect_to planned_outfits_path
  end

  private

  def plan_params = params.require(:planned_outfit).permit(:outfit_id, :planned_date, :notes)
end

rails/amber/app/app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :set_post, only: %i[show destroy like]

  def index
    @pagy, @posts = pagy(Post.recent.includes(:user, :outfit, :item))
  end

  def feed
    @pagy, @posts = pagy(Current.user.feed_posts.includes(:user, :outfit, :item))
  end

  def show; end

  def new
    @post = Post.new
  end

  def create
    @post = Current.user.posts.build(post_params)
    @post.save ? redirect_to(posts_path, notice: "Posted") : render(:new, status: :unprocessable_entity)
  end

  def destroy
    @post.destroy!
    redirect_to posts_path
  end

  def like
    @post.like!
    redirect_back fallback_location: posts_path
  end

  private

  def set_post = @post = Post.find(params[:id])
  def post_params = params.require(:post).permit(:body, :outfit_id, :item_id)
end

rails/amber/app/app/controllers/registrations_controller.rb

class RegistrationsController < ApplicationController
  allow_unauthenticated_access only: %i[new create]

  def new = render

  def create
    user = User.new(registration_params)
    if user.save
      start_new_session_for user
      redirect_to root_path, notice: "Welcome to Amber!"
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def registration_params
    params.require(:user).permit(:email_address, :password, :password_confirmation)
  end
end

rails/amber/app/app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  allow_unauthenticated_access only: %i[ new create ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

  def destroy
    terminate_session
    redirect_to new_session_path, status: :see_other
  end
end

rails/amber/app/app/controllers/users_controller.rb

class UsersController < ApplicationController
  def show
    @user    = User.find(params[:id])
    @items   = @user.items.recent.limit(12)
    @outfits = @user.outfits.order(created_at: :desc).limit(6)
    @posts   = @user.posts.recent.limit(10)
  end
end

rails/amber/app/app/helpers/application_helper.rb

module ApplicationHelper
  include Pagy::Frontend
end

rails/amber/app/app/javascript/application.js

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"

import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)

rails/amber/app/app/javascript/controllers/animated_number_controller.js

import AnimatedNumber from "@stimulus-components/animated-number"
export default class extends AnimatedNumber {}

rails/amber/app/app/javascript/controllers/application.js

import { Application } from "@hotwired/stimulus"
const application = Application.start()
application.debug = false
window.Stimulus = application
export { application }

rails/amber/app/app/javascript/controllers/auto_submit_controller.js

import AutoSubmit from "@stimulus-components/auto-submit"
export default class extends AutoSubmit {}

rails/amber/app/app/javascript/controllers/character_counter_controller.js

import CharacterCounter from "@stimulus-components/character-counter"
export default class extends CharacterCounter {}

rails/amber/app/app/javascript/controllers/clipboard_controller.js

import Clipboard from "@stimulus-components/clipboard"
export default class extends Clipboard {}

rails/amber/app/app/javascript/controllers/dialog_controller.js

import Dialog from "@stimulus-components/dialog"
export default class extends Dialog {}

rails/amber/app/app/javascript/controllers/dropdown_controller.js

import Dropdown from "@stimulus-components/dropdown"
export default class extends Dropdown {}

rails/amber/app/app/javascript/controllers/filter_controller.js

import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
  static targets = ["select", "grid"]
  filter() {
    const val = this.selectTarget.value
    this.gridTarget.querySelectorAll("[data-category]").forEach(c => {
      c.hidden = val && c.dataset.category !== val
    })
  }
}

rails/amber/app/app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.textContent = "Hello World!"
  }
}

rails/amber/app/app/javascript/controllers/index.js

import { application } from "./application"
// controllers are auto-imported via eagerLoadControllersFrom in application.js
// or listed here explicitly:

rails/amber/app/app/javascript/controllers/notification_controller.js

import Notification from "@stimulus-components/notification"
export default class extends Notification {}

rails/amber/app/app/javascript/controllers/sortable_controller.js

import Sortable from "@stimulus-components/sortable"
export default class extends Sortable {}

rails/amber/app/app/javascript/controllers/textarea_autogrow_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.resize()
    this.element.addEventListener("input", this.resize)
  }

  disconnect() {
    this.element.removeEventListener("input", this.resize)
  }

  resize = () => {
    this.element.style.height = "auto"
    this.element.style.height = `${this.element.scrollHeight}px`
  }
}

rails/amber/app/app/javascript/controllers/timeago_controller.js

import TimeAgo from "@stimulus-components/timeago"
export default class extends TimeAgo {}

rails/amber/app/app/jobs/application_job.rb

class ApplicationJob < ActiveJob::Base
  # Automatically retry jobs that encountered a deadlock
  # retry_on ActiveRecord::Deadlocked

  # Most jobs are safe to ignore if the underlying records are no longer available
  # discard_on ActiveJob::DeserializationError
end

rails/amber/app/app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout "mailer"
end

rails/amber/app/app/mailers/passwords_mailer.rb

class PasswordsMailer < ApplicationMailer
  def reset(user)
    @user = user
    mail subject: "Reset your password", to: user.email_address
  end
end

rails/amber/app/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
end

rails/amber/app/app/models/current.rb

class Current < ActiveSupport::CurrentAttributes
  attribute :session
  delegate :user, to: :session, allow_nil: true
end

rails/amber/app/app/models/follow.rb

class Follow < ApplicationRecord
  belongs_to :follower, class_name: "User", touch: true
  belongs_to :followee, class_name: "User", touch: true

  validates :follower_id, uniqueness: { scope: :followee_id }
  validate :no_self_follow

  private

  def no_self_follow
    errors.add(:followee, "can't follow yourself") if follower_id == followee_id
  end
end

rails/amber/app/app/models/item.rb

class Item < ApplicationRecord
  belongs_to :user
  has_many :outfit_items, dependent: :destroy
  has_many :outfits, through: :outfit_items
  has_many_attached :photos

  validates :title, :category, presence: true
  validates :times_worn, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
  validates :price, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true

  broadcasts_refreshes

  scope :joy,          -> { where(spark_joy: true) }
  scope :by_category,  ->(c) { where(category: c) }
  scope :by_mood,      ->(m) { where(mood_effect: m) }
  scope :by_occasion,  ->(o) { where("occasion_tags LIKE ?", "%#{o}%") }
  scope :current_self, -> { where(life_phase: "current") }
  scope :recent,       -> { order(created_at: :desc) }
  scope :worn_most,    -> { order(times_worn: :desc) }
  scope :never_worn,   -> { where("times_worn = 0 OR times_worn IS NULL") }
  scope :aging_unworn, -> { never_worn.where("purchase_date < ?", 6.months.ago) }

  CATEGORIES   = %w[Tops Bottoms Dresses Shoes Accessories Outerwear].freeze
  SEASONS      = %w[Spring Summer Autumn Winter All-Season].freeze
  MOOD_EFFECTS = %w[energising calming confident playful neutral].freeze
  LIFE_PHASES  = %w[current past-self aspirational].freeze
  OCCASIONS    = %w[work casual formal gym date travel].freeze

  def cost_per_wear
    return nil unless price.present? && times_worn.to_i > 0
    (price / times_worn).round(2)
  end

  def occasions
    occasion_tags.to_s.split(",").map(&:strip)
  end

  def wear!
    increment!(:times_worn)
    touch
  end
end

rails/amber/app/app/models/outfit.rb

class Outfit < ApplicationRecord
  belongs_to :user
  has_many :outfit_items, dependent: :destroy
  has_many :items, through: :outfit_items

  validates :name, presence: true

  broadcasts_refreshes

  def like!
    increment!(:likes_count)
  end
end

rails/amber/app/app/models/outfit_item.rb

class OutfitItem < ApplicationRecord
  belongs_to :outfit
  belongs_to :item

  validates :outfit, :item, presence: true
  validates :item_id, uniqueness: { scope: :outfit_id }
  default_scope { order(:position) }
end

rails/amber/app/app/models/planned_outfit.rb

class PlannedOutfit < ApplicationRecord
  belongs_to :user
  belongs_to :outfit

  validates :planned_date, presence: true
  validates :planned_date, uniqueness: { scope: :user_id }

  scope :upcoming, -> { where("planned_date >= ?", Date.today).order(:planned_date) }
  scope :this_week, -> { where(planned_date: Date.today..7.days.from_now) }

  broadcasts_refreshes
end

rails/amber/app/app/models/post.rb

class Post < ApplicationRecord
  belongs_to :user
  belongs_to :outfit, optional: true, touch: true
  belongs_to :item,   optional: true, touch: true

  validates :body, presence: true, length: { maximum: 500 }

  scope :recent, -> { order(created_at: :desc) }

  broadcasts_refreshes

  def like! = increment!(:likes_count)
end

rails/amber/app/app/models/session.rb

class Session < ApplicationRecord
  belongs_to :user
end

rails/amber/app/app/models/user.rb

class User < ApplicationRecord
  has_secure_password
  has_many :posts,           dependent: :destroy
  has_many :items,           dependent: :destroy
  has_many :outfits,         dependent: :destroy
  has_many :planned_outfits, dependent: :destroy
  has_many :follows_as_follower, class_name: "Follow", foreign_key: :follower_id, dependent: :destroy
  has_many :follows_as_followee, class_name: "Follow", foreign_key: :followee_id, dependent: :destroy
  has_many :following,       through: :follows_as_follower, source: :followee
  has_many :followers,       through: :follows_as_followee, source: :follower
  has_many :sessions,        dependent: :destroy

  normalizes :email_address, with: ->(e) { e.strip.downcase }

  broadcasts_refreshes

  def following?(other) = follows_as_follower.exists?(followee: other)
  def feed_posts        = Post.where(user: [self] + following.to_a).recent
end

rails/amber/app/app/services/wardrobe_ai_service.rb

# frozen_string_literal: true

class WardrobeAiService
  OPENROUTER_BASE = "https://openrouter.ai/api/v1"
  MODEL = "google/gemini-2.0-flash-001"

  def initialize(user)
    @user   = user
    @client = OpenAI::Client.new(
      access_token: ENV.fetch("OPENROUTER_API_KEY"),
      uri_base:     OPENROUTER_BASE
    )
  end

  def analyze_joy(item)
    prompt = <<~PROMPT
      Analyze this clothing item from a Marie Kondo perspective.
      Reply with JSON: {"sparks_joy": true/false, "reason": "brief explanation", "suggestion": "action to take"}

      Item: #{item.title}
      Category: #{item.category}
      Color: #{item.color}
      Times worn: #{item.times_worn || 0}
      Age: #{item.purchase_date ? "#{((Date.today - item.purchase_date) / 365).to_i} years" : "unknown"}
    PROMPT

    chat(prompt).tap do |r|
      r["sparks_joy"] = nil unless r.key?("sparks_joy")
      r["reason"]     ||= "Analysis unavailable"
      r["suggestion"] ||= "Trust your instincts"
    end
  end

  def suggest_outfits(occasion: nil, season: nil)
    items_summary = @user.items.joy.limit(20).map { |i| "#{i.title} (#{i.category}, #{i.color})" }.join(", ")
    prompt = <<~PROMPT
      Suggest 3 outfit combinations from these wardrobe items.
      #{occasion ? "Occasion: #{occasion}" : ""}
      #{season ? "Season: #{season}" : ""}
      Items: #{items_summary}
      Reply with JSON: {"outfits": [{"name": "outfit name", "items": ["item1", ...], "description": "why it works"}]}
    PROMPT
    chat(prompt)["outfits"] || []
  end

  def declutter_candidates
    @user.items.aging_unworn.order(price: :desc)
  end

  def capsule_optimizer
    catalog = @user.items.map { |i| "#{i.id}:#{i.title}(#{i.category},#{i.color})" }.join("; ")
    prompt = <<~P
      You are a capsule wardrobe expert. Given this wardrobe catalog, select a minimum keep-set
      that maximises outfit combinations. For each item return: keep/consider/release and reason.
      Respond with JSON: {"items":[{"id":N,"title":"...","decision":"keep|consider|release","reason":"..."}],"gap_items":["description of missing pieces"]}
      Catalog: #{catalog}
    P
    chat(prompt)
  end

  def color_palette_analysis
    items_desc = @user.items.map { |i| "#{i.title}: #{i.color}" }.join(", ")
    prompt = <<~P
      Analyse this wardrobe color list and identify the dominant palette, harmony gaps,
      and any clashing items. Map to a seasonal color system where possible.
      Respond with JSON: {"palette":"...","season_type":"...","harmonious":["item desc"],"clashing":["item desc"],"suggestions":["..."]}
      Items: #{items_desc}
    P
    chat(prompt)
  end

  def natural_language_search(query)
    catalog = @user.items.map { |i| "id=#{i.id} #{i.title} #{i.category} #{i.color} #{i.material} #{i.occasion_tags} #{i.season}" }.join("\n")
    prompt = <<~P
      From this wardrobe, find items matching: "#{query}"
      Return JSON: {"item_ids":[array of matching ids],"explanation":"..."}
      Wardrobe:
      #{catalog}
    P
    chat(prompt)
  end

  def mood_board_match(description)
    catalog = @user.items.map { |i| "id=#{i.id} #{i.title} #{i.category} #{i.color} #{i.material}" }.join("\n")
    prompt = <<~P
      Style reference: "#{description}"
      From this wardrobe, suggest the best outfit matching that aesthetic.
      Return JSON: {"item_ids":[array of ids],"outfit_name":"...","description":"why this matches"}
      Wardrobe:
      #{catalog}
    P
    chat(prompt)
  end

  def enclothed_cognition_tag(item)
    prompt = <<~P
      For this clothing item, suggest the most likely psychological/mood effect when worn.
      Choose one: energising, calming, confident, playful, neutral.
      Also suggest life_phase: current, past-self, or aspirational.
      Reply JSON: {"mood_effect":"...","life_phase":"...","reason":"..."}
      Item: #{item.title}, category: #{item.category}, color: #{item.color}, brand: #{item.brand}
    P
    chat(prompt)
  end

  private

  def chat(prompt)
    response = @client.chat(
      parameters: {
        model: MODEL,
        messages: [{ role: "user", content: prompt }],
        response_format: { type: "json_object" }
      }
    )
    JSON.parse(response.dig("choices", 0, "message", "content"))
  rescue => e
    Rails.logger.error("WardrobeAI error: #{e.message}")
    {}
  end
end

rails/amber/app/app/services/weather_service.rb

# frozen_string_literal: true

class WeatherService
  BERGEN_LAT  = 60.39
  BERGEN_LNG  = 5.32
  API_URL     = "https://api.open-meteo.com/v1/forecast"

  def self.today
    uri = URI("#{API_URL}?latitude=#{BERGEN_LAT}&longitude=#{BERGEN_LNG}" \
              "&current=temperature_2m,weathercode,windspeed_10m" \
              "&forecast_days=1")
    data = JSON.parse(Net::HTTP.get(uri))
    current = data["current"]
    {
      temp:        current["temperature_2m"].to_f,
      code:        current["weathercode"].to_i,
      wind:        current["windspeed_10m"].to_f,
      description: decode_weather(current["weathercode"].to_i)
    }
  rescue => e
    Rails.logger.warn("WeatherService: #{e.message}")
    nil
  end

  def self.decode_weather(code)
    case code
    when 0       then "Clear"
    when 1..3    then "Partly cloudy"
    when 45, 48  then "Foggy"
    when 51..67  then "Rainy"
    when 71..77  then "Snowy"
    when 80..82  then "Showers"
    when 95..99  then "Thunderstorm"
    else              "Mixed"
    end
  end
end

rails/amber/app/app/views/ai/_analysis.html.erb

<aside class="ai-card">
  <% if result["sparks_joy"].nil? %>
    <p class="dim">Analysis unavailable</p>
  <% else %>
    <strong><%= result["sparks_joy"] ? "Sparks joy" : "Does not spark joy" %></strong>
    <p><%= result["reason"] %></p>
    <p class="dim"><em><%= result["suggestion"] %></em></p>
  <% end %>
</aside>

rails/amber/app/app/views/ai/_item_tags.html.erb

<div id="item_<%= item.id %>_tags" class="ai-card">
  <% if item.mood_effect.present? %>
    <span class="tag">Mood: <%= item.mood_effect %></span>
  <% end %>
  <% if item.life_phase.present? %>
    <span class="tag tag--phase"><%= item.life_phase %></span>
  <% end %>
  <% if result["reason"].present? %>
    <p class="dim"><%= result["reason"] %></p>
  <% end %>
</div>

rails/amber/app/app/views/ai/capsule.html.erb

<% content_for :title, "Capsule Optimizer" %>
<h1>Capsule Wardrobe Optimizer</h1>
<% if @result["items"] %>
  <div class="capsule-list">
    <% @result["items"].each do |item| %>
      <div class="capsule-row capsule-row--<%= item["decision"] %>">
        <span class="capsule-decision"><%= item["decision"] %></span>
        <strong><%= item["title"] %></strong>
        <span class="dim"><%= item["reason"] %></span>
      </div>
    <% end %>
  </div>
  <% if @result["gap_items"]&.any? %>
    <h2>Gap items to consider buying</h2>
    <ul><% @result["gap_items"].each do |g| %><li><%= g %></li><% end %></ul>
  <% end %>
<% else %>
  <p class="dim">Add more items to your wardrobe first.</p>
<% end %>
<p><%= link_to "← Dashboard", root_path %></p>

rails/amber/app/app/views/ai/color_palette.html.erb

<% content_for :title, "Colour Palette" %>
<h1>Wardrobe Colour Palette</h1>
<% if @result["palette"] %>
  <div class="ai-card">
    <p><strong>Palette:</strong> <%= @result["palette"] %></p>
    <% if @result["season_type"].present? %><p><strong>Seasonal type:</strong> <%= @result["season_type"] %></p><% end %>
  </div>
  <% if @result["clashing"]&.any? %>
    <h2>Clashing items</h2>
    <ul><% @result["clashing"].each do |i| %><li><%= i %></li><% end %></ul>
  <% end %>
  <% if @result["suggestions"]&.any? %>
    <h2>Suggestions</h2>
    <ul><% @result["suggestions"].each do |s| %><li><%= s %></li><% end %></ul>
  <% end %>
<% else %>
  <p class="dim">Not enough items to analyse.</p>
<% end %>
<p><%= link_to "← Dashboard", root_path %></p>

rails/amber/app/app/views/ai/declutter_guide.html.erb

<% content_for :title, "Declutter guide" %>
<h1>Declutter guide</h1>
<% if @candidates.any? %>
  <p class="dim">Items to consider letting go:</p>
  <div class="item-grid"><%= render @candidates %></div>
<% else %>
  <p>No declutter candidates — your wardrobe is in great shape.</p>
<% end %>
<p><%= link_to "Back", items_path %></p>

rails/amber/app/app/views/ai/mood_board.html.erb

<% content_for :title, "Mood Board Match" %>
<h1>Mood board match</h1>
<%= form_with url: ai_mood_board_path, method: :get do |f| %>
  <div class="field">
    <%= f.label :description, "Describe the aesthetic or paste a style reference" %>
    <%= f.text_area :description, value: @description, rows: 3, class: "input input--wide" %>
  </div>
  <div class="actions"><%= f.submit "Match from wardrobe", class: "btn" %></div>
<% end %>
<% if @outfit_name.present? %>
  <div class="ai-card">
    <h2><%= @outfit_name %></h2>
    <p><%= @reasoning %></p>
  </div>
  <div class="item-grid"><%= render @items %></div>
<% end %>

rails/amber/app/app/views/ai/occasion_map.html.erb

<% content_for :title, "Occasion Coverage" %>
<h1>Occasion coverage map</h1>
<div class="occasion-grid">
  <% @coverage.each do |occasion, items| %>
    <div class="occasion-card occasion-card--<%= items.size < 2 ? 'sparse' : 'covered' %>">
      <h3><%= occasion.capitalize %></h3>
      <span class="occasion-count"><%= items.size %> items</span>
      <% if items.size < 2 %>
        <p class="dim occasion-warn">Gap — consider adding pieces</p>
      <% end %>
      <% items.first(3).each do |item| %>
        <div class="dim"><%= link_to item.title, item %></div>
      <% end %>
    </div>
  <% end %>
</div>
<p><%= link_to "← Dashboard", root_path %></p>

rails/amber/app/app/views/ai/search.html.erb

<% content_for :title, "Search Wardrobe" %>
<h1>Search your wardrobe</h1>
<%= form_with url: ai_search_path, method: :get do |f| %>
  <div class="field-row">
    <%= f.search_field :q, value: @query, placeholder: "e.g. something warm but not bulky for a meeting", autofocus: true, class: "input input--wide" %>
    <%= f.submit "Search", class: "btn" %>
  </div>
<% end %>
<% if @explanation.present? %>
  <p class="dim"><%= @explanation %></p>
<% end %>
<% if @items&.any? %>
  <div class="item-grid"><%= render @items %></div>
<% elsif @query.present? %>
  <p class="dim">No matches found.</p>
<% end %>

rails/amber/app/app/views/ai/suggest_outfits.html.erb

<% content_for :title, "Outfit suggestions" %>
<h1>Outfit suggestions</h1>
<% @suggestions.each_with_index do |s, i| %>
  <article class="ai-card">
    <h2><%= s["name"] || "Option #{i + 1}" %></h2>
    <p class="dim"><%= s["items"]&.join(", ") %></p>
    <p><%= s["description"] %></p>
  </article>
<% end %>
<p><%= link_to "Back to wardrobe", items_path %></p>

rails/amber/app/app/views/home/index.html.erb

<% content_for :title, "Dashboard" %>
<% if authenticated? %>
  <% if @weather %>
    <div class="weather-bar">
      <%= @weather[:description] %> · <%= @weather[:temp] %>°C
      <% if @weather[:temp] < 10 %>· Wear layers<% elsif @weather[:temp] > 20 %>· Light fabrics<% end %>
    </div>
  <% end %>

  <header class="dash-stats">
    <dl>
      <div><dt>Items</dt><dd><%= @items_count %></dd></div>
      <div><dt>Spark joy</dt><dd><%= @joy_count %></dd></div>
      <div><dt>Never worn</dt><dd><%= @never_worn_count %></dd></div>
      <div><dt>Utilisation</dt><dd class="<%= @utilization_rate < 20 ? 'stat-warn' : '' %>"><%= @utilization_rate %>%</dd></div>
    </dl>
    <nav>
      <%= link_to "Add item", new_item_path, class: "btn" %>
      <%= link_to "Search wardrobe", ai_search_path, class: "btn" %>
      <%= link_to "Capsule plan", ai_capsule_path, class: "btn" %>
    </nav>
  </header>

  <% if @planned_this_week.any? %>
    <section>
      <h2>This week</h2>
      <div class="plan-list">
        <% @planned_this_week.each do |plan| %>
          <div class="plan-row">
            <span class="plan-date"><%= plan.planned_date.strftime("%a %-d") %></span>
            <%= link_to plan.outfit.name, plan.outfit %>
            <%= button_to "✕", planned_outfit_path(plan), method: :delete, class: "btn-link" %>
          </div>
        <% end %>
      </div>
    </section>
  <% end %>

  <% if @worst_cpw.any? %>
    <section>
      <h2>Worst cost-per-wear</h2>
      <div class="cpw-list">
        <% @worst_cpw.each do |item| %>
          <div class="cpw-row">
            <%= link_to item.title, item %>
            <span class="cpw-val">£<%= item.cost_per_wear %>/wear · worn <%= item.times_worn %>×</span>
          </div>
        <% end %>
      </div>
    </section>
  <% end %>

  <% if @aging_unworn.any? %>
    <section>
      <h2>Aging unworn</h2>
      <div class="item-grid"><%= render @aging_unworn %></div>
    </section>
  <% end %>

  <% if @recent_items.any? %>
    <h2>Recent</h2>
    <div class="item-grid"><%= render @recent_items %></div>
    <p><%= link_to "All items →", items_path %></p>
  <% else %>
    <p><%= link_to "Add your first item", new_item_path %></p>
  <% end %>
<% else %>
  <p>Welcome to Amber. <%= link_to "Sign in", new_session_path %> to manage your wardrobe.</p>
<% end %>

rails/amber/app/app/views/items/_form.html.erb

<%= form_with model: item, class: "form" do |f| %>
  <%= render "shared/errors", object: item %>
  <div class="field"><%= f.label :title %><%= f.text_field :title, autofocus: true %></div>
  <div class="field">
    <%= f.label :category %>
    <%= f.select :category, Item::CATEGORIES, include_blank: "Select…" %>
  </div>
  <div class="field"><%= f.label :color %><%= f.text_field :color %></div>
  <div class="field"><%= f.label :size %><%= f.text_field :size %></div>
  <div class="field"><%= f.label :material %><%= f.text_field :material %></div>
  <div class="field"><%= f.label :brand %><%= f.text_field :brand %></div>
  <div class="field"><%= f.label :price %><%= f.number_field :price, step: "0.01", min: 0 %></div>
  <div class="field"><%= f.label :purchase_date %><%= f.date_field :purchase_date %></div>
  <div class="field">
    <%= f.label :season %>
    <%= f.select :season, Item::SEASONS, include_blank: "Select…" %>
  </div>
  <div class="field">
    <%= f.label :occasion_tags, "Occasions (comma-separated)" %>
    <%= f.text_field :occasion_tags, placeholder: "work, casual, formal" %>
  </div>
  <div class="field">
    <%= f.label :mood_effect, "Mood effect" %>
    <%= f.select :mood_effect, Item::MOOD_EFFECTS, include_blank: "Not set" %>
  </div>
  <div class="field">
    <%= f.label :life_phase, "Life phase" %>
    <%= f.select :life_phase, Item::LIFE_PHASES, include_blank: "Not set" %>
  </div>
  <div class="field"><%= f.label :photos %><%= f.file_field :photos, multiple: true, accept: "image/*" %></div>
  <div class="actions"><%= f.submit class: "btn" %> <%= link_to "Cancel", items_path %></div>
<% end %>

rails/amber/app/app/views/items/_item.html.erb

<article class="item-card" id="<%= dom_id(item) %>" data-category="<%= item.category %>">
  <% if item.photos.attached? %>
    <%= image_tag item.photos.first.variant(resize_to_fill: [300, 300]), class: "item-photo" %>
  <% end %>
  <%= link_to item.title, item, class: "item-title" %>
  <span class="dim"><%= item.category %><%= " · #{item.color}" if item.color.present? %></span>
  <span class="dim">Worn <%= item.times_worn.to_i %>×<%= " · #{number_to_currency(item.price)}" if item.price? %></span>
  <nav>
    <%= button_to "Wear", wear_item_path(item), method: :post, class: "btn-sm" %>
    <% unless item.spark_joy? %>
      <%= button_to "Joy", spark_joy_item_path(item), method: :post, class: "btn-sm" %>
    <% end %>
    <%= link_to "Edit", edit_item_path(item), class: "btn-sm" %>
  </nav>
</article>

rails/amber/app/app/views/items/edit.html.erb

<% content_for :title, "Edit" %>
<h1>Edit <%= @item.title %></h1>
<%= render "form", item: @item %>

rails/amber/app/app/views/items/index.html.erb

<% content_for :title, "Wardrobe" %>
<%= turbo_stream_from "items" %>
<section data-controller="filter">
  <header>
    <h1>Wardrobe (<%= @pagy.count %>)</h1>
    <%= link_to "Add item", new_item_path, class: "btn" %>
    <select data-action="change->filter#filter" data-filter-target="select">
      <option value="">All</option>
      <% Item::CATEGORIES.each do |cat| %>
        <option value="<%= cat %>"><%= cat %></option>
      <% end %>
    </select>
  </header>
  <div class="item-grid" id="items" data-filter-target="grid">
    <%= render @items %>
  </div>
  <%= pagy_nav(@pagy) if @pagy.pages > 1 %>
</section>

rails/amber/app/app/views/items/new.html.erb

<% content_for :title, "Add item" %>
<h1>Add item</h1>
<%= render "form", item: @item %>

rails/amber/app/app/views/items/show.html.erb

<% content_for :title, @item.title %>
<article class="item-detail">
  <% if @item.photos.attached? %>
    <div class="item-photos">
      <% @item.photos.each do |p| %>
        <%= image_tag p.variant(resize_to_limit: [600, 600]) %>
      <% end %>
    </div>
  <% end %>
  <header>
    <h1><%= @item.title %></h1>
    <% if @item.spark_joy? %><span class="badge">Sparks joy</span><% end %>
  </header>
  <dl class="meta">
    <dt>Category</dt><dd><%= @item.category %></dd>
    <% if @item.color.present? %><dt>Color</dt><dd><%= @item.color %></dd><% end %>
    <% if @item.size.present? %><dt>Size</dt><dd><%= @item.size %></dd><% end %>
    <% if @item.material.present? %><dt>Material</dt><dd><%= @item.material %></dd><% end %>
    <% if @item.brand.present? %><dt>Brand</dt><dd><%= @item.brand %></dd><% end %>
    <% if @item.price? %><dt>Price</dt><dd><%= number_to_currency(@item.price) %></dd><% end %>
    <dt>Worn</dt><dd><%= @item.times_worn.to_i %> times</dd>
    <% if @item.purchase_date? %><dt>Purchased</dt><dd><%= @item.purchase_date.strftime("%b %Y") %></dd><% end %>
  </dl>
  <% if @item.mood_effect.present? || @item.life_phase.present? %>
    <div class="tag-row">
      <% if @item.mood_effect.present? %><span class="tag">Mood: <%= @item.mood_effect %></span><% end %>
      <% if @item.life_phase.present? %><span class="tag tag--phase"><%= @item.life_phase %></span><% end %>
    </div>
  <% end %>
  <div id="item_<%= @item.id %>_analysis"></div>
  <div id="item_<%= @item.id %>_tags"></div>
  <nav>
    <%= button_to "Worn today", wear_item_path(@item), method: :post, class: "btn" %>
    <%= button_to "AI analyse", ai_analyze_item_path(@item), method: :post, class: "btn" %>
    <%= button_to "AI tag mood", ai_tag_item_path(@item), method: :post, class: "btn" %>
    <%= link_to "Edit", edit_item_path(@item), class: "btn" %>
    <%= button_to "Delete", @item, method: :delete, data: { turbo_confirm: "Remove this item?" }, class: "btn btn-danger" %>
  </nav>
</article>

rails/amber/app/app/views/layouts/application.html.erb

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <meta name="turbo-cache-control" content="no-preview">
  <title><%= content_for?(:title) ? yield(:title) + " – Amber" : "Amber" %></title>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>
  <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
  <%= javascript_importmap_tags %>
</head>
<body>
<nav>
  <%= link_to root_path, class: "brand" do %>
    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.38 3.46L16 2a4 4 0 01-8 0L3.62 3.46a2 2 0 00-1.34 2.23l.58 3.57a1 1 0 00.99.86H5v10c0 1.1.9 2 2 2h10a2 2 0 002-2V10h1.15a1 1 0 00.99-.86l.58-3.57a2 2 0 00-1.34-2.23z"/></svg>
    Amber
  <% end %>
  <% if authenticated? %>
    <%= link_to "Feed",      feed_posts_path %>
    <%= link_to "Post",      new_post_path %>
    <%= link_to "Wardrobe",  items_path %>
    <%= link_to "Outfits",   outfits_path %>
    <%= link_to "Planner",   planned_outfits_path %>
    <%= link_to "Search",    ai_search_path %>
    <%= link_to "Occasions", ai_occasions_path %>
    <%= link_to "Sign out",  session_path, data: { turbo_method: :delete } %>
  <% else %>
    <%= link_to "Sign in", new_session_path %>
    <%= link_to "Sign up", new_registration_path %>
  <% end %>
</nav>
<%= render "shared/flash" %>
<main><%= yield %></main>
</body>
</html>

rails/amber/app/app/views/layouts/mailer.html.erb

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

rails/amber/app/app/views/layouts/mailer.text.erb

<%= yield %>

rails/amber/app/app/views/outfits/_form.html.erb

<%= form_with model: outfit, class: "form" do |f| %>
  <%= render "shared/errors", object: outfit %>
  <div class="field"><%= f.label :name %><%= f.text_field :name, autofocus: true %></div>
  <div class="field"><%= f.label :description %><%= f.text_area :description, rows: 3 %></div>
  <div class="field">
    <%= f.label :category %>
    <%= f.select :category, %w[Casual Formal Work Workout Evening], include_blank: "Select…" %>
  </div>
  <div class="field">
    <%= f.label :season %>
    <%= f.select :season, Item::SEASONS, include_blank: "Select…" %>
  </div>
  <div class="field"><%= f.label :occasion %><%= f.text_field :occasion %></div>
  <div class="actions"><%= f.submit class: "btn" %> <%= link_to "Cancel", outfits_path %></div>
<% end %>

rails/amber/app/app/views/outfits/_outfit.html.erb

<article class="item-card" id="<%= dom_id(outfit) %>">
  <%= link_to outfit.name, outfit, class: "item-title" %>
  <span class="dim"><%= [outfit.season, outfit.occasion].compact.join(" · ") %></span>
  <span class="dim"><%= outfit.items.count %> items · <%= outfit.likes_count %> likes</span>
</article>

rails/amber/app/app/views/outfits/edit.html.erb

<% content_for :title, "Edit outfit" %>
<h1>Edit <%= @outfit.name %></h1>
<%= render "form", outfit: @outfit %>

rails/amber/app/app/views/outfits/index.html.erb

<% content_for :title, "Outfits" %>
<%= turbo_stream_from "outfits" %>
<header>
  <h1>Outfits</h1>
  <%= link_to "New outfit", new_outfit_path, class: "btn" %>
</header>
<div class="item-grid" id="outfits"><%= render @outfits %></div>
<%= pagy_nav(@pagy) if @pagy.pages > 1 %>

rails/amber/app/app/views/outfits/new.html.erb

<% content_for :title, "New outfit" %>
<h1>New outfit</h1>
<%= render "form", outfit: @outfit %>

rails/amber/app/app/views/outfits/show.html.erb

<% content_for :title, @outfit.name %>
<header>
  <h1><%= @outfit.name %></h1>
  <span class="dim"><%= [@outfit.season, @outfit.category, @outfit.occasion].compact.join(" · ") %></span>
</header>
<p><%= @outfit.description %></p>
<div class="item-grid"><%= render @outfit.items %></div>
<nav>
  <%= button_to "Like (#{@outfit.likes_count})", like_outfit_path(@outfit), method: :post, class: "btn" %>
  <%= link_to "Edit", edit_outfit_path(@outfit), class: "btn" %>
  <%= button_to "Delete", @outfit, method: :delete, data: { turbo_confirm: "Delete?" }, class: "btn btn-danger" %>
</nav>

rails/amber/app/app/views/passwords/edit.html.erb

<div class="auth-form">
  <h1>New password</h1>
  <%= form_with model: @user, url: password_path(params[:token]), method: :put do |f| %>
    <div class="field">
      <%= f.label :password, "New password" %>
      <%= f.password_field :password, autocomplete: "new-password" %>
    </div>
    <div class="field">
      <%= f.label :password_confirmation, "Confirm password" %>
      <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
    </div>
    <div class="actions">
      <%= f.submit "Set password", class: "btn btn--primary" %>
    </div>
  <% end %>
</div>

rails/amber/app/app/views/passwords/new.html.erb

<div class="auth-form">
  <h1>Reset password</h1>
  <%= form_with url: passwords_path do |f| %>
    <div class="field">
      <%= f.label :email_address, "Email" %>
      <%= f.email_field :email_address, autofocus: true, autocomplete: "email" %>
    </div>
    <div class="actions">
      <%= f.submit "Send reset link", class: "btn btn--primary" %>
    </div>
  <% end %>
</div>

rails/amber/app/app/views/passwords_mailer/reset.html.erb

<p>
  You can reset your password on
  <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.

  This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
</p>

rails/amber/app/app/views/passwords_mailer/reset.text.erb

You can reset your password on
<%= edit_password_url(@user.password_reset_token) %>

This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.

rails/amber/app/app/views/planned_outfits/index.html.erb

<% content_for :title, "Planner" %>
<%= turbo_stream_from "planned_outfits" %>
<h1>Outfit Planner</h1>
<%= form_with model: PlannedOutfit.new, url: planned_outfits_path do |f| %>
  <div class="field-row">
    <%= f.date_field :planned_date, min: Date.today, class: "input" %>
    <%= f.select :outfit_id, @outfits.map { |o| [o.name, o.id] }, { include_blank: "Select outfit…" } %>
    <%= f.text_field :notes, placeholder: "Notes…" %>
    <%= f.submit "Plan it", class: "btn" %>
  </div>
<% end %>
<div class="plan-list">
  <% @planned.each do |plan| %>
    <div class="plan-row">
      <span class="plan-date"><%= plan.planned_date.strftime("%A %-d %b") %></span>
      <%= link_to plan.outfit.name, plan.outfit %>
      <% if plan.notes.present? %><span class="dim"><%= plan.notes %></span><% end %>
      <%= button_to "✕", planned_outfit_path(plan), method: :delete, class: "btn-link" %>
    </div>
  <% end %>
  <% if @planned.empty? %>
    <p class="dim">No outfits planned yet.</p>
  <% end %>
</div>

rails/amber/app/app/views/posts/_post.html.erb

<article>
  <header>
    <%= link_to post.user.email_address.split("@").first, user_path(post.user) %>
    <time datetime="<%= post.created_at.iso8601 %>"><%= time_ago_in_words(post.created_at) %> ago</time>
  </header>
  <p><%= post.body %></p>
  <% if post.outfit %><p><em>Outfit: <%= link_to post.outfit.name, outfit_path(post.outfit) %></em></p><% end %>
  <% if post.item %><p><em>Item: <%= link_to post.item.title, item_path(post.item) %></em></p><% end %>
  <footer>
    <%= button_to "♥ #{post.likes_count}", like_post_path(post), method: :post %>
    <% if post.user == Current.user %>
      <%= button_to "Delete", post_path(post), method: :delete, data: { turbo_confirm: "Delete?" } %>
    <% end %>
  </footer>
</article>

rails/amber/app/app/views/posts/feed.html.erb

<h1>Your Feed</h1>
<%= link_to "New post", new_post_path, class: "btn" %>
<%= render @posts %>
<%= pagy_nav(@pagy) if @pagy.pages > 1 %>

rails/amber/app/app/views/posts/index.html.erb

<%= turbo_stream_from "posts" %>
<h1>Community</h1>
<%= render @posts %>
<%= pagy_nav(@pagy) if @pagy.pages > 1 %>

rails/amber/app/app/views/posts/new.html.erb

<h1>Share a look</h1>
<%= form_with model: @post do |f| %>
  <%= render "shared/errors", object: @post %>
  <div class="field">
    <%= f.label :body, "What are you wearing?" %>
    <%= f.text_area :body, rows: 3, maxlength: 500, placeholder: "Share your outfit…" %>
  </div>
  <div class="field">
    <%= f.label :outfit_id, "Tag an outfit (optional)" %>
    <%= f.select :outfit_id, Current.user.outfits.map { |o| [o.name, o.id] }, { include_blank: "—" } %>
  </div>
  <div class="field">
    <%= f.label :item_id, "Tag an item (optional)" %>
    <%= f.select :item_id, Current.user.items.map { |i| [i.title, i.id] }, { include_blank: "—" } %>
  </div>
  <div class="actions"><%= f.submit "Post", class: "btn btn--primary" %></div>
<% end %>

rails/amber/app/app/views/posts/show.html.erb

<%= turbo_stream_from @post %>
<%= render @post %>
<%= link_to 'Back', posts_path %>

rails/amber/app/app/views/pwa/manifest.json.erb

{
  "name": "App",
  "icons": [
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512"
    },
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "maskable"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "scope": "/",
  "description": "App.",
  "theme_color": "red",
  "background_color": "red"
}

rails/amber/app/app/views/pwa/service-worker.js

// Add a service worker for processing Web Push notifications:
//
// self.addEventListener("push", async (event) => {
//   const { title, options } = await event.data.json()
//   event.waitUntil(self.registration.showNotification(title, options))
// })
//
// self.addEventListener("notificationclick", function(event) {
//   event.notification.close()
//   event.waitUntil(
//     clients.matchAll({ type: "window" }).then((clientList) => {
//       for (let i = 0; i < clientList.length; i++) {
//         let client = clientList[i]
//         let clientPath = (new URL(client.url)).pathname
//
//         if (clientPath == event.notification.data.path && "focus" in client) {
//           return client.focus()
//         }
//       }
//
//       if (clients.openWindow) {
//         return clients.openWindow(event.notification.data.path)
//       }
//     })
//   )
// })

rails/amber/app/app/views/registrations/new.html.erb

<div class="auth-form">
  <h1>Create account</h1>
  <%= form_with model: User.new, url: registration_path do |f| %>
    <div class="field">
      <%= f.label :email_address, "Email" %>
      <%= f.email_field :email_address, autofocus: true, autocomplete: "email" %>
    </div>
    <div class="field">
      <%= f.label :password %>
      <%= f.password_field :password, autocomplete: "new-password" %>
    </div>
    <div class="field">
      <%= f.label :password_confirmation, "Confirm password" %>
      <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
    </div>
    <div class="actions">
      <%= f.submit "Create account", class: "btn btn--primary" %>
    </div>
    <p><%= link_to "Already have an account? Sign in", new_session_path %></p>
  <% end %>
</div>

rails/amber/app/app/views/sessions/new.html.erb

<div class="auth-form">
  <h1>Sign in</h1>
  <%= form_with url: session_path do |f| %>
    <%= render "shared/errors", object: f.object if f.object.respond_to?(:errors) %>
    <div class="field">
      <%= f.label :email_address, "Email" %>
      <%= f.email_field :email_address, autofocus: true, autocomplete: "email" %>
    </div>
    <div class="field">
      <%= f.label :password %>
      <%= f.password_field :password, autocomplete: "current-password" %>
    </div>
    <div class="actions">
      <%= f.submit "Sign in", class: "btn btn--primary" %>
    </div>
    <p><%= link_to "Forgot password?", new_password_path %></p>
  <% end %>
</div>

rails/amber/app/app/views/shared/_errors.html.erb

<% if object.errors.any? %>
  <div class="errors">
    <% object.errors.full_messages.each do |msg| %>
      <p class="error-msg"><%= msg %></p>
    <% end %>
  </div>
<% end %>

rails/amber/app/app/views/shared/_flash.html.erb

<% flash.each do |type, msg| %>
  <div class="flash flash--<%= type %>"><%= msg %></div>
<% end %>

rails/amber/app/app/views/shared/_pagination.html.erb

<%= pagy_nav(pagy) if pagy.pages > 1 %>

rails/amber/app/app/views/users/show.html.erb

<%= turbo_stream_from @user %>
<header class="profile-header">
  <h1><%= @user.email_address.split("@").first %></h1>
  <p><%= @user.items.count %> items · <%= @user.followers.count %> followers · <%= @user.following.count %> following</p>
  <% if authenticated? && Current.user != @user %>
    <% if Current.user.following?(@user) %>
      <%= button_to "Unfollow", unfollow_user_path(@user), method: :delete, class: "btn" %>
    <% else %>
      <%= button_to "Follow", follow_user_path(@user), method: :post, class: "btn btn--primary" %>
    <% end %>
  <% end %>
</header>
<h2>Recent items</h2>
<div class="item-grid">
  <% @items.each do |item| %>
    <%= link_to item_path(item) do %>
      <% if item.photos.attached? %>
        <%= image_tag item.photos.first, alt: item.title %>
      <% else %>
        <div class="item-placeholder"><%= item.category %></div>
      <% end %>
      <p><%= item.title %></p>
    <% end %>
  <% end %>
</div>
<h2>Posts</h2>
<%= render @posts %>

rails/amber/app/config/application.rb

require_relative "boot"

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module App
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 8.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w[assets tasks])

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Don't generate system test files.
    config.generators.system_tests = nil
  end
end

rails/amber/app/config/boot.rb

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

require "bundler/setup" # Set up gems listed in the Gemfile.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.

rails/amber/app/config/bundler-audit.yml

# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit.
# CVEs that are not relevant to the application can be enumerated on the ignore list below.

ignore:
  - CVE-THAT-DOES-NOT-APPLY

rails/amber/app/config/cable.yml

# Async adapter only works within the same process, so for manually triggering cable updates from a console,
# and seeing results in the browser, you must do so from the web console (running inside the dev process),
# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
# to make the web console appear.
development:
  adapter: async

test:
  adapter: test

production:
  adapter: solid_cable
  connects_to:
    database:
      writing: cable
  polling_interval: 0.1.seconds
  message_retention: 1.day

rails/amber/app/config/cache.yml

default: &default
  store_options:
    # Cap age of oldest cache entry to fulfill retention policies
    # max_age: <%= 60.days.to_i %>
    max_size: <%= 256.megabytes %>
    namespace: <%= Rails.env %>

development:
  <<: *default

test:
  <<: *default

production:
  database: cache
  <<: *default

rails/amber/app/config/ci.rb

# Run using bin/ci

CI.run do
  step "Setup", "bin/setup --skip-server"

  step "Style: Ruby", "bin/rubocop"

  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Importmap vulnerability audit", "bin/importmap audit"
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"


  # Optional: set a green GitHub commit status to unblock PR merge.
  # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
  # if success?
  #   step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
  # else
  #   failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
  # end
end

rails/amber/app/config/database.yml

# SQLite. Versions 3.8.0 and up are supported.
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem "sqlite3"
#
default: &default
  adapter: sqlite3
  max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: storage/development.sqlite3

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: storage/test.sqlite3

# Store production database in the storage/ directory, which by default
# is mounted as a persistent Docker volume in config/deploy.yml.
production:
  primary:
    <<: *default
    database: storage/production.sqlite3
  cache:
    <<: *default
    database: storage/production_cache.sqlite3
    migrations_paths: db/cache_migrate
  queue:
    <<: *default
    database: storage/production_queue.sqlite3
    migrations_paths: db/queue_migrate
  cable:
    <<: *default
    database: storage/production_cable.sqlite3
    migrations_paths: db/cable_migrate

rails/amber/app/config/deploy.yml

# Name of your application. Used to uniquely configure containers.
service: app

# Name of the container image (use your-user/app-name on external registries).
image: app

# Deploy to these servers.
servers:
  web:
    - 192.168.0.1
  # job:
  #   hosts:
  #     - 192.168.0.1
  #   cmd: bin/jobs

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
#
# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb!
#
# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer).
#
# proxy:
#   ssl: true
#   host: app.example.com

# Where you keep your container images.
registry:
  # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ...
  server: localhost:5555

  # Needed for authenticated registries.
  # username: your-user

  # Always use an access token rather than real password when possible.
  # password:
  #   - KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
  secret:
    - RAILS_MASTER_KEY
  clear:
    # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
    # When you start using multiple servers, you should split out job processing to a dedicated machine.
    SOLID_QUEUE_IN_PUMA: true

    # Set number of processes dedicated to Solid Queue (default: 1)
    # JOB_CONCURRENCY: 3

    # Set number of cores available to the application on each server (default: 1).
    # WEB_CONCURRENCY: 2

    # Match this to any external database server to configure Active Record correctly
    # Use app-db for a db accessory server on same machine via local kamal docker network.
    # DB_HOST: 192.168.0.2

    # Log everything from Rails
    # RAILS_LOG_LEVEL: debug

# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
  console: app exec --interactive --reuse "bin/rails console"
  shell: app exec --interactive --reuse "bash"
  logs: app logs -f
  dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"

# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
  - "app_storage:/rails/storage"

# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets

# Configure the image builder.
builder:
  arch: amd64

  # # Build image via remote server (useful for faster amd64 builds on arm64 computers)
  # remote: ssh://docker@docker-builder-server
  #
  # # Pass arguments and secrets to the Docker build process
  # args:
  #   RUBY_VERSION: ruby-3.4.9
  # secrets:
  #   - GITHUB_TOKEN
  #   - RAILS_MASTER_KEY

# Use a different ssh user than root
# ssh:
#   user: app

# Use accessory services (secrets come from .kamal/secrets).
# accessories:
#   db:
#     image: mysql:8.0
#     host: 192.168.0.2
#     # Change to 3306 to expose port to the world instead of just local network.
#     port: "127.0.0.1:3306:3306"
#     env:
#       clear:
#         MYSQL_ROOT_HOST: '%'
#       secret:
#         - MYSQL_ROOT_PASSWORD
#     files:
#       - config/mysql/production.cnf:/etc/mysql/my.cnf
#       - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
#     directories:
#       - data:/var/lib/mysql
#   redis:
#     image: valkey/valkey:8
#     host: 192.168.0.2
#     port: 6379
#     directories:
#       - data:/data

rails/amber/app/config/environment.rb

# Load the Rails application.
require_relative "application"

# Initialize the Rails application.
Rails.application.initialize!

rails/amber/app/config/environments/development.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Make code changes take effect immediately without server restart.
  config.enable_reloading = true

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports.
  config.consider_all_requests_local = true

  # Enable server timing.
  config.server_timing = true

  # Enable/disable Action Controller caching. By default Action Controller caching is disabled.
  # Run rails dev:cache to toggle Action Controller caching.
  if Rails.root.join("tmp/caching-dev.txt").exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true
    config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
  else
    config.action_controller.perform_caching = false
  end

  # Change to :null_store to avoid any caching.
  config.cache_store = :memory_store

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  # Make template changes take effect immediately.
  config.action_mailer.perform_caching = false

  # Set localhost to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Highlight code that triggered database queries in logs.
  config.active_record.verbose_query_logs = true

  # Append comments with runtime information tags to SQL queries in logs.
  config.active_record.query_log_tags_enabled = true

  # Highlight code that enqueued background job in logs.
  config.active_job.verbose_enqueue_logs = true

  # Highlight code that triggered redirect in logs.
  config.action_dispatch.verbose_redirect_logs = true

  # Suppress logger output for asset requests.
  config.assets.quiet = true

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  config.action_view.annotate_rendered_view_with_filenames = true

  # Uncomment if you wish to allow Action Cable access from any origin.
  # config.action_cable.disable_request_forgery_protection = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true

  # Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
  # config.generators.apply_rubocop_autocorrect_after_generate!
end

rails/amber/app/config/environments/production.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Code is not reloaded between requests.
  config.enable_reloading = false

  # Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
  config.eager_load = true

  # Full error reports are disabled.
  config.consider_all_requests_local = false

  # Turn on fragment caching in view templates.
  config.action_controller.perform_caching = true

  # Cache assets for far-future expiry since they are all digest stamped.
  config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }

  # Enable serving of images, stylesheets, and JavaScripts from an asset server.
  # config.asset_host = "http://assets.example.com"

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Assume all access to the app is happening through a SSL-terminating reverse proxy.
  # config.assume_ssl = true

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  # config.force_ssl = true

  # Skip http-to-https redirect for the default health check endpoint.
  # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }

  # Log to STDOUT with the current request id as a default log tag.
  config.log_tags = [ :request_id ]
  config.logger   = ActiveSupport::TaggedLogging.logger(STDOUT)

  # Change to "debug" to log everything (including potentially personally-identifiable information!).
  config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")

  # Prevent health checks from clogging up the logs.
  config.silence_healthcheck_path = "/up"

  # Don't log any deprecations.
  config.active_support.report_deprecations = false

  # Replace the default in-process memory cache store with a durable alternative.
  config.cache_store = :solid_cache_store

  # Replace the default in-process and non-durable queuing backend for Active Job.
  config.active_job.queue_adapter = :solid_queue
  config.solid_queue.connects_to = { database: { writing: :queue } }

  # Ignore bad email addresses and do not raise email delivery errors.
  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
  # config.action_mailer.raise_delivery_errors = false

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
  # config.action_mailer.smtp_settings = {
  #   user_name: Rails.application.credentials.dig(:smtp, :user_name),
  #   password: Rails.application.credentials.dig(:smtp, :password),
  #   address: "smtp.example.com",
  #   port: 587,
  #   authentication: :plain
  # }

  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
  # the I18n.default_locale when a translation cannot be found).
  config.i18n.fallbacks = true

  # Do not dump schema after migrations.
  config.active_record.dump_schema_after_migration = false

  # Only use :id for inspections in production.
  config.active_record.attributes_for_inspect = [ :id ]

  # Enable DNS rebinding protection and other `Host` header attacks.
  # config.hosts = [
  #   "example.com",     # Allow requests from example.com
  #   /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
  # ]
  #
  # Skip DNS rebinding protection for the default health check endpoint.
  # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end

rails/amber/app/config/environments/test.rb

# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # While tests run files are not watched, reloading is not necessary.
  config.enable_reloading = false

  # Eager loading loads your entire application. When running a single test locally,
  # this is usually not necessary, and can slow down your test suite. However, it's
  # recommended that you enable it in continuous integration systems to ensure eager
  # loading is working properly before deploying your code.
  config.eager_load = ENV["CI"].present?

  # Configure public file server for tests with cache-control for performance.
  config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }

  # Show full error reports.
  config.consider_all_requests_local = true
  config.cache_store = :null_store

  # Render exception templates for rescuable exceptions and raise for other exceptions.
  config.action_dispatch.show_exceptions = :rescuable

  # Disable request forgery protection in test environment.
  config.action_controller.allow_forgery_protection = false

  # Store uploaded files on the local file system in a temporary directory.
  config.active_storage.service = :test

  # Tell Action Mailer not to deliver emails to the real world.
  # The :test delivery method accumulates sent emails in the
  # ActionMailer::Base.deliveries array.
  config.action_mailer.delivery_method = :test

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Print deprecation notices to the stderr.
  config.active_support.deprecation = :stderr

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  # config.action_view.annotate_rendered_view_with_filenames = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true
end

rails/amber/app/config/falcon.rb

# frozen_string_literal: true

load :rack, :supervisor

hostname = File.basename(__dir__)
port = ENV.fetch("PORT", 61352).to_i

rack hostname do
  endpoint Async::HTTP::Endpoint.parse("http://0.0.0.0:\#{port}")
end

rails/amber/app/config/importmap.rb

# Pin npm packages by running ./bin/importmap

pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "@stimulus-components/dialog", to: "@stimulus-components--dialog.js" # @1.0.1
pin "@stimulus-components/auto-submit", to: "@stimulus-components--auto-submit.js" # @6.0.0
pin "@stimulus-components/character-counter", to: "@stimulus-components--character-counter.js" # @5.1.0
pin "@stimulus-components/dropdown", to: "@stimulus-components--dropdown.js" # @3.0.0
pin "stimulus-use" # @0.52.3
pin "@stimulus-components/clipboard", to: "@stimulus-components--clipboard.js" # @5.0.0
pin "@stimulus-components/notification", to: "@stimulus-components--notification.js" # @3.0.0
pin "@stimulus-components/timeago", to: "@stimulus-components--timeago.js" # @5.0.2
pin "date-fns" # @4.1.0
pin "@stimulus-components/animated-number", to: "@stimulus-components--animated-number.js" # @5.0.0
pin "@stimulus-components/sortable", to: "@stimulus-components--sortable.js" # @5.0.3
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_request", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_request.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_response", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_response.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/lib/utils", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--lib--utils.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/request_interceptor", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--request_interceptor.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/verbs", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--verbs.js" # @0.0.13
pin "@rails/request.js", to: "@rails--request.js.js" # @0.0.13
pin "sortablejs" # @1.15.7

rails/amber/app/config/initializers/assets.rb

# Be sure to restart your server when you modify this file.

# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = "1.0"

# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path

rails/amber/app/config/initializers/content_security_policy.rb

# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header

# Rails.application.configure do
#   config.content_security_policy do |policy|
#     policy.default_src :self, :https
#     policy.font_src    :self, :https, :data
#     policy.img_src     :self, :https, :data
#     policy.object_src  :none
#     policy.script_src  :self, :https
#     policy.style_src   :self, :https
#     # Specify URI for violation reports
#     # policy.report_uri "/csp-violation-report-endpoint"
#   end
#
#   # Generate session nonces for permitted importmap, inline scripts, and inline styles.
#   config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
#   config.content_security_policy_nonce_directives = %w(script-src style-src)
#
#   # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
#   # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
#   # config.content_security_policy_nonce_auto = true
#
#   # Report violations without enforcing the policy.
#   # config.content_security_policy_report_only = true
# end

rails/amber/app/config/initializers/filter_parameter_logging.rb

# Be sure to restart your server when you modify this file.

# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
  :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
]

rails/amber/app/config/initializers/inflections.rb

# Be sure to restart your server when you modify this file.

# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.plural /^(ox)$/i, "\\1en"
#   inflect.singular /^(ox)en/i, "\\1"
#   inflect.irregular "person", "people"
#   inflect.uncountable %w( fish sheep )
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.acronym "RESTful"
# end

rails/amber/app/config/initializers/pagy.rb

require "pagy/extras/overflow"
Pagy::DEFAULT[:items]    = 25
Pagy::DEFAULT[:overflow] = :last_page

rails/amber/app/config/initializers/requires.rb

require "net/http"
require "uri"
require "json"

rails/amber/app/config/locales/en.yml

# Files in the config/locales directory are used for internationalization and
# are automatically loaded by Rails. If you want to use locales other than
# English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
#     I18n.t "hello"
#
# In views, this is aliased to just `t`:
#
#     <%= t("hello") %>
#
# To use a different locale, set it with `I18n.locale`:
#
#     I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# To learn more about the API, please read the Rails Internationalization guide
# at https://guides.rubyonrails.org/i18n.html.
#
# Be aware that YAML interprets the following case-insensitive strings as
# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
# must be quoted to be interpreted as strings. For example:
#
#     en:
#       "yes": yup
#       enabled: "ON"

en:
  hello: "Hello world"

rails/amber/app/config/puma.rb

# This configuration file will be evaluated by Puma. The top-level methods that
# are invoked here are part of Puma's configuration DSL. For more information
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
#
# Puma starts a configurable number of processes (workers) and each process
# serves each request in a thread from an internal thread pool.
#
# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You
# should only set this value when you want to run 2 or more workers. The
# default is already 1. You can set it to `auto` to automatically start a worker
# for each available processor.
#
# The ideal number of threads per worker depends both on how much time the
# application spends waiting for IO operations and on how much you wish to
# prioritize throughput over latency.
#
# As a rule of thumb, increasing the number of threads will increase how much
# traffic a given process can handle (throughput), but due to CRuby's
# Global VM Lock (GVL) it has diminishing returns and will degrade the
# response time (latency) of the application.
#
# The default is set to 3 threads as it's deemed a decent compromise between
# throughput and latency for the average Rails application.
#
# Any libraries that use a connection pool or another resource pool should
# be configured to provide at least as many connections as the number of
# threads. This includes Active Record's `pool` parameter in `database.yml`.
threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
port ENV.fetch("PORT", 3000)

# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart

# Run the Solid Queue supervisor inside of Puma for single-server deployments.
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]

# Specify the PID file. Defaults to tmp/pids/server.pid in development.
# In other environments, only set the PID file if requested.
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]

rails/amber/app/config/queue.yml

default: &default
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: "*"
      threads: 3
      processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
      polling_interval: 0.1

development:
  <<: *default

test:
  <<: *default

production:
  <<: *default

rails/amber/app/config/recurring.yml

# examples:
#   periodic_cleanup:
#     class: CleanSoftDeletedRecordsJob
#     queue: background
#     args: [ 1000, { batch_size: 500 } ]
#     schedule: every hour
#   periodic_cleanup_with_command:
#     command: "SoftDeletedRecord.due.delete_all"
#     priority: 2
#     schedule: at 5am every day

production:
  clear_solid_queue_finished_jobs:
    command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
    schedule: every hour at minute 12

rails/amber/app/config/routes.rb

Rails.application.routes.draw do
  resource :registration, only: %i[new create]

  resource  :session
  resources :passwords, param: :token

  resources :items do
    member do
      post :spark_joy
      post :declutter
      post :wear
    end
  end

  resources :outfits do
    member { post :like }
  end

  resources :planned_outfits, only: %i[index create destroy]

  resources :posts, only: %i[index show new create destroy] do
    member { post :like }
    collection { get :feed }
  end

  resources :users, only: :show do
    member { post :follow; delete :unfollow }
  end

  scope :ai do
    post "items/:id/analyze", to: "ai#analyze_item",    as: :ai_analyze_item
    post "items/:id/tag",     to: "ai#tag_item",        as: :ai_tag_item
    get  "outfits/suggest",   to: "ai#suggest_outfits", as: :ai_suggest_outfits
    get  "declutter",         to: "ai#declutter_guide", as: :ai_declutter
    get  "capsule",           to: "ai#capsule",         as: :ai_capsule
    get  "palette",           to: "ai#color_palette",   as: :ai_palette
    get  "search",            to: "ai#search",          as: :ai_search
    get  "moodboard",         to: "ai#mood_board",      as: :ai_mood_board
    get  "occasions",         to: "ai#occasion_map",    as: :ai_occasions
  end

  root "home#index"
  get "up", to: "rails/health#show", as: :rails_health_check
end

rails/amber/app/config/storage.yml

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket-<%= Rails.env %>

# Remember not to checkin your GCS keyfile to a repository
# google:
#   service: GCS
#   project: your_project
#   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
#   bucket: your_own_bucket-<%= Rails.env %>

# mirror:
#   service: Mirror
#   primary: local
#   mirrors: [ amazon, google, microsoft ]

rails/amber/app/db/cable_schema.rb

ActiveRecord::Schema[7.1].define(version: 1) do
  create_table "solid_cable_messages", force: :cascade do |t|
    t.binary "channel", limit: 1024, null: false
    t.binary "payload", limit: 536870912, null: false
    t.datetime "created_at", null: false
    t.integer "channel_hash", limit: 8, null: false
    t.index ["channel"], name: "index_solid_cable_messages_on_channel"
    t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
    t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
  end
end

rails/amber/app/db/cache_schema.rb

ActiveRecord::Schema[7.2].define(version: 1) do
  create_table "solid_cache_entries", force: :cascade do |t|
    t.binary "key", limit: 1024, null: false
    t.binary "value", limit: 536870912, null: false
    t.datetime "created_at", null: false
    t.integer "key_hash", limit: 8, null: false
    t.integer "byte_size", limit: 4, null: false
    t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size"
    t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
    t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true
  end
end

rails/amber/app/db/migrate/20260504180350_create_users.rb

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users do |t|
      t.string :email_address, null: false
      t.string :password_digest, null: false

      t.timestamps
    end
    add_index :users, :email_address, unique: true
  end
end

rails/amber/app/db/migrate/20260504180352_create_sessions.rb

class CreateSessions < ActiveRecord::Migration[8.1]
  def change
    create_table :sessions do |t|
      t.references :user, null: false, foreign_key: true
      t.string :ip_address
      t.string :user_agent

      t.timestamps
    end
  end
end

rails/amber/app/db/migrate/20260504180357_create_active_storage_tables.active_storage.rb

# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
  def change
    # Use Active Record's configured type for primary and foreign keys
    primary_key_type, foreign_key_type = primary_and_foreign_key_types

    create_table :active_storage_blobs, id: primary_key_type do |t|
      t.string   :key,          null: false
      t.string   :filename,     null: false
      t.string   :content_type
      t.text     :metadata
      t.string   :service_name, null: false
      t.bigint   :byte_size,    null: false
      t.string   :checksum

      if connection.supports_datetime_with_precision?
        t.datetime :created_at, precision: 6, null: false
      else
        t.datetime :created_at, null: false
      end

      t.index [ :key ], unique: true
    end

    create_table :active_storage_attachments, id: primary_key_type do |t|
      t.string     :name,     null: false
      t.references :record,   null: false, polymorphic: true, index: false, type: foreign_key_type
      t.references :blob,     null: false, type: foreign_key_type

      if connection.supports_datetime_with_precision?
        t.datetime :created_at, precision: 6, null: false
      else
        t.datetime :created_at, null: false
      end

      t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end

    create_table :active_storage_variant_records, id: primary_key_type do |t|
      t.belongs_to :blob, null: false, index: false, type: foreign_key_type
      t.string :variation_digest, null: false

      t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end
  end

  private
    def primary_and_foreign_key_types
      config = Rails.configuration.generators
      setting = config.options[config.orm][:primary_key_type]
      primary_key_type = setting || :primary_key
      foreign_key_type = setting || :bigint
      [ primary_key_type, foreign_key_type ]
    end
end

rails/amber/app/db/migrate/20260504180401_create_items.rb

class CreateItems < ActiveRecord::Migration[8.1]
  def change
    create_table :items do |t|
      t.string :title
      t.string :category
      t.string :color
      t.string :size
      t.string :material
      t.string :brand
      t.decimal :price
      t.integer :times_worn
      t.date :purchase_date
      t.boolean :spark_joy
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

rails/amber/app/db/migrate/20260504180405_create_outfit_items.rb

class CreateOutfitItems < ActiveRecord::Migration[8.1]
  def change
    create_table :outfit_items do |t|
      t.references :outfit, null: false, foreign_key: true
      t.references :item, null: false, foreign_key: true
      t.integer :position

      t.timestamps
    end
  end
end

rails/amber/app/db/migrate/20260504180406_create_planned_outfits.rb

class CreatePlannedOutfits < ActiveRecord::Migration[8.1]
  def change
    create_table :planned_outfits do |t|
      t.references :user, null: false, foreign_key: true
      t.references :outfit, null: false, foreign_key: true
      t.date :planned_date
      t.text :notes

      t.timestamps
    end
  end
end

rails/amber/app/db/migrate/20260504180410_add_extended_fields_to_items.rb

class AddExtendedFieldsToItems < ActiveRecord::Migration[8.1]
  def change
    add_column :items, :mood_effect, :string
    add_column :items, :life_phase, :string
    add_column :items, :occasion_tags, :string
    add_column :items, :season, :string
  end
end

rails/amber/app/db/migrate/20260504205505_create_outfits.rb

class CreateOutfits < ActiveRecord::Migration[8.1]
  def change
    create_table :outfits do |t|
      t.string :name
      t.text :description
      t.string :category
      t.string :season
      t.string :occasion
      t.integer :likes_count
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

rails/amber/app/db/migrate/20260504211952_create_follows.rb

class CreateFollows < ActiveRecord::Migration[8.1]
  def change
    create_table :follows do |t|
      t.references :follower, null: false, foreign_key: true
      t.references :followee, null: false, foreign_key: true

      t.timestamps
    end
  end
end

rails/amber/app/db/migrate/20260504212306_create_posts.rb

class CreatePosts < ActiveRecord::Migration[8.1]
  def change
    create_table :posts do |t|
      t.text :body
      t.references :user, null: false, foreign_key: true
      t.references :outfit, null: false, foreign_key: true
      t.references :item, null: false, foreign_key: true
      t.integer :likes_count

      t.timestamps
    end
  end
end

rails/amber/app/db/queue_schema.rb

ActiveRecord::Schema[7.1].define(version: 1) do
  create_table "solid_queue_blocked_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.string "queue_name", null: false
    t.integer "priority", default: 0, null: false
    t.string "concurrency_key", null: false
    t.datetime "expires_at", null: false
    t.datetime "created_at", null: false
    t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release"
    t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance"
    t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
  end

  create_table "solid_queue_claimed_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.bigint "process_id"
    t.datetime "created_at", null: false
    t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
    t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
  end

  create_table "solid_queue_failed_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.text "error"
    t.datetime "created_at", null: false
    t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true
  end

  create_table "solid_queue_jobs", force: :cascade do |t|
    t.string "queue_name", null: false
    t.string "class_name", null: false
    t.text "arguments"
    t.integer "priority", default: 0, null: false
    t.string "active_job_id"
    t.datetime "scheduled_at"
    t.datetime "finished_at"
    t.string "concurrency_key"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id"
    t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name"
    t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at"
    t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering"
    t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting"
  end

  create_table "solid_queue_pauses", force: :cascade do |t|
    t.string "queue_name", null: false
    t.datetime "created_at", null: false
    t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true
  end

  create_table "solid_queue_processes", force: :cascade do |t|
    t.string "kind", null: false
    t.datetime "last_heartbeat_at", null: false
    t.bigint "supervisor_id"
    t.integer "pid", null: false
    t.string "hostname"
    t.text "metadata"
    t.datetime "created_at", null: false
    t.string "name", null: false
    t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at"
    t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
    t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id"
  end

  create_table "solid_queue_ready_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.string "queue_name", null: false
    t.integer "priority", default: 0, null: false
    t.datetime "created_at", null: false
    t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true
    t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all"
    t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue"
  end

  create_table "solid_queue_recurring_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.string "task_key", null: false
    t.datetime "run_at", null: false
    t.datetime "created_at", null: false
    t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
    t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
  end

  create_table "solid_queue_recurring_tasks", force: :cascade do |t|
    t.string "key", null: false
    t.string "schedule", null: false
    t.string "command", limit: 2048
    t.string "class_name"
    t.text "arguments"
    t.string "queue_name"
    t.integer "priority", default: 0
    t.boolean "static", default: true, null: false
    t.text "description"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true
    t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static"
  end

  create_table "solid_queue_scheduled_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.string "queue_name", null: false
    t.integer "priority", default: 0, null: false
    t.datetime "scheduled_at", null: false
    t.datetime "created_at", null: false
    t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
    t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all"
  end

  create_table "solid_queue_semaphores", force: :cascade do |t|
    t.string "key", null: false
    t.integer "value", default: 1, null: false
    t.datetime "expires_at", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at"
    t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value"
    t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true
  end

  add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
  add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
  add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
  add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
  add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
  add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
end

rails/amber/app/db/schema.rb

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 2026_05_04_180410) do
  create_table "active_storage_attachments", force: :cascade do |t|
    t.bigint "blob_id", null: false
    t.datetime "created_at", null: false
    t.string "name", null: false
    t.bigint "record_id", null: false
    t.string "record_type", null: false
    t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
    t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
  end

  create_table "active_storage_blobs", force: :cascade do |t|
    t.bigint "byte_size", null: false
    t.string "checksum"
    t.string "content_type"
    t.datetime "created_at", null: false
    t.string "filename", null: false
    t.string "key", null: false
    t.text "metadata"
    t.string "service_name", null: false
    t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
  end

  create_table "active_storage_variant_records", force: :cascade do |t|
    t.bigint "blob_id", null: false
    t.string "variation_digest", null: false
    t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
  end

  create_table "items", force: :cascade do |t|
    t.string "brand"
    t.string "category"
    t.string "color"
    t.datetime "created_at", null: false
    t.string "life_phase"
    t.string "material"
    t.string "mood_effect"
    t.string "occasion_tags"
    t.decimal "price"
    t.date "purchase_date"
    t.string "season"
    t.string "size"
    t.boolean "spark_joy"
    t.integer "times_worn"
    t.string "title"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["user_id"], name: "index_items_on_user_id"
  end

  create_table "outfit_items", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.integer "item_id", null: false
    t.integer "outfit_id", null: false
    t.integer "position"
    t.datetime "updated_at", null: false
    t.index ["item_id"], name: "index_outfit_items_on_item_id"
    t.index ["outfit_id"], name: "index_outfit_items_on_outfit_id"
  end

  create_table "planned_outfits", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.text "notes"
    t.integer "outfit_id", null: false
    t.date "planned_date"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["outfit_id"], name: "index_planned_outfits_on_outfit_id"
    t.index ["user_id"], name: "index_planned_outfits_on_user_id"
  end

  create_table "sessions", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.string "ip_address"
    t.datetime "updated_at", null: false
    t.string "user_agent"
    t.integer "user_id", null: false
    t.index ["user_id"], name: "index_sessions_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.string "email_address", null: false
    t.string "password_digest", null: false
    t.datetime "updated_at", null: false
    t.index ["email_address"], name: "index_users_on_email_address", unique: true
  end

  add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
  add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
  add_foreign_key "items", "users"
  add_foreign_key "outfit_items", "items"
  add_foreign_key "outfit_items", "outfits"
  add_foreign_key "planned_outfits", "outfits"
  add_foreign_key "planned_outfits", "users"
  add_foreign_key "sessions", "users"
end

rails/amber/app/db/seeds.rb

# This file should ensure the existence of records required to run the application in every environment (production,
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
#
# Example:
#
#   ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
#     MovieGenre.find_or_create_by!(name: genre_name)
#   end

rails/amber/app/public/robots.txt

# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file

rails/baibl/README.md

# baibl

Bible study and annotation app. Rails 8. PostgreSQL.

## Deploy

```zsh
cd ~/pub4/MASTER/DEPLOY/rails/baibl
doas zsh baibl.sh

## `rails/baibl/app/Dockerfile`
```text
# syntax=docker/dockerfile:1
# check=error=true

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t app .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name app app

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.9
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
    ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment variables and enable jemalloc for reduced memory usage and latency.
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development" \
    LD_PRELOAD="/usr/local/lib/libjemalloc.so"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY vendor/* ./vendor/
COPY Gemfile Gemfile.lock ./

RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
    bundle exec bootsnap precompile -j 1 --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times.
# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
RUN bundle exec bootsnap precompile -j 1 app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile




# Final stage for app image
FROM base

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
USER 1000:1000

# Copy built artifacts: gems, application
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --chown=rails:rails --from=build /rails /rails

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]

rails/baibl/app/Gemfile

source "https://rubygems.org"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.1.2"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Use sqlite3 as the database for Active Record
gem "sqlite3", ">= 2.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]

# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable
gem "solid_cache"
gem "solid_queue"
gem "solid_cable"

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
gem "kamal", require: false

# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
gem "thruster", require: false

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
gem "image_processing", "~> 1.2"

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

  # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues)
  gem "bundler-audit", require: false

  # Static analysis for security vulnerabilities [https://brakemanscanner.org/]
  gem "brakeman", require: false

  # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
  gem "rubocop-rails-omakase", require: false
end

group :development do
  # Use console on exceptions pages [https://github.com/rails/web-console]
  gem "web-console"
end
gem "pagy"
gem "prism", "1.9.0"
gem "falcon"

rails/baibl/app/README.md

# README

This README would normally document whatever steps are necessary to get the
application up and running.

Things you may want to cover:

* Ruby version

* System dependencies

* Configuration

* Database creation

* Database initialization

* How to run the test suite

* Services (job queues, cache servers, search engines, etc.)

* Deployment instructions

* ...

rails/baibl/app/Rakefile

# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative "config/application"

Rails.application.load_tasks

rails/baibl/app/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Authentication
  include Pagy::Method
  allow_browser versions: :modern
end

rails/baibl/app/app/controllers/bookmarks_controller.rb

class BookmarksController < ApplicationController
  before_action :require_authentication

  def index
    @pagy, @bookmarks = pagy(Current.user.bookmarks.includes(verse: [:book, :chapter]))
  end

  def create
    verse = Verse.find(params[:verse_id])
    @bookmark = Current.user.bookmarks.find_or_create_by!(verse: verse)
    respond_to do |format|
      format.turbo_stream
      format.json { render json: { status: "ok" } }
    end
  end

  def destroy
    @bookmark = Current.user.bookmarks.find(params[:id])
    @bookmark.destroy!
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to bookmarks_path }
    end
  end
end

rails/baibl/app/app/controllers/concerns/authentication.rb

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

rails/baibl/app/app/controllers/highlights_controller.rb

class HighlightsController < ApplicationController
  before_action :require_authentication

  def create
    verse = Verse.find(params[:verse_id])
    @highlight = Current.user.highlights.find_or_initialize_by(verse: verse)
    @highlight.update!(color: params[:color] || "yellow")
    respond_to do |format|
      format.turbo_stream
      format.json { render json: { status: "ok" } }
    end
  end

  def destroy
    @highlight = Current.user.highlights.find(params[:id])
    @highlight.destroy!
    respond_to do |format|
      format.turbo_stream
      format.json { render json: { status: "ok" } }
    end
  end
end

rails/baibl/app/app/controllers/passwords_controller.rb

class PasswordsController < ApplicationController
  allow_unauthenticated_access
  before_action :set_user_by_token, only: %i[ edit update ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.find_by(email_address: params[:email_address])
      PasswordsMailer.reset(user).deliver_later
    end

    redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
  end

  def edit
  end

  def update
    if @user.update(params.permit(:password, :password_confirmation))
      @user.sessions.destroy_all
      redirect_to new_session_path, notice: "Password has been reset."
    else
      redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
    end
  end

  private
    def set_user_by_token
      @user = User.find_by_password_reset_token!(params[:token])
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
    end
end

rails/baibl/app/app/controllers/scriptures_controller.rb

class ScripturesController < ApplicationController
  allow_unauthenticated_access only: %i[index book chapter search]

  def index
    @books = Book.ordered
    @daily_verse = Verse.order("RANDOM()").limit(1).first
  end

  def book
    @book     = Book.find_by!(abbreviation: params[:abbreviation])
    @chapters = @book.chapters.order(:number)
  end

  def chapter
    @book    = Book.find_by!(abbreviation: params[:book_abbreviation])
    @chapter = @book.chapters.find_by!(number: params[:number])
    @verses  = @chapter.verses.order(:number).includes(:highlights, :bookmarks)
  end

  def search
    @pagy, @verses = pagy(Verse.full_text_search(params[:q]).includes(:book, :chapter), items: 20)
    render :search
  end
end

rails/baibl/app/app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  allow_unauthenticated_access only: %i[ new create ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

  def destroy
    terminate_session
    redirect_to new_session_path, status: :see_other
  end
end

rails/baibl/app/app/helpers/application_helper.rb

module ApplicationHelper
end

rails/baibl/app/app/javascript/application.js

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"

rails/baibl/app/app/javascript/controllers/animated_number_controller.js

import AnimatedNumber from "@stimulus-components/animated-number"
export default class extends AnimatedNumber {}

rails/baibl/app/app/javascript/controllers/application.js

import { Application } from "@hotwired/stimulus"

const application = Application.start()

// Configure Stimulus development experience
application.debug = false
window.Stimulus   = application

export { application }

rails/baibl/app/app/javascript/controllers/auto_submit_controller.js

import AutoSubmit from "@stimulus-components/auto-submit"
export default class extends AutoSubmit {}

rails/baibl/app/app/javascript/controllers/character_counter_controller.js

import CharacterCounter from "@stimulus-components/character-counter"
export default class extends CharacterCounter {}

rails/baibl/app/app/javascript/controllers/clipboard_controller.js

import Clipboard from "@stimulus-components/clipboard"
export default class extends Clipboard {}

rails/baibl/app/app/javascript/controllers/dialog_controller.js

import Dialog from "@stimulus-components/dialog"
export default class extends Dialog {}

rails/baibl/app/app/javascript/controllers/dropdown_controller.js

import Dropdown from "@stimulus-components/dropdown"
export default class extends Dropdown {}

rails/baibl/app/app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.textContent = "Hello World!"
  }
}

rails/baibl/app/app/javascript/controllers/index.js

// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)

rails/baibl/app/app/javascript/controllers/notification_controller.js

import Notification from "@stimulus-components/notification"
export default class extends Notification {}

rails/baibl/app/app/javascript/controllers/sortable_controller.js

import Sortable from "@stimulus-components/sortable"
export default class extends Sortable {}

rails/baibl/app/app/javascript/controllers/textarea_autogrow_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.resize()
    this.element.addEventListener("input", this.resize)
  }

  disconnect() {
    this.element.removeEventListener("input", this.resize)
  }

  resize = () => {
    this.element.style.height = "auto"
    this.element.style.height = `${this.element.scrollHeight}px`
  }
}

rails/baibl/app/app/javascript/controllers/timeago_controller.js

import TimeAgo from "@stimulus-components/timeago"
export default class extends TimeAgo {}

rails/baibl/app/app/jobs/application_job.rb

class ApplicationJob < ActiveJob::Base
  # Automatically retry jobs that encountered a deadlock
  # retry_on ActiveRecord::Deadlocked

  # Most jobs are safe to ignore if the underlying records are no longer available
  # discard_on ActiveJob::DeserializationError
end

rails/baibl/app/app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout "mailer"
end

rails/baibl/app/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
end

rails/baibl/app/app/models/book.rb

class Book < ApplicationRecord
  has_many :chapters, dependent: :destroy
  has_many :verses, dependent: :destroy

  TESTAMENTS = %w[Old New].freeze

  validates :name, :abbreviation, :testament, presence: true
  validates :testament, inclusion: { in: TESTAMENTS }
  validates :abbreviation, uniqueness: true

  scope :old_testament, -> { where(testament: "Old").order(:order_index) }
  scope :new_testament, -> { where(testament: "New").order(:order_index) }
  scope :ordered,       -> { order(:order_index) }
end

rails/baibl/app/app/models/bookmark.rb

class Bookmark < ApplicationRecord
  belongs_to :verse
  belongs_to :user

  validates :verse_id, uniqueness: { scope: :user_id }

  after_create_commit -> { broadcast_append_to [user, "bookmarks"] }
end

rails/baibl/app/app/models/chapter.rb

class Chapter < ApplicationRecord
  belongs_to :book
  has_many :verses, dependent: :destroy

  validates :number, presence: true
  validates :number, uniqueness: { scope: :book_id }

  scope :ordered, -> { order(:number) }

  def reference = "#{book.name} #{number}"
end

rails/baibl/app/app/models/current.rb

class Current < ActiveSupport::CurrentAttributes
  attribute :session
  delegate :user, to: :session, allow_nil: true
end

rails/baibl/app/app/models/highlight.rb

class Highlight < ApplicationRecord
  belongs_to :verse
  belongs_to :user

  COLORS = %w[yellow green blue pink orange].freeze

  validates :color, inclusion: { in: COLORS }
  validates :verse_id, uniqueness: { scope: :user_id }

  after_create_commit -> { broadcast_replace_to [user, "highlights"] }
end

rails/baibl/app/app/models/reading_plan.rb

class ReadingPlan < ApplicationRecord
  belongs_to :user, optional: true
  has_many :reading_plan_days, dependent: :destroy

  validates :name, presence: true
  validates :duration_days, numericality: { greater_than: 0 }, allow_nil: true

  def progress
    return 0.0 if reading_plan_days.empty?
    reading_plan_days.where.not(completed_at: nil).count.to_f / reading_plan_days.count
  end
end

rails/baibl/app/app/models/reading_plan_day.rb

class ReadingPlanDay < ApplicationRecord
  belongs_to :reading_plan
  belongs_to :book

  validates :day_number, presence: true
  validates :day_number, uniqueness: { scope: :reading_plan_id }

  scope :ordered, -> { order(:day_number) }

  def completed? = completed_at.present?
end

rails/baibl/app/app/models/session.rb

class Session < ApplicationRecord
  belongs_to :user
end

rails/baibl/app/app/models/user.rb

class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy
  has_many :highlights, dependent: :destroy
  has_many :bookmarks, dependent: :destroy
  has_many :reading_plans, dependent: :destroy

  normalizes :email_address, with: ->(e) { e.strip.downcase }
end

rails/baibl/app/app/models/verse.rb

class Verse < ApplicationRecord
  include PgSearch::Model

  belongs_to :chapter
  belongs_to :book

  has_many :highlights, dependent: :destroy
  has_many :bookmarks, dependent: :destroy

  validates :number, :content, presence: true
  validates :number, uniqueness: { scope: :chapter_id }

  pg_search_scope :full_text_search,
    against: :content,
    using: { tsearch: { prefix: true, dictionary: "english" } }

  scope :in_chapter, ->(chapter) { where(chapter: chapter).order(:number) }

  def reference
    "#{book.name} #{chapter.number}:#{number}"
  end
end

rails/baibl/app/app/views/bookmarks/index.html.erb

<% content_for :title, "Bookmarks" %>
<h1>Bookmarks</h1>
<% if @bookmarks.any? %>
  <% @bookmarks.each do |bookmark| %>
    <article id="<%= dom_id(bookmark) %>">
      <p><%= link_to "#{bookmark.verse.book.abbreviation} #{bookmark.verse.chapter.number}:#{bookmark.verse.number}", scripture_chapter_path(bookmark.verse.book.abbreviation, bookmark.verse.chapter.number) %></p>
      <p><%= bookmark.verse.content %></p>
      <% if bookmark.note.present? %><p><%= bookmark.note %></p><% end %>
      <%= button_to "Remove", bookmark, method: :delete %>
    </article>
  <% end %>
  <%= @pagy.series_nav if @pagy.pages > 1 %>
<% else %>
  <p>No bookmarks yet. Bookmark verses while reading.</p>
<% end %>

rails/baibl/app/app/views/highlights/create.turbo_stream.erb

<%= turbo_stream.replace "highlight_#{@highlight.verse_id}", partial: "highlights/toggle", locals: { verse: @highlight.verse, highlight: @highlight } %>

rails/baibl/app/app/views/highlights/destroy.turbo_stream.erb

<%= turbo_stream.replace "highlight_#{@highlight.verse_id}", partial: "highlights/toggle", locals: { verse: @highlight.verse, highlight: nil } %>

rails/baibl/app/app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for(:title) || "App" %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="application-name" content="App">
    <meta name="mobile-web-app-capable" content="yes">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= yield :head %>

    <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
    <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>

    <link rel="icon" href="/icon.png" type="image/png">
    <link rel="icon" href="/icon.svg" type="image/svg+xml">
    <link rel="apple-touch-icon" href="/icon.png">

    <%# Includes all stylesheet files in app/assets/stylesheets %>
    <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

rails/baibl/app/app/views/layouts/mailer.html.erb

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

rails/baibl/app/app/views/layouts/mailer.text.erb

<%= yield %>

rails/baibl/app/app/views/pwa/manifest.json.erb

{
  "name": "App",
  "icons": [
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512"
    },
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "maskable"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "scope": "/",
  "description": "App.",
  "theme_color": "red",
  "background_color": "red"
}

rails/baibl/app/app/views/pwa/service-worker.js

// Add a service worker for processing Web Push notifications:
//
// self.addEventListener("push", async (event) => {
//   const { title, options } = await event.data.json()
//   event.waitUntil(self.registration.showNotification(title, options))
// })
//
// self.addEventListener("notificationclick", function(event) {
//   event.notification.close()
//   event.waitUntil(
//     clients.matchAll({ type: "window" }).then((clientList) => {
//       for (let i = 0; i < clientList.length; i++) {
//         let client = clientList[i]
//         let clientPath = (new URL(client.url)).pathname
//
//         if (clientPath == event.notification.data.path && "focus" in client) {
//           return client.focus()
//         }
//       }
//
//       if (clients.openWindow) {
//         return clients.openWindow(event.notification.data.path)
//       }
//     })
//   )
// })

rails/baibl/app/app/views/scriptures/book.html.erb

<% content_for :title, @book.name %>
<h1><%= @book.name %></h1>
<nav>
  <% @chapters.each do |chapter| %>
    <%= link_to chapter.number, scripture_chapter_path(@book.abbreviation, chapter.number) %>
  <% end %>
</nav>

rails/baibl/app/app/views/scriptures/chapter.html.erb

<% content_for :title, "#{@book.name} #{@chapter.number}" %>
<header>
  <h1><%= @book.name %> <%= @chapter.number %></h1>
  <nav>
    <% if @prev_chapter %>
      <%= link_to "← #{@book.abbreviation} #{@prev_chapter}", scripture_chapter_path(@book.abbreviation, @prev_chapter) %>
    <% end %>
    <%= link_to @book.name, scripture_book_path(@book.abbreviation) %>
    <% if @next_chapter %>
      <%= link_to "#{@book.abbreviation} #{@next_chapter} →", scripture_chapter_path(@book.abbreviation, @next_chapter) %>
    <% end %>
  </nav>
</header>
<section>
  <% @verses.each do |verse| %>
    <span id="v<%= verse.number %>" data-controller="verse" data-verse-id="<%= verse.id %>">
      <sup><%= verse.number %></sup>
      <span><%= verse.content %></span>
      <% if authenticated? %>
        <% highlight = @highlights[verse.id] %>
        <% bookmark  = @bookmarks[verse.id] %>
        <%= button_to highlight ? "★" : "☆", highlights_path(verse_id: verse.id), method: highlight ? :delete : :post, params: highlight ? { id: highlight.id } : {}, class: ("active" if highlight), data: { turbo_stream: true } %>
        <%= button_to bookmark ? "🔖" : "📌", bookmark ? bookmark_path(bookmark) : bookmarks_path(verse_id: verse.id), method: bookmark ? :delete : :post, class: ("active" if bookmark), data: { turbo_stream: true } %>
      <% end %>
    </span>
  <% end %>
</section>

rails/baibl/app/app/views/scriptures/index.html.erb

<% content_for :title, "Scripture" %>
<nav>
  <% @books.each do |book| %>
    <%= link_to book.abbreviation, scripture_book_path(book.abbreviation), title: book.name %>
  <% end %>
</nav>
<% if @books.any? %>
  <p>Select a book to begin reading.</p>
<% end %>

rails/baibl/app/app/views/scriptures/search.html.erb

<% content_for :title, "Search" %>
<%= form_with url: scripture_search_path, method: :get do |f| %>
  <%= f.search_field :q, value: @query, placeholder: "Search scripture…", autofocus: true %>
  <%= f.submit "Search" %>
<% end %>
<% if @results %>
  <p><%= @results.size %> results for "<%= @query %>"</p>
  <% @results.each do |verse| %>
    <article>
      <p><%= verse.book.abbreviation %> <%= verse.chapter.number %>:<%= verse.number %></p>
      <p><%= verse.content %></p>
    </article>
  <% end %>
<% end %>

rails/baibl/app/config/application.rb

require_relative "boot"

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module App
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 8.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w[assets tasks])

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Don't generate system test files.
    config.generators.system_tests = nil
  end
end

rails/baibl/app/config/boot.rb

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

require "bundler/setup" # Set up gems listed in the Gemfile.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.

rails/baibl/app/config/bundler-audit.yml

# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit.
# CVEs that are not relevant to the application can be enumerated on the ignore list below.

ignore:
  - CVE-THAT-DOES-NOT-APPLY

rails/baibl/app/config/cable.yml

development:
  adapter: async

test:
  adapter: test

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: app_production

rails/baibl/app/config/ci.rb

# Run using bin/ci

CI.run do
  step "Setup", "bin/setup --skip-server"

  step "Style: Ruby", "bin/rubocop"

  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Importmap vulnerability audit", "bin/importmap audit"
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"


  # Optional: set a green GitHub commit status to unblock PR merge.
  # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
  # if success?
  #   step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
  # else
  #   failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
  # end
end

rails/baibl/app/config/database.yml

# SQLite. Versions 3.8.0 and up are supported.
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem "sqlite3"
#
default: &default
  adapter: sqlite3
  max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: storage/development.sqlite3

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: storage/test.sqlite3

# Store production database in the storage/ directory, which by default
# is mounted as a persistent Docker volume in config/deploy.yml.
production:
  primary:
    <<: *default
    database: storage/production.sqlite3
  cache:
    <<: *default
    database: storage/production_cache.sqlite3
    migrations_paths: db/cache_migrate
  queue:
    <<: *default
    database: storage/production_queue.sqlite3
    migrations_paths: db/queue_migrate
  cable:
    <<: *default
    database: storage/production_cable.sqlite3
    migrations_paths: db/cable_migrate

rails/baibl/app/config/deploy.yml

# Name of your application. Used to uniquely configure containers.
service: app

# Name of the container image (use your-user/app-name on external registries).
image: app

# Deploy to these servers.
servers:
  web:
    - 192.168.0.1
  # job:
  #   hosts:
  #     - 192.168.0.1
  #   cmd: bin/jobs

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
#
# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb!
#
# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer).
#
# proxy:
#   ssl: true
#   host: app.example.com

# Where you keep your container images.
registry:
  # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ...
  server: localhost:5555

  # Needed for authenticated registries.
  # username: your-user

  # Always use an access token rather than real password when possible.
  # password:
  #   - KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
  secret:
    - RAILS_MASTER_KEY
  clear:
    # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
    # When you start using multiple servers, you should split out job processing to a dedicated machine.
    SOLID_QUEUE_IN_PUMA: true

    # Set number of processes dedicated to Solid Queue (default: 1)
    # JOB_CONCURRENCY: 3

    # Set number of cores available to the application on each server (default: 1).
    # WEB_CONCURRENCY: 2

    # Match this to any external database server to configure Active Record correctly
    # Use app-db for a db accessory server on same machine via local kamal docker network.
    # DB_HOST: 192.168.0.2

    # Log everything from Rails
    # RAILS_LOG_LEVEL: debug

# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
  console: app exec --interactive --reuse "bin/rails console"
  shell: app exec --interactive --reuse "bash"
  logs: app logs -f
  dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"

# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
  - "app_storage:/rails/storage"

# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets

# Configure the image builder.
builder:
  arch: amd64

  # # Build image via remote server (useful for faster amd64 builds on arm64 computers)
  # remote: ssh://docker@docker-builder-server
  #
  # # Pass arguments and secrets to the Docker build process
  # args:
  #   RUBY_VERSION: ruby-3.4.9
  # secrets:
  #   - GITHUB_TOKEN
  #   - RAILS_MASTER_KEY

# Use a different ssh user than root
# ssh:
#   user: app

# Use accessory services (secrets come from .kamal/secrets).
# accessories:
#   db:
#     image: mysql:8.0
#     host: 192.168.0.2
#     # Change to 3306 to expose port to the world instead of just local network.
#     port: "127.0.0.1:3306:3306"
#     env:
#       clear:
#         MYSQL_ROOT_HOST: '%'
#       secret:
#         - MYSQL_ROOT_PASSWORD
#     files:
#       - config/mysql/production.cnf:/etc/mysql/my.cnf
#       - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
#     directories:
#       - data:/var/lib/mysql
#   redis:
#     image: valkey/valkey:8
#     host: 192.168.0.2
#     port: 6379
#     directories:
#       - data:/data

rails/baibl/app/config/environment.rb

# Load the Rails application.
require_relative "application"

# Initialize the Rails application.
Rails.application.initialize!

rails/baibl/app/config/environments/development.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Make code changes take effect immediately without server restart.
  config.enable_reloading = true

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports.
  config.consider_all_requests_local = true

  # Enable server timing.
  config.server_timing = true

  # Enable/disable Action Controller caching. By default Action Controller caching is disabled.
  # Run rails dev:cache to toggle Action Controller caching.
  if Rails.root.join("tmp/caching-dev.txt").exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true
    config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
  else
    config.action_controller.perform_caching = false
  end

  # Change to :null_store to avoid any caching.
  config.cache_store = :memory_store

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  # Make template changes take effect immediately.
  config.action_mailer.perform_caching = false

  # Set localhost to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Highlight code that triggered database queries in logs.
  config.active_record.verbose_query_logs = true

  # Append comments with runtime information tags to SQL queries in logs.
  config.active_record.query_log_tags_enabled = true

  # Highlight code that enqueued background job in logs.
  config.active_job.verbose_enqueue_logs = true

  # Highlight code that triggered redirect in logs.
  config.action_dispatch.verbose_redirect_logs = true

  # Suppress logger output for asset requests.
  config.assets.quiet = true

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  config.action_view.annotate_rendered_view_with_filenames = true

  # Uncomment if you wish to allow Action Cable access from any origin.
  # config.action_cable.disable_request_forgery_protection = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true

  # Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
  # config.generators.apply_rubocop_autocorrect_after_generate!
end

rails/baibl/app/config/environments/production.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Code is not reloaded between requests.
  config.enable_reloading = false

  # Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
  config.eager_load = true

  # Full error reports are disabled.
  config.consider_all_requests_local = false

  # Turn on fragment caching in view templates.
  config.action_controller.perform_caching = true

  # Cache assets for far-future expiry since they are all digest stamped.
  config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }

  # Enable serving of images, stylesheets, and JavaScripts from an asset server.
  # config.asset_host = "http://assets.example.com"

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Assume all access to the app is happening through a SSL-terminating reverse proxy.
  # config.assume_ssl = true

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  # config.force_ssl = true

  # Skip http-to-https redirect for the default health check endpoint.
  # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }

  # Log to STDOUT with the current request id as a default log tag.
  config.log_tags = [ :request_id ]
  config.logger   = ActiveSupport::TaggedLogging.logger(STDOUT)

  # Change to "debug" to log everything (including potentially personally-identifiable information!).
  config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")

  # Prevent health checks from clogging up the logs.
  config.silence_healthcheck_path = "/up"

  # Don't log any deprecations.
  config.active_support.report_deprecations = false

  # Replace the default in-process memory cache store with a durable alternative.
  # config.cache_store = :mem_cache_store

  # Replace the default in-process and non-durable queuing backend for Active Job.
  # config.active_job.queue_adapter = :resque

  # Ignore bad email addresses and do not raise email delivery errors.
  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
  # config.action_mailer.raise_delivery_errors = false

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
  # config.action_mailer.smtp_settings = {
  #   user_name: Rails.application.credentials.dig(:smtp, :user_name),
  #   password: Rails.application.credentials.dig(:smtp, :password),
  #   address: "smtp.example.com",
  #   port: 587,
  #   authentication: :plain
  # }

  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
  # the I18n.default_locale when a translation cannot be found).
  config.i18n.fallbacks = true

  # Do not dump schema after migrations.
  config.active_record.dump_schema_after_migration = false

  # Only use :id for inspections in production.
  config.active_record.attributes_for_inspect = [ :id ]

  # Enable DNS rebinding protection and other `Host` header attacks.
  # config.hosts = [
  #   "example.com",     # Allow requests from example.com
  #   /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
  # ]
  #
  # Skip DNS rebinding protection for the default health check endpoint.
  # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end

rails/baibl/app/config/environments/test.rb

# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # While tests run files are not watched, reloading is not necessary.
  config.enable_reloading = false

  # Eager loading loads your entire application. When running a single test locally,
  # this is usually not necessary, and can slow down your test suite. However, it's
  # recommended that you enable it in continuous integration systems to ensure eager
  # loading is working properly before deploying your code.
  config.eager_load = ENV["CI"].present?

  # Configure public file server for tests with cache-control for performance.
  config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }

  # Show full error reports.
  config.consider_all_requests_local = true
  config.cache_store = :null_store

  # Render exception templates for rescuable exceptions and raise for other exceptions.
  config.action_dispatch.show_exceptions = :rescuable

  # Disable request forgery protection in test environment.
  config.action_controller.allow_forgery_protection = false

  # Store uploaded files on the local file system in a temporary directory.
  config.active_storage.service = :test

  # Tell Action Mailer not to deliver emails to the real world.
  # The :test delivery method accumulates sent emails in the
  # ActionMailer::Base.deliveries array.
  config.action_mailer.delivery_method = :test

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Print deprecation notices to the stderr.
  config.active_support.deprecation = :stderr

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  # config.action_view.annotate_rendered_view_with_filenames = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true
end

rails/baibl/app/config/importmap.rb

# Pin npm packages by running ./bin/importmap

pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "@stimulus-components/dialog", to: "@stimulus-components--dialog.js" # @1.0.1
pin "@stimulus-components/auto-submit", to: "@stimulus-components--auto-submit.js" # @6.0.0
pin "@stimulus-components/character-counter", to: "@stimulus-components--character-counter.js" # @5.1.0
pin "@stimulus-components/dropdown", to: "@stimulus-components--dropdown.js" # @3.0.0
pin "stimulus-use" # @0.52.3
pin "@stimulus-components/clipboard", to: "@stimulus-components--clipboard.js" # @5.0.0
pin "@stimulus-components/notification", to: "@stimulus-components--notification.js" # @3.0.0
pin "@stimulus-components/timeago", to: "@stimulus-components--timeago.js" # @5.0.2
pin "date-fns" # @4.1.0
pin "@stimulus-components/animated-number", to: "@stimulus-components--animated-number.js" # @5.0.0
pin "@stimulus-components/sortable", to: "@stimulus-components--sortable.js" # @5.0.3
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_request", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_request.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_response", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_response.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/lib/utils", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--lib--utils.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/request_interceptor", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--request_interceptor.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/verbs", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--verbs.js" # @0.0.13
pin "@rails/request.js", to: "@rails--request.js.js" # @0.0.13
pin "sortablejs" # @1.15.7

rails/baibl/app/config/initializers/assets.rb

# Be sure to restart your server when you modify this file.

# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = "1.0"

# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path

rails/baibl/app/config/initializers/content_security_policy.rb

# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header

# Rails.application.configure do
#   config.content_security_policy do |policy|
#     policy.default_src :self, :https
#     policy.font_src    :self, :https, :data
#     policy.img_src     :self, :https, :data
#     policy.object_src  :none
#     policy.script_src  :self, :https
#     policy.style_src   :self, :https
#     # Specify URI for violation reports
#     # policy.report_uri "/csp-violation-report-endpoint"
#   end
#
#   # Generate session nonces for permitted importmap, inline scripts, and inline styles.
#   config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
#   config.content_security_policy_nonce_directives = %w(script-src style-src)
#
#   # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
#   # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
#   # config.content_security_policy_nonce_auto = true
#
#   # Report violations without enforcing the policy.
#   # config.content_security_policy_report_only = true
# end

rails/baibl/app/config/initializers/filter_parameter_logging.rb

# Be sure to restart your server when you modify this file.

# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
  :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
]

rails/baibl/app/config/initializers/inflections.rb

# Be sure to restart your server when you modify this file.

# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.plural /^(ox)$/i, "\\1en"
#   inflect.singular /^(ox)en/i, "\\1"
#   inflect.irregular "person", "people"
#   inflect.uncountable %w( fish sheep )
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.acronym "RESTful"
# end

rails/baibl/app/config/locales/en.yml

# Files in the config/locales directory are used for internationalization and
# are automatically loaded by Rails. If you want to use locales other than
# English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
#     I18n.t "hello"
#
# In views, this is aliased to just `t`:
#
#     <%= t("hello") %>
#
# To use a different locale, set it with `I18n.locale`:
#
#     I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# To learn more about the API, please read the Rails Internationalization guide
# at https://guides.rubyonrails.org/i18n.html.
#
# Be aware that YAML interprets the following case-insensitive strings as
# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
# must be quoted to be interpreted as strings. For example:
#
#     en:
#       "yes": yup
#       enabled: "ON"

en:
  hello: "Hello world"

rails/baibl/app/config/puma.rb

# This configuration file will be evaluated by Puma. The top-level methods that
# are invoked here are part of Puma's configuration DSL. For more information
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
#
# Puma starts a configurable number of processes (workers) and each process
# serves each request in a thread from an internal thread pool.
#
# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You
# should only set this value when you want to run 2 or more workers. The
# default is already 1. You can set it to `auto` to automatically start a worker
# for each available processor.
#
# The ideal number of threads per worker depends both on how much time the
# application spends waiting for IO operations and on how much you wish to
# prioritize throughput over latency.
#
# As a rule of thumb, increasing the number of threads will increase how much
# traffic a given process can handle (throughput), but due to CRuby's
# Global VM Lock (GVL) it has diminishing returns and will degrade the
# response time (latency) of the application.
#
# The default is set to 3 threads as it's deemed a decent compromise between
# throughput and latency for the average Rails application.
#
# Any libraries that use a connection pool or another resource pool should
# be configured to provide at least as many connections as the number of
# threads. This includes Active Record's `pool` parameter in `database.yml`.
threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
port ENV.fetch("PORT", 3000)

# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart

# Run the Solid Queue supervisor inside of Puma for single-server deployments.
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]

# Specify the PID file. Defaults to tmp/pids/server.pid in development.
# In other environments, only set the PID file if requested.
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]

rails/baibl/app/config/routes.rb

Rails.application.routes.draw do
  resource  :session
  resources :passwords, param: :token

  root "scriptures#index"

  get "scripture",                to: "scriptures#index",   as: :scripture_index
  get "scripture/:abbreviation",  to: "scriptures#book",    as: :scripture_book
  get "scripture/:book_abbreviation/:number", to: "scriptures#chapter", as: :scripture_chapter
  get "search",                   to: "scriptures#search",  as: :scripture_search

  resources :highlights, only: %i[create destroy]
  resources :bookmarks

  get "up", to: "rails/health#show", as: :rails_health_check
end

rails/baibl/app/config/storage.yml

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket-<%= Rails.env %>

# Remember not to checkin your GCS keyfile to a repository
# google:
#   service: GCS
#   project: your_project
#   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
#   bucket: your_own_bucket-<%= Rails.env %>

# mirror:
#   service: Mirror
#   primary: local
#   mirrors: [ amazon, google, microsoft ]

rails/baibl/app/db/migrate/20260501020807_create_users.rb

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users do |t|
      t.string :email_address, null: false
      t.string :password_digest, null: false

      t.timestamps
    end
    add_index :users, :email_address, unique: true
  end
end

rails/baibl/app/db/migrate/20260501020818_create_sessions.rb

class CreateSessions < ActiveRecord::Migration[8.1]
  def change
    create_table :sessions do |t|
      t.references :user, null: false, foreign_key: true
      t.string :ip_address
      t.string :user_agent

      t.timestamps
    end
  end
end

rails/baibl/app/db/migrate/20260507120001_create_books.rb

class CreateBooks < ActiveRecord::Migration[8.1]
  def change
    create_table :books do |t|
      t.string :name
      t.string :abbreviation
      t.string :testament
      t.integer :chapter_count
      t.integer :order_index
      t.timestamps
    end
  end
end

rails/baibl/app/db/migrate/20260507120002_create_chapters.rb

class CreateChapters < ActiveRecord::Migration[8.1]
  def change
    create_table :chapters do |t|
      t.references :book, foreign_key: true
      t.integer :number
      t.timestamps
    end
  end
end

rails/baibl/app/db/migrate/20260507120003_create_verses.rb

class CreateVerses < ActiveRecord::Migration[8.1]
  def change
    create_table :verses do |t|
      t.references :chapter, foreign_key: true
      t.references :book, foreign_key: true
      t.integer :number
      t.text :content
      t.timestamps
    end
  end
end

rails/baibl/app/db/migrate/20260507120004_create_highlights.rb

class CreateHighlights < ActiveRecord::Migration[8.1]
  def change
    create_table :highlights do |t|
      t.references :verse, foreign_key: true
      t.references :user, foreign_key: true
      t.string :color
      t.text :note
      t.timestamps
    end
  end
end

rails/baibl/app/db/migrate/20260507120005_create_bookmarks.rb

class CreateBookmarks < ActiveRecord::Migration[8.1]
  def change
    create_table :bookmarks do |t|
      t.references :verse, foreign_key: true
      t.references :user, foreign_key: true
      t.text :note
      t.timestamps
    end
  end
end

rails/baibl/app/db/migrate/20260507120006_create_reading_plans.rb

class CreateReadingPlans < ActiveRecord::Migration[8.1]
  def change
    create_table :reading_plans do |t|
      t.string :name
      t.text :description
      t.integer :duration_days
      t.references :user, foreign_key: true
      t.timestamps
    end
  end
end

rails/baibl/app/db/migrate/20260507120007_create_reading_plan_days.rb

class CreateReadingPlanDays < ActiveRecord::Migration[8.1]
  def change
    create_table :reading_plan_days do |t|
      t.references :reading_plan, foreign_key: true
      t.integer :day_number
      t.references :book, foreign_key: true
      t.integer :chapter_start
      t.integer :chapter_end
      t.datetime :completed_at
      t.timestamps
    end
  end
end

rails/baibl/app/db/seeds.rb

# This file should ensure the existence of records required to run the application in every environment (production,
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
#
# Example:
#
#   ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
#     MovieGenre.find_or_create_by!(name: genre_name)
#   end

rails/baibl/app/public/robots.txt

# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file

rails/baibl/baibl.sh

#!/usr/bin/env zsh
# baibl.sh — deploys tracked Rails tree at app/ as %APP_NAME%
set -euo pipefail

APP_NAME=%APP_NAME%
APP_DIR=/home/${APP_NAME}/app
APP_PORT=10007
APP_DOMAIN=baibl.no
SCRIPT_DIR=${0:a:h}
SRC_DIR=${SCRIPT_DIR}/app

. "${SCRIPT_DIR:h}/@shared_functions.sh"

need_cmd ruby34 bundle doas

[[ -d $SRC_DIR ]] || { log_err "missing source tree: $SRC_DIR"; exit 1 }

log "${APP_NAME} — deploying tracked tree → ${APP_DIR}"

id "$APP_NAME" >/dev/null 2>&1 || doas useradd -m -L daemon -s /bin/ksh "$APP_NAME"
doas mkdir -p "$APP_DIR"

doas cp -R "${SRC_DIR}/." "${APP_DIR}/"
doas chown -R "${APP_NAME}:${APP_NAME}" "$APP_DIR"

cd "$APP_DIR"

typeset bundle_home="/home/${APP_NAME}/.bundle"
if [[ ! -d ${bundle_home}/gems ]]; then
  log "Bootstrapping gems from amber"
  doas mkdir -p "$bundle_home"
  doas cp -R /home/amber/.bundle/gems "$bundle_home/"
  doas chown -R "${APP_NAME}:${APP_NAME}" "$bundle_home"
fi
print "---\nBUNDLE_PATH: \"${bundle_home}/gems\"" | doas tee "${APP_DIR}/.bundle/config" >/dev/null

doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bundle install --deployment --without development:test"
doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:create db:migrate"
[[ -f ${APP_DIR}/db/seeds.rb ]] && doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:seed" || true

install_rcd "$APP_NAME" "$APP_DIR" "$APP_PORT" "$APP_NAME"
[[ -n $APP_DOMAIN ]] && relayd_add_relay "$APP_DOMAIN" "$APP_PORT"

doas rcctl restart "$APP_NAME" || doas rcctl start "$APP_NAME"
log_ok "$APP_NAME live on :$APP_PORT"

rails/blognet/README.md

# blognet

Blog network platform. Rails 8. PostgreSQL.

## Deploy

```zsh
cd ~/pub4/MASTER/DEPLOY/rails/blognet
doas zsh blognet.sh

## `rails/blognet/app/Dockerfile`
```text
# syntax=docker/dockerfile:1
# check=error=true

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t app .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name app app

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.9
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
    ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment variables and enable jemalloc for reduced memory usage and latency.
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development" \
    LD_PRELOAD="/usr/local/lib/libjemalloc.so"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY vendor/* ./vendor/
COPY Gemfile Gemfile.lock ./

RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
    bundle exec bootsnap precompile -j 1 --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times.
# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
RUN bundle exec bootsnap precompile -j 1 app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile




# Final stage for app image
FROM base

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
USER 1000:1000

# Copy built artifacts: gems, application
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --chown=rails:rails --from=build /rails /rails

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]

rails/blognet/app/Gemfile

source "https://rubygems.org"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.1.2"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Use sqlite3 as the database for Active Record
gem "sqlite3", ">= 2.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]

# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable
gem "solid_cache"
gem "solid_queue"
gem "solid_cable"

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
gem "kamal", require: false

# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
gem "thruster", require: false

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
gem "image_processing", "~> 1.2"

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

  # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues)
  gem "bundler-audit", require: false

  # Static analysis for security vulnerabilities [https://brakemanscanner.org/]
  gem "brakeman", require: false

  # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
  gem "rubocop-rails-omakase", require: false
end

group :development do
  # Use console on exceptions pages [https://github.com/rails/web-console]
  gem "web-console"
end
gem "pagy"
gem "friendly_id"
gem "falcon"

rails/blognet/app/README.md

# README

This README would normally document whatever steps are necessary to get the
application up and running.

Things you may want to cover:

* Ruby version

* System dependencies

* Configuration

* Database creation

* Database initialization

* How to run the test suite

* Services (job queues, cache servers, search engines, etc.)

* Deployment instructions

* ...

rails/blognet/app/Rakefile

# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative "config/application"

Rails.application.load_tasks

rails/blognet/app/app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      set_current_user || reject_unauthorized_connection
    end

    private
      def set_current_user
        if session = Session.find_by(id: cookies.signed[:session_id])
          self.current_user = session.user
        end
      end
  end
end

rails/blognet/app/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Pagy::Method
  allow_browser versions: :modern
end

rails/blognet/app/app/controllers/blogs_controller.rb

class BlogsController < ApplicationController
  before_action :require_authentication, except: %i[index show]
  before_action :set_blog, only: %i[show edit update destroy]
  before_action :authorize!, only: %i[edit update destroy]

  def index
    @pagy, @blogs = pagy(Blog.published.includes(:user).recent)
  end

  def show
    @pagy, @posts = pagy(@blog.posts.published.includes(:user, :tags))
  end

  def new
    @blog = Current.user.blogs.build
  end

  def create
    @blog = Current.user.blogs.build(blog_params)
    @blog.save ? redirect_to(@blog, notice: "Blog created") : render(:new, status: :unprocessable_entity)
  end

  def edit; end

  def update
    @blog.update(blog_params) ? redirect_to(@blog, notice: "Updated") : render(:edit, status: :unprocessable_entity)
  end

  def destroy
    @blog.destroy
    redirect_to blogs_path, notice: "Blog deleted"
  end

  private

  def set_blog   = @blog = Blog.find_by!(slug: params[:id])
  def authorize! = redirect_to(blogs_path, alert: "Unauthorized") unless @blog.user == Current.user
  def blog_params = params.require(:blog).permit(:name, :description, :published, :banner)
end

rails/blognet/app/app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :require_authentication
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params.merge(user: Current.user))
    if @comment.save
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_to [@post.blog, @post] }
      end
    else
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    @comment = @post.comments.find(params[:id])
    @comment.destroy! if @comment.user == Current.user
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to [@post.blog, @post] }
    end
  end

  private

  def set_post = @post = Post.find_by!(slug: params[:post_id])

  def comment_params
    params.require(:comment).permit(:content, :parent_id)
  end
end

rails/blognet/app/app/controllers/concerns/authentication.rb

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

rails/blognet/app/app/controllers/passwords_controller.rb

class PasswordsController < ApplicationController
  allow_unauthenticated_access
  before_action :set_user_by_token, only: %i[ edit update ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.find_by(email_address: params[:email_address])
      PasswordsMailer.reset(user).deliver_later
    end

    redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
  end

  def edit
  end

  def update
    if @user.update(params.permit(:password, :password_confirmation))
      @user.sessions.destroy_all
      redirect_to new_session_path, notice: "Password has been reset."
    else
      redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
    end
  end

  private
    def set_user_by_token
      @user = User.find_by_password_reset_token!(params[:token])
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
    end
end

rails/blognet/app/app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :require_authentication, except: %i[index show]
  before_action :set_blog
  before_action :set_post, only: %i[show edit update destroy]
  before_action :authorize!, only: %i[edit update destroy]

  def index
    @pagy, @posts = pagy(@blog.posts.published.includes(:user, :tags))
  end

  def show
    @post.increment!(:views_count)
    @comments = @post.comments.approved.roots.includes(:user, :replies)
    @comment  = Comment.new
  end

  def new
    @post = @blog.posts.build
  end

  def create
    @post = @blog.posts.build(post_params.merge(user: Current.user))
    @post.save ? redirect_to([@blog, @post], notice: "Post created") : render(:new, status: :unprocessable_entity)
  end

  def edit; end

  def update
    @post.update(post_params) ? redirect_to([@blog, @post], notice: "Updated") : render(:edit, status: :unprocessable_entity)
  end

  def destroy
    @post.destroy
    redirect_to @blog, notice: "Post deleted"
  end

  private

  def set_blog   = @blog = Blog.find_by!(slug: params[:blog_id])
  def set_post   = @post = @blog.posts.find_by!(slug: params[:id])
  def authorize! = redirect_to(@blog, alert: "Unauthorized") unless @post.user == Current.user

  def post_params
    params.require(:post).permit(:title, :body, :published, :slug, images: [])
  end
end

rails/blognet/app/app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  allow_unauthenticated_access only: %i[ new create ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

  def destroy
    terminate_session
    redirect_to new_session_path, status: :see_other
  end
end

rails/blognet/app/app/helpers/application_helper.rb

module ApplicationHelper
end

rails/blognet/app/app/javascript/application.js

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"

rails/blognet/app/app/javascript/controllers/animated_number_controller.js

import AnimatedNumber from "@stimulus-components/animated-number"
export default class extends AnimatedNumber {}

rails/blognet/app/app/javascript/controllers/application.js

import { Application } from "@hotwired/stimulus"

const application = Application.start()

// Configure Stimulus development experience
application.debug = false
window.Stimulus   = application

export { application }

rails/blognet/app/app/javascript/controllers/auto_submit_controller.js

import AutoSubmit from "@stimulus-components/auto-submit"
export default class extends AutoSubmit {}

rails/blognet/app/app/javascript/controllers/character_counter_controller.js

import CharacterCounter from "@stimulus-components/character-counter"
export default class extends CharacterCounter {}

rails/blognet/app/app/javascript/controllers/clipboard_controller.js

import Clipboard from "@stimulus-components/clipboard"
export default class extends Clipboard {}

rails/blognet/app/app/javascript/controllers/dialog_controller.js

import Dialog from "@stimulus-components/dialog"
export default class extends Dialog {}

rails/blognet/app/app/javascript/controllers/dropdown_controller.js

import Dropdown from "@stimulus-components/dropdown"
export default class extends Dropdown {}

rails/blognet/app/app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.textContent = "Hello World!"
  }
}

rails/blognet/app/app/javascript/controllers/index.js

// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)

rails/blognet/app/app/javascript/controllers/notification_controller.js

import Notification from "@stimulus-components/notification"
export default class extends Notification {}

rails/blognet/app/app/javascript/controllers/sortable_controller.js

import Sortable from "@stimulus-components/sortable"
export default class extends Sortable {}

rails/blognet/app/app/javascript/controllers/textarea_autogrow_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.resize()
    this.element.addEventListener("input", this.resize)
  }

  disconnect() {
    this.element.removeEventListener("input", this.resize)
  }

  resize = () => {
    this.element.style.height = "auto"
    this.element.style.height = `${this.element.scrollHeight}px`
  }
}

rails/blognet/app/app/javascript/controllers/timeago_controller.js

import TimeAgo from "@stimulus-components/timeago"
export default class extends TimeAgo {}

rails/blognet/app/app/jobs/application_job.rb

class ApplicationJob < ActiveJob::Base
  # Automatically retry jobs that encountered a deadlock
  # retry_on ActiveRecord::Deadlocked

  # Most jobs are safe to ignore if the underlying records are no longer available
  # discard_on ActiveJob::DeserializationError
end

rails/blognet/app/app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout "mailer"
end

rails/blognet/app/app/mailers/passwords_mailer.rb

class PasswordsMailer < ApplicationMailer
  def reset(user)
    @user = user
    mail subject: "Reset your password", to: user.email_address
  end
end

rails/blognet/app/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
end

rails/blognet/app/app/models/blog.rb

class Blog < ApplicationRecord
  belongs_to :user
  has_many :posts, dependent: :destroy
  has_one_attached :banner

  validates :name, :slug, presence: true
  validates :slug, uniqueness: true, format: { with: /\A[a-z0-9-]+\z/ }

  before_validation :generate_slug, on: :create

  scope :published, -> { where(published: true) }
  scope :recent,    -> { order(created_at: :desc) }

  def to_param = slug

  private

  def generate_slug
    self.slug ||= name.to_s.parameterize
  end
end

rails/blognet/app/app/models/categorization.rb

class Categorization < ApplicationRecord
  belongs_to :post
  belongs_to :category

  validates :post_id, uniqueness: { scope: :category_id }
end

rails/blognet/app/app/models/category.rb

class Category < ApplicationRecord
  has_many :categorizations, dependent: :destroy
  has_many :posts, through: :categorizations

  validates :name, :slug, presence: true
  validates :slug, uniqueness: true, format: { with: /\A[a-z0-9-]+\z/ }

  before_validation :generate_slug, on: :create

  def to_param = slug

  private

  def generate_slug
    self.slug ||= name.to_s.parameterize
  end
end

rails/blognet/app/app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :post
  belongs_to :user
  belongs_to :parent, class_name: "Comment", optional: true
  has_many :replies, class_name: "Comment", foreign_key: :parent_id, dependent: :destroy

  validates :content, presence: true, length: { maximum: 5000 }

  scope :roots,    -> { where(parent_id: nil).order(created_at: :asc) }
  scope :approved, -> { where(approved: true) }

  after_create_commit :broadcast_comment

  private

  def broadcast_comment
    broadcast_append_to [post, "comments"], partial: "comments/comment", locals: { comment: self }
    post.increment!(:comments_count)
  end
end

rails/blognet/app/app/models/current.rb

class Current < ActiveSupport::CurrentAttributes
  attribute :session
  delegate :user, to: :session, allow_nil: true
end

rails/blognet/app/app/models/post.rb

class Post < ApplicationRecord
  belongs_to :blog
  belongs_to :user
  has_rich_text :body
  has_many_attached :images
  has_many :comments, dependent: :destroy
  has_many :categorizations, dependent: :destroy
  has_many :categories, through: :categorizations
  has_many :taggings, dependent: :destroy
  has_many :tags, through: :taggings

  validates :title, :slug, presence: true
  validates :slug, uniqueness: true, format: { with: /\A[a-z0-9-]+\z/ }

  before_validation :generate_slug, on: :create
  before_save :set_published_at

  scope :published, -> { where(published: true).order(published_at: :desc) }
  scope :drafts,    -> { where(published: false) }
  scope :recent,    -> { order(created_at: :desc) }

  def to_param = slug

  def reading_time
    words = body.to_plain_text.split.size
    [(words / 200.0).ceil, 1].max
  end

  private

  def generate_slug
    self.slug ||= title.to_s.parameterize
  end

  def set_published_at
    self.published_at = Time.current if published? && published_at.nil?
  end
end

rails/blognet/app/app/models/session.rb

class Session < ApplicationRecord
  belongs_to :user
end

rails/blognet/app/app/models/tag.rb

class Tag < ApplicationRecord
  has_many :taggings, dependent: :destroy
  has_many :posts, through: :taggings

  validates :name, presence: true, uniqueness: true

  before_validation -> { self.name = name.to_s.strip.downcase }, on: :create

  scope :popular, -> { where("posts_count > 0").order(posts_count: :desc) }
end

rails/blognet/app/app/models/tagging.rb

class Tagging < ApplicationRecord
  belongs_to :post
  belongs_to :tag, counter_cache: :posts_count

  validates :post_id, uniqueness: { scope: :tag_id }
end

rails/blognet/app/app/models/user.rb

class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy

  normalizes :email_address, with: ->(e) { e.strip.downcase }
end

rails/blognet/app/app/views/active_storage/blobs/_blob.html.erb

<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
  <% if blob.representable? %>
    <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
  <% end %>

  <figcaption class="attachment__caption">
    <% if caption = blob.try(:caption) %>
      <%= caption %>
    <% else %>
      <span class="attachment__name"><%= blob.filename %></span>
      <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
    <% end %>
  </figcaption>
</figure>

rails/blognet/app/app/views/blogs/_form.html.erb

<%= form_with model: blog do |f| %>
  <%= render "shared/errors", object: blog %>
  <p><%= f.label :name %><%= f.text_field :name, autofocus: true %></p>
  <p><%= f.label :description %><%= f.text_area :description, rows: 2 %></p>
  <p><%= f.label :published %><%= f.check_box :published %></p>
  <p><%= f.submit %> <%= link_to "Cancel", blogs_path %></p>
<% end %>

rails/blognet/app/app/views/blogs/edit.html.erb

<% content_for :title, "Edit blog" %>
<h1>Edit <%= @blog.name %></h1>
<%= render "form", blog: @blog %>

rails/blognet/app/app/views/blogs/index.html.erb

<% content_for :title, "Blogs" %>
<header>
  <h1>Blogs</h1>
  <% if authenticated? %><%= link_to "New blog", new_blog_path %><% end %>
</header>
<section id="blogs">
  <% @blogs.each do |blog| %>
    <article>
      <%= link_to blog.name, blog_path(blog) %>
      <p><%= blog.description %></p>
      <small><%= blog.posts_count %> posts</small>
    </article>
  <% end %>
</section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/blognet/app/app/views/blogs/new.html.erb

<% content_for :title, "New blog" %>
<h1>New blog</h1>
<%= render "form", blog: @blog %>

rails/blognet/app/app/views/blogs/show.html.erb

<% content_for :title, @blog.name %>
<header>
  <h1><%= @blog.name %></h1>
  <p><%= @blog.description %></p>
  <% if @blog.user == Current.user %>
    <%= link_to "New post", new_blog_post_path(@blog) %>
    <%= link_to "Edit", edit_blog_path(@blog) %>
  <% end %>
</header>
<section id="posts">
  <% @posts.each do |post| %>
    <article>
      <%= link_to post.title, blog_post_path(@blog, post) %>
      <small><%= post.published_at&.strftime("%b %-d, %Y") %> · <%= post.views_count %> views · <%= post.comments_count %> comments</small>
    </article>
  <% end %>
</section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/blognet/app/app/views/comments/_comment.html.erb

<article id="<%= dom_id(comment) %>">
  <small><%= comment.user.email_address.split("@").first %></small>
  <p><%= comment.content %></p>
  <% if authenticated? && (comment.user == Current.user || @blog.user == Current.user) %>
    <%= button_to "Delete", blog_post_comment_path(@blog, @post, comment), method: :delete %>
  <% end %>
</article>

rails/blognet/app/app/views/layouts/action_text/contents/_content.html.erb

<div class="trix-content">
  <%= yield -%>
</div>

rails/blognet/app/app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for(:title) || "App" %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="application-name" content="App">
    <meta name="mobile-web-app-capable" content="yes">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= yield :head %>

    <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
    <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>

    <link rel="icon" href="/icon.png" type="image/png">
    <link rel="icon" href="/icon.svg" type="image/svg+xml">
    <link rel="apple-touch-icon" href="/icon.png">

    <%# Includes all stylesheet files in app/assets/stylesheets %>
    <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

rails/blognet/app/app/views/layouts/mailer.html.erb

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

rails/blognet/app/app/views/layouts/mailer.text.erb

<%= yield %>

rails/blognet/app/app/views/passwords/edit.html.erb

<h1>Update your password</h1>

<% if flash[:alert] %><p role="alert"><%= flash[:alert] %></p><% end %>

<%= form_with url: password_path(params[:token]), method: :put do |form| %>
  <p><%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %></p>
  <p><%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %></p>
  <p><%= form.submit "Save" %></p>
<% end %>

rails/blognet/app/app/views/passwords/new.html.erb

<h1>Forgot your password?</h1>

<% if flash[:alert] %><p role="alert"><%= flash[:alert] %></p><% end %>

<%= form_with url: passwords_path do |form| %>
  <p><%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %></p>
  <p><%= form.submit "Email reset instructions" %></p>
<% end %>

rails/blognet/app/app/views/passwords_mailer/reset.html.erb

<p>
  You can reset your password on
  <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.

  This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
</p>

rails/blognet/app/app/views/passwords_mailer/reset.text.erb

You can reset your password on
<%= edit_password_url(@user.password_reset_token) %>

This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.

rails/blognet/app/app/views/posts/_form.html.erb

<%= form_with model: [@blog, post] do |f| %>
  <%= render "shared/errors", object: post %>
  <p><%= f.label :title %><%= f.text_field :title, autofocus: true %></p>
  <p><%= f.label :body %><%= f.rich_text_area :body %></p>
  <p><%= f.label :published %><%= f.check_box :published %></p>
  <p><%= f.submit %> <%= link_to "Cancel", blog_path(@blog) %></p>
<% end %>

rails/blognet/app/app/views/posts/edit.html.erb

<% content_for :title, "Edit post" %>
<h1>Edit post</h1>
<%= render "form", blog: @blog, post: @post %>

rails/blognet/app/app/views/posts/new.html.erb

<% content_for :title, "New post" %>
<h1>New post</h1>
<%= render "form", blog: @blog, post: @post %>

rails/blognet/app/app/views/posts/show.html.erb

<% content_for :title, @post.title %>
<article>
  <header>
    <h1><%= @post.title %></h1>
    <small><%= @post.user.email_address.split("@").first %> · <%= @post.published_at&.strftime("%b %-d, %Y") %> · <%= @post.views_count %> views</small>
    <% if @post.user == Current.user %>
      <%= link_to "Edit", edit_blog_post_path(@blog, @post) %>
      <%= button_to "Delete", blog_post_path(@blog, @post), method: :delete, data: { turbo_confirm: "Delete post?" } %>
    <% end %>
  </header>
  <%= @post.body %>
</article>
<section id="comments">
  <h2>Comments (<%= @post.comments_count %>)</h2>
  <%= render @comments %>
  <% if authenticated? %>
    <%= form_with url: blog_post_comments_path(@blog, @post) do |f| %>
      <p><%= f.text_area :content, rows: 3, placeholder: "Add a comment…" %></p>
      <p><%= f.submit "Comment" %></p>
    <% end %>
  <% end %>
</section>

rails/blognet/app/app/views/pwa/manifest.json.erb

{
  "name": "App",
  "icons": [
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512"
    },
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "maskable"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "scope": "/",
  "description": "App.",
  "theme_color": "red",
  "background_color": "red"
}

rails/blognet/app/app/views/pwa/service-worker.js

// Add a service worker for processing Web Push notifications:
//
// self.addEventListener("push", async (event) => {
//   const { title, options } = await event.data.json()
//   event.waitUntil(self.registration.showNotification(title, options))
// })
//
// self.addEventListener("notificationclick", function(event) {
//   event.notification.close()
//   event.waitUntil(
//     clients.matchAll({ type: "window" }).then((clientList) => {
//       for (let i = 0; i < clientList.length; i++) {
//         let client = clientList[i]
//         let clientPath = (new URL(client.url)).pathname
//
//         if (clientPath == event.notification.data.path && "focus" in client) {
//           return client.focus()
//         }
//       }
//
//       if (clients.openWindow) {
//         return clients.openWindow(event.notification.data.path)
//       }
//     })
//   )
// })

rails/blognet/app/app/views/sessions/new.html.erb

<% if flash[:alert] %><p role="alert"><%= flash[:alert] %></p><% end %>
<% if flash[:notice] %><p role="status"><%= flash[:notice] %></p><% end %>

<%= form_with url: session_path do |form| %>
  <p><%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %></p>
  <p><%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %></p>
  <p><%= form.submit "Sign in" %></p>
<% end %>

<p><%= link_to "Forgot password?", new_password_path %></p>

rails/blognet/app/config/application.rb

require_relative "boot"

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module App
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 8.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w[assets tasks])

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Don't generate system test files.
    config.generators.system_tests = nil
  end
end

rails/blognet/app/config/boot.rb

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

require "bundler/setup" # Set up gems listed in the Gemfile.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.

rails/blognet/app/config/bundler-audit.yml

# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit.
# CVEs that are not relevant to the application can be enumerated on the ignore list below.

ignore:
  - CVE-THAT-DOES-NOT-APPLY

rails/blognet/app/config/cable.yml

# Async adapter only works within the same process, so for manually triggering cable updates from a console,
# and seeing results in the browser, you must do so from the web console (running inside the dev process),
# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
# to make the web console appear.
development:
  adapter: async

test:
  adapter: test

production:
  adapter: solid_cable
  connects_to:
    database:
      writing: cable
  polling_interval: 0.1.seconds
  message_retention: 1.day

rails/blognet/app/config/cache.yml

default: &default
  store_options:
    # Cap age of oldest cache entry to fulfill retention policies
    # max_age: <%= 60.days.to_i %>
    max_size: <%= 256.megabytes %>
    namespace: <%= Rails.env %>

development:
  <<: *default

test:
  <<: *default

production:
  database: cache
  <<: *default

rails/blognet/app/config/ci.rb

# Run using bin/ci

CI.run do
  step "Setup", "bin/setup --skip-server"

  step "Style: Ruby", "bin/rubocop"

  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Importmap vulnerability audit", "bin/importmap audit"
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"


  # Optional: set a green GitHub commit status to unblock PR merge.
  # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
  # if success?
  #   step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
  # else
  #   failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
  # end
end

rails/blognet/app/config/database.yml

# SQLite. Versions 3.8.0 and up are supported.
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem "sqlite3"
#
default: &default
  adapter: sqlite3
  max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: storage/development.sqlite3

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: storage/test.sqlite3

# Store production database in the storage/ directory, which by default
# is mounted as a persistent Docker volume in config/deploy.yml.
production:
  primary:
    <<: *default
    database: storage/production.sqlite3
  cache:
    <<: *default
    database: storage/production_cache.sqlite3
    migrations_paths: db/cache_migrate
  queue:
    <<: *default
    database: storage/production_queue.sqlite3
    migrations_paths: db/queue_migrate
  cable:
    <<: *default
    database: storage/production_cable.sqlite3
    migrations_paths: db/cable_migrate

rails/blognet/app/config/deploy.yml

# Name of your application. Used to uniquely configure containers.
service: app

# Name of the container image (use your-user/app-name on external registries).
image: app

# Deploy to these servers.
servers:
  web:
    - 192.168.0.1
  # job:
  #   hosts:
  #     - 192.168.0.1
  #   cmd: bin/jobs

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
#
# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb!
#
# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer).
#
# proxy:
#   ssl: true
#   host: app.example.com

# Where you keep your container images.
registry:
  # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ...
  server: localhost:5555

  # Needed for authenticated registries.
  # username: your-user

  # Always use an access token rather than real password when possible.
  # password:
  #   - KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
  secret:
    - RAILS_MASTER_KEY
  clear:
    # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
    # When you start using multiple servers, you should split out job processing to a dedicated machine.
    SOLID_QUEUE_IN_PUMA: true

    # Set number of processes dedicated to Solid Queue (default: 1)
    # JOB_CONCURRENCY: 3

    # Set number of cores available to the application on each server (default: 1).
    # WEB_CONCURRENCY: 2

    # Match this to any external database server to configure Active Record correctly
    # Use app-db for a db accessory server on same machine via local kamal docker network.
    # DB_HOST: 192.168.0.2

    # Log everything from Rails
    # RAILS_LOG_LEVEL: debug

# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
  console: app exec --interactive --reuse "bin/rails console"
  shell: app exec --interactive --reuse "bash"
  logs: app logs -f
  dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"

# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
  - "app_storage:/rails/storage"

# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets

# Configure the image builder.
builder:
  arch: amd64

  # # Build image via remote server (useful for faster amd64 builds on arm64 computers)
  # remote: ssh://docker@docker-builder-server
  #
  # # Pass arguments and secrets to the Docker build process
  # args:
  #   RUBY_VERSION: ruby-3.4.9
  # secrets:
  #   - GITHUB_TOKEN
  #   - RAILS_MASTER_KEY

# Use a different ssh user than root
# ssh:
#   user: app

# Use accessory services (secrets come from .kamal/secrets).
# accessories:
#   db:
#     image: mysql:8.0
#     host: 192.168.0.2
#     # Change to 3306 to expose port to the world instead of just local network.
#     port: "127.0.0.1:3306:3306"
#     env:
#       clear:
#         MYSQL_ROOT_HOST: '%'
#       secret:
#         - MYSQL_ROOT_PASSWORD
#     files:
#       - config/mysql/production.cnf:/etc/mysql/my.cnf
#       - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
#     directories:
#       - data:/var/lib/mysql
#   redis:
#     image: valkey/valkey:8
#     host: 192.168.0.2
#     port: 6379
#     directories:
#       - data:/data

rails/blognet/app/config/environment.rb

# Load the Rails application.
require_relative "application"

# Initialize the Rails application.
Rails.application.initialize!

rails/blognet/app/config/environments/development.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Make code changes take effect immediately without server restart.
  config.enable_reloading = true

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports.
  config.consider_all_requests_local = true

  # Enable server timing.
  config.server_timing = true

  # Enable/disable Action Controller caching. By default Action Controller caching is disabled.
  # Run rails dev:cache to toggle Action Controller caching.
  if Rails.root.join("tmp/caching-dev.txt").exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true
    config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
  else
    config.action_controller.perform_caching = false
  end

  # Change to :null_store to avoid any caching.
  config.cache_store = :memory_store

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  # Make template changes take effect immediately.
  config.action_mailer.perform_caching = false

  # Set localhost to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Highlight code that triggered database queries in logs.
  config.active_record.verbose_query_logs = true

  # Append comments with runtime information tags to SQL queries in logs.
  config.active_record.query_log_tags_enabled = true

  # Highlight code that enqueued background job in logs.
  config.active_job.verbose_enqueue_logs = true

  # Highlight code that triggered redirect in logs.
  config.action_dispatch.verbose_redirect_logs = true

  # Suppress logger output for asset requests.
  config.assets.quiet = true

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  config.action_view.annotate_rendered_view_with_filenames = true

  # Uncomment if you wish to allow Action Cable access from any origin.
  # config.action_cable.disable_request_forgery_protection = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true

  # Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
  # config.generators.apply_rubocop_autocorrect_after_generate!
end

rails/blognet/app/config/environments/production.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Code is not reloaded between requests.
  config.enable_reloading = false

  # Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
  config.eager_load = true

  # Full error reports are disabled.
  config.consider_all_requests_local = false

  # Turn on fragment caching in view templates.
  config.action_controller.perform_caching = true

  # Cache assets for far-future expiry since they are all digest stamped.
  config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }

  # Enable serving of images, stylesheets, and JavaScripts from an asset server.
  # config.asset_host = "http://assets.example.com"

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Assume all access to the app is happening through a SSL-terminating reverse proxy.
  # config.assume_ssl = true

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  # config.force_ssl = true

  # Skip http-to-https redirect for the default health check endpoint.
  # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }

  # Log to STDOUT with the current request id as a default log tag.
  config.log_tags = [ :request_id ]
  config.logger   = ActiveSupport::TaggedLogging.logger(STDOUT)

  # Change to "debug" to log everything (including potentially personally-identifiable information!).
  config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")

  # Prevent health checks from clogging up the logs.
  config.silence_healthcheck_path = "/up"

  # Don't log any deprecations.
  config.active_support.report_deprecations = false

  # Replace the default in-process memory cache store with a durable alternative.
  config.cache_store = :solid_cache_store

  # Replace the default in-process and non-durable queuing backend for Active Job.
  config.active_job.queue_adapter = :solid_queue
  config.solid_queue.connects_to = { database: { writing: :queue } }

  # Ignore bad email addresses and do not raise email delivery errors.
  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
  # config.action_mailer.raise_delivery_errors = false

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
  # config.action_mailer.smtp_settings = {
  #   user_name: Rails.application.credentials.dig(:smtp, :user_name),
  #   password: Rails.application.credentials.dig(:smtp, :password),
  #   address: "smtp.example.com",
  #   port: 587,
  #   authentication: :plain
  # }

  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
  # the I18n.default_locale when a translation cannot be found).
  config.i18n.fallbacks = true

  # Do not dump schema after migrations.
  config.active_record.dump_schema_after_migration = false

  # Only use :id for inspections in production.
  config.active_record.attributes_for_inspect = [ :id ]

  # Enable DNS rebinding protection and other `Host` header attacks.
  # config.hosts = [
  #   "example.com",     # Allow requests from example.com
  #   /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
  # ]
  #
  # Skip DNS rebinding protection for the default health check endpoint.
  # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end

rails/blognet/app/config/environments/test.rb

# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # While tests run files are not watched, reloading is not necessary.
  config.enable_reloading = false

  # Eager loading loads your entire application. When running a single test locally,
  # this is usually not necessary, and can slow down your test suite. However, it's
  # recommended that you enable it in continuous integration systems to ensure eager
  # loading is working properly before deploying your code.
  config.eager_load = ENV["CI"].present?

  # Configure public file server for tests with cache-control for performance.
  config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }

  # Show full error reports.
  config.consider_all_requests_local = true
  config.cache_store = :null_store

  # Render exception templates for rescuable exceptions and raise for other exceptions.
  config.action_dispatch.show_exceptions = :rescuable

  # Disable request forgery protection in test environment.
  config.action_controller.allow_forgery_protection = false

  # Store uploaded files on the local file system in a temporary directory.
  config.active_storage.service = :test

  # Tell Action Mailer not to deliver emails to the real world.
  # The :test delivery method accumulates sent emails in the
  # ActionMailer::Base.deliveries array.
  config.action_mailer.delivery_method = :test

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Print deprecation notices to the stderr.
  config.active_support.deprecation = :stderr

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  # config.action_view.annotate_rendered_view_with_filenames = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true
end

rails/blognet/app/config/importmap.rb

# Pin npm packages by running ./bin/importmap

pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "@stimulus-components/dialog", to: "@stimulus-components--dialog.js" # @1.0.1
pin "@stimulus-components/auto-submit", to: "@stimulus-components--auto-submit.js" # @6.0.0
pin "@stimulus-components/character-counter", to: "@stimulus-components--character-counter.js" # @5.1.0
pin "@stimulus-components/dropdown", to: "@stimulus-components--dropdown.js" # @3.0.0
pin "stimulus-use" # @0.52.3
pin "@stimulus-components/clipboard", to: "@stimulus-components--clipboard.js" # @5.0.0
pin "@stimulus-components/notification", to: "@stimulus-components--notification.js" # @3.0.0
pin "@stimulus-components/timeago", to: "@stimulus-components--timeago.js" # @5.0.2
pin "date-fns" # @4.1.0
pin "@stimulus-components/animated-number", to: "@stimulus-components--animated-number.js" # @5.0.0
pin "@stimulus-components/sortable", to: "@stimulus-components--sortable.js" # @5.0.3
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_request", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_request.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_response", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_response.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/lib/utils", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--lib--utils.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/request_interceptor", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--request_interceptor.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/verbs", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--verbs.js" # @0.0.13
pin "@rails/request.js", to: "@rails--request.js.js" # @0.0.13
pin "sortablejs" # @1.15.7

rails/blognet/app/config/initializers/assets.rb

# Be sure to restart your server when you modify this file.

# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = "1.0"

# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path

rails/blognet/app/config/initializers/content_security_policy.rb

# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header

# Rails.application.configure do
#   config.content_security_policy do |policy|
#     policy.default_src :self, :https
#     policy.font_src    :self, :https, :data
#     policy.img_src     :self, :https, :data
#     policy.object_src  :none
#     policy.script_src  :self, :https
#     policy.style_src   :self, :https
#     # Specify URI for violation reports
#     # policy.report_uri "/csp-violation-report-endpoint"
#   end
#
#   # Generate session nonces for permitted importmap, inline scripts, and inline styles.
#   config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
#   config.content_security_policy_nonce_directives = %w(script-src style-src)
#
#   # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
#   # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
#   # config.content_security_policy_nonce_auto = true
#
#   # Report violations without enforcing the policy.
#   # config.content_security_policy_report_only = true
# end

rails/blognet/app/config/initializers/filter_parameter_logging.rb

# Be sure to restart your server when you modify this file.

# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
  :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
]

rails/blognet/app/config/initializers/inflections.rb

# Be sure to restart your server when you modify this file.

# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.plural /^(ox)$/i, "\\1en"
#   inflect.singular /^(ox)en/i, "\\1"
#   inflect.irregular "person", "people"
#   inflect.uncountable %w( fish sheep )
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.acronym "RESTful"
# end

rails/blognet/app/config/locales/en.yml

# Files in the config/locales directory are used for internationalization and
# are automatically loaded by Rails. If you want to use locales other than
# English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
#     I18n.t "hello"
#
# In views, this is aliased to just `t`:
#
#     <%= t("hello") %>
#
# To use a different locale, set it with `I18n.locale`:
#
#     I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# To learn more about the API, please read the Rails Internationalization guide
# at https://guides.rubyonrails.org/i18n.html.
#
# Be aware that YAML interprets the following case-insensitive strings as
# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
# must be quoted to be interpreted as strings. For example:
#
#     en:
#       "yes": yup
#       enabled: "ON"

en:
  hello: "Hello world"

rails/blognet/app/config/puma.rb

# This configuration file will be evaluated by Puma. The top-level methods that
# are invoked here are part of Puma's configuration DSL. For more information
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
#
# Puma starts a configurable number of processes (workers) and each process
# serves each request in a thread from an internal thread pool.
#
# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You
# should only set this value when you want to run 2 or more workers. The
# default is already 1. You can set it to `auto` to automatically start a worker
# for each available processor.
#
# The ideal number of threads per worker depends both on how much time the
# application spends waiting for IO operations and on how much you wish to
# prioritize throughput over latency.
#
# As a rule of thumb, increasing the number of threads will increase how much
# traffic a given process can handle (throughput), but due to CRuby's
# Global VM Lock (GVL) it has diminishing returns and will degrade the
# response time (latency) of the application.
#
# The default is set to 3 threads as it's deemed a decent compromise between
# throughput and latency for the average Rails application.
#
# Any libraries that use a connection pool or another resource pool should
# be configured to provide at least as many connections as the number of
# threads. This includes Active Record's `pool` parameter in `database.yml`.
threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
port ENV.fetch("PORT", 3000)

# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart

# Run the Solid Queue supervisor inside of Puma for single-server deployments.
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]

# Specify the PID file. Defaults to tmp/pids/server.pid in development.
# In other environments, only set the PID file if requested.
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]

rails/blognet/app/config/queue.yml

default: &default
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: "*"
      threads: 3
      processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
      polling_interval: 0.1

development:
  <<: *default

test:
  <<: *default

production:
  <<: *default

rails/blognet/app/config/recurring.yml

# examples:
#   periodic_cleanup:
#     class: CleanSoftDeletedRecordsJob
#     queue: background
#     args: [ 1000, { batch_size: 500 } ]
#     schedule: every hour
#   periodic_cleanup_with_command:
#     command: "SoftDeletedRecord.due.delete_all"
#     priority: 2
#     schedule: at 5am every day

production:
  clear_solid_queue_finished_jobs:
    command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
    schedule: every hour at minute 12

rails/blognet/app/config/routes.rb

Rails.application.routes.draw do
  resource  :session
  resources :passwords, param: :token

  resources :blogs, path: "b" do
    resources :posts, path: "p" do
      resources :comments, only: %i[create destroy]
    end
  end

  root "blogs#index"
  get "up", to: "rails/health#show", as: :rails_health_check
end

rails/blognet/app/config/storage.yml

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket-<%= Rails.env %>

# Remember not to checkin your GCS keyfile to a repository
# google:
#   service: GCS
#   project: your_project
#   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
#   bucket: your_own_bucket-<%= Rails.env %>

# mirror:
#   service: Mirror
#   primary: local
#   mirrors: [ amazon, google, microsoft ]

rails/blognet/app/db/cable_schema.rb

ActiveRecord::Schema[7.1].define(version: 1) do
  create_table "solid_cable_messages", force: :cascade do |t|
    t.binary "channel", limit: 1024, null: false
    t.binary "payload", limit: 536870912, null: false
    t.datetime "created_at", null: false
    t.integer "channel_hash", limit: 8, null: false
    t.index ["channel"], name: "index_solid_cable_messages_on_channel"
    t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
    t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
  end
end

rails/blognet/app/db/cache_schema.rb

ActiveRecord::Schema[7.2].define(version: 1) do
  create_table "solid_cache_entries", force: :cascade do |t|
    t.binary "key", limit: 1024, null: false
    t.binary "value", limit: 536870912, null: false
    t.datetime "created_at", null: false
    t.integer "key_hash", limit: 8, null: false
    t.integer "byte_size", limit: 4, null: false
    t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size"
    t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
    t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true
  end
end

rails/blognet/app/db/migrate/20260501020807_create_users.rb

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users do |t|
      t.string :email_address, null: false
      t.string :password_digest, null: false

      t.timestamps
    end
    add_index :users, :email_address, unique: true
  end
end

rails/blognet/app/db/migrate/20260501020818_create_sessions.rb

class CreateSessions < ActiveRecord::Migration[8.1]
  def change
    create_table :sessions do |t|
      t.references :user, null: false, foreign_key: true
      t.string :ip_address
      t.string :user_agent

      t.timestamps
    end
  end
end

rails/blognet/app/db/migrate/20260501020848_create_active_storage_tables.active_storage.rb

# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
  def change
    # Use Active Record's configured type for primary and foreign keys
    primary_key_type, foreign_key_type = primary_and_foreign_key_types

    create_table :active_storage_blobs, id: primary_key_type do |t|
      t.string   :key,          null: false
      t.string   :filename,     null: false
      t.string   :content_type
      t.text     :metadata
      t.string   :service_name, null: false
      t.bigint   :byte_size,    null: false
      t.string   :checksum

      if connection.supports_datetime_with_precision?
        t.datetime :created_at, precision: 6, null: false
      else
        t.datetime :created_at, null: false
      end

      t.index [ :key ], unique: true
    end

    create_table :active_storage_attachments, id: primary_key_type do |t|
      t.string     :name,     null: false
      t.references :record,   null: false, polymorphic: true, index: false, type: foreign_key_type
      t.references :blob,     null: false, type: foreign_key_type

      if connection.supports_datetime_with_precision?
        t.datetime :created_at, precision: 6, null: false
      else
        t.datetime :created_at, null: false
      end

      t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end

    create_table :active_storage_variant_records, id: primary_key_type do |t|
      t.belongs_to :blob, null: false, index: false, type: foreign_key_type
      t.string :variation_digest, null: false

      t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end
  end

  private
    def primary_and_foreign_key_types
      config = Rails.configuration.generators
      setting = config.options[config.orm][:primary_key_type]
      primary_key_type = setting || :primary_key
      foreign_key_type = setting || :bigint
      [ primary_key_type, foreign_key_type ]
    end
end

rails/blognet/app/db/migrate/20260501020920_create_action_text_tables.action_text.rb

# This migration comes from action_text (originally 20180528164100)
class CreateActionTextTables < ActiveRecord::Migration[6.0]
  def change
    # Use Active Record's configured type for primary and foreign keys
    primary_key_type, foreign_key_type = primary_and_foreign_key_types

    create_table :action_text_rich_texts, id: primary_key_type do |t|
      t.string     :name, null: false
      t.text       :body, size: :long
      t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type

      t.timestamps

      t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
    end
  end

  private
    def primary_and_foreign_key_types
      config = Rails.configuration.generators
      setting = config.options[config.orm][:primary_key_type]
      primary_key_type = setting || :primary_key
      foreign_key_type = setting || :bigint
      [ primary_key_type, foreign_key_type ]
    end
end

rails/blognet/app/db/migrate/20260507120001_create_blogs.rb

class CreateBlogs < ActiveRecord::Migration[8.1]
  def change
    create_table :blogs do |t|
      t.string :name
      t.text :description
      t.string :slug
      t.references :user, foreign_key: true
      t.boolean :published, default: false
      t.integer :posts_count, default: 0
      t.timestamps
    end
    add_index :blogs, :slug, unique: true
  end
end

rails/blognet/app/db/migrate/20260507120002_create_posts.rb

class CreatePosts < ActiveRecord::Migration[8.1]
  def change
    create_table :posts do |t|
      t.string :title
      t.string :slug
      t.references :blog, foreign_key: true
      t.references :user, foreign_key: true
      t.boolean :published, default: false
      t.datetime :published_at
      t.integer :views_count, default: 0
      t.integer :comments_count, default: 0
      t.timestamps
    end
    add_index :posts, :slug, unique: true
  end
end

rails/blognet/app/db/migrate/20260507120003_create_categories.rb

class CreateCategories < ActiveRecord::Migration[8.1]
  def change
    create_table :categories do |t|
      t.string :name
      t.string :slug
      t.text :description
      t.timestamps
    end
    add_index :categories, :slug, unique: true
  end
end

rails/blognet/app/db/migrate/20260507120004_create_categorizations.rb

class CreateCategorizations < ActiveRecord::Migration[8.1]
  def change
    create_table :categorizations do |t|
      t.references :post, foreign_key: true
      t.references :category, foreign_key: true
      t.timestamps
    end
  end
end

rails/blognet/app/db/migrate/20260507120005_create_comments.rb

class CreateComments < ActiveRecord::Migration[8.1]
  def change
    create_table :comments do |t|
      t.references :post, foreign_key: true
      t.references :user, foreign_key: true
      t.integer :parent_id
      t.text :content
      t.boolean :approved, default: true
      t.timestamps
    end
  end
end

rails/blognet/app/db/migrate/20260507120006_create_tags.rb

class CreateTags < ActiveRecord::Migration[8.1]
  def change
    create_table :tags do |t|
      t.string :name
      t.integer :posts_count, default: 0
      t.timestamps
    end
    add_index :tags, :name, unique: true
  end
end

rails/blognet/app/db/migrate/20260507120007_create_taggings.rb

class CreateTaggings < ActiveRecord::Migration[8.1]
  def change
    create_table :taggings do |t|
      t.references :post, foreign_key: true
      t.references :tag, foreign_key: true
      t.timestamps
    end
  end
end

rails/blognet/app/db/queue_schema.rb

ActiveRecord::Schema[7.1].define(version: 1) do
  create_table "solid_queue_blocked_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.string "queue_name", null: false
    t.integer "priority", default: 0, null: false
    t.string "concurrency_key", null: false
    t.datetime "expires_at", null: false
    t.datetime "created_at", null: false
    t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release"
    t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance"
    t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
  end

  create_table "solid_queue_claimed_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.bigint "process_id"
    t.datetime "created_at", null: false
    t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
    t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
  end

  create_table "solid_queue_failed_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.text "error"
    t.datetime "created_at", null: false
    t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true
  end

  create_table "solid_queue_jobs", force: :cascade do |t|
    t.string "queue_name", null: false
    t.string "class_name", null: false
    t.text "arguments"
    t.integer "priority", default: 0, null: false
    t.string "active_job_id"
    t.datetime "scheduled_at"
    t.datetime "finished_at"
    t.string "concurrency_key"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id"
    t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name"
    t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at"
    t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering"
    t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting"
  end

  create_table "solid_queue_pauses", force: :cascade do |t|
    t.string "queue_name", null: false
    t.datetime "created_at", null: false
    t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true
  end

  create_table "solid_queue_processes", force: :cascade do |t|
    t.string "kind", null: false
    t.datetime "last_heartbeat_at", null: false
    t.bigint "supervisor_id"
    t.integer "pid", null: false
    t.string "hostname"
    t.text "metadata"
    t.datetime "created_at", null: false
    t.string "name", null: false
    t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at"
    t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
    t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id"
  end

  create_table "solid_queue_ready_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.string "queue_name", null: false
    t.integer "priority", default: 0, null: false
    t.datetime "created_at", null: false
    t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true
    t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all"
    t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue"
  end

  create_table "solid_queue_recurring_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.string "task_key", null: false
    t.datetime "run_at", null: false
    t.datetime "created_at", null: false
    t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
    t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
  end

  create_table "solid_queue_recurring_tasks", force: :cascade do |t|
    t.string "key", null: false
    t.string "schedule", null: false
    t.string "command", limit: 2048
    t.string "class_name"
    t.text "arguments"
    t.string "queue_name"
    t.integer "priority", default: 0
    t.boolean "static", default: true, null: false
    t.text "description"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true
    t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static"
  end

  create_table "solid_queue_scheduled_executions", force: :cascade do |t|
    t.bigint "job_id", null: false
    t.string "queue_name", null: false
    t.integer "priority", default: 0, null: false
    t.datetime "scheduled_at", null: false
    t.datetime "created_at", null: false
    t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
    t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all"
  end

  create_table "solid_queue_semaphores", force: :cascade do |t|
    t.string "key", null: false
    t.integer "value", default: 1, null: false
    t.datetime "expires_at", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at"
    t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value"
    t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true
  end

  add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
  add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
  add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
  add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
  add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
  add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
end

rails/blognet/app/db/schema.rb

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 2026_05_01_020920) do
  create_table "action_text_rich_texts", force: :cascade do |t|
    t.text "body"
    t.datetime "created_at", null: false
    t.string "name", null: false
    t.bigint "record_id", null: false
    t.string "record_type", null: false
    t.datetime "updated_at", null: false
    t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
  end

  create_table "active_storage_attachments", force: :cascade do |t|
    t.bigint "blob_id", null: false
    t.datetime "created_at", null: false
    t.string "name", null: false
    t.bigint "record_id", null: false
    t.string "record_type", null: false
    t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
    t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
  end

  create_table "active_storage_blobs", force: :cascade do |t|
    t.bigint "byte_size", null: false
    t.string "checksum"
    t.string "content_type"
    t.datetime "created_at", null: false
    t.string "filename", null: false
    t.string "key", null: false
    t.text "metadata"
    t.string "service_name", null: false
    t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
  end

  create_table "active_storage_variant_records", force: :cascade do |t|
    t.bigint "blob_id", null: false
    t.string "variation_digest", null: false
    t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
  end

  create_table "sessions", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.string "ip_address"
    t.datetime "updated_at", null: false
    t.string "user_agent"
    t.integer "user_id", null: false
    t.index ["user_id"], name: "index_sessions_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.string "email_address", null: false
    t.string "password_digest", null: false
    t.datetime "updated_at", null: false
    t.index ["email_address"], name: "index_users_on_email_address", unique: true
  end

  add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
  add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
  add_foreign_key "sessions", "users"
end

rails/blognet/app/db/seeds.rb

user = User.find_or_create_by!(email_address: "admin@blognet.example") do |u|
  u.password = u.password_confirmation = "password123"
end

blog = Blog.find_or_create_by!(slug: "demo-blog") do |b|
  b.name        = "Demo Blog"
  b.description = "A demonstration blog"
  b.user        = user
  b.published   = true
end

5.times do |i|
  Post.find_or_create_by!(slug: "post-#{i + 1}") do |p|
    p.title     = "Post #{i + 1}: Getting Started with Rails 8"
    p.body      = "Rails 8 ships with Solid Cache, Solid Queue, and Solid Cable out of the box. This post covers what changed."
    p.blog      = blog
    p.user      = user
    p.published = true
  end
end
puts "Seeded #{Post.count} posts"

rails/blognet/app/public/robots.txt

# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file

rails/blognet/blognet.sh

#!/usr/bin/env zsh
# blognet.sh — deploys tracked Rails tree at app/ as %APP_NAME%
set -euo pipefail

APP_NAME=%APP_NAME%
APP_DIR=/home/${APP_NAME}/app
APP_PORT=10002
APP_DOMAIN=
SCRIPT_DIR=${0:a:h}
SRC_DIR=${SCRIPT_DIR}/app

. "${SCRIPT_DIR:h}/@shared_functions.sh"

need_cmd ruby34 bundle doas

[[ -d $SRC_DIR ]] || { log_err "missing source tree: $SRC_DIR"; exit 1 }

log "${APP_NAME} — deploying tracked tree → ${APP_DIR}"

id "$APP_NAME" >/dev/null 2>&1 || doas useradd -m -L daemon -s /bin/ksh "$APP_NAME"
doas mkdir -p "$APP_DIR"

doas cp -R "${SRC_DIR}/." "${APP_DIR}/"
doas chown -R "${APP_NAME}:${APP_NAME}" "$APP_DIR"

cd "$APP_DIR"

typeset bundle_home="/home/${APP_NAME}/.bundle"
if [[ ! -d ${bundle_home}/gems ]]; then
  log "Bootstrapping gems from amber"
  doas mkdir -p "$bundle_home"
  doas cp -R /home/amber/.bundle/gems "$bundle_home/"
  doas chown -R "${APP_NAME}:${APP_NAME}" "$bundle_home"
fi
print "---\nBUNDLE_PATH: \"${bundle_home}/gems\"" | doas tee "${APP_DIR}/.bundle/config" >/dev/null

doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bundle install --deployment --without development:test"
doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:create db:migrate"
[[ -f ${APP_DIR}/db/seeds.rb ]] && doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:seed" || true

install_rcd "$APP_NAME" "$APP_DIR" "$APP_PORT" "$APP_NAME"
[[ -n $APP_DOMAIN ]] && relayd_add_relay "$APP_DOMAIN" "$APP_PORT"

doas rcctl restart "$APP_NAME" || doas rcctl start "$APP_NAME"
log_ok "$APP_NAME live on :$APP_PORT"

rails/blognet/blognet_test.sh

rails/brgen/README.md

# brgen

### brgen.no, oshlo.no, trndheim.no, stvanger.no, trmso.no, longyearbyn.no, reykjavk.is, kbenhvn.dk, stholm.se, gtebrg.se, mlmoe.se, hlsinki.fi, lndon.uk, cardff.uk, mnchester.uk, brmingham.uk, lverpool.uk, edinbrgh.uk, glasgw.uk, amstrdam.nl, rottrdam.nl, utrcht.nl, brssels.be, zrich.ch, lchtenstein.li, frankfrt.de, wrsawa.pl, gdnsk.pl, brdeaux.fr, mrseille.fr, mlan.it, lsbon.pt, lsangeles.com, newyrk.us, chcago.us, houstn.us, dllas.us, austn.us, prtland.com, mnneapolis.com

Brgen is a hyperlocal social network unique to every major city. One Rails 8 codebase, served per-domain via SNI, with sub-applications for marketplace, dating, music, TV, and street-food takeaway. Monetization: SEO, PPC, affiliate marketing, targeted email.

### Sub-applications

| Namespace | Subdomain | Models |
|---|---|---|
| `Marketplace::` | `markedsplass.brgen.no` (and locale aliases: markadur, marknadsplats, marktplaats, marche, mercato, mercado, markkinapaikka, marketplace) | Category, Listing, Order |
| `Dating::` | `dating.brgen.no` | Profile, Like, Dislike, Match |
| `Playlist::` | `playlist.brgen.no` | Playlist, Track, PlaylistTrack, Listen |
| `Tv::` | `tv.brgen.no` | Channel, Video, Broadcast, Subscription, ViewEvent |
| `Takeaway::` | `takeaway.brgen.no` | Restaurant, MenuItem, Order, OrderItem |

### Stack

Rails 8 · SQLite3 · Solid Queue · Solid Cache · Hotwire (Turbo + Stimulus) · Devise · OmniAuth · Falcon · Active Storage · ImageProcessing · I18n.

### Deploy

```zsh
doas zsh DEPLOY/rails/brgen/brgen.sh

Idempotent. Installs gems, migrates, seeds, registers rc.d/brgen on the random app port, and adds the relayd backend.

DNS, TLS, HAProxy SNI routing, and per-city domain registration are handled by DEPLOY/openbsd/openbsd.sh.


## `rails/brgen/README_takeaway.md`
```markdown
# brgen takeaway

Food ordering subapp for brgen.no. Rails 8. PostgreSQL.

## Models

- `Restaurant` — dining location with geocoding
- `MenuItem` — menu item with availability states and monetized price
- `Order` — lifecycle: placed → accepted → preparing → dispatched → delivered / canceled

## Deploy

```zsh
doas zsh brgen_takeaway.sh

## `rails/brgen/README_tv.md`
```markdown
# brgen tv

Video and live-streaming subapp for brgen.no. Rails 8. PostgreSQL + Redis.

## Deploy

```zsh
doas zsh brgen_tv.sh

## `rails/brgen/app/Dockerfile`
```text
# syntax=docker/dockerfile:1
# check=error=true

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t app .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name app app

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.3.7
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips postgresql-client && \
    ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment variables and enable jemalloc for reduced memory usage and latency.
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development" \
    LD_PRELOAD="/usr/local/lib/libjemalloc.so"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git libpq-dev libyaml-dev pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY Gemfile Gemfile.lock vendor ./

RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
    bundle exec bootsnap precompile -j 1 --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times.
# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
RUN bundle exec bootsnap precompile -j 1 app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile




# Final stage for app image
FROM base

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
USER 1000:1000

# Copy built artifacts: gems, application
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --chown=rails:rails --from=build /rails /rails

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]

rails/brgen/app/Gemfile

source "https://rubygems.org"
ruby "~> 3.3"

gem "rails", "~> 8.0"
gem "sqlite3", "~> 2.1"
gem "falcon"
gem "async"
gem "async-http"

# Real-time
gem "turbo-rails"
gem "stimulus-rails"
gem "importmap-rails"

# Solid Stack (Rails 8)
gem "solid_queue"
gem "solid_cache"
gem "solid_cable"

# Authentication
gem "bcrypt", "~> 3.1"

# Social
gem "acts_as_tenant"

# Features
gem "pagy"
gem "image_processing"
gem "geocoder"

# Discovery — vision-LLM scrapers (lib/tasks/{reddit,amazon}.rake)
gem "ferrum"

group :development, :test do
  gem "brakeman"
  gem "rubocop-rails-omakase"
  gem "faker"
end

rails/brgen/app/README.md

# README

This README would normally document whatever steps are necessary to get the
application up and running.

Things you may want to cover:

* Ruby version

* System dependencies

* Configuration

* Database creation

* Database initialization

* How to run the test suite

* Services (job queues, cache servers, search engines, etc.)

* Deployment instructions

* ...

rails/brgen/app/Rakefile

# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative "config/application"

Rails.application.load_tasks

rails/brgen/app/app/channels/application_cable/channel.rb

module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end

rails/brgen/app/app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      set_current_user || reject_unauthorized_connection
    end

    private
      def set_current_user
        if session = Session.find_by(id: cookies.signed[:session_id])
          self.current_user = session.user
        end
      end
  end
end

rails/brgen/app/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Authentication
  include Pagy::Method
  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
  allow_browser versions: :modern

  # Changes to the importmap will invalidate the etag for HTML responses
  stale_when_importmap_changes
end

rails/brgen/app/app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :require_real_user
  before_action :set_commentable

  def create
    @comment = @commentable.comments.build(comment_params)
    @comment.user      = Current.user
    @comment.parent_id = params[:parent_id] if params[:parent_id]

    if @comment.save
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_back fallback_location: root_path }
      end
    else
      respond_to do |format|
        format.turbo_stream { render turbo_stream: turbo_stream.replace("comment_form", partial: "comments/form", locals: { comment: @comment, commentable: @commentable }) }
        format.html         { redirect_back fallback_location: root_path, alert: @comment.errors.full_messages.to_sentence }
      end
    end
  end

  def destroy
    @comment = Comment.find(params[:id])
    @comment.destroy if @comment.user == Current.user
    respond_to do |format|
      format.turbo_stream { render turbo_stream: turbo_stream.remove(dom_id(@comment)) }
      format.html         { redirect_back fallback_location: root_path }
    end
  end

  private

  def set_commentable
    if params[:post_id]
      @commentable = Post.find(params[:post_id])
    elsif params[:comment_id]
      @commentable = Comment.find(params[:comment_id])
    end
  end

  def comment_params
    params.require(:comment).permit(:content)
  end
end

rails/brgen/app/app/controllers/communities_controller.rb

class CommunitiesController < ApplicationController
  before_action :require_real_user, only: [:new, :create]
  before_action :set_community,     only: [:show]

  def index
    @communities = Community.popular.includes(:user)
  end

  def show
    @posts = @community.posts.hot.includes(:user, :votes)
  end

  def new
    @community = Community.new
  end

  def create
    @community = Community.new(community_params)
    @community.user = Current.user
    if @community.save
      redirect_to @community, notice: "Community created."
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_community    = @community = Community.find(params[:id])
  def community_params = params.require(:community).permit(:name, :description)
end

rails/brgen/app/app/controllers/concerns/authentication.rb

module Authentication
  extend ActiveSupport::Concern

  included do
    before_action :resume_session
    helper_method :authenticated?, :current_user
  end

  class_methods do
    # Rails 8 compat: controllers can declare they don't need auth enforcement
    def allow_unauthenticated_access(**options)
      skip_before_action :resume_session, **options rescue nil
    end
  end

  private

  def authenticated?
    Current.user.present? && !Current.user.guest?
  end

  def current_user
    Current.user
  end

  def resume_session
    Current.session = find_session_by_cookie
    if Current.session
      Current.user = Current.session.user
    else
      Current.user = find_or_create_guest_user
    end
  end

  def find_session_by_cookie
    Session.find_by(id: cookies.signed[:session_id])
  end

  def find_or_create_guest_user
    guest_id = session[:guest_user_id]
    if guest_id
      User.find_by(id: guest_id, guest: true) || create_guest_user
    else
      create_guest_user
    end
  end

  def create_guest_user
    guest = User.create!(
      email_address: "guest_#{SecureRandom.hex(8)}@guest.local",
      password: SecureRandom.hex(16),
      guest: true
    )
    session[:guest_user_id] = guest.id
    guest
  end

  def require_real_user
    unless authenticated?
      redirect_to new_session_path, alert: "Sign in to continue"
    end
  end

  # Rails 8 compat alias
  alias_method :require_authentication, :resume_session
end

rails/brgen/app/app/controllers/conversations_controller.rb

class ConversationsController < ApplicationController
  before_action :require_real_user

  def index
    @conversations = Conversation.for_user(Current.user)
                                 .includes(:participants, :messages)
                                 .order("messages.created_at DESC")
  end

  def show
    @conversation = Conversation.for_user(Current.user).find(params[:id])
    @conversation.mark_read_for!(Current.user)
    @messages = @conversation.messages.recent.limit(50).reverse
    @message  = Message.new
  end

  def create
    other         = User.find(params[:user_id])
    @conversation = Conversation.find_or_create_direct(Current.user, other)
    redirect_to @conversation
  end
end

rails/brgen/app/app/controllers/dating/base_controller.rb

class Dating::BaseController < ApplicationController

end

rails/brgen/app/app/controllers/dating/dislikes_controller.rb

class Dating::DislikesController < Dating::BaseController
  def create
    user = User.find(params[:user_id])
    Dating::Dislike.find_or_create_by!(disliker: Current.user, dislikee: user)
    redirect_to dating_root_path
  end
end

rails/brgen/app/app/controllers/dating/home_controller.rb

class Dating::HomeController < Dating::BaseController
  def index
    profile = Current.user.dating_profile
    unless profile&.visible?
      redirect_to edit_dating_profile_path and return
    end
    liked_ids    = Dating::Like.where(liker: Current.user).pluck(:likee_id)
    disliked_ids = Dating::Dislike.where(disliker: Current.user).pluck(:dislikee_id)
    excluded     = (liked_ids + disliked_ids + [Current.user.id]).uniq
    @pagy, @profiles = pagy(
      Dating::Profile.visible
        .where.not(user_id: excluded)
        .includes(:user)
        .order(Arel.sql("RANDOM()"))
    )
  end
end

rails/brgen/app/app/controllers/dating/likes_controller.rb

class Dating::LikesController < Dating::BaseController
  def create
    user = User.find(params[:user_id])
    Dating::Like.find_or_create_by!(liker: Current.user, likee: user)
    redirect_to dating_root_path
  end
end

rails/brgen/app/app/controllers/dating/matches_controller.rb

class Dating::MatchesController < Dating::BaseController
  def index
    @pagy, @matches = pagy(
      Dating::Match.active
        .where("initiator_id = ? OR receiver_id = ?", Current.user.id, Current.user.id)
        .includes(:initiator, :receiver)
    )
  end
end

rails/brgen/app/app/controllers/dating/profiles_controller.rb

class Dating::ProfilesController < Dating::BaseController
  before_action :set_profile, only: %i[show edit update]

  def show; end
  def edit; end

  def new
    @profile = Current.user.build_dating_profile
  end

  def create
    @profile = Current.user.build_dating_profile(profile_params)
    @profile.save ?
      redirect_to(dating_root_path, notice: "Profile created") :
      render(:new, status: :unprocessable_entity)
  end

  def update
    @profile.update(profile_params) ?
      redirect_to(dating_root_path, notice: "Profile updated") :
      render(:edit, status: :unprocessable_entity)
  end

  private
  def set_profile    = (@profile = Current.user.dating_profile || redirect_to(new_dating_profile_path))
  def profile_params = params.require(:dating_profile).permit(:bio, :gender, :looking_for, :age, :location, :latitude, :longitude, :visible, photos: [])
end

rails/brgen/app/app/controllers/follows_controller.rb

class FollowsController < ApplicationController
  before_action :require_real_user

  def create
    user = User.find(params[:user_id])
    Current.user.follow!(user)
    redirect_back fallback_location: root_path
  end

  def destroy
    user = User.find(params[:user_id])
    Current.user.unfollow!(user)
    redirect_back fallback_location: root_path
  end
end

rails/brgen/app/app/controllers/home_controller.rb

class HomeController < ApplicationController
  def index
    @posts = if authenticated?
               Current.user.timeline_posts.hot.includes(:user, :community, :votes).limit(50)
             else
               Post.hot.includes(:user, :community, :votes).limit(50)
             end
    @communities = Community.popular.limit(10)
  end
end

rails/brgen/app/app/controllers/marketplace/base_controller.rb

class Marketplace::BaseController < ApplicationController

end

rails/brgen/app/app/controllers/marketplace/categories_controller.rb

class Marketplace::CategoriesController < Marketplace::BaseController
  allow_unauthenticated_access only: %i[show]

  def show
    @category = Marketplace::Category.find_by!(slug: params[:id])
    @pagy, @listings = pagy(@category.listings.active.recent)
  end
end

rails/brgen/app/app/controllers/marketplace/listings_controller.rb

class Marketplace::ListingsController < Marketplace::BaseController
  allow_unauthenticated_access only: %i[index show]
  before_action :set_listing, only: %i[show edit update destroy]

  def index
    scope = Marketplace::Listing.active.includes(:user, :category)
    scope = scope.where("title LIKE ?", "%#{params[:q]}%") if params[:q].present?
    scope = scope.where(category_id: params[:category_id]) if params[:category_id].present?
    @pagy, @listings = pagy(scope.recent)
    @categories = Marketplace::Category.roots.includes(:children)
  end

  def show
    @listing.increment!(:views_count)
    @order = Marketplace::Order.new if authenticated?
  end

  def new
    @listing   = Marketplace::Listing.new
    @categories = Marketplace::Category.all
  end

  def create
    @listing = Current.user.marketplace_listings.build(listing_params)
    @listing.save ?
      redirect_to(marketplace_listing_path(@listing), notice: "Listed") :
      render(:new, status: :unprocessable_entity)
  end

  def edit
    @categories = Marketplace::Category.all
  end

  def update
    @listing.update(listing_params) ?
      redirect_to(marketplace_listing_path(@listing)) :
      render(:edit, status: :unprocessable_entity)
  end

  def destroy
    @listing.update!(status: "removed")
    redirect_to marketplace_listings_path
  end

  private
  def set_listing    = (@listing = Marketplace::Listing.find(params[:id]))
  def listing_params = params.require(:marketplace_listing).permit(
    :title, :description, :price_cents, :condition, :status, :location,
    :category_id, photos: []
  )
end

rails/brgen/app/app/controllers/marketplace/orders_controller.rb

class Marketplace::OrdersController < Marketplace::BaseController
  before_action :set_listing

  def create
    @order = @listing.orders.build(buyer: Current.user,
                                   message: params.dig(:marketplace_order, :message),
                                   price_cents: @listing.price_cents)
    @order.save ?
      redirect_to(marketplace_listing_path(@listing), notice: "Offer sent") :
      redirect_to(marketplace_listing_path(@listing), alert: "Could not send offer")
  end

  def update
    @order = Marketplace::Order.find(params[:id])
    if @order.seller == Current.user
      @order.accept!   if params[:accept]
      @order.decline!  if params[:decline]
    end
    redirect_to marketplace_listing_path(@listing)
  end

  private
  def set_listing = (@listing = Marketplace::Listing.find(params[:listing_id]))
end

rails/brgen/app/app/controllers/messages_controller.rb

class MessagesController < ApplicationController
  before_action :require_real_user
  before_action :set_conversation

  def create
    @message        = @conversation.messages.build(message_params)
    @message.sender = Current.user

    if @message.save
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_to @conversation }
      end
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_conversation
    @conversation = Conversation.for_user(Current.user).find(params[:conversation_id])
  end

  def message_params
    params.require(:message).permit(:content, :message_type)
  end
end

rails/brgen/app/app/controllers/passwords_controller.rb

class PasswordsController < ApplicationController
  allow_unauthenticated_access
  before_action :set_user_by_token, only: %i[ edit update ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.find_by(email_address: params[:email_address])
      PasswordsMailer.reset(user).deliver_later
    end

    redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
  end

  def edit
  end

  def update
    if @user.update(params.permit(:password, :password_confirmation))
      @user.sessions.destroy_all
      redirect_to new_session_path, notice: "Password has been reset."
    else
      redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
    end
  end

  private
    def set_user_by_token
      @user = User.find_by_password_reset_token!(params[:token])
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
    end
end

rails/brgen/app/app/controllers/playlist/base_controller.rb

class Playlist::BaseController < ApplicationController

end

rails/brgen/app/app/controllers/playlist/listens_controller.rb

class Playlist::ListensController < Playlist::BaseController
  def create
    track = Playlist::Track.find(params[:track_id])
    Playlist::Listen.create!(user: Current.user, track: track)
    render json: { ok: true }
  end
end

rails/brgen/app/app/controllers/playlist/playlists_controller.rb

class Playlist::PlaylistsController < Playlist::BaseController
  allow_unauthenticated_access only: %i[index show]
  before_action :set_playlist, only: %i[show edit update destroy]

  def index
    @pagy, @playlists = pagy(Playlist::Playlist.public_playlists.popular.includes(:user))
  end

  def show
    @tracks = @playlist.playlist_tracks.includes(:track)
  end

  def new
    @playlist = Playlist::Playlist.new
  end

  def create
    @playlist = Current.user.playlist_playlists.build(playlist_params)
    @playlist.save ?
      redirect_to(playlist_playlist_path(@playlist), notice: "Playlist created") :
      render(:new, status: :unprocessable_entity)
  end

  def edit; end

  def update
    @playlist.update(playlist_params) ?
      redirect_to(playlist_playlist_path(@playlist)) :
      render(:edit, status: :unprocessable_entity)
  end

  def destroy
    @playlist.destroy
    redirect_to playlist_playlists_path
  end

  private
  def set_playlist    = (@playlist = Playlist::Playlist.find(params[:id]))
  def playlist_params = params.require(:playlist_playlist).permit(:name, :description, :public_access)
end

rails/brgen/app/app/controllers/playlist/tracks_controller.rb

class Playlist::TracksController < Playlist::BaseController
  before_action :set_playlist

  def create
    track = Playlist::Track.find_or_create_by!(title: params.dig(:playlist_track, :title),
                                               artist: params.dig(:playlist_track, :artist)) do |t|
      t.assign_attributes(track_params.except(:title, :artist))
    end
    @playlist.add_track!(track, user: Current.user)
    redirect_to playlist_playlist_path(@playlist), notice: "Track added"
  end

  def destroy
    pt = @playlist.playlist_tracks.find(params[:id])
    pt.destroy
    redirect_to playlist_playlist_path(@playlist)
  end

  private
  def set_playlist  = (@playlist = Playlist::Playlist.find(params[:playlist_id]))
  def track_params  = params.require(:playlist_track).permit(:title, :artist, :album, :duration_seconds, :source_type, :source_url, :genre)
end

rails/brgen/app/app/controllers/playlist_controller.rb

class PlaylistController < ApplicationController
  def index
    @playlists = [
      {name: "Bergen Beats", tracks: 12, genre: "Electronic"},
      {name: "Norwegian Folk", tracks: 8, genre: "Folk"},
      {name: "Midnight Jazz", tracks: 15, genre: "Jazz"}
    ]
  end
end

rails/brgen/app/app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :require_real_user, only: [:new, :create, :edit, :update, :destroy]
  before_action :set_post,          only: [:show, :edit, :update, :destroy]
  before_action :set_community,     only: [:new, :create]

  def index
    @posts = Post.hot.includes(:user, :community, :votes)
  end

  def show
    @comments    = @post.comments.where(parent_id: nil).best.includes(:user, :votes, replies: [:user, :votes])
    @new_comment = Comment.new
  end

  def new
    @post = Post.new(community: @community)
  end

  def create
    @post           = Post.new(post_params)
    @post.user      = Current.user
    @post.community = @community if @community
    if @post.save
      redirect_to @post, notice: "Posted."
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit; end

  def update
    if @post.update(post_params)
      redirect_to @post
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @post.destroy
    redirect_to posts_path
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end

  def set_community
    @community = Community.find_by(id: params[:community_id])
  end

  def post_params
    params.require(:post).permit(:title, :content, :community_id)
  end
end

rails/brgen/app/app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  allow_unauthenticated_access only: %i[ new create ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

  def destroy
    terminate_session
    redirect_to new_session_path, status: :see_other
  end
end

rails/brgen/app/app/controllers/takeaway/base_controller.rb

class Takeaway::BaseController < ApplicationController

end

rails/brgen/app/app/controllers/takeaway/menu_items_controller.rb

class Takeaway::MenuItemsController < Takeaway::BaseController
  before_action :set_restaurant

  def create
    @item = @restaurant.menu_items.build(item_params)
    @item.save ?
      redirect_to(takeaway_restaurant_path(@restaurant), notice: "Item added") :
      redirect_to(takeaway_restaurant_path(@restaurant), alert: @item.errors.full_messages.to_sentence)
  end

  def destroy
    @restaurant.menu_items.find(params[:id]).destroy
    redirect_to takeaway_restaurant_path(@restaurant)
  end

  private
  def set_restaurant = (@restaurant = Current.user.takeaway_restaurants.find(params[:restaurant_id]))
  def item_params    = params.require(:takeaway_menu_item).permit(:name, :description, :price_cents, :available, :vegetarian, :vegan, :photo)
end

rails/brgen/app/app/controllers/takeaway/orders_controller.rb

class Takeaway::OrdersController < Takeaway::BaseController
  before_action :set_restaurant, only: %i[new create]

  def index
    @pagy, @orders = pagy(Current.user.takeaway_orders.recent.includes(:restaurant))
  end

  def show
    @order = Current.user.takeaway_orders.find(params[:id])
  end

  def new
    @order      = Takeaway::Order.new
    @menu_items = @restaurant.menu_items.available
  end

  def create
    @order = @restaurant.orders.build(order_params.merge(user: Current.user))
    item_params.each do |item_id, qty|
      next unless qty.to_i > 0
      item = @restaurant.menu_items.find_by(id: item_id)
      next unless item
      @order.order_items.build(menu_item: item, quantity: qty.to_i, unit_price_cents: item.price_cents)
    end
    if @order.save
      @order.calculate_totals!
      redirect_to takeaway_order_path(@order), notice: "Order placed"
    else
      @menu_items = @restaurant.menu_items.available
      render :new, status: :unprocessable_entity
    end
  end

  def update
    @order = Takeaway::Order.find(params[:id])
    @order.advance_status! if @order.restaurant.user == Current.user
    redirect_to takeaway_order_path(@order)
  end

  private
  def set_restaurant = (@restaurant = Takeaway::Restaurant.find(params[:restaurant_id]))
  def order_params   = params.require(:takeaway_order).permit(:delivery_address, :special_instructions)
  def item_params    = params.dig(:takeaway_order, :items) || {}
end

rails/brgen/app/app/controllers/takeaway/restaurants_controller.rb

class Takeaway::RestaurantsController < Takeaway::BaseController
  allow_unauthenticated_access only: %i[index show]
  before_action :set_restaurant, only: %i[show edit update destroy]

  def index
    scope = Takeaway::Restaurant.active.includes(:user)
    scope = scope.where(cuisine_type: params[:cuisine]) if params[:cuisine].present?
    scope = scope.where("name LIKE ?", "%#{params[:q]}%") if params[:q].present?
    @pagy, @restaurants = pagy(scope.popular)
  end

  def show
    @menu_items = @restaurant.menu_items.available
  end

  def new
    @restaurant = Takeaway::Restaurant.new
  end

  def create
    @restaurant = Current.user.takeaway_restaurants.build(restaurant_params)
    @restaurant.save ?
      redirect_to(takeaway_restaurant_path(@restaurant), notice: "Restaurant created") :
      render(:new, status: :unprocessable_entity)
  end

  def edit; end

  def update
    @restaurant.update(restaurant_params) ?
      redirect_to(takeaway_restaurant_path(@restaurant)) :
      render(:edit, status: :unprocessable_entity)
  end

  def destroy
    @restaurant.destroy
    redirect_to takeaway_restaurants_path
  end

  private
  def set_restaurant    = (@restaurant = Takeaway::Restaurant.find(params[:id]))
  def restaurant_params = params.require(:takeaway_restaurant).permit(
    :name, :description, :address, :city, :phone, :cuisine_type,
    :delivery_fee_cents, :min_order_cents, :active
  )
end

rails/brgen/app/app/controllers/tv/base_controller.rb

class Tv::BaseController < ApplicationController

end

rails/brgen/app/app/controllers/tv/channels_controller.rb

class Tv::ChannelsController < Tv::BaseController
  allow_unauthenticated_access only: %i[index show]
  before_action :set_channel, only: %i[show edit update destroy subscribe unsubscribe]

  def index    = (@pagy, @channels = pagy(Tv::Channel.popular.includes(:user)))
  def show     = (@pagy, @videos = pagy(@channel.videos.published))
  def new      = (@channel = Tv::Channel.new)
  def edit;    end

  def create
    @channel = Current.user.tv_channels.build(channel_params)
    @channel.save ? redirect_to(tv_channel_path(@channel), notice: "Channel created") : render(:new, status: :unprocessable_entity)
  end

  def update
    @channel.update(channel_params) ? redirect_to(tv_channel_path(@channel)) : render(:edit, status: :unprocessable_entity)
  end

  def destroy = (@channel.destroy and redirect_to tv_channels_path)

  def subscribe
    Tv::Subscription.find_or_create_by!(user: Current.user, tv_channel: @channel)
    redirect_back fallback_location: tv_channel_path(@channel)
  end

  def unsubscribe
    Tv::Subscription.find_by(user: Current.user, tv_channel: @channel)&.destroy
    redirect_back fallback_location: tv_channel_path(@channel)
  end

  private
  def set_channel    = (@channel = Tv::Channel.find_by!(slug: params[:id]))
  def channel_params = params.require(:tv_channel).permit(:name, :description, :banner, :avatar)
end

rails/brgen/app/app/controllers/tv/home_controller.rb

class Tv::HomeController < Tv::BaseController
  allow_unauthenticated_access

  def index
    @pagy_trending, @trending = pagy(Tv::Video.trending.includes(:channel), limit: 12)
    @live   = Tv::Broadcast.live.includes(:channel).limit(6)
    @recent = Tv::Video.recent.includes(:channel).limit(8)
  end
end

rails/brgen/app/app/controllers/tv/videos_controller.rb

class Tv::VideosController < Tv::BaseController
  allow_unauthenticated_access only: %i[show]
  before_action :set_video, only: %i[show destroy]

  def show
    @video.view_events.create!(user: Current.user) if authenticated?
    @video.increment!(:views_count)
  end

  def new  = (@video = Tv::Video.new)

  def create
    channel = Current.user.tv_channels.find(params[:tv_channel_id])
    @video  = channel.videos.build(video_params.merge(user: Current.user, status: "ready"))
    @video.save ? redirect_to(tv_video_path(@video), notice: "Video uploaded") : render(:new, status: :unprocessable_entity)
  end

  def destroy = (@video.destroy and redirect_to tv_root_path)

  private
  def set_video    = (@video = Tv::Video.find(params[:id]))
  def video_params = params.require(:tv_video).permit(:title, :description, :video_file, :thumbnail, :tv_channel_id)
end

rails/brgen/app/app/controllers/typing_indicators_controller.rb

class TypingIndicatorsController < ApplicationController
  before_action :authenticate_user!

  def create
    conversation = Conversation.for_user(current_user).find(params[:conversation_id])
    TypingIndicator.set!(conversation:, user: current_user)
    head :ok
  end
end

rails/brgen/app/app/controllers/votes_controller.rb

class VotesController < ApplicationController
  before_action :require_authentication

  def create
    @votable = find_votable
    vote     = @votable.votes.find_or_initialize_by(user: Current.user)
    value    = params.dig(:vote, :value).to_i

    if vote.persisted? && vote.value == value
      vote.destroy
    else
      vote.update!(value:)
    end

    respond_to do |format|
      format.turbo_stream
      format.html { redirect_back fallback_location: root_path }
    end
  end

  private

  def find_votable
    return Post.find(params[:post_id])       if params[:post_id]
    return Comment.find(params[:comment_id]) if params[:comment_id]
    raise ActiveRecord::RecordNotFound, "no votable in params"
  end
end

rails/brgen/app/app/helpers/application_helper.rb

module ApplicationHelper
end

rails/brgen/app/app/javascript/application.js

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"

rails/brgen/app/app/javascript/controllers/animated_number_controller.js

import AnimatedNumber from "@stimulus-components/animated-number"
export default class extends AnimatedNumber {}

rails/brgen/app/app/javascript/controllers/application.js

import { Application } from "@hotwired/stimulus"

const application = Application.start()

// Configure Stimulus development experience
application.debug = false
window.Stimulus   = application

export { application }

rails/brgen/app/app/javascript/controllers/auto_submit_controller.js

import AutoSubmit from "@stimulus-components/auto-submit"
export default class extends AutoSubmit {}

rails/brgen/app/app/javascript/controllers/character_counter_controller.js

import CharacterCounter from "@stimulus-components/character-counter"
export default class extends CharacterCounter {}

rails/brgen/app/app/javascript/controllers/clipboard_controller.js

import Clipboard from "@stimulus-components/clipboard"
export default class extends Clipboard {}

rails/brgen/app/app/javascript/controllers/dialog_controller.js

import Dialog from "@stimulus-components/dialog"
export default class extends Dialog {}

rails/brgen/app/app/javascript/controllers/dropdown_controller.js

import Dropdown from "@stimulus-components/dropdown"
export default class extends Dropdown {}

rails/brgen/app/app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.textContent = "Hello World!"
  }
}

rails/brgen/app/app/javascript/controllers/index.js

// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)

rails/brgen/app/app/javascript/controllers/notification_controller.js

import Notification from "@stimulus-components/notification"
export default class extends Notification {}

rails/brgen/app/app/javascript/controllers/sortable_controller.js

import Sortable from "@stimulus-components/sortable"
export default class extends Sortable {}

rails/brgen/app/app/javascript/controllers/textarea_autogrow_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.resize()
    this.element.addEventListener("input", this.resize)
  }

  disconnect() {
    this.element.removeEventListener("input", this.resize)
  }

  resize = () => {
    this.element.style.height = "auto"
    this.element.style.height = `${this.element.scrollHeight}px`
  }
}

rails/brgen/app/app/javascript/controllers/timeago_controller.js

import TimeAgo from "@stimulus-components/timeago"
export default class extends TimeAgo {}

rails/brgen/app/app/javascript/controllers/typing_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { conversationId: Number }

  connect() {
    this.timer = setInterval(() => this.expire(), 6000)
  }

  disconnect() {
    clearInterval(this.timer)
  }

  expire() {
    if (!this.element.children.length) return
    this.element.replaceChildren()
  }
}

rails/brgen/app/app/javascript/controllers/typing_input_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { url: String }

  connect() {
    this.lastPing = 0
  }

  ping() {
    const now = Date.now()
    if (now - this.lastPing < 2000) return
    this.lastPing = now
    fetch(this.urlValue, {
      method: "POST",
      headers: { "X-CSRF-Token": this.csrf, "Accept": "text/vnd.turbo-stream.html" }
    })
  }

  get csrf() {
    return document.querySelector("meta[name=csrf-token]")?.content || ""
  }
}

rails/brgen/app/app/jobs/application_job.rb

class ApplicationJob < ActiveJob::Base
  # Automatically retry jobs that encountered a deadlock
  # retry_on ActiveRecord::Deadlocked

  # Most jobs are safe to ignore if the underlying records are no longer available
  # discard_on ActiveJob::DeserializationError
end

rails/brgen/app/app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout "mailer"
end

rails/brgen/app/app/mailers/passwords_mailer.rb

class PasswordsMailer < ApplicationMailer
  def reset(user)
    @user = user
    mail subject: "Reset your password", to: user.email_address
  end
end

rails/brgen/app/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
end

rails/brgen/app/app/models/comment.rb

class Comment < ApplicationRecord
  include Votable

  belongs_to :user
  belongs_to :commentable, polymorphic: true, touch: true
  belongs_to :parent, class_name: "Comment", optional: true

  has_many :replies, class_name: "Comment", foreign_key: :parent_id, dependent: :destroy
  has_many :votes, as: :votable, dependent: :destroy

  validates :content, presence: true, length: { minimum: 1, maximum: 10000 }

  scope :best,          -> { left_joins(:votes).group(:id).order("SUM(COALESCE(votes.value, 0)) DESC") }
  scope :top,           -> { best }
  scope :new_first,     -> { order(created_at: :desc) }
  scope :controversial, -> {
    left_joins(:votes).group(:id)
      .having("COUNT(CASE WHEN votes.value =  1 THEN 1 END) > 0")
      .having("COUNT(CASE WHEN votes.value = -1 THEN 1 END) > 0")
      .order("ABS(SUM(votes.value)) ASC")
  }

  def root?  = parent_id.nil?
  def depth  = parent ? parent.depth + 1 : 0
end

rails/brgen/app/app/models/community.rb

class Community < ApplicationRecord
  belongs_to :user, optional: true

  has_many :posts, dependent: :destroy

  validates :name,        presence: true, uniqueness: true, length: { maximum: 100 }
  validates :description, length: { maximum: 500 }

  POPULAR_SQL = Arel.sql("COUNT(posts.id) DESC")
  scope :popular, -> { left_joins(:posts).group(:id).order(POPULAR_SQL) }
end

rails/brgen/app/app/models/concerns/commentable.rb

module Commentable
  extend ActiveSupport::Concern

  included do
    has_many :comments, as: :commentable, dependent: :destroy
  end

  def root_comments = comments.where(parent_id: nil)
  def comment_count = comments.count
end

rails/brgen/app/app/models/concerns/mentionable.rb

module Mentionable
  extend ActiveSupport::Concern

  included do
    after_save :sync_mentions
  end

  private

  def sync_mentions
    usernames = (try(:content).to_s + " " + try(:title).to_s).scan(/@(\w+)/).flatten.uniq
    usernames.each do |uname|
      user = User.find_by(username: uname)
      mentions.find_or_create_by!(mentioned_user: user) if user && user != try(:user)
    end
  end
end

rails/brgen/app/app/models/concerns/taggable.rb

module Taggable
  extend ActiveSupport::Concern

  included do
    has_many :taggings, as: :taggable, dependent: :destroy
    has_many :hashtags, through: :taggings
    after_save :sync_hashtags
  end

  def hashtag_list = hashtags.pluck(:name).join(" ")

  private

  def sync_hashtags
    names = Hashtag.extract(try(:content).to_s + " " + try(:title).to_s)
    tags  = names.map { |n| Hashtag.find_or_create_by!(name: n).tap { |h| h.increment!(:usage_count) } }
    self.hashtags = tags
  end
end

rails/brgen/app/app/models/concerns/votable.rb

module Votable
  extend ActiveSupport::Concern

  included do
    has_many :votes, as: :votable, dependent: :destroy
  end

  def score         = votes.sum(:value)
  def upvotes       = votes.where(value: 1).count
  def downvotes     = votes.where(value: -1).count
  def voted_by?(u)  = u && votes.find_by(user: u)&.value
  def upvoted_by?(u)   = voted_by?(u) == 1
  def downvoted_by?(u) = voted_by?(u) == -1
end

rails/brgen/app/app/models/conversation.rb

class Conversation < ApplicationRecord
  has_many :conversation_participants, dependent: :destroy
  has_many :participants, through: :conversation_participants, source: :user
  has_many :messages, dependent: :destroy
  has_many :typing_indicators, dependent: :destroy

  validates :conversation_type, inclusion: { in: %w[direct group] }

  scope :for_user, ->(u) { joins(:conversation_participants).where(conversation_participants: { user: u }) }

  def self.direct_between(a, b)
    for_user(a).for_user(b).where(conversation_type: "direct").first
  end

  def self.find_or_create_direct(a, b)
    direct_between(a, b) || create!(conversation_type: "direct").tap do |c|
      c.participants << a << b
    end
  end

  def unread_count_for(user)
    participant = conversation_participants.find_by(user:)
    return 0 unless participant
    messages.where("created_at > ?", participant.last_read_at || Time.at(0)).count
  end

  def mark_read_for!(user)
    conversation_participants.find_by(user:)&.update!(last_read_at: Time.now)
  end
end

rails/brgen/app/app/models/conversation_participant.rb

class ConversationParticipant < ApplicationRecord
  belongs_to :conversation
  belongs_to :user
end

rails/brgen/app/app/models/current.rb

class Current < ActiveSupport::CurrentAttributes
  attribute :session
  attribute :user
end

rails/brgen/app/app/models/dating.rb

module Dating
  def self.table_name_prefix
    "dating_"
  end
end

rails/brgen/app/app/models/dating/dislike.rb

class Dating::Dislike < ApplicationRecord
  belongs_to :disliker, class_name: "User"
  belongs_to :dislikee, class_name: "User"
  validates :disliker_id, uniqueness: { scope: :dislikee_id }
  validate  :no_self_dislike

  private
  def no_self_dislike
    errors.add(:dislikee, "can't dislike yourself") if disliker_id == dislikee_id
  end
end

rails/brgen/app/app/models/dating/like.rb

class Dating::Like < ApplicationRecord
  belongs_to :liker, class_name: "User"
  belongs_to :likee, class_name: "User"
  validates :liker_id, uniqueness: { scope: :likee_id }
  validate  :no_self_like
  after_create :check_mutual_match

  private

  def no_self_like
    errors.add(:likee, "can't like yourself") if liker_id == likee_id
  end

  def check_mutual_match
    return unless Dating::Like.exists?(liker_id: likee_id, likee_id: liker_id)
    Dating::Match.find_or_create_by!(initiator_id: liker_id, receiver_id: likee_id) do |m|
      m.status = "matched"
    end
  end
end

rails/brgen/app/app/models/dating/match.rb

class Dating::Match < ApplicationRecord
  belongs_to :initiator, class_name: "User"
  belongs_to :receiver,  class_name: "User"
  validates :initiator_id, uniqueness: { scope: :receiver_id }
  validates :status, inclusion: { in: %w[pending matched unmatched] }

  scope :active, -> { where(status: "matched") }

  def other_user(user)
    initiator_id == user.id ? receiver : initiator
  end
end

rails/brgen/app/app/models/dating/profile.rb

class Dating::Profile < ApplicationRecord
  belongs_to :user
  has_many_attached :photos

  GENDERS     = %w[man woman nonbinary other].freeze
  LOOKING_FOR = %w[man woman everyone].freeze

  validates :bio,         length: { maximum: 500 }
  validates :age,         numericality: { greater_than: 17, less_than: 100 }, allow_nil: true
  validates :gender,      inclusion: { in: GENDERS },     allow_nil: true
  validates :looking_for, inclusion: { in: LOOKING_FOR }, allow_nil: true

  scope :visible, -> { where(visible: true) }
  scope :nearby, ->(lat, lng, km = 50) {
    where("ABS(latitude - ?) < ? AND ABS(longitude - ?) < ?", lat, km / 111.0, lng, km / 111.0)
  }

  def liked_by?(user)    = Dating::Like.exists?(liker: user, likee: self.user)
  def disliked_by?(user) = Dating::Dislike.exists?(disliker: user, dislikee: self.user)
  def matched_with?(user)
    Dating::Match.where(status: "matched")
      .where("(initiator_id = ? AND receiver_id = ?) OR (initiator_id = ? AND receiver_id = ?)",
             self.user_id, user.id, user.id, self.user_id).exists?
  end
end

rails/brgen/app/app/models/follow.rb

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(:base, "cannot follow yourself") if follower_id == followed_id
  end
end

rails/brgen/app/app/models/hashtag.rb

class Hashtag < ApplicationRecord
  has_many :taggings, dependent: :destroy

  validates :name, presence: true, uniqueness: { case_sensitive: false }

  before_validation { self.name = name.to_s.downcase.gsub(/[^a-z0-9_]/, "") }

  scope :trending, -> { order(usage_count: :desc) }

  def self.extract(text)
    text.to_s.scan(/#([a-zA-Z0-9_]+)/).flatten.map(&:downcase).uniq
  end
end

rails/brgen/app/app/models/marketplace.rb

module Marketplace
  def self.table_name_prefix
    "marketplace_"
  end
end

rails/brgen/app/app/models/marketplace/category.rb

class Marketplace::Category < ApplicationRecord
  belongs_to :parent, class_name: "Marketplace::Category", optional: true
  has_many :children, class_name: "Marketplace::Category", foreign_key: :parent_id, dependent: :nullify
  has_many :listings, class_name: "Marketplace::Listing", foreign_key: :category_id, dependent: :nullify

  validates :name, :slug, presence: true
  validates :slug, uniqueness: true

  before_validation { self.slug ||= name.to_s.parameterize }

  scope :roots, -> { where(parent_id: nil) }

  def to_param = slug
end

rails/brgen/app/app/models/marketplace/listing.rb

class Marketplace::Listing < ApplicationRecord
  belongs_to :user
  belongs_to :category, class_name: "Marketplace::Category",
             foreign_key: :category_id, optional: true
  has_many :orders, class_name: "Marketplace::Order",
           foreign_key: :listing_id, dependent: :destroy
  has_many_attached :photos

  CONDITIONS = %w[new like_new good fair poor].freeze
  STATUSES   = %w[active sold reserved removed].freeze

  validates :title, presence: true, length: { maximum: 200 }
  validates :price_cents, numericality: { greater_than_or_equal_to: 0 }
  validates :condition, inclusion: { in: CONDITIONS }, allow_nil: true
  validates :status, inclusion: { in: STATUSES }

  before_validation { self.status ||= "active"; self.currency ||= "NOK" }

  scope :active,   -> { where(status: "active") }
  scope :recent,   -> { order(created_at: :desc) }
  scope :popular,  -> { order(views_count: :desc) }

  def price_display  = "#{price_cents / 100.0} #{currency}"
  def sold?          = status == "sold"
end

rails/brgen/app/app/models/marketplace/order.rb

class Marketplace::Order < ApplicationRecord
  belongs_to :buyer,   class_name: "User"
  belongs_to :listing, class_name: "Marketplace::Listing"

  STATUSES = %w[pending accepted declined completed].freeze

  validates :status, inclusion: { in: STATUSES }
  before_validation { self.status ||= "pending" }

  def seller = listing.user
  def accept! = update!(status: "accepted")
  def decline! = update!(status: "declined")
end

rails/brgen/app/app/models/mention.rb

class Mention < ApplicationRecord
  belongs_to :mentionable, polymorphic: true
  belongs_to :mentioned_user
end

rails/brgen/app/app/models/message.rb

class Message < ApplicationRecord
  belongs_to :conversation
  belongs_to :sender, class_name: "User", foreign_key: :sender_id
  has_many :message_receipts, dependent: :destroy
  has_one_attached :attachment

  validates :content, presence: true, length: { maximum: 10_000 }
  validates :message_type, inclusion: { in: %w[text image file audio] }

  broadcasts_to :conversation, inserts_by: :append, target: "messages"

  after_create :deliver_receipts
  after_create :clear_typing_indicators

  scope :recent, -> { order(created_at: :desc) }

  def expired? = expires_at&.past?

  private

  def deliver_receipts
    conversation.participants.where.not(id: sender_id).each do |u|
      message_receipts.create!(user: u, delivered_at: Time.now)
    end
  end

  def clear_typing_indicators
    TypingIndicator.where(conversation:, user: sender).delete_all
  end
end

rails/brgen/app/app/models/message_receipt.rb

class MessageReceipt < ApplicationRecord
  belongs_to :message
  belongs_to :user
end

rails/brgen/app/app/models/playlist.rb

module Playlist
  def self.table_name_prefix
    "playlist_"
  end
end

rails/brgen/app/app/models/playlist/listen.rb

class Playlist::Listen < ApplicationRecord
  belongs_to :user
  belongs_to :track, class_name: "Playlist::Track", foreign_key: :playlist_track_id

  after_create :increment_plays

  private
  def increment_plays
    track.playlists.each { |pl| pl.increment!(:plays_count) }
  end
end

rails/brgen/app/app/models/playlist/playlist.rb

class Playlist::Playlist < ApplicationRecord
  belongs_to :user
  has_many :playlist_tracks, class_name: "Playlist::PlaylistTrack",
           foreign_key: :playlist_playlist_id, dependent: :destroy
  has_many :tracks, through: :playlist_tracks, class_name: "Playlist::Track",
           source: :track

  validates :name, presence: true, length: { maximum: 100 }

  scope :public_playlists, -> { where(public_access: true) }
  scope :popular,           -> { order(plays_count: :desc) }
  scope :recent,            -> { order(created_at: :desc) }

  def add_track!(track, user:)
    pos = playlist_tracks.maximum(:position).to_i + 1
    playlist_tracks.find_or_create_by!(track: track) do |pt|
      pt.position = pos
      pt.user     = user
    end
    increment!(:tracks_count)
  end
end

rails/brgen/app/app/models/playlist/playlist_track.rb

class Playlist::PlaylistTrack < ApplicationRecord
  belongs_to :playlist, class_name: "Playlist::Playlist", foreign_key: :playlist_playlist_id
  belongs_to :track,    class_name: "Playlist::Track",    foreign_key: :playlist_track_id
  belongs_to :user

  validates :playlist_playlist_id, uniqueness: { scope: :playlist_track_id }
  default_scope { order(:position) }
end

rails/brgen/app/app/models/playlist/track.rb

class Playlist::Track < ApplicationRecord
  has_many :playlist_tracks, class_name: "Playlist::PlaylistTrack",
           foreign_key: :playlist_track_id, dependent: :destroy
  has_many :playlists, through: :playlist_tracks, class_name: "Playlist::Playlist"
  has_many :listens, class_name: "Playlist::Listen",
           foreign_key: :playlist_track_id, dependent: :destroy

  SOURCE_TYPES = %w[youtube spotify soundcloud direct].freeze

  validates :title, :artist, presence: true
  validates :source_type, inclusion: { in: SOURCE_TYPES }, allow_nil: true

  def duration_formatted
    return "—" unless duration_seconds
    min, sec = duration_seconds.divmod(60)
    "#{min}:%02d" % sec
  end
end

rails/brgen/app/app/models/post.rb

class Post < ApplicationRecord
  include Votable

  belongs_to :user
  belongs_to :community, optional: true

  has_many :comments, as: :commentable, dependent: :destroy
  has_many :votes, as: :votable, dependent: :destroy
  has_many :taggings, dependent: :destroy
  has_many :hashtags, through: :taggings
  has_many :mentions, dependent: :destroy

  validates :title,   presence: true, length: { maximum: 300 }
  validates :content, length: { maximum: 40_000 }

  broadcasts_refreshes

  VOTE_SQL = Arel.sql("SUM(COALESCE(votes.value,0)) DESC, posts.created_at DESC")
  TOP_SQL  = Arel.sql("SUM(COALESCE(votes.value,0)) DESC")

  scope :hot,   -> { left_joins(:votes).group(:id).order(VOTE_SQL) }
  scope :fresh, -> { order(created_at: :desc) }
  scope :top,   -> { left_joins(:votes).group(:id).order(TOP_SQL) }

  def comment_count = comments.count
  def author_name   = user&.username.presence || "anon"
end

rails/brgen/app/app/models/reaction.rb

class Reaction < ApplicationRecord
  belongs_to :user
  belongs_to :post
end

rails/brgen/app/app/models/session.rb

class Session < ApplicationRecord
  belongs_to :user
end

rails/brgen/app/app/models/stream.rb

class Stream < ApplicationRecord
  belongs_to :user
  belongs_to :post
end

rails/brgen/app/app/models/tagging.rb

class Tagging < ApplicationRecord
  belongs_to :taggable, polymorphic: true
  belongs_to :hashtag
end

rails/brgen/app/app/models/takeaway.rb

module Takeaway
  def self.table_name_prefix
    "takeaway_"
  end
end

rails/brgen/app/app/models/takeaway/menu_item.rb

class Takeaway::MenuItem < ApplicationRecord
  belongs_to :restaurant, class_name: "Takeaway::Restaurant"
  has_one_attached :photo

  validates :name, :price_cents, presence: true
  validates :price_cents, numericality: { greater_than: 0 }

  scope :available, -> { where(available: true) }

  def price_display = "#{price_cents / 100.0} NOK"
end

rails/brgen/app/app/models/takeaway/order.rb

class Takeaway::Order < ApplicationRecord
  belongs_to :user
  belongs_to :restaurant, class_name: "Takeaway::Restaurant"
  has_many :order_items, class_name: "Takeaway::OrderItem", dependent: :destroy

  STATUSES = %w[pending confirmed preparing out_for_delivery delivered cancelled].freeze

  validates :status, inclusion: { in: STATUSES }
  validates :delivery_address, presence: true

  before_validation { self.status ||= "pending" }

  scope :active,  -> { where.not(status: %w[delivered cancelled]) }
  scope :recent,  -> { order(created_at: :desc) }

  def calculate_totals!
    sub = order_items.sum { |oi| oi.unit_price_cents * oi.quantity }
    fee = restaurant.delivery_fee_cents.to_i
    update!(subtotal_cents: sub, delivery_fee_cents: fee, total_cents: sub + fee)
  end

  def advance_status!
    idx = STATUSES.index(status)
    update!(status: STATUSES[idx + 1]) if idx && idx < STATUSES.length - 1
  end

  def total_display   = "#{total_cents.to_i / 100.0} NOK"
end

rails/brgen/app/app/models/takeaway/order_item.rb

class Takeaway::OrderItem < ApplicationRecord
  belongs_to :order,     class_name: "Takeaway::Order"
  belongs_to :menu_item, class_name: "Takeaway::MenuItem"

  validates :quantity, numericality: { greater_than: 0 }

  def subtotal_cents = unit_price_cents * quantity
  def subtotal_display = "#{subtotal_cents / 100.0} NOK"
end

rails/brgen/app/app/models/takeaway/restaurant.rb

class Takeaway::Restaurant < ApplicationRecord
  belongs_to :user
  has_many :menu_items, class_name: "Takeaway::MenuItem", dependent: :destroy
  has_many :orders,     class_name: "Takeaway::Order",    dependent: :destroy

  CUISINE_TYPES = %w[Norwegian Italian Chinese Japanese Indian Thai Mexican Pizza Burger Kebab Sushi Vegetarian Vegan].freeze

  validates :name, :address, :cuisine_type, presence: true
  validates :delivery_fee_cents, :min_order_cents,
            numericality: { greater_than_or_equal_to: 0 }, allow_nil: true

  scope :active,  -> { where(active: true) }
  scope :popular, -> { order(rating: :desc) }

  def update_rating!
    avg = orders.joins(:reviews).average("takeaway_reviews.rating") rescue nil
    update_columns(rating: avg&.round(1) || 0)
  end
end

rails/brgen/app/app/models/tv.rb

module Tv
  def self.table_name_prefix
    "tv_"
  end
end

rails/brgen/app/app/models/tv/broadcast.rb

class Tv::Broadcast < ApplicationRecord
  belongs_to :channel, class_name: "Tv::Channel", foreign_key: :tv_channel_id
  belongs_to :user
  has_one_attached :thumbnail

  validates :title, presence: true
  before_create { self.stream_key = SecureRandom.hex(16) }

  scope :live,      -> { where(status: "live") }
  scope :scheduled, -> { where(status: "scheduled") }

  def go_live!  = update!(status: "live",  started_at: Time.current)
  def end_live! = update!(status: "ended", ended_at: Time.current)
end

rails/brgen/app/app/models/tv/channel.rb

class Tv::Channel < ApplicationRecord
  belongs_to :user
  has_many :videos,        class_name: "Tv::Video",        foreign_key: :tv_channel_id, dependent: :destroy
  has_many :broadcasts,    class_name: "Tv::Broadcast",    foreign_key: :tv_channel_id, dependent: :destroy
  has_many :subscriptions, class_name: "Tv::Subscription", foreign_key: :tv_channel_id, dependent: :destroy
  has_many :subscribers,   through: :subscriptions, source: :user
  has_one_attached :banner
  has_one_attached :avatar

  validates :name, :slug, presence: true
  validates :slug, uniqueness: true, format: { with: /\A[a-z0-9_-]+\z/ }
  before_validation { self.slug ||= name.to_s.parameterize }

  scope :popular, -> { order(subscribers_count: :desc) }

  def to_param = slug
  def live?    = broadcasts.where(status: "live").exists?
end

rails/brgen/app/app/models/tv/subscription.rb

class Tv::Subscription < ApplicationRecord
  belongs_to :user
  belongs_to :channel, class_name: "Tv::Channel", foreign_key: :tv_channel_id
  validates :user_id, uniqueness: { scope: :tv_channel_id }
end

rails/brgen/app/app/models/tv/video.rb

class Tv::Video < ApplicationRecord
  belongs_to :channel,     class_name: "Tv::Channel",   foreign_key: :tv_channel_id
  belongs_to :user
  has_many :view_events,   class_name: "Tv::ViewEvent", foreign_key: :tv_video_id, dependent: :destroy
  has_one_attached :video_file
  has_one_attached :thumbnail

  STATUSES = %w[processing ready published unlisted].freeze
  validates :title, presence: true
  validates :status, inclusion: { in: STATUSES }, allow_nil: true

  scope :published, -> { where(status: "published").order(published_at: :desc) }
  scope :trending,  -> { published.order(views_count: :desc) }
  scope :recent,    -> { published.order(published_at: :desc) }

  def duration_formatted
    return "—" unless duration_seconds
    h, rem = duration_seconds.divmod(3600)
    m, s   = rem.divmod(60)
    h > 0 ? "%d:%02d:%02d" % [h, m, s] : "%d:%02d" % [m, s]
  end
end

rails/brgen/app/app/models/tv/view_event.rb

class Tv::ViewEvent < ApplicationRecord
  belongs_to :user
  belongs_to :video, class_name: "Tv::Video", foreign_key: :tv_video_id
end

rails/brgen/app/app/models/typing_indicator.rb

class TypingIndicator < ApplicationRecord
  belongs_to :conversation
  belongs_to :user

  scope :active, -> { where("expires_at > ?", Time.now) }

  def self.set!(conversation:, user:)
    rec = find_or_create_by(conversation:, user:)
    rec.update!(expires_at: 5.seconds.from_now)
    Turbo::StreamsChannel.broadcast_replace_to(
      conversation,
      target:  "typing-indicator",
      partial: "typing_indicators/indicator",
      locals:  { conversation:, except_user: user }
    )
    rec
  end
end

rails/brgen/app/app/models/user.rb

class User < ApplicationRecord
  has_secure_password
  has_many :marketplace_listings, class_name: 'Marketplace::Listing', dependent: :destroy
  has_many :marketplace_orders,   class_name: 'Marketplace::Order',   foreign_key: :buyer_id, dependent: :destroy
  has_many :takeaway_restaurants, class_name: 'Takeaway::Restaurant', dependent: :destroy
  has_many :takeaway_orders,      class_name: 'Takeaway::Order',      dependent: :destroy
  has_many :playlist_playlists, class_name: 'Playlist::Playlist', dependent: :destroy
  has_many :playlist_listens,   class_name: 'Playlist::Listen',   dependent: :destroy
  has_one  :dating_profile,              class_name: 'Dating::Profile',  dependent: :destroy
  has_many :dating_likes,                class_name: 'Dating::Like',     foreign_key: :liker_id,    dependent: :destroy
  has_many :dating_dislikes,             class_name: 'Dating::Dislike',  foreign_key: :disliker_id, dependent: :destroy
  has_many :dating_matches_as_initiator, class_name: 'Dating::Match',    foreign_key: :initiator_id, dependent: :destroy
  has_many :dating_matches_as_receiver,  class_name: 'Dating::Match',    foreign_key: :receiver_id,  dependent: :destroy
  has_many :tv_channels,      class_name: "Tv::Channel",      dependent: :destroy
  has_many :tv_subscriptions, class_name: "Tv::Subscription", dependent: :destroy
  has_many :subscribed_channels, through: :tv_subscriptions, source: :tv_channel

  has_many :sessions, dependent: :destroy
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_many :communities

  # Voting
  has_many :votes, dependent: :destroy

  # Social follows
  has_many :follows_as_follower, class_name: "Follow", foreign_key: :follower_id, dependent: :destroy
  has_many :follows_as_followed, class_name: "Follow", foreign_key: :followed_id, dependent: :destroy
  has_many :following, through: :follows_as_follower, source: :followed
  has_many :followers,  through: :follows_as_followed, source: :follower

  # Messaging
  has_many :conversation_participants, dependent: :destroy
  has_many :conversations, through: :conversation_participants

  validates :email_address, presence: true, uniqueness: true
  validates :username, uniqueness: true, allow_nil: true
  normalizes :email_address, with: ->(e) { e.strip.downcase }

  def follow!(other)
    follows_as_follower.find_or_create_by!(followed: other) unless other == self
  end

  def unfollow!(other)
    follows_as_follower.find_by(followed: other)&.destroy
  end

  def following?(other) = follows_as_follower.exists?(followed: other)

  def timeline_posts
    Post.where(user: [self] + following).order(created_at: :desc)
  end

  def update_karma!
    k  = Vote.joins("JOIN posts    ON posts.id    = votes.votable_id AND votes.votable_type = 'Post'")
             .where(posts: { user_id: id }).sum(:value)
    k += Vote.joins("JOIN comments ON comments.id = votes.votable_id AND votes.votable_type = 'Comment'")
             .where(comments: { user_id: id }).sum(:value)
    update_column(:karma, k)
  end
end

rails/brgen/app/app/models/vote.rb

class Vote < ApplicationRecord
  belongs_to :user
  belongs_to :votable, polymorphic: true, touch: true

  validates :value, inclusion: { in: [-1, 1] }
  validates :user_id, uniqueness: { scope: [:votable_type, :votable_id] }

  after_save    :update_author_karma
  after_destroy :update_author_karma

  private

  def update_author_karma
    votable.user.update_karma! if votable.respond_to?(:user)
  end
end

rails/brgen/app/app/services/scrape.rb

require "ferrum"
require "net/http"
require "json"
require "base64"

class Scrape
  MODEL    = ENV.fetch("SCRAPE_MODEL", "google/gemini-2.0-flash-001")
  ENDPOINT = URI("https://openrouter.ai/api/v1/chat/completions")
  HTML_MAX = 60_000

  def self.call(url, schema:, hint: nil)
    browser = Ferrum::Browser.new(headless: true, timeout: 30,
                                  browser_options: { "no-sandbox": nil })
    browser.go_to(url)
    browser.network.wait_for_idle(timeout: 10)
    html = browser.body
    png  = Base64.strict_encode64(browser.screenshot(encoding: :binary, full: true))
    browser.quit
    reason(url:, html:, png:, schema:, hint:)
  end

  def self.reason(url:, html:, png:, schema:, hint:)
    prompt = <<~TXT
      Source: #{url}
      Extract every listed item on the page as JSON. Use the screenshot to read visual layout (cards, sponsored banners, hidden overlays); use the HTML for exact text and links.
      #{hint}
      Reply with one JSON object: {"items":[{#{schema.join(', ')}}, ...]}.
      HTML (truncated to #{HTML_MAX} bytes):
      #{html.byteslice(0, HTML_MAX)}
    TXT
    payload = {
      model: MODEL,
      messages: [{
        role: "user",
        content: [
          { type: "text",      text: prompt },
          { type: "image_url", image_url: { url: "data:image/png;base64,#{png}" } }
        ]
      }],
      response_format: { type: "json_object" }
    }
    req = Net::HTTP::Post.new(ENDPOINT,
                              "Content-Type"  => "application/json",
                              "Authorization" => "Bearer #{ENV.fetch('OPENROUTER_API_KEY')}")
    req.body = payload.to_json
    res = Net::HTTP.start(ENDPOINT.hostname, ENDPOINT.port, use_ssl: true, read_timeout: 60) { |h| h.request(req) }
    JSON.parse(JSON.parse(res.body).dig("choices", 0, "message", "content")).fetch("items", [])
  end
end

rails/brgen/app/app/views/comments/_comment.html.erb

<%= turbo_frame_tag dom_id(comment) do %>
<article data-depth="<%= [comment.depth, 4].min %>">
  <header>
    <span><%= comment.user&.username || "anon" %></span>
    <span> · <%= time_ago_in_words(comment.created_at) %> ago</span>
    <% if authenticated? %>
      ·
      <% root = comment.commentable.is_a?(Post) ? comment.commentable : comment.commentable.commentable %>
      <%= link_to "reply", "#reply-#{comment.id}", data: { action: "click->reply#toggle" } %>
      <% if comment.user == Current.user %>
        <%= button_to "delete", comment, method: :delete, data: { turbo_confirm: "Delete?" } %>
      <% end %>
    <% end %>
  </header>
  <p><%= comment.content %></p>

  <% if authenticated? %>
    <section id="reply-<%= comment.id %>" hidden>
      <%= form_with url: post_comments_path(comment.commentable.is_a?(Post) ? comment.commentable : comment.commentable), data: { turbo: true } do |f| %>
        <%= f.hidden_field :parent_id, value: comment.id %>
        <p><%= f.text_area :content, placeholder: "Reply...", rows: 3 %></p>
        <p><%= f.submit "Reply" %></p>
      <% end %>
    </section>
  <% end %>

  <% comment.replies.best.each do |reply| %>
    <%= render "comments/comment", comment: reply %>
  <% end %>
</article>
<% end %>

rails/brgen/app/app/views/communities/index.html.erb

<h1>Communities</h1>
<% if authenticated? %><%= link_to "+ New", new_community_path %><% end %>

<% if @communities.any? %>
  <ul>
    <% @communities.each do |c| %>
      <li>
        <%= link_to c.name, community_path(c) %>
        <% if c.description.present? %><p><%= c.description %></p><% end %>
        <small><%= c.posts.count %> posts</small>
      </li>
    <% end %>
  </ul>
<% else %>
  <p>No communities yet. <%= link_to "Create one", new_community_path if authenticated? %></p>
<% end %>

rails/brgen/app/app/views/communities/new.html.erb

<h1>New community</h1>
<%= form_with model: @community do |f| %>
  <% if @community.errors.any? %>
    <ul>
      <% @community.errors.full_messages.each do |msg| %><li><%= msg %></li><% end %>
    </ul>
  <% end %>
  <p>
    <%= f.label :name %>
    <%= f.text_field :name, placeholder: "e.g. bergen, tech, musikk" %>
  </p>
  <p>
    <%= f.label :description %>
    <%= f.text_area :description, placeholder: "What is this community about?", rows: 3 %>
  </p>
  <p>
    <%= f.submit "Create" %>
    <%= link_to "Cancel", communities_path %>
  </p>
<% end %>

rails/brgen/app/app/views/communities/show.html.erb

<% content_for :title, @community.name %>

<header>
  <h1><%= @community.name %></h1>
  <% if @community.description.present? %><p><%= @community.description %></p><% end %>
  <p><strong><%= @community.posts.count %></strong> posts</p>
</header>

<% if authenticated? %>
  <p><%= link_to "+ New post in #{@community.name}", new_community_post_path(@community) %></p>
<% end %>

<% if @posts.any? %>
  <% @posts.each do |post| %>
    <%= render "posts/post", post: post %>
  <% end %>
<% else %>
  <p>No posts yet in this community.</p>
<% end %>

<aside>
  <h3>About <%= @community.name %></h3>
  <p><%= @community.description.presence || "No description." %></p>
  <% if authenticated? %>
    <p><%= link_to "New post", new_community_post_path(@community) %></p>
  <% end %>
</aside>

rails/brgen/app/app/views/conversations/index.html.erb

<h1>Messages</h1>

<% if @conversations.any? %>
  <ul>
    <% @conversations.each do |c| %>
      <% other = (c.participants - [Current.user]).first %>
      <% last  = c.messages.last %>
      <li>
        <%= link_to conversation_path(c) do %>
          <strong><%= other&.username || "unknown" %></strong>
          <% if last %>
            <span><%= truncate(last.content.to_s, length: 80) %></span>
            <time datetime="<%= last.created_at.iso8601 %>" data-controller="timeago"><%= time_ago_in_words(last.created_at) %> ago</time>
          <% end %>
          <% unread = c.unread_count_for(Current.user) %>
          <% if unread.positive? %><mark><%= unread %></mark><% end %>
        <% end %>
      </li>
    <% end %>
  </ul>
<% else %>
  <p>No conversations yet.</p>
<% end %>

rails/brgen/app/app/views/conversations/show.html.erb

<%= turbo_stream_from @conversation %>

<% other = (@conversation.participants - [Current.user]).first %>
<header>
  <h1><%= other&.username || "conversation" %></h1>
  <%= link_to "← all", conversations_path %>
</header>

<ul id="messages">
  <% @messages.each do |m| %>
    <%= render "messages/message", message: m %>
  <% end %>
</ul>

<p id="typing-indicator" data-controller="typing" data-typing-conversation-id-value="<%= @conversation.id %>"></p>

<%= form_with model: @message, url: conversation_messages_path(@conversation),
              id: "new_message",
              data: { turbo: true, controller: "typing-input", typing_input_url_value: conversation_typing_indicators_path(@conversation) } do |f| %>
  <p><%= f.text_area :content, rows: 3, placeholder: "Send a message...", data: { action: "input->typing-input#ping" } %></p>
  <%= f.hidden_field :message_type, value: "text" %>
  <p><%= f.submit "Send" %></p>
<% end %>

rails/brgen/app/app/views/dating/home/index.html.erb

<% content_for :title, "Dating" %>
<h1>Discover</h1>
<nav>
  <%= link_to "Matches", dating_matches_path %>
  <%= link_to "Edit profile", edit_dating_profile_path %>
</nav>
<section>
  <% @profiles.each do |p| %>
    <article>
      <% if p.photos.attached? %>
        <%= image_tag p.photos.first %>
      <% end %>
      <h2><%= p.user.email_address.split("@").first %><% if p.age %>, <%= p.age %><% end %></h2>
      <% if p.location.present? %><p><%= p.location %></p><% end %>
      <p><%= p.bio %></p>
      <p>
        <%= button_to "Like", dating_likes_path(user_id: p.user_id), method: :post %>
        <%= button_to "Pass", dating_dislikes_path(user_id: p.user_id), method: :post %>
      </p>
    </article>
  <% end %>
</section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/brgen/app/app/views/dating/matches/index.html.erb

<% content_for :title, "Matches" %>
<h1>Matches</h1>
<%= link_to "Back to discover", dating_root_path %>
<% if @matches.none? %>
  <p>No matches yet.</p>
<% else %>
  <section>
    <% @matches.each do |m| %>
      <% other = m.other_user(Current.user) %>
      <article>
        <% if other.dating_profile&.photos&.attached? %>
          <%= image_tag other.dating_profile.photos.first %>
        <% end %>
        <p><%= other.email_address.split("@").first %></p>
      </article>
    <% end %>
  </section>
<% end %>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/brgen/app/app/views/dating/profiles/edit.html.erb

<h1>Edit profile</h1>
<%= form_with model: @profile, url: dating_profile_path, method: :patch do |f| %>
  <%= render "shared/errors", object: @profile %>
  <p>
    <%= f.label :bio, "About you" %>
    <%= f.text_area :bio, rows: 4, maxlength: 500 %>
  </p>
  <p>
    <%= f.label :gender %>
    <%= f.select :gender, Dating::Profile::GENDERS, { include_blank: "—" } %>
  </p>
  <p>
    <%= f.label :looking_for, "Looking for" %>
    <%= f.select :looking_for, Dating::Profile::LOOKING_FOR, { include_blank: "—" } %>
  </p>
  <p>
    <%= f.label :age %>
    <%= f.number_field :age, min: 18, max: 99 %>
  </p>
  <p>
    <%= f.label :location %>
    <%= f.text_field :location %>
  </p>
  <p>
    <%= f.label :photos, "Photos" %>
    <%= f.file_field :photos, multiple: true, accept: "image/*" %>
  </p>
  <p>
    <%= f.check_box :visible %> <%= f.label :visible, "Make profile visible" %>
  </p>
  <p><%= f.submit "Save" %></p>
<% end %>

rails/brgen/app/app/views/dating/profiles/new.html.erb

<h1>Create your profile</h1>
<%= form_with model: @profile, url: dating_profile_path do |f| %>
  <%= render "shared/errors", object: @profile %>
  <p>
    <%= f.label :bio, "About you" %>
    <%= f.text_area :bio, rows: 4, maxlength: 500 %>
  </p>
  <p>
    <%= f.label :gender %>
    <%= f.select :gender, Dating::Profile::GENDERS, { include_blank: "—" } %>
  </p>
  <p>
    <%= f.label :looking_for, "Looking for" %>
    <%= f.select :looking_for, Dating::Profile::LOOKING_FOR, { include_blank: "—" } %>
  </p>
  <p>
    <%= f.label :age %>
    <%= f.number_field :age, min: 18, max: 99 %>
  </p>
  <p>
    <%= f.label :location %>
    <%= f.text_field :location %>
  </p>
  <p>
    <%= f.label :photos, "Photos" %>
    <%= f.file_field :photos, multiple: true, accept: "image/*" %>
  </p>
  <p>
    <%= f.check_box :visible %> <%= f.label :visible, "Make profile visible" %>
  </p>
  <p><%= f.submit "Save" %></p>
<% end %>

rails/brgen/app/app/views/dating/profiles/show.html.erb

<% content_for :title, "Your profile" %>
<h1>Your profile</h1>

<% if @profile.photos.attached? %>
  <ul>
    <% @profile.photos.each do |photo| %>
      <li><%= image_tag photo %></li>
    <% end %>
  </ul>
<% end %>

<dl>
  <dt>Bio</dt>           <dd><%= @profile.bio.presence || "—" %></dd>
  <dt>Age</dt>           <dd><%= @profile.age || "—" %></dd>
  <dt>Gender</dt>        <dd><%= @profile.gender || "—" %></dd>
  <dt>Looking for</dt>   <dd><%= @profile.looking_for || "—" %></dd>
  <dt>Location</dt>      <dd><%= @profile.location.presence || "—" %></dd>
  <dt>Visibility</dt>    <dd><%= @profile.visible? ? "visible" : "hidden" %></dd>
</dl>

<%= link_to "Edit profile", edit_dating_profile_path %>

rails/brgen/app/app/views/home/index.html.erb

<div class="main-col">
  <div class="sort-tabs">
    <span class="sort-tab active">hot</span>
    <%= link_to "fresh", posts_path(sort: "fresh"), class: "sort-tab" %>
  </div>

  <% if @posts.any? %>
    <% @posts.each do |post| %>
      <%= render "posts/post", post: post %>
    <% end %>
  <% else %>
    <div class="empty">
      <p>No posts yet. <%= authenticated? ? link_to("Create one", new_post_path) : link_to("Sign in to post", new_session_path) %>.</p>
    </div>
  <% end %>
</div>

<div class="side-col">
  <div class="sidebar-card">
    <h3>Communities</h3>
    <ul>
      <% @communities.each do |c| %>
        <li><%= link_to c.name, community_path(c) %></li>
      <% end %>
    </ul>
    <% if authenticated? %>
      <div style="margin-top:12px"><%= link_to "+ New community", new_community_path, class: "btn btn-ghost" %></div>
    <% end %>
  </div>
  <% if authenticated? %>
    <div class="sidebar-card">
      <%= link_to "New post", new_post_path, class: "btn", style: "width:100%;text-align:center" %>
    </div>
  <% end %>
</div>

rails/brgen/app/app/views/layouts/application.html.erb

<!DOCTYPE html>
<html lang="no">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title><%= content_for?(:title) ? "#{yield :title} — brgen" : "brgen" %></title>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>
  <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
  <%= javascript_importmap_tags %>
</head>
<body>

<nav>
  <%= link_to "brgen", root_path %>
  <%= link_to "communities", communities_path %>
  <%= link_to "posts",       posts_path %>
  <% if authenticated? %>
    <%= link_to "inbox",   conversations_path %>
    <%= link_to "sign out", session_path, data: { turbo_method: :delete } %>
  <% else %>
    <%= link_to "sign in",  new_session_path %>
  <% end %>
</nav>

<main>
  <% if notice %><p role="status"><%= notice %></p><% end %>
  <% if alert  %><p role="alert"><%=  alert  %></p><% end %>
  <%= yield %>
</main>

</body>
</html>

rails/brgen/app/app/views/layouts/mailer.html.erb

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

rails/brgen/app/app/views/layouts/mailer.text.erb

<%= yield %>

rails/brgen/app/app/views/marketplace/categories/show.html.erb

<% content_for :title, @category.name %>
<h1><%= @category.name %></h1>
<section>
  <% @listings.each do |l| %>
    <article>
      <%= link_to l.title, marketplace_listing_path(l) %>
      <p><strong><%= l.price_display %></strong></p>
    </article>
  <% end %>
</section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/brgen/app/app/views/marketplace/listings/edit.html.erb

<h1>Edit listing</h1>
<%= form_with model: @listing, url: marketplace_listing_path(@listing), method: :patch do |f| %>
  <%= render "shared/errors", object: @listing %>
  <p><%= f.label :title %><%= f.text_field :title %></p>
  <p><%= f.label :description %><%= f.text_area :description, rows: 4 %></p>
  <p><%= f.label :price_cents, "Price (øre)" %><%= f.number_field :price_cents %></p>
  <p>
    <%= f.label :condition %>
    <%= f.select :condition, Marketplace::Listing::CONDITIONS, { include_blank: "—" } %>
  </p>
  <p>
    <%= f.label :category_id, "Category" %>
    <%= f.collection_select :category_id, @categories, :id, :name, { include_blank: "—" } %>
  </p>
  <p><%= f.label :location %><%= f.text_field :location %></p>
  <p>
    <%= f.label :status %>
    <%= f.select :status, Marketplace::Listing::STATUSES %>
  </p>
  <p><%= f.label :photos %><%= f.file_field :photos, multiple: true, accept: "image/*" %></p>
  <p><%= f.submit "Save" %></p>
<% end %>

rails/brgen/app/app/views/marketplace/listings/index.html.erb

<% content_for :title, "Marketplace" %>
<h1>Marketplace</h1>
<% if authenticated? %><%= link_to "Sell something", new_marketplace_listing_path %><% end %>

<form method="get">
  <p><input name="q" value="<%= params[:q] %>" placeholder="Search listings…"></p>
  <p><button type="submit">Search</button></p>
</form>

<% if @categories.any? %>
  <nav>
    <% @categories.each do |cat| %>
      <%= link_to cat.name, marketplace_category_path(cat) %>
    <% end %>
  </nav>
<% end %>

<section>
  <% @listings.each do |l| %>
    <article>
      <% if l.photos.attached? %>
        <%= link_to marketplace_listing_path(l) do %>
          <%= image_tag l.photos.first %>
        <% end %>
      <% end %>
      <%= link_to l.title, marketplace_listing_path(l) %>
      <p><strong><%= l.price_display %></strong></p>
      <small><%= l.location %> · <%= l.condition %></small>
    </article>
  <% end %>
</section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/brgen/app/app/views/marketplace/listings/new.html.erb

<h1>New listing</h1>
<%= form_with model: @listing, url: marketplace_listings_path do |f| %>
  <%= render "shared/errors", object: @listing %>
  <p><%= f.label :title %><%= f.text_field :title %></p>
  <p><%= f.label :description %><%= f.text_area :description, rows: 4 %></p>
  <p><%= f.label :price_cents, "Price (øre)" %><%= f.number_field :price_cents %></p>
  <p>
    <%= f.label :condition %>
    <%= f.select :condition, Marketplace::Listing::CONDITIONS, { include_blank: "—" } %>
  </p>
  <p>
    <%= f.label :category_id, "Category" %>
    <%= f.collection_select :category_id, @categories, :id, :name, { include_blank: "—" } %>
  </p>
  <p><%= f.label :location %><%= f.text_field :location %></p>
  <p><%= f.label :photos %><%= f.file_field :photos, multiple: true, accept: "image/*" %></p>
  <p><%= f.submit "List it" %></p>
<% end %>

rails/brgen/app/app/views/marketplace/listings/show.html.erb

<% content_for :title, @listing.title %>
<% if @listing.photos.attached? %>
  <ul>
    <% @listing.photos.each do |photo| %>
      <li><%= image_tag photo %></li>
    <% end %>
  </ul>
<% end %>

<h1><%= @listing.title %></h1>
<p><strong><%= @listing.price_display %></strong></p>
<p><%= @listing.description %></p>
<p>Condition: <%= @listing.condition %> · Location: <%= @listing.location %></p>
<p>Seller: <%= @listing.user.email_address.split("@").first %></p>

<% if authenticated? && Current.user != @listing.user && !@listing.sold? %>
  <h2>Make an offer</h2>
  <%= form_with model: @order, url: marketplace_listing_orders_path(@listing) do |f| %>
    <p><%= f.text_area :message, placeholder: "Message to seller (optional)", rows: 3 %></p>
    <p><%= f.submit "Send offer" %></p>
  <% end %>
<% end %>

<% if authenticated? && Current.user == @listing.user %>
  <h2>Offers</h2>
  <% @listing.orders.includes(:buyer).each do |o| %>
    <article>
      <p><%= o.buyer.email_address.split("@").first %><%= o.status %></p>
      <% if o.message.present? %><p><%= o.message %></p><% end %>
      <% if o.status == "pending" %>
        <%= button_to "Accept",  marketplace_listing_order_path(@listing, o), params: { accept:  true }, method: :patch %>
        <%= button_to "Decline", marketplace_listing_order_path(@listing, o), params: { decline: true }, method: :patch %>
      <% end %>
    </article>
  <% end %>
  <nav>
    <%= link_to "Edit", edit_marketplace_listing_path(@listing) %>
    <%= button_to "Remove", marketplace_listing_path(@listing), method: :delete %>
  </nav>
<% end %>

rails/brgen/app/app/views/messages/_message.html.erb

<%= turbo_frame_tag dom_id(message) do %>
<article data-from="<%= message.sender == Current.user ? "self" : "peer" %>">
  <header>
    <span><%= message.sender.username %></span>
    <time datetime="<%= message.created_at.iso8601 %>" data-controller="timeago"><%= time_ago_in_words(message.created_at) %> ago</time>
  </header>
  <p><%= message.content %></p>
  <% if message.attachment.attached? %>
    <% case message.message_type %>
    <% when "image" %>
      <%= image_tag message.attachment %>
    <% when "audio" %>
      <%= audio_tag message.attachment, controls: true %>
    <% else %>
      <%= link_to message.attachment.filename, message.attachment %>
    <% end %>
  <% end %>
</article>
<% end %>

rails/brgen/app/app/views/messages/create.turbo_stream.erb

<%= turbo_stream.replace "new_message" do %>
  <%= form_with model: Message.new, url: conversation_messages_path(@conversation),
                id: "new_message",
                data: { turbo: true, controller: "typing-input", typing_input_url_value: conversation_typing_indicators_path(@conversation) } do |f| %>
    <div class="field">
      <%= f.text_area :content, rows: 3, placeholder: "Send a message...", data: { action: "input->typing-input#ping" } %>
    </div>
    <%= f.hidden_field :message_type, value: "text" %>
    <%= f.submit "Send", class: "btn" %>
  <% end %>
<% end %>

rails/brgen/app/app/views/messages/new.html.erb

<h1>New message</h1>

<% if @message&.errors&.any? %>
  <ul>
    <% @message.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
  </ul>
<% end %>

<%= form_with model: @message, url: conversation_messages_path(@conversation), id: "new_message" do |f| %>
  <p><%= f.text_area :content, rows: 4, placeholder: "Message..." %></p>
  <%= f.hidden_field :message_type, value: "text" %>
  <p><%= f.submit "Send" %></p>
<% end %>

rails/brgen/app/app/views/passwords/edit.html.erb

<h1>Update your password</h1>

<% if flash[:alert] %><p><%= flash[:alert] %></p><% end %>

<%= form_with url: password_path(params[:token]), method: :put do |form| %>
  <p><%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %></p>
  <p><%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %></p>
  <p><%= form.submit "Save" %></p>
<% end %>

rails/brgen/app/app/views/passwords/new.html.erb

<h1>Forgot your password?</h1>

<% if flash[:alert] %><p><%= flash[:alert] %></p><% end %>

<%= form_with url: passwords_path do |form| %>
  <p><%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %></p>
  <p><%= form.submit "Email reset instructions" %></p>
<% end %>

rails/brgen/app/app/views/passwords_mailer/reset.html.erb

<p>
  You can reset your password on
  <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.

  This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
</p>

rails/brgen/app/app/views/passwords_mailer/reset.text.erb

You can reset your password on
<%= edit_password_url(@user.password_reset_token) %>

This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.

rails/brgen/app/app/views/playlist/index.html.erb

<section>
  <h2>Playlists</h2>
  <p>Discover music from Bergen and beyond.</p>
</section>

<% @playlists.each do |playlist| %>
  <article>
    <h2><%= playlist[:name] %></h2>
    <p><strong><%= playlist[:tracks] %></strong> tracks · <%= playlist[:genre] %></p>
  </article>
<% end %>

rails/brgen/app/app/views/playlist/playlists/edit.html.erb

<h1>Edit playlist</h1>
<%= form_with model: @playlist, url: playlist_playlist_path(@playlist), method: :patch do |f| %>
  <%= render "shared/errors", object: @playlist %>
  <p><%= f.label :name %><%= f.text_field :name %></p>
  <p><%= f.label :description %><%= f.text_area :description, rows: 3 %></p>
  <p><%= f.check_box :public_access %> <%= f.label :public_access, "Public" %></p>
  <p><%= f.submit "Save" %></p>
<% end %>

rails/brgen/app/app/views/playlist/playlists/index.html.erb

<% content_for :title, "Playlists" %>
<h1>Playlists</h1>
<% if authenticated? %><%= link_to "New playlist", new_playlist_playlist_path %><% end %>
<section>
  <% @playlists.each do |pl| %>
    <article>
      <%= link_to pl.name, playlist_playlist_path(pl) %>
      <small><%= pl.tracks_count %> tracks · <%= pl.plays_count %> plays · <%= pl.user.email_address.split("@").first %></small>
    </article>
  <% end %>
</section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/brgen/app/app/views/playlist/playlists/new.html.erb

<h1>New playlist</h1>
<%= form_with model: @playlist, url: playlist_playlists_path do |f| %>
  <%= render "shared/errors", object: @playlist %>
  <p><%= f.label :name %><%= f.text_field :name %></p>
  <p><%= f.label :description %><%= f.text_area :description, rows: 3 %></p>
  <p><%= f.check_box :public_access %> <%= f.label :public_access, "Public" %></p>
  <p><%= f.submit "Create" %></p>
<% end %>

rails/brgen/app/app/views/playlist/playlists/show.html.erb

<% content_for :title, @playlist.name %>
<h1><%= @playlist.name %></h1>
<p><%= @playlist.description %></p>
<p><small><%= @playlist.tracks_count %> tracks · <%= @playlist.plays_count %> plays</small></p>

<% if authenticated? && Current.user == @playlist.user %>
  <nav>
    <%= link_to "Edit", edit_playlist_playlist_path(@playlist) %>
    <%= button_to "Delete", playlist_playlist_path(@playlist), method: :delete, data: { turbo_confirm: "Delete?" } %>
  </nav>
<% end %>

<ol>
  <% @tracks.each do |pt| %>
    <li>
      <strong><%= pt.track.title %></strong><%= pt.track.artist %>
      <small><%= pt.track.duration_formatted %></small>
      <% if authenticated? && (Current.user == pt.user || Current.user == @playlist.user) %>
        <%= button_to "Remove", playlist_playlist_track_path(@playlist, pt), method: :delete %>
      <% end %>
    </li>
  <% end %>
</ol>

<% if authenticated? %>
  <h2>Add a track</h2>
  <%= form_with url: playlist_playlist_tracks_path(@playlist) do |f| %>
    <p><%= f.text_field :title, placeholder: "Title", name: "playlist_track[title]" %></p>
    <p><%= f.text_field :artist, placeholder: "Artist", name: "playlist_track[artist]" %></p>
    <p><%= f.text_field :source_url, placeholder: "URL (optional)", name: "playlist_track[source_url]" %></p>
    <p><%= f.submit "Add" %></p>
  <% end %>
<% end %>

rails/brgen/app/app/views/posts/_post.html.erb

<%= turbo_frame_tag dom_id(post) do %>
<article>
  <% if authenticated? %>
    <%= button_to "▲", post_vote_path(post), params: { vote: { value: 1 } },
          class: ("active-up" if post.upvoted_by?(Current.user)) %>
  <% else %>
    <span></span>
  <% end %>
  <span data-controller="animated-number"><%= post.score %></span>
  <% if authenticated? %>
    <%= button_to "▼", post_vote_path(post), params: { vote: { value: -1 } },
          class: ("active-down" if post.downvoted_by?(Current.user)) %>
  <% else %>
    <span></span>
  <% end %>

  <header>
    <% if post.community %>
      <%= link_to post.community.name, community_path(post.community) %>
      ·
    <% end %>
    posted by <span><%= post.author_name %></span>
    ·
    <time datetime="<%= post.created_at.iso8601 %>" data-controller="timeago"><%= time_ago_in_words(post.created_at) %> ago</time>
  </header>

  <h2><%= link_to post.title, post %></h2>

  <footer>
    <%= link_to "#{post.comment_count} comments", post_path(post) %>
    <%= button_to "share", "#", data: { controller: "clipboard", clipboard_source_value: post_url(post), action: "click->clipboard#copy" } %>
    <% if authenticated? && post.user == Current.user %>
      <%= link_to "edit", edit_post_path(post) %>
      <%= button_to "delete", post, method: :delete, data: { turbo_confirm: "Delete this post?" } %>
    <% end %>
  </footer>
</article>
<% end %>

rails/brgen/app/app/views/posts/index.html.erb

<%= turbo_stream_from "posts" %>

<header>
  <h1>All posts</h1>
  <% if authenticated? %><%= link_to "+ New post", new_post_path %><% end %>
</header>

<nav>
  <%= link_to "hot",   posts_path,                class: ("active" unless params[:sort]) %>
  <%= link_to "fresh", posts_path(sort: "fresh"), class: ("active" if params[:sort] == "fresh") %>
  <%= link_to "top",   posts_path(sort: "top"),   class: ("active" if params[:sort] == "top") %>
</nav>

<% if @posts.any? %>
  <% @posts.each do |post| %>
    <%= render "posts/post", post: post %>
  <% end %>
<% else %>
  <p>No posts yet.</p>
<% end %>

<aside>
  <h3>Communities</h3>
  <ul>
    <% Community.popular.limit(8).each do |c| %>
      <li><%= link_to c.name, community_path(c) %></li>
    <% end %>
  </ul>
</aside>

rails/brgen/app/app/views/posts/new.html.erb

<h1><%= @post.new_record? ? "New post" : "Edit post" %></h1>
<%= form_with model: [@community, @post].compact do |f| %>
  <% if @post.errors.any? %>
    <ul>
      <% @post.errors.full_messages.each do |msg| %><li><%= msg %></li><% end %>
    </ul>
  <% end %>
  <p>
    <%= f.label :community_id, "Community (optional)" %>
    <%= f.collection_select :community_id, Community.order(:name), :id, :name,
          { include_blank: "— no community —" } %>
  </p>
  <p>
    <%= f.label :title %>
    <%= f.text_field :title, placeholder: "Title", autofocus: true %>
  </p>
  <p>
    <%= f.label :content, "Body (optional)" %>
    <%= f.text_area :content, placeholder: "Text (optional)", rows: 8 %>
  </p>
  <p>
    <%= f.submit @post.new_record? ? "Post" : "Save" %>
    <%= link_to "Cancel", @post.new_record? ? posts_path : @post %>
  </p>
<% end %>

rails/brgen/app/app/views/posts/show.html.erb

<% content_for :title, @post.title %>
<%= turbo_stream_from @post %>

<article>
  <% if authenticated? %>
    <%= button_to "▲", post_vote_path(@post), params: { vote: { value: 1 } },
          class: ("active-up" if @post.upvoted_by?(Current.user)) %>
  <% else %>
    <span></span>
  <% end %>
  <span data-controller="animated-number"><%= @post.score %></span>
  <% if authenticated? %>
    <%= button_to "▼", post_vote_path(@post), params: { vote: { value: -1 } },
          class: ("active-down" if @post.downvoted_by?(Current.user)) %>
  <% else %>
    <span></span>
  <% end %>

  <header>
    <% if @post.community %>
      <%= link_to @post.community.name, community_path(@post.community) %> ·
    <% end %>
    posted by <%= @post.author_name %> ·
    <time datetime="<%= @post.created_at.iso8601 %>" data-controller="timeago"><%= time_ago_in_words(@post.created_at) %> ago</time>
  </header>
  <h1><%= @post.title %></h1>
  <% if @post.content.present? %>
    <p><%= @post.content %></p>
  <% end %>
</article>

<section id="comments-section">
  <% if authenticated? %>
    <h3>Comment as <%= Current.user.username || "guest" %></h3>
    <%= form_with model: [@post, @new_comment], data: { turbo: true } do |f| %>
      <p>
        <%= f.text_area :content, placeholder: "What do you think?", rows: 4,
              data: { controller: "textarea-autogrow character-counter", "character-counter-max-value": 10000 } %>
        <span data-character-counter-target="counter"></span>
      </p>
      <p><%= f.submit "Comment" %></p>
    <% end %>
  <% end %>

  <ul id="comments">
    <% if @comments.any? %>
      <% @comments.each do |comment| %>
        <%= render "comments/comment", comment: comment %>
      <% end %>
    <% else %>
      <li>No comments yet. Be first.</li>
    <% end %>
  </ul>
</section>

<aside>
  <h3>About</h3>
  <% if @post.community %>
    <p><%= @post.community.description.presence || @post.community.name %></p>
    <%= link_to "View community", community_path(@post.community) %>
  <% else %>
    <p>brgen — Bergen community platform</p>
  <% end %>
</aside>

rails/brgen/app/app/views/pwa/manifest.json.erb

{
  "name": "App",
  "icons": [
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512"
    },
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "maskable"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "scope": "/",
  "description": "App.",
  "theme_color": "red",
  "background_color": "red"
}

rails/brgen/app/app/views/pwa/service-worker.js

// Add a service worker for processing Web Push notifications:
//
// self.addEventListener("push", async (event) => {
//   const { title, options } = await event.data.json()
//   event.waitUntil(self.registration.showNotification(title, options))
// })
//
// self.addEventListener("notificationclick", function(event) {
//   event.notification.close()
//   event.waitUntil(
//     clients.matchAll({ type: "window" }).then((clientList) => {
//       for (let i = 0; i < clientList.length; i++) {
//         let client = clientList[i]
//         let clientPath = (new URL(client.url)).pathname
//
//         if (clientPath == event.notification.data.path && "focus" in client) {
//           return client.focus()
//         }
//       }
//
//       if (clients.openWindow) {
//         return clients.openWindow(event.notification.data.path)
//       }
//     })
//   )
// })

rails/brgen/app/app/views/sessions/new.html.erb

<% if flash[:alert] %><p><%= flash[:alert] %></p><% end %>
<% if flash[:notice] %><p><%= flash[:notice] %></p><% end %>

<%= form_with url: session_path do |form| %>
  <p><%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %></p>
  <p><%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %></p>
  <p><%= form.submit "Sign in" %></p>
<% end %>

<p><%= link_to "Forgot password?", new_password_path %></p>

rails/brgen/app/app/views/shared/_vote.html.erb

<%= form_with url: votes_path(votable_type: votable.class.name, votable_id: votable.id), method: :post do |f| %>
  <%= f.hidden_field :value, value: 1 %>
  <%= f.button "▲", type: :submit %>
<% end %>
<span><%= votable.score %></span>
<%= form_with url: votes_path(votable_type: votable.class.name, votable_id: votable.id), method: :post do |f| %>
  <%= f.hidden_field :value, value: -1 %>
  <%= f.button "▼", type: :submit %>
<% end %>

rails/brgen/app/app/views/takeaway/orders/index.html.erb

<% content_for :title, "My Orders" %>
<h1>My Orders</h1>
<% @orders.each do |o| %>
  <article>
    <%= link_to o.restaurant.name, takeaway_order_path(o) %>
    <span><%= o.status %></span>
    <span><%= o.total_display %></span>
    <time><%= o.created_at.strftime("%d %b %Y") %></time>
  </article>
<% end %>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/brgen/app/app/views/takeaway/orders/show.html.erb

<% content_for :title, "Order ##{@order.id}" %>
<h1>Order #<%= @order.id %></h1>
<p>Status: <strong><%= @order.status %></strong></p>
<p>Restaurant: <%= link_to @order.restaurant.name, takeaway_restaurant_path(@order.restaurant) %></p>
<p>Delivery to: <%= @order.delivery_address %></p>
<ul>
  <% @order.order_items.includes(:menu_item).each do |oi| %>
    <li><%= oi.menu_item.name %> × <%= oi.quantity %><%= oi.subtotal_display %></li>
  <% end %>
</ul>
<p>Total: <strong><%= @order.total_display %></strong></p>
<% if Current.user == @order.restaurant.user && @order.status != "delivered" %>
  <%= button_to "Advance status", takeaway_order_path(@order), method: :patch, class: "btn btn--primary" %>
<% end %>

rails/brgen/app/app/views/takeaway/restaurants/edit.html.erb

<h1>Edit restaurant</h1>
<%= form_with model: @restaurant, url: takeaway_restaurant_path(@restaurant), method: :patch do |f| %>
  <%= render "shared/errors", object: @restaurant %>
  <p><%= f.label :name %><%= f.text_field :name %></p>
  <p><%= f.label :description %><%= f.text_area :description, rows: 3 %></p>
  <p><%= f.label :address %><%= f.text_field :address %></p>
  <p><%= f.label :city %><%= f.text_field :city %></p>
  <p><%= f.label :phone %><%= f.text_field :phone %></p>
  <p>
    <%= f.label :cuisine_type %>
    <%= f.select :cuisine_type, Takeaway::Restaurant::CUISINE_TYPES, { include_blank: "—" } %>
  </p>
  <p><%= f.label :delivery_fee_cents, "Delivery fee (øre)" %><%= f.number_field :delivery_fee_cents %></p>
  <p><%= f.label :min_order_cents, "Min order (øre)" %><%= f.number_field :min_order_cents %></p>
  <p><%= f.check_box :active %> <%= f.label :active, "Active" %></p>
  <p><%= f.submit "Save" %></p>
<% end %>

rails/brgen/app/app/views/takeaway/restaurants/index.html.erb

<% content_for :title, "Takeaway" %>
<h1>Order food</h1>
<% if authenticated? %><%= link_to "Add restaurant", new_takeaway_restaurant_path %><% end %>

<form method="get">
  <p><input name="q" value="<%= params[:q] %>" placeholder="Search restaurants…"></p>
  <p>
    <select name="cuisine">
      <option value="">All cuisines</option>
      <% Takeaway::Restaurant::CUISINE_TYPES.each do |c| %>
        <option value="<%= c %>" <%= "selected" if params[:cuisine] == c %>><%= c %></option>
      <% end %>
    </select>
  </p>
  <p><button type="submit">Search</button></p>
</form>

<section>
  <% @restaurants.each do |r| %>
    <article>
      <%= link_to r.name, takeaway_restaurant_path(r) %>
      <small><%= r.cuisine_type %> · <%= r.city %> · ★ <%= r.rating || "—" %></small>
      <small>Min order: <%= r.min_order_cents.to_i / 100.0 %> NOK · Delivery: <%= r.delivery_fee_cents.to_i / 100.0 %> NOK</small>
    </article>
  <% end %>
</section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/brgen/app/app/views/takeaway/restaurants/new.html.erb

<h1>Add restaurant</h1>
<%= form_with model: @restaurant, url: takeaway_restaurants_path do |f| %>
  <%= render "shared/errors", object: @restaurant %>
  <p><%= f.label :name %><%= f.text_field :name %></p>
  <p><%= f.label :description %><%= f.text_area :description, rows: 3 %></p>
  <p><%= f.label :address %><%= f.text_field :address %></p>
  <p><%= f.label :city %><%= f.text_field :city %></p>
  <p><%= f.label :phone %><%= f.text_field :phone %></p>
  <p>
    <%= f.label :cuisine_type %>
    <%= f.select :cuisine_type, Takeaway::Restaurant::CUISINE_TYPES, { include_blank: "—" } %>
  </p>
  <p><%= f.label :delivery_fee_cents, "Delivery fee (øre)" %><%= f.number_field :delivery_fee_cents %></p>
  <p><%= f.label :min_order_cents, "Min order (øre)" %><%= f.number_field :min_order_cents %></p>
  <p><%= f.check_box :active %> <%= f.label :active, "Active" %></p>
  <p><%= f.submit "Save" %></p>
<% end %>

rails/brgen/app/app/views/takeaway/restaurants/show.html.erb

<% content_for :title, @restaurant.name %>
<h1><%= @restaurant.name %></h1>
<p><%= @restaurant.description %></p>
<p><%= @restaurant.cuisine_type %> · <%= @restaurant.address %>, <%= @restaurant.city %></p>
<p>Min order: <%= @restaurant.min_order_cents.to_i / 100.0 %> NOK · Delivery: <%= @restaurant.delivery_fee_cents.to_i / 100.0 %> NOK</p>

<% if authenticated? && Current.user == @restaurant.user %>
  <nav>
    <%= link_to "Edit", edit_takeaway_restaurant_path(@restaurant) %>
  </nav>
  <h2>Add menu item</h2>
  <%= form_with url: takeaway_restaurant_menu_items_path(@restaurant) do |f| %>
    <p><input name="takeaway_menu_item[name]" placeholder="Name" required></p>
    <p><input name="takeaway_menu_item[price_cents]" type="number" placeholder="Price (øre)" required></p>
    <p><input name="takeaway_menu_item[description]" placeholder="Description"></p>
    <p><button type="submit">Add</button></p>
  <% end %>
<% end %>

<h2>Menu</h2>
<% if authenticated? %>
  <%= form_with url: takeaway_restaurant_orders_path(@restaurant) do |f| %>
    <ul>
      <% @menu_items.each do |item| %>
        <li>
          <strong><%= item.name %></strong><%= item.price_display %>
          <% if item.description.present? %><br><small><%= item.description %></small><% end %>
          <input type="number" name="takeaway_order[items][<%= item.id %>]" min="0" value="0">
        </li>
      <% end %>
    </ul>
    <p><input name="takeaway_order[delivery_address]" placeholder="Delivery address" required></p>
    <p><textarea name="takeaway_order[special_instructions]" placeholder="Special instructions (optional)"></textarea></p>
    <p><button type="submit">Place order</button></p>
  <% end %>
<% else %>
  <%= link_to "Sign in to order", new_session_path %>
<% end %>

rails/brgen/app/app/views/tv/channels/edit.html.erb

<% content_for :title, "Edit channel" %>
<h1>Edit channel</h1>
<%= form_with model: @channel, url: tv_channel_path(@channel), method: :patch do |f| %>
  <%= render "shared/errors", object: @channel %>
  <p>
    <%= f.label :name %>
    <%= f.text_field :name, required: true %>
  </p>
  <p>
    <%= f.label :description %>
    <%= f.text_area :description, rows: 4 %>
  </p>
  <p>
    <%= f.label :avatar %>
    <%= f.file_field :avatar, accept: "image/*" %>
  </p>
  <p>
    <%= f.label :banner %>
    <%= f.file_field :banner, accept: "image/*" %>
  </p>
  <p><%= f.submit "Save" %></p>
<% end %>

rails/brgen/app/app/views/tv/channels/index.html.erb

<h1>Channels</h1>
<section>
  <% @channels.each do |c| %>
    <article>
      <%= link_to c.name, tv_channel_path(c) %>
      <small><%= c.subscribers_count %> subscribers<% if c.live? %> · <mark>LIVE</mark><% end %></small>
    </article>
  <% end %>
</section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/brgen/app/app/views/tv/channels/new.html.erb

<% content_for :title, "New channel" %>
<h1>New channel</h1>
<%= form_with model: @channel, url: tv_channels_path do |f| %>
  <%= render "shared/errors", object: @channel %>
  <p>
    <%= f.label :name %>
    <%= f.text_field :name, required: true %>
  </p>
  <p>
    <%= f.label :description %>
    <%= f.text_area :description, rows: 4 %>
  </p>
  <p>
    <%= f.label :avatar %>
    <%= f.file_field :avatar, accept: "image/*" %>
  </p>
  <p>
    <%= f.label :banner %>
    <%= f.file_field :banner, accept: "image/*" %>
  </p>
  <p><%= f.submit "Create channel" %></p>
<% end %>

rails/brgen/app/app/views/tv/channels/show.html.erb

<% content_for :title, @channel.name %>
<h1><%= @channel.name %></h1>
<p><%= @channel.subscribers_count %> subscribers</p>
<p><%= @channel.description %></p>
<% if authenticated? && Current.user != @channel.user %>
  <% if @channel.subscriptions.exists?(user: Current.user) %>
    <%= button_to "Unsubscribe", unsubscribe_tv_channel_path(@channel), method: :delete %>
  <% else %>
    <%= button_to "Subscribe", subscribe_tv_channel_path(@channel), method: :post %>
  <% end %>
<% end %>
<section><%= render @videos %></section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/brgen/app/app/views/tv/home/index.html.erb

<% content_for :title, "Brgen TV" %>
<h1>Brgen TV</h1>
<nav>
  <%= link_to "All channels", tv_channels_path %>
  <% if authenticated? %> · <%= link_to "Create channel", new_tv_channel_path %><% end %>
</nav>

<% if @live.any? %>
  <section>
    <h2>Live now</h2>
    <% @live.each do |b| %>
      <article>
        <%= link_to tv_channel_path(b.channel) do %>
          <mark>LIVE</mark> <%= b.title %><%= b.channel.name %>
        <% end %>
      </article>
    <% end %>
  </section>
<% end %>

<section>
  <h2>Trending</h2>
  <%= render @trending %>
  <%= @pagy_trending.series_nav if @pagy_trending.pages > 1 %>
</section>

<section>
  <h2>Recent</h2>
  <%= render @recent %>
</section>

rails/brgen/app/app/views/tv/videos/_tv_video.html.erb

<article>
  <%= link_to tv_video_path(tv_video) do %>
    <% if tv_video.thumbnail.attached? %>
      <%= image_tag tv_video.thumbnail %>
    <% else %>
      <span><%= tv_video.title[0] %></span>
    <% end %>
    <p><strong><%= tv_video.title %></strong></p>
    <small><%= tv_video.channel.name %> · <%= tv_video.views_count %> views · <%= tv_video.duration_formatted %></small>
  <% end %>
</article>

rails/brgen/app/app/views/tv/videos/new.html.erb

<% content_for :title, "Upload video" %>
<h1>Upload video</h1>
<%= form_with model: @video, url: tv_videos_path, multipart: true do |f| %>
  <%= render "shared/errors", object: @video %>
  <p>
    <%= f.label :tv_channel_id, "Channel" %>
    <%= f.collection_select :tv_channel_id, Current.user.tv_channels.order(:name), :id, :name, include_blank: false %>
  </p>
  <p>
    <%= f.label :title %>
    <%= f.text_field :title, required: true %>
  </p>
  <p>
    <%= f.label :description %>
    <%= f.text_area :description, rows: 4 %>
  </p>
  <p>
    <%= f.label :video_file %>
    <%= f.file_field :video_file, accept: "video/*", required: true %>
  </p>
  <p>
    <%= f.label :thumbnail %>
    <%= f.file_field :thumbnail, accept: "image/*" %>
  </p>
  <p><%= f.submit "Upload" %></p>
<% end %>

rails/brgen/app/app/views/tv/videos/show.html.erb

<% content_for :title, @video.title %>
<% if @video.video_file.attached? %>
  <%= video_tag url_for(@video.video_file), controls: true, class: "video-player" %>
<% end %>
<h1><%= @video.title %></h1>
<p><%= link_to @video.channel.name, tv_channel_path(@video.channel) %> · <%= @video.views_count %> views · <%= @video.duration_formatted %></p>
<p><%= @video.description %></p>

rails/brgen/app/app/views/typing_indicators/_indicator.html.erb

<p id="typing-indicator">
  <% typers = conversation.typing_indicators.active.includes(:user).reject { |t| t.user_id == except_user&.id } %>
  <% if typers.any? %>
    <%= typers.map { |t| t.user.username }.join(", ") %> typing...
  <% end %>
</p>

rails/brgen/app/app/views/votes/create.turbo_stream.erb

<%= turbo_stream.replace dom_id(@votable) do %>
  <% case @votable %>
  <% when Post %><%= render "posts/post", post: @votable %>
  <% when Comment %><%= render "comments/comment", comment: @votable %>
  <% end %>
<% end %>

rails/brgen/app/config/application.rb

require_relative "boot"

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module App
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.2

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w[assets tasks])

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Don't generate system test files.
    config.generators.system_tests = nil
  end
end

rails/brgen/app/config/boot.rb

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

require "bundler/setup" # Set up gems listed in the Gemfile.

rails/brgen/app/config/bundler-audit.yml

# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit.
# CVEs that are not relevant to the application can be enumerated on the ignore list below.

ignore:
  - CVE-THAT-DOES-NOT-APPLY

rails/brgen/app/config/cable.yml

# Async adapter only works within the same process, so for manually triggering cable updates from a console,
# and seeing results in the browser, you must do so from the web console (running inside the dev process),
# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
# to make the web console appear.
development:
  adapter: async

test:
  adapter: test

production:
  adapter: solid_cable
  connects_to:
    database:
      writing: cable
  polling_interval: 0.1.seconds
  message_retention: 1.day

rails/brgen/app/config/cache.yml

default: &default
  store_options:
    # Cap age of oldest cache entry to fulfill retention policies
    # max_age: <%= 60.days.to_i %>
    max_size: <%= 256.megabytes %>
    namespace: <%= Rails.env %>

development:
  <<: *default

test:
  <<: *default

production:
  database: cache
  <<: *default

rails/brgen/app/config/ci.rb

# Run using bin/ci

CI.run do
  step "Setup", "bin/setup --skip-server"

  step "Style: Ruby", "bin/rubocop"

  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Importmap vulnerability audit", "bin/importmap audit"
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"

  step "Tests: Rails", "bin/rails test"
  step "Tests: System", "bin/rails test:system"
  step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"

  # Optional: set a green GitHub commit status to unblock PR merge.
  # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
  # if success?
  #   step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
  # else
  #   failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
  # end
end

rails/brgen/app/config/database.yml

default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  primary:
    <<: *default
    database: db/development.sqlite3
  cache:
    <<: *default
    database: db/development_cache.sqlite3
    migrations_paths: db/cache_schema_migrations
  queue:
    <<: *default
    database: db/development_queue.sqlite3
    migrations_paths: db/queue_schema_migrations
  cable:
    <<: *default
    database: db/development_cable.sqlite3
    migrations_paths: db/cable_schema_migrations

test:
  primary:
    <<: *default
    database: db/test.sqlite3
  cache:
    <<: *default
    database: db/test_cache.sqlite3
    migrations_paths: db/cache_schema_migrations
  queue:
    <<: *default
    database: db/test_queue.sqlite3
    migrations_paths: db/queue_schema_migrations
  cable:
    <<: *default
    database: db/test_cable.sqlite3
    migrations_paths: db/cable_schema_migrations

production:
  primary:
    <<: *default
    database: db/production.sqlite3
  cache:
    <<: *default
    database: db/production_cache.sqlite3
    migrations_paths: db/cache_schema_migrations
  queue:
    <<: *default
    database: db/production_queue.sqlite3
    migrations_paths: db/queue_schema_migrations
  cable:
    <<: *default
    database: db/production_cable.sqlite3
    migrations_paths: db/cable_schema_migrations

rails/brgen/app/config/deploy.yml

# Name of your application. Used to uniquely configure containers.
service: app

# Name of the container image (use your-user/app-name on external registries).
image: app

# Deploy to these servers.
servers:
  web:
    - 192.168.0.1
  # job:
  #   hosts:
  #     - 192.168.0.1
  #   cmd: bin/jobs

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
#
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
# proxy:
#   ssl: true
#   host: app.example.com

# Where you keep your container images.
registry:
  # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ...
  server: localhost:5555

  # Needed for authenticated registries.
  # username: your-user

  # Always use an access token rather than real password when possible.
  # password:
  #   - KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
  secret:
    - RAILS_MASTER_KEY
  clear:
    # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
    # When you start using multiple servers, you should split out job processing to a dedicated machine.
    SOLID_QUEUE_IN_PUMA: true

    # Set number of processes dedicated to Solid Queue (default: 1)
    # JOB_CONCURRENCY: 3

    # Set number of cores available to the application on each server (default: 1).
    # WEB_CONCURRENCY: 2

    # Match this to any external database server to configure Active Record correctly
    # Use app-db for a db accessory server on same machine via local kamal docker network.
    # DB_HOST: 192.168.0.2

    # Log everything from Rails
    # RAILS_LOG_LEVEL: debug

# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
  console: app exec --interactive --reuse "bin/rails console"
  shell: app exec --interactive --reuse "bash"
  logs: app logs -f
  dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"

# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
  - "app_storage:/rails/storage"

# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets


# Configure the image builder.
builder:
  arch: amd64

  # # Build image via remote server (useful for faster amd64 builds on arm64 computers)
  # remote: ssh://docker@docker-builder-server
  #
  # # Pass arguments and secrets to the Docker build process
  # args:
  #   RUBY_VERSION: ruby-3.3.7
  # secrets:
  #   - GITHUB_TOKEN
  #   - RAILS_MASTER_KEY

# Use a different ssh user than root
# ssh:
#   user: app

# Use accessory services (secrets come from .kamal/secrets).
# accessories:
#   db:
#     image: mysql:8.0
#     host: 192.168.0.2
#     # Change to 3306 to expose port to the world instead of just local network.
#     port: "127.0.0.1:3306:3306"
#     env:
#       clear:
#         MYSQL_ROOT_HOST: '%'
#       secret:
#         - MYSQL_ROOT_PASSWORD
#     files:
#       - config/mysql/production.cnf:/etc/mysql/my.cnf
#       - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
#     directories:
#       - data:/var/lib/mysql
#   redis:
#     image: valkey/valkey:8
#     host: 192.168.0.2
#     port: 6379
#     directories:
#       - data:/data

rails/brgen/app/config/environment.rb

# Load the Rails application.
require_relative "application"

# Initialize the Rails application.
Rails.application.initialize!

rails/brgen/app/config/environments/development.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Make code changes take effect immediately without server restart.
  config.enable_reloading = true

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports.
  config.consider_all_requests_local = true

  # Enable server timing.
  config.server_timing = true

  # Enable/disable Action Controller caching. By default Action Controller caching is disabled.
  # Run rails dev:cache to toggle Action Controller caching.
  if Rails.root.join("tmp/caching-dev.txt").exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true
    config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
  else
    config.action_controller.perform_caching = false
  end

  # Change to :null_store to avoid any caching.
  config.cache_store = :memory_store

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  # Make template changes take effect immediately.
  config.action_mailer.perform_caching = false

  # Set localhost to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Highlight code that triggered database queries in logs.
  config.active_record.verbose_query_logs = true

  # Append comments with runtime information tags to SQL queries in logs.
  config.active_record.query_log_tags_enabled = true

  # Highlight code that enqueued background job in logs.
  config.active_job.verbose_enqueue_logs = true

  # Highlight code that triggered redirect in logs.
  config.action_dispatch.verbose_redirect_logs = true

  # Suppress logger output for asset requests.
  # config.assets.quiet = true  # sprockets only — not used with propshaft

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  config.action_view.annotate_rendered_view_with_filenames = true

  # Uncomment if you wish to allow Action Cable access from any origin.
  # config.action_cable.disable_request_forgery_protection = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true

  # Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
  # config.generators.apply_rubocop_autocorrect_after_generate!
end

rails/brgen/app/config/environments/production.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Code is not reloaded between requests.
  config.enable_reloading = false

  # Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
  config.eager_load = true

  # Full error reports are disabled.
  config.consider_all_requests_local = false

  # Turn on fragment caching in view templates.
  config.action_controller.perform_caching = true

  # Cache assets for far-future expiry since they are all digest stamped.
  config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }

  # Enable serving of images, stylesheets, and JavaScripts from an asset server.
  # config.asset_host = "http://assets.example.com"

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Assume all access to the app is happening through a SSL-terminating reverse proxy.
  config.assume_ssl = true

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  config.force_ssl = true

  # Skip http-to-https redirect for the default health check endpoint.
  # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }

  # Log to STDOUT with the current request id as a default log tag.
  config.log_tags = [ :request_id ]
  config.logger   = Logger.new(STDOUT)

  # Change to "debug" to log everything (including potentially personally-identifiable information!).
  config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")

  # Prevent health checks from clogging up the logs.
  config.silence_healthcheck_path = "/up"

  # Don't log any deprecations.
  config.active_support.report_deprecations = false

  # Replace the default in-process memory cache store with a durable alternative.
  config.cache_store = :solid_cache_store

  # Replace the default in-process and non-durable queuing backend for Active Job.
  config.active_job.queue_adapter = :solid_queue
  config.solid_queue.connects_to = { database: { writing: :queue } }

  # Ignore bad email addresses and do not raise email delivery errors.
  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
  # config.action_mailer.raise_delivery_errors = false

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
  # config.action_mailer.smtp_settings = {
  #   user_name: Rails.application.credentials.dig(:smtp, :user_name),
  #   password: Rails.application.credentials.dig(:smtp, :password),
  #   address: "smtp.example.com",
  #   port: 587,
  #   authentication: :plain
  # }

  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
  # the I18n.default_locale when a translation cannot be found).
  config.i18n.fallbacks = true

  # Do not dump schema after migrations.
  config.active_record.dump_schema_after_migration = false

  # Only use :id for inspections in production.
  config.active_record.attributes_for_inspect = [ :id ]

  config.hosts = ["brgen.no", /.*\.brgen\.no\z/]
  config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end

rails/brgen/app/config/environments/test.rb

# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # While tests run files are not watched, reloading is not necessary.
  config.enable_reloading = false

  # Eager loading loads your entire application. When running a single test locally,
  # this is usually not necessary, and can slow down your test suite. However, it's
  # recommended that you enable it in continuous integration systems to ensure eager
  # loading is working properly before deploying your code.
  config.eager_load = ENV["CI"].present?

  # Configure public file server for tests with cache-control for performance.
  config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }

  # Show full error reports.
  config.consider_all_requests_local = true
  config.cache_store = :null_store

  # Render exception templates for rescuable exceptions and raise for other exceptions.
  config.action_dispatch.show_exceptions = :rescuable

  # Disable request forgery protection in test environment.
  config.action_controller.allow_forgery_protection = false

  # Store uploaded files on the local file system in a temporary directory.
  config.active_storage.service = :test

  # Tell Action Mailer not to deliver emails to the real world.
  # The :test delivery method accumulates sent emails in the
  # ActionMailer::Base.deliveries array.
  config.action_mailer.delivery_method = :test

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Print deprecation notices to the stderr.
  config.active_support.deprecation = :stderr

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  # config.action_view.annotate_rendered_view_with_filenames = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true
end

rails/brgen/app/config/falcon.rb

# frozen_string_literal: true

load :rack, :supervisor

hostname = File.basename(__dir__)
port = ENV.fetch("PORT", 11006).to_i

rack hostname do
  endpoint Async::HTTP::Endpoint.parse("http://0.0.0.0:#{port}").with(protocol: Async::HTTP::Protocol::HTTP2)
end

rails/brgen/app/config/importmap.rb

# Pin npm packages by running ./bin/importmap

pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "@stimulus-components/dialog", to: "@stimulus-components--dialog.js" # @1.0.1
pin "@stimulus-components/auto-submit", to: "@stimulus-components--auto-submit.js" # @6.0.0
pin "@stimulus-components/character-counter", to: "@stimulus-components--character-counter.js" # @5.1.0
pin "@stimulus-components/dropdown", to: "@stimulus-components--dropdown.js" # @3.0.0
pin "stimulus-use" # @0.52.3
pin "@stimulus-components/clipboard", to: "@stimulus-components--clipboard.js" # @5.0.0
pin "@stimulus-components/notification", to: "@stimulus-components--notification.js" # @3.0.0
pin "@stimulus-components/timeago", to: "@stimulus-components--timeago.js" # @5.0.2
pin "date-fns" # @4.1.0
pin "@stimulus-components/animated-number", to: "@stimulus-components--animated-number.js" # @5.0.0
pin "@stimulus-components/sortable", to: "@stimulus-components--sortable.js" # @5.0.3
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_request", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_request.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_response", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_response.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/lib/utils", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--lib--utils.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/request_interceptor", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--request_interceptor.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/verbs", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--verbs.js" # @0.0.13
pin "@rails/request.js", to: "@rails--request.js.js" # @0.0.13
pin "sortablejs" # @1.15.7

rails/brgen/app/config/initializers/assets.rb

# assets initializer disabled - using Propshaft not Sprockets

rails/brgen/app/config/initializers/content_security_policy.rb

# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header

# Rails.application.configure do
#   config.content_security_policy do |policy|
#     policy.default_src :self, :https
#     policy.font_src    :self, :https, :data
#     policy.img_src     :self, :https, :data
#     policy.object_src  :none
#     policy.script_src  :self, :https
#     policy.style_src   :self, :https
#     # Specify URI for violation reports
#     # policy.report_uri "/csp-violation-report-endpoint"
#   end
#
#   # Generate session nonces for permitted importmap, inline scripts, and inline styles.
#   config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
#   config.content_security_policy_nonce_directives = %w(script-src style-src)
#
#   # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
#   # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
#   # config.content_security_policy_nonce_auto = true
#
#   # Report violations without enforcing the policy.
#   # config.content_security_policy_report_only = true
# end

rails/brgen/app/config/initializers/filter_parameter_logging.rb

# Be sure to restart your server when you modify this file.

# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
  :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
]

rails/brgen/app/config/initializers/inflections.rb

# Be sure to restart your server when you modify this file.

# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.plural /^(ox)$/i, "\\1en"
#   inflect.singular /^(ox)en/i, "\\1"
#   inflect.irregular "person", "people"
#   inflect.uncountable %w( fish sheep )
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.acronym "RESTful"
# end

rails/brgen/app/config/locales/en.yml

# Files in the config/locales directory are used for internationalization and
# are automatically loaded by Rails. If you want to use locales other than
# English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
#     I18n.t "hello"
#
# In views, this is aliased to just `t`:
#
#     <%= t("hello") %>
#
# To use a different locale, set it with `I18n.locale`:
#
#     I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# To learn more about the API, please read the Rails Internationalization guide
# at https://guides.rubyonrails.org/i18n.html.
#
# Be aware that YAML interprets the following case-insensitive strings as
# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
# must be quoted to be interpreted as strings. For example:
#
#     en:
#       "yes": yup
#       enabled: "ON"

en:
  hello: "Hello world"

rails/brgen/app/config/puma.rb

# This configuration file will be evaluated by Puma. The top-level methods that
# are invoked here are part of Puma's configuration DSL. For more information
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
#
# Puma starts a configurable number of processes (workers) and each process
# serves each request in a thread from an internal thread pool.
#
# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You
# should only set this value when you want to run 2 or more workers. The
# default is already 1. You can set it to `auto` to automatically start a worker
# for each available processor.
#
# The ideal number of threads per worker depends both on how much time the
# application spends waiting for IO operations and on how much you wish to
# prioritize throughput over latency.
#
# As a rule of thumb, increasing the number of threads will increase how much
# traffic a given process can handle (throughput), but due to CRuby's
# Global VM Lock (GVL) it has diminishing returns and will degrade the
# response time (latency) of the application.
#
# The default is set to 3 threads as it's deemed a decent compromise between
# throughput and latency for the average Rails application.
#
# Any libraries that use a connection pool or another resource pool should
# be configured to provide at least as many connections as the number of
# threads. This includes Active Record's `pool` parameter in `database.yml`.
threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
port ENV.fetch("PORT", 3000)

# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart

# Run the Solid Queue supervisor inside of Puma for single-server deployments.
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]

# Specify the PID file. Defaults to tmp/pids/server.pid in development.
# In other environments, only set the PID file if requested.
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]

rails/brgen/app/config/queue.yml

default: &default
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: "*"
      threads: 3
      processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
      polling_interval: 0.1

development:
  <<: *default

test:
  <<: *default

production:
  <<: *default

rails/brgen/app/config/recurring.yml

# examples:
#   periodic_cleanup:
#     class: CleanSoftDeletedRecordsJob
#     queue: background
#     args: [ 1000, { batch_size: 500 } ]
#     schedule: every hour
#   periodic_cleanup_with_command:
#     command: "SoftDeletedRecord.due.delete_all"
#     priority: 2
#     schedule: at 5am every day

production:
  clear_solid_queue_finished_jobs:
    command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
    schedule: every hour at minute 12

rails/brgen/app/config/routes.rb

Rails.application.routes.draw do
  # Vertical subdomains share one app. The Rails module name (Tv:: etc.) is fixed;
  # the public subdomain varies per locale (markedsplass = NO marketplace).
  TV_SUBDOMAINS         = %w[tv].freeze
  DATING_SUBDOMAINS     = %w[dating].freeze
  PLAYLIST_SUBDOMAINS   = %w[playlist].freeze
  TAKEAWAY_SUBDOMAINS   = %w[takeaway].freeze
  MARKETPLACE_SUBDOMAINS = %w[
    markedsplass markadur marknadsplats marktplaats marktplatz
    marche mercato mercado markkinapaikka marketplace
  ].freeze

  resource  :session
  resources :passwords, param: :token

  resources :communities do
    resources :posts, shallow: true do
      resources :comments, shallow: true do
        resources :comments, shallow: true, as: :replies
      end
      resource  :vote, only: [:create], controller: "votes"
    end
  end

  resources :posts do
    resources :comments, shallow: true
    resource  :vote, only: [:create], controller: "votes"
  end

  resources :comments do
    resource  :vote, only: [:create], controller: "votes"
    resources :comments, only: [:create], as: :replies
  end

  resources :users, only: [:show] do
    member do
      post   :follow,   to: "follows#create"
      delete :unfollow, to: "follows#destroy"
    end
    resources :conversations, only: [:create]
  end

  resources :conversations, only: [:index, :show] do
    resources :messages, only: [:create]
  end

  constraints(subdomain: TV_SUBDOMAINS) do
    scope module: "tv", as: "tv" do
      root "home#index", as: :tv_root
      resources :channels, param: :slug do
        member { post :subscribe; delete :unsubscribe }
        resources :videos, only: %i[new create]
      end
      resources :videos, only: %i[show destroy]
    end
  end

  constraints(subdomain: DATING_SUBDOMAINS) do
    scope module: "dating", as: "dating" do
      root "home#index", as: :dating_root
      resource  :profile,  only: %i[new create edit update show]
      resources :likes,    only: :create
      resources :dislikes, only: :create
      resources :matches,  only: :index
    end
  end

  constraints(subdomain: PLAYLIST_SUBDOMAINS) do
    scope module: "playlist", as: "playlist" do
      root "playlists#index", as: :playlist_root
      resources :playlists do
        resources :tracks, only: %i[create destroy]
      end
      resources :listens, only: :create
    end
  end

  constraints(subdomain: TAKEAWAY_SUBDOMAINS) do
    scope module: "takeaway", as: "takeaway" do
      root "restaurants#index", as: :takeaway_root
      resources :restaurants do
        resources :menu_items, only: %i[create destroy]
        resources :orders,     only: %i[new create]
      end
      resources :orders, only: %i[index show update]
    end
  end

  constraints(subdomain: MARKETPLACE_SUBDOMAINS) do
    scope module: "marketplace", as: "marketplace" do
      root "listings#index", as: :marketplace_root
      resources :listings do
        resources :orders, only: %i[create update]
      end
      resources :categories, only: :show, param: :id
    end
  end

  root "home#index"
  get  "up" => "rails/health#show", as: :rails_health_check
end

rails/brgen/app/config/storage.yml

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket-<%= Rails.env %>

# Remember not to checkin your GCS keyfile to a repository
# google:
#   service: GCS
#   project: your_project
#   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
#   bucket: your_own_bucket-<%= Rails.env %>

# mirror:
#   service: Mirror
#   primary: local
#   mirrors: [ amazon, google, microsoft ]

rails/brgen/app/db/cable_schema.rb

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 0) do
end

rails/brgen/app/db/cache_schema.rb

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 0) do
end

rails/brgen/app/db/migrate/20260311162114_create_users.rb

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users do |t|
      t.string :email_address, null: false
      t.string :password_digest, null: false

      t.timestamps
    end
    add_index :users, :email_address, unique: true
  end
end

rails/brgen/app/db/migrate/20260311162121_create_sessions.rb

class CreateSessions < ActiveRecord::Migration[8.1]
  def change
    create_table :sessions do |t|
      t.references :user, null: false, foreign_key: true
      t.string :ip_address
      t.string :user_agent

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311162206_create_communities.rb

class CreateCommunities < ActiveRecord::Migration[8.1]
  def change
    create_table :communities do |t|
      t.string :name
      t.text :description
      t.string :subdomain
      t.string :slug

      t.timestamps
    end
    add_index :communities, :subdomain, unique: true
    add_index :communities, :slug, unique: true
  end
end

rails/brgen/app/db/migrate/20260311162227_create_reactions.rb

class CreateReactions < ActiveRecord::Migration[8.1]
  def change
    create_table :reactions do |t|
      t.string :kind
      t.references :user, null: false, foreign_key: true
      t.references :post, null: false, foreign_key: true

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311162235_create_streams.rb

class CreateStreams < ActiveRecord::Migration[8.1]
  def change
    create_table :streams do |t|
      t.string :content_type
      t.string :url
      t.references :user, null: false, foreign_key: true
      t.references :post, null: false, foreign_key: true
      t.integer :duration

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311162345_create_posts.rb

class CreatePosts < ActiveRecord::Migration[8.1]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :content
      t.references :user, null: false, foreign_key: true
      t.references :community, null: false, foreign_key: true
      t.integer :karma
      t.boolean :anonymous

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311162350_create_comments.rb

class CreateComments < ActiveRecord::Migration[8.1]
  def change
    create_table :comments do |t|
      t.text :content
      t.references :user, null: false, foreign_key: true
      t.references :commentable, polymorphic: true, null: false
      t.integer :parent_id

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311162355_add_fields_to_users.rb

class AddFieldsToUsers < ActiveRecord::Migration[8.1]
  def change
    add_column :users, :username, :string
    add_column :users, :karma, :integer
  end
end

rails/brgen/app/db/migrate/20260311163039_create_votes.rb

class CreateVotes < ActiveRecord::Migration[8.1]
  def change
    create_table :votes do |t|
      t.integer :value
      t.references :user, null: false, foreign_key: true
      t.references :votable, polymorphic: true, null: false

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311163634_create_follows.rb

class CreateFollows < ActiveRecord::Migration[8.1]
  def change
    create_table :follows do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :follows, :follower_id
    add_index :follows, :followed_id
  end
end

rails/brgen/app/db/migrate/20260311163641_create_hashtags.rb

class CreateHashtags < ActiveRecord::Migration[8.1]
  def change
    create_table :hashtags do |t|
      t.string :name
      t.integer :usage_count

      t.timestamps
    end
    add_index :hashtags, :name, unique: true
  end
end

rails/brgen/app/db/migrate/20260311163648_create_taggings.rb

class CreateTaggings < ActiveRecord::Migration[8.1]
  def change
    create_table :taggings do |t|
      t.references :taggable, polymorphic: true, null: false
      t.references :hashtag, null: false, foreign_key: true

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311163655_create_mentions.rb

class CreateMentions < ActiveRecord::Migration[8.0]
  def change
    create_table :mentions do |t|
      t.references :mentionable, polymorphic: true, null: false
      t.references :mentioned_user, null: false, foreign_key: { to_table: :users }

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311164112_create_conversations.rb

class CreateConversations < ActiveRecord::Migration[8.1]
  def change
    create_table :conversations do |t|
      t.string :conversation_type
      t.string :name

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311164119_create_conversation_participants.rb

class CreateConversationParticipants < ActiveRecord::Migration[8.1]
  def change
    create_table :conversation_participants do |t|
      t.references :conversation, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
      t.datetime :last_read_at

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311164127_create_messages.rb

class CreateMessages < ActiveRecord::Migration[8.1]
  def change
    create_table :messages do |t|
      t.references :conversation, null: false, foreign_key: true
      t.integer :sender_id
      t.text :content
      t.string :message_type
      t.datetime :expires_at

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311164134_create_message_receipts.rb

class CreateMessageReceipts < ActiveRecord::Migration[8.1]
  def change
    create_table :message_receipts do |t|
      t.references :message, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
      t.datetime :delivered_at
      t.datetime :read_at

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311164141_create_typing_indicators.rb

class CreateTypingIndicators < ActiveRecord::Migration[8.1]
  def change
    create_table :typing_indicators do |t|
      t.references :conversation, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
      t.datetime :expires_at

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260311165000_add_guest_to_users.rb

class AddGuestToUsers < ActiveRecord::Migration[8.0]
  def change
    add_column :users, :guest, :boolean, default: false, null: false
    add_column :users, :display_name, :string
  end
end

rails/brgen/app/db/migrate/20260311221744_add_user_description_to_communities.rb

class AddUserDescriptionToCommunities < ActiveRecord::Migration[8.1]
  def change
    add_column :communities, :user_id, :integer unless column_exists?(:communities, :user_id)
    add_column :communities, :description, :text unless column_exists?(:communities, :description)
  end
end

rails/brgen/app/db/migrate/20260505002649_create_tv_channels.rb

class CreateTvChannels < ActiveRecord::Migration[8.1]
  def change
    create_table :tv_channels do |t|
      t.references :user, null: false, foreign_key: true
      t.string :name
      t.text :description
      t.string :slug
      t.integer :subscribers_count
      t.integer :total_views

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505002659_create_tv_videos.rb

class CreateTvVideos < ActiveRecord::Migration[8.1]
  def change
    create_table :tv_videos do |t|
      t.references :tv_channel, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
      t.string :title
      t.text :description
      t.string :status
      t.integer :duration_seconds
      t.integer :views_count
      t.integer :likes_count
      t.integer :comments_count
      t.datetime :published_at
      t.string :thumbnail_url

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505002711_create_tv_broadcasts.rb

class CreateTvBroadcasts < ActiveRecord::Migration[8.1]
  def change
    create_table :tv_broadcasts do |t|
      t.references :tv_channel, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
      t.string :title
      t.text :description
      t.string :status
      t.string :stream_key
      t.integer :viewer_count
      t.datetime :started_at
      t.datetime :ended_at

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505002719_create_tv_subscriptions.rb

class CreateTvSubscriptions < ActiveRecord::Migration[8.1]
  def change
    create_table :tv_subscriptions do |t|
      t.references :user, null: false, foreign_key: true
      t.references :tv_channel, null: false, foreign_key: true
      t.boolean :notify_on_upload

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505002729_create_tv_view_events.rb

class CreateTvViewEvents < ActiveRecord::Migration[8.1]
  def change
    create_table :tv_view_events do |t|
      t.references :user, null: false, foreign_key: true
      t.references :tv_video, null: false, foreign_key: true
      t.integer :watch_time_seconds
      t.boolean :completed

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505014447_create_dating_profiles.rb

class CreateDatingProfiles < ActiveRecord::Migration[8.1]
  def change
    create_table :dating_profiles do |t|
      t.references :user, null: false, foreign_key: true
      t.text :bio
      t.string :gender
      t.string :looking_for
      t.integer :age
      t.string :location
      t.decimal :latitude
      t.decimal :longitude
      t.boolean :visible

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505014452_create_dating_likes.rb

class CreateDatingLikes < ActiveRecord::Migration[8.1]
  def change
    create_table :dating_likes do |t|
      t.references :liker, null: false, foreign_key: true
      t.references :likee, null: false, foreign_key: true

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505014457_create_dating_dislikes.rb

class CreateDatingDislikes < ActiveRecord::Migration[8.1]
  def change
    create_table :dating_dislikes do |t|
      t.references :disliker, null: false, foreign_key: true
      t.references :dislikee, null: false, foreign_key: true

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505014503_create_dating_matches.rb

class CreateDatingMatches < ActiveRecord::Migration[8.1]
  def change
    create_table :dating_matches do |t|
      t.references :initiator, null: false, foreign_key: true
      t.references :receiver, null: false, foreign_key: true
      t.string :status

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015400_create_playlist_playlists.rb

class CreatePlaylistPlaylists < ActiveRecord::Migration[8.1]
  def change
    create_table :playlist_playlists do |t|
      t.references :user, null: false, foreign_key: true
      t.string :name
      t.text :description
      t.boolean :public_access
      t.integer :plays_count
      t.integer :likes_count
      t.integer :tracks_count

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015406_create_playlist_tracks.rb

class CreatePlaylistTracks < ActiveRecord::Migration[8.1]
  def change
    create_table :playlist_tracks do |t|
      t.string :title
      t.string :artist
      t.string :album
      t.integer :duration_seconds
      t.string :genre
      t.string :source_type
      t.string :source_url

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015411_create_playlist_playlist_tracks.rb

class CreatePlaylistPlaylistTracks < ActiveRecord::Migration[8.1]
  def change
    create_table :playlist_playlist_tracks do |t|
      t.references :playlist_playlist, null: false, foreign_key: true
      t.references :playlist_track, null: false, foreign_key: true
      t.integer :position
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015416_create_playlist_listens.rb

class CreatePlaylistListens < ActiveRecord::Migration[8.1]
  def change
    create_table :playlist_listens do |t|
      t.references :user, null: false, foreign_key: true
      t.references :playlist_track, null: false, foreign_key: true

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015440_create_takeaway_restaurants.rb

class CreateTakeawayRestaurants < ActiveRecord::Migration[8.1]
  def change
    create_table :takeaway_restaurants do |t|
      t.references :user, null: false, foreign_key: true
      t.string :name
      t.text :description
      t.string :address
      t.string :city
      t.string :phone
      t.string :cuisine_type
      t.integer :delivery_fee_cents
      t.integer :min_order_cents
      t.decimal :rating
      t.integer :reviews_count
      t.boolean :active

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015446_create_takeaway_menu_items.rb

class CreateTakeawayMenuItems < ActiveRecord::Migration[8.1]
  def change
    create_table :takeaway_menu_items do |t|
      t.references :restaurant, null: false, foreign_key: true
      t.string :name
      t.text :description
      t.integer :price_cents
      t.boolean :available
      t.boolean :vegetarian
      t.boolean :vegan

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015451_create_takeaway_orders.rb

class CreateTakeawayOrders < ActiveRecord::Migration[8.1]
  def change
    create_table :takeaway_orders do |t|
      t.references :user, null: false, foreign_key: true
      t.references :restaurant, null: false, foreign_key: true
      t.string :status
      t.string :delivery_address
      t.integer :subtotal_cents
      t.integer :delivery_fee_cents
      t.integer :total_cents
      t.text :special_instructions

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015456_create_takeaway_order_items.rb

class CreateTakeawayOrderItems < ActiveRecord::Migration[8.1]
  def change
    create_table :takeaway_order_items do |t|
      t.references :order, null: false, foreign_key: true
      t.references :menu_item, null: false, foreign_key: true
      t.integer :quantity
      t.integer :unit_price_cents

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015518_create_marketplace_categories.rb

class CreateMarketplaceCategories < ActiveRecord::Migration[8.1]
  def change
    create_table :marketplace_categories do |t|
      t.string :name
      t.string :slug
      t.integer :parent_id

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015523_create_marketplace_listings.rb

class CreateMarketplaceListings < ActiveRecord::Migration[8.1]
  def change
    create_table :marketplace_listings do |t|
      t.references :user, null: false, foreign_key: true
      t.references :category, null: false, foreign_key: true
      t.string :title
      t.text :description
      t.integer :price_cents
      t.string :currency
      t.string :condition
      t.string :status
      t.string :location
      t.integer :views_count

      t.timestamps
    end
  end
end

rails/brgen/app/db/migrate/20260505015530_create_marketplace_orders.rb

class CreateMarketplaceOrders < ActiveRecord::Migration[8.1]
  def change
    create_table :marketplace_orders do |t|
      t.references :buyer, null: false, foreign_key: true
      t.references :listing, null: false, foreign_key: true
      t.string :status
      t.text :message
      t.integer :price_cents

      t.timestamps
    end
  end
end

rails/brgen/app/db/queue_schema.rb

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 0) do
end

rails/brgen/app/db/schema.rb

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 2026_05_05_015530) do
  create_table "comments", force: :cascade do |t|
    t.integer "commentable_id", null: false
    t.string "commentable_type", null: false
    t.text "content"
    t.datetime "created_at", null: false
    t.integer "parent_id"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["commentable_type", "commentable_id"], name: "index_comments_on_commentable"
    t.index ["user_id"], name: "index_comments_on_user_id"
  end

  create_table "communities", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.text "description"
    t.string "name"
    t.string "slug"
    t.string "subdomain"
    t.datetime "updated_at", null: false
    t.integer "user_id"
    t.index ["slug"], name: "index_communities_on_slug", unique: true
    t.index ["subdomain"], name: "index_communities_on_subdomain", unique: true
  end

  create_table "conversation_participants", force: :cascade do |t|
    t.integer "conversation_id", null: false
    t.datetime "created_at", null: false
    t.datetime "last_read_at"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["conversation_id"], name: "index_conversation_participants_on_conversation_id"
    t.index ["user_id"], name: "index_conversation_participants_on_user_id"
  end

  create_table "conversations", force: :cascade do |t|
    t.string "conversation_type"
    t.datetime "created_at", null: false
    t.string "name"
    t.datetime "updated_at", null: false
  end

  create_table "dating_dislikes", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.integer "dislikee_id", null: false
    t.integer "disliker_id", null: false
    t.datetime "updated_at", null: false
    t.index ["dislikee_id"], name: "index_dating_dislikes_on_dislikee_id"
    t.index ["disliker_id"], name: "index_dating_dislikes_on_disliker_id"
  end

  create_table "dating_likes", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.integer "likee_id", null: false
    t.integer "liker_id", null: false
    t.datetime "updated_at", null: false
    t.index ["likee_id"], name: "index_dating_likes_on_likee_id"
    t.index ["liker_id"], name: "index_dating_likes_on_liker_id"
  end

  create_table "dating_matches", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.integer "initiator_id", null: false
    t.integer "receiver_id", null: false
    t.string "status"
    t.datetime "updated_at", null: false
    t.index ["initiator_id"], name: "index_dating_matches_on_initiator_id"
    t.index ["receiver_id"], name: "index_dating_matches_on_receiver_id"
  end

  create_table "dating_profiles", force: :cascade do |t|
    t.integer "age"
    t.text "bio"
    t.datetime "created_at", null: false
    t.string "gender"
    t.decimal "latitude"
    t.string "location"
    t.decimal "longitude"
    t.string "looking_for"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.boolean "visible"
    t.index ["user_id"], name: "index_dating_profiles_on_user_id"
  end

  create_table "follows", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.integer "followed_id"
    t.integer "follower_id"
    t.datetime "updated_at", null: false
    t.index ["followed_id"], name: "index_follows_on_followed_id"
    t.index ["follower_id"], name: "index_follows_on_follower_id"
  end

  create_table "hashtags", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.string "name"
    t.datetime "updated_at", null: false
    t.integer "usage_count"
    t.index ["name"], name: "index_hashtags_on_name", unique: true
  end

  create_table "marketplace_categories", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.string "name"
    t.integer "parent_id"
    t.string "slug"
    t.datetime "updated_at", null: false
  end

  create_table "marketplace_listings", force: :cascade do |t|
    t.integer "category_id", null: false
    t.string "condition"
    t.datetime "created_at", null: false
    t.string "currency"
    t.text "description"
    t.string "location"
    t.integer "price_cents"
    t.string "status"
    t.string "title"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.integer "views_count"
    t.index ["category_id"], name: "index_marketplace_listings_on_category_id"
    t.index ["user_id"], name: "index_marketplace_listings_on_user_id"
  end

  create_table "marketplace_orders", force: :cascade do |t|
    t.integer "buyer_id", null: false
    t.datetime "created_at", null: false
    t.integer "listing_id", null: false
    t.text "message"
    t.integer "price_cents"
    t.string "status"
    t.datetime "updated_at", null: false
    t.index ["buyer_id"], name: "index_marketplace_orders_on_buyer_id"
    t.index ["listing_id"], name: "index_marketplace_orders_on_listing_id"
  end

  create_table "mentions", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.integer "mentionable_id", null: false
    t.string "mentionable_type", null: false
    t.integer "mentioned_user_id", null: false
    t.datetime "updated_at", null: false
    t.index ["mentionable_type", "mentionable_id"], name: "index_mentions_on_mentionable"
    t.index ["mentioned_user_id"], name: "index_mentions_on_mentioned_user_id"
  end

  create_table "message_receipts", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.datetime "delivered_at"
    t.integer "message_id", null: false
    t.datetime "read_at"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["message_id"], name: "index_message_receipts_on_message_id"
    t.index ["user_id"], name: "index_message_receipts_on_user_id"
  end

  create_table "messages", force: :cascade do |t|
    t.text "content"
    t.integer "conversation_id", null: false
    t.datetime "created_at", null: false
    t.datetime "expires_at"
    t.string "message_type"
    t.integer "sender_id"
    t.datetime "updated_at", null: false
    t.index ["conversation_id"], name: "index_messages_on_conversation_id"
  end

  create_table "playlist_listens", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.integer "playlist_track_id", null: false
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["playlist_track_id"], name: "index_playlist_listens_on_playlist_track_id"
    t.index ["user_id"], name: "index_playlist_listens_on_user_id"
  end

  create_table "playlist_playlist_tracks", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.integer "playlist_playlist_id", null: false
    t.integer "playlist_track_id", null: false
    t.integer "position"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["playlist_playlist_id"], name: "index_playlist_playlist_tracks_on_playlist_playlist_id"
    t.index ["playlist_track_id"], name: "index_playlist_playlist_tracks_on_playlist_track_id"
    t.index ["user_id"], name: "index_playlist_playlist_tracks_on_user_id"
  end

  create_table "playlist_playlists", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.text "description"
    t.integer "likes_count"
    t.string "name"
    t.integer "plays_count"
    t.boolean "public_access"
    t.integer "tracks_count"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["user_id"], name: "index_playlist_playlists_on_user_id"
  end

  create_table "playlist_tracks", force: :cascade do |t|
    t.string "album"
    t.string "artist"
    t.datetime "created_at", null: false
    t.integer "duration_seconds"
    t.string "genre"
    t.string "source_type"
    t.string "source_url"
    t.string "title"
    t.datetime "updated_at", null: false
  end

  create_table "posts", force: :cascade do |t|
    t.boolean "anonymous"
    t.integer "community_id", null: false
    t.text "content"
    t.datetime "created_at", null: false
    t.integer "karma"
    t.string "title"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["community_id"], name: "index_posts_on_community_id"
    t.index ["user_id"], name: "index_posts_on_user_id"
  end

  create_table "reactions", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.string "kind"
    t.integer "post_id", null: false
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["post_id"], name: "index_reactions_on_post_id"
    t.index ["user_id"], name: "index_reactions_on_user_id"
  end

  create_table "sessions", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.string "ip_address"
    t.datetime "updated_at", null: false
    t.string "user_agent"
    t.integer "user_id", null: false
    t.index ["user_id"], name: "index_sessions_on_user_id"
  end

  create_table "streams", force: :cascade do |t|
    t.string "content_type"
    t.datetime "created_at", null: false
    t.integer "duration"
    t.integer "post_id", null: false
    t.datetime "updated_at", null: false
    t.string "url"
    t.integer "user_id", null: false
    t.index ["post_id"], name: "index_streams_on_post_id"
    t.index ["user_id"], name: "index_streams_on_user_id"
  end

  create_table "taggings", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.integer "hashtag_id", null: false
    t.integer "taggable_id", null: false
    t.string "taggable_type", null: false
    t.datetime "updated_at", null: false
    t.index ["hashtag_id"], name: "index_taggings_on_hashtag_id"
    t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable"
  end

  create_table "takeaway_menu_items", force: :cascade do |t|
    t.boolean "available"
    t.datetime "created_at", null: false
    t.text "description"
    t.string "name"
    t.integer "price_cents"
    t.integer "restaurant_id", null: false
    t.datetime "updated_at", null: false
    t.boolean "vegan"
    t.boolean "vegetarian"
    t.index ["restaurant_id"], name: "index_takeaway_menu_items_on_restaurant_id"
  end

  create_table "takeaway_order_items", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.integer "menu_item_id", null: false
    t.integer "order_id", null: false
    t.integer "quantity"
    t.integer "unit_price_cents"
    t.datetime "updated_at", null: false
    t.index ["menu_item_id"], name: "index_takeaway_order_items_on_menu_item_id"
    t.index ["order_id"], name: "index_takeaway_order_items_on_order_id"
  end

  create_table "takeaway_orders", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.string "delivery_address"
    t.integer "delivery_fee_cents"
    t.integer "restaurant_id", null: false
    t.text "special_instructions"
    t.string "status"
    t.integer "subtotal_cents"
    t.integer "total_cents"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["restaurant_id"], name: "index_takeaway_orders_on_restaurant_id"
    t.index ["user_id"], name: "index_takeaway_orders_on_user_id"
  end

  create_table "takeaway_restaurants", force: :cascade do |t|
    t.boolean "active"
    t.string "address"
    t.string "city"
    t.datetime "created_at", null: false
    t.string "cuisine_type"
    t.integer "delivery_fee_cents"
    t.text "description"
    t.integer "min_order_cents"
    t.string "name"
    t.string "phone"
    t.decimal "rating"
    t.integer "reviews_count"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["user_id"], name: "index_takeaway_restaurants_on_user_id"
  end

  create_table "tv_broadcasts", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.text "description"
    t.datetime "ended_at"
    t.datetime "started_at"
    t.string "status"
    t.string "stream_key"
    t.string "title"
    t.integer "tv_channel_id", null: false
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.integer "viewer_count"
    t.index ["tv_channel_id"], name: "index_tv_broadcasts_on_tv_channel_id"
    t.index ["user_id"], name: "index_tv_broadcasts_on_user_id"
  end

  create_table "tv_channels", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.text "description"
    t.string "name"
    t.string "slug"
    t.integer "subscribers_count"
    t.integer "total_views"
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["user_id"], name: "index_tv_channels_on_user_id"
  end

  create_table "tv_subscriptions", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.boolean "notify_on_upload"
    t.integer "tv_channel_id", null: false
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.index ["tv_channel_id"], name: "index_tv_subscriptions_on_tv_channel_id"
    t.index ["user_id"], name: "index_tv_subscriptions_on_user_id"
  end

  create_table "tv_videos", force: :cascade do |t|
    t.integer "comments_count"
    t.datetime "created_at", null: false
    t.text "description"
    t.integer "duration_seconds"
    t.integer "likes_count"
    t.datetime "published_at"
    t.string "status"
    t.string "thumbnail_url"
    t.string "title"
    t.integer "tv_channel_id", null: false
    t.datetime "updated_at", null: false
    t.integer "user_id", null: false
    t.integer "views_count"
    t.index ["tv_channel_id"], name: "index_tv_videos_on_tv_channel_id"
    t.index ["user_id"], name: "index_tv_videos_on_user_id"
  end

  create_table "tv_view_events", force: :cascade do |t|
    t.boolean "completed"
    t.datetime "created_at", null: false
    t.integer "tv_video_id", null: false
... 91 lines truncated (491 total)

rails/brgen/app/db/seeds.rb

admin = User.find_or_create_by!(email_address: "admin@brgen.no") do |u|
  u.username = "admin"
  u.password = u.password_confirmation = "password123"
end

["news", "tech", "bergen", "norge", "kultur"].each do |slug|
  Community.find_or_create_by!(slug: slug) do |c|
    c.name        = slug.capitalize
    c.description = "#{slug.capitalize} community"
    c.user        = admin
  end
end

puts "Seeded #{Community.count} communities, admin id #{admin.id}"

rails/brgen/app/public/robots.txt

# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file

rails/brgen/app/test/test_helper.rb

ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"

module ActiveSupport
  class TestCase
    # Run tests in parallel with specified workers
    parallelize(workers: :number_of_processors)

    # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
    fixtures :all

    # Add more helper methods to be used by all tests here...
  end
end

rails/brgen/brgen.sh

#!/usr/bin/env zsh
# brgen.sh — Brgen social network (Rails 8). Deploys the tracked tree at app/.
set -euo pipefail

APP_NAME=brgen
APP_DIR=/home/${APP_NAME}/app
APP_PORT=38182
SCRIPT_DIR=${0:a:h}
SRC_DIR=${SCRIPT_DIR}/app

. "${SCRIPT_DIR:h}/@shared_functions.sh"

need_cmd ruby34 bundle doas

[[ -d $SRC_DIR ]] || { log_err "missing source tree: $SRC_DIR"; exit 1; }

log "Brgen — deploying tracked tree → ${APP_DIR}"

# ── User + dirs ────────────────────────────────────────────────────────────
id "$APP_NAME" >/dev/null 2>&1 || doas useradd -m -L daemon -s /bin/ksh "$APP_NAME"
doas mkdir -p "$APP_DIR"

# ── Sync tree ──────────────────────────────────────────────────────────────
doas cp -R "${SRC_DIR}/." "${APP_DIR}/"
doas chown -R "${APP_NAME}:${APP_NAME}" "$APP_DIR"

cd "$APP_DIR"

# ── Bundle path inherits from sibling app to avoid OOM on first install ────
typeset bundle_home="/home/${APP_NAME}/.bundle"
if [[ ! -d ${bundle_home}/gems ]]; then
  log "Bootstrapping gems from amber"
  doas mkdir -p "$bundle_home"
  doas cp -R /home/amber/.bundle/gems "$bundle_home/"
  doas chown -R "${APP_NAME}:${APP_NAME}" "$bundle_home"
fi
print -- "---\nBUNDLE_PATH: \"${bundle_home}/gems\"" | doas tee "${APP_DIR}/.bundle/config" >/dev/null

# ── Install + migrate + seed ───────────────────────────────────────────────
doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && bundle config set --local deployment true && bundle config set --local without 'development test' && RAILS_ENV=production bundle install"
doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:create db:migrate"
[[ -f ${APP_DIR}/db/seeds.rb ]] && doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:seed" || true

# ── Service + relay ────────────────────────────────────────────────────────
install_rcd "$APP_NAME" "$APP_DIR" "$APP_PORT" "$APP_NAME"
relayd_add_relay "${APP_NAME}.no" "$APP_PORT"

doas rcctl restart "$APP_NAME" || doas rcctl start "$APP_NAME"
log_ok "$APP_NAME live on :$APP_PORT"

rails/brgen/subapps/dating/README.md

# brgen :: dating

Local-first dating. Discover people in your city.

- Namespace: `Dating::`
- Subdomain: `dating.brgen.no`
- Route prefix: `/dating`

## Models

| Model | Notes |
|---|---|
| `Dating::Profile` | Display name, bio, photos, age range, distance preference; one per `User` |
| `Dating::Like` | One-way interest signal between profiles |
| `Dating::Dislike` | Hide a profile from future swipes |
| `Dating::Match` | Reciprocal `Like` pair; opens chat via existing `Conversation` model |

## Discovery

Profiles are filtered by city (subdomain) and distance radius. Matching unlocks the shared messaging stack (`messages_controller`, Action Cable `MessagesChannel`).

rails/brgen/subapps/marketplace/README.md

# brgen :: marketplace

Hyperlocal classifieds. Buy, sell, trade within your city.

- Namespace: `Marketplace::`
- Subdomain: `markedsplass.brgen.no` (Norway) — locale aliases: `markadur` (IS), `marknadsplats` (SE), `marketplace` (UK/US), `marktplaats` (NL), `marche` (FR/BE), `mercato` (IT), `mercado` (PT/ES), `markkinapaikka` (FI)
- Route prefix: `/marketplace`

## Models

| Model | Notes |
|---|---|
| `Marketplace::Category` | Top-level classification (clothing, electronics, vehicles, housing, …) |
| `Marketplace::Listing` | Individual ad: title, body, price, photos (Active Storage), location |
| `Marketplace::Order` | Buyer ↔ seller transaction; payment + delivery state machine |

## Routes

Wrapped in `constraints(subdomain: MARKETPLACE_SUBDOMAINS)` in `config/routes.rb`. Same Rails app serves every locale alias.

rails/brgen/subapps/playlist/README.md

# brgen :: playlist

Local music discovery. Share playlists, find what your city listens to.

- Namespace: `Playlist::`
- Subdomain: `playlist.brgen.no`
- Route prefix: `/playlist`

## Models

| Model | Notes |
|---|---|
| `Playlist::Playlist` | Owner, title, public/private, cover art (Active Storage) |
| `Playlist::Track` | Title, artist, duration, source URL (Spotify/SoundCloud/local) |
| `Playlist::PlaylistTrack` | Join with `position` for ordering |
| `Playlist::Listen` | Per-user play event; powers trending and personal history |

## Trending

City-scoped trending feed: aggregates `Listen` rows over a rolling window, filtered by user-city.

rails/brgen/subapps/takeaway/README.md

# brgen :: takeaway

Local street-food and restaurant ordering.

- Namespace: `Takeaway::`
- Subdomain: `takeaway.brgen.no`
- Route prefix: `/takeaway`

## Models

| Model | Notes |
|---|---|
| `Takeaway::Restaurant` | Owner, name, address, hours, cuisine tags, photos |
| `Takeaway::MenuItem` | Restaurant menu entry: name, description, price, photo, availability |
| `Takeaway::Order` | Customer ↔ restaurant order; status machine (placed → accepted → ready → delivered) |
| `Takeaway::OrderItem` | Line items linking `Order``MenuItem` with quantity + per-item notes |

## Discovery

Restaurants filtered by city (subdomain) and distance from delivery address. Live order status pushed via Action Cable `OrdersChannel`.

rails/brgen/subapps/tv/README.md

# brgen :: tv

Local-channel television. User-run channels broadcast to a city.

- Namespace: `Tv::`
- Subdomain: `tv.brgen.no`
- Route prefix: `/tv`

## Models

| Model | Notes |
|---|---|
| `Tv::Channel` | Owner, name, description, logo |
| `Tv::Video` | Uploaded MP4 (Active Storage), title, runtime, channel |
| `Tv::Broadcast` | Live or scheduled airing of a `Video` on a `Channel` |
| `Tv::Subscription` | User → Channel follow; drives notifications |
| `Tv::ViewEvent` | Per-user playback event; powers analytics + recommendations |

## Streaming

Recorded video served via Active Storage + Cloudflare CDN. Live broadcast via Action Cable signaling + WebRTC peer relay (planned).

rails/bsdports/README.md

# bsdports

Browseable web index of the OpenBSD ports tree. Rails 8 · PostgreSQL · Falcon.

## How it works

Seeds itself from the OpenBSD FTP mirror: downloads `ports.tar.gz`, untars, walks each `Makefile`, and imports name/summary/url/description into Postgres. One row per port, scoped by category and platform.

## Models

| Model | Purpose |
|---|---|
| `Platform` | OpenBSD release branch (e.g. `7.8`, `-current`) |
| `Category` | Top-level ports category (`net`, `databases`, `lang`, …) belongs_to platform |
| `Port` | Individual port (`name`, `summary`, `url`, `description`) belongs_to category + platform |

## Features

- LangChain-backed semantic search across summaries + descriptions.
- StimulusReflex live filtering by category and platform.
- Periodic re-seed via Solid Queue job to track upstream churn.

## Deploy

```zsh
doas zsh DEPLOY/rails/bsdports/bsdports.sh

## `rails/bsdports/app/Dockerfile`
```text
# syntax=docker/dockerfile:1
# check=error=true

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t app .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name app app

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.9
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
    ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment variables and enable jemalloc for reduced memory usage and latency.
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development" \
    LD_PRELOAD="/usr/local/lib/libjemalloc.so"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY vendor/* ./vendor/
COPY Gemfile Gemfile.lock ./

RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
    bundle exec bootsnap precompile -j 1 --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times.
# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
RUN bundle exec bootsnap precompile -j 1 app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile




# Final stage for app image
FROM base

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
USER 1000:1000

# Copy built artifacts: gems, application
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --chown=rails:rails --from=build /rails /rails

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]

rails/bsdports/app/Gemfile

source "https://rubygems.org"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.1.2"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Use sqlite3 as the database for Active Record
gem "sqlite3", ">= 2.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]

# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable
gem "solid_cache"
gem "solid_queue"
gem "solid_cable"

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
gem "kamal", require: false

# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
gem "thruster", require: false

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
gem "image_processing", "~> 1.2"

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

  # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues)
  gem "bundler-audit", require: false

  # Static analysis for security vulnerabilities [https://brakemanscanner.org/]
  gem "brakeman", require: false

  # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
  gem "rubocop-rails-omakase", require: false
end

group :development do
  # Use console on exceptions pages [https://github.com/rails/web-console]
  gem "web-console"
end
gem "pagy"

gem "falcon"

rails/bsdports/app/README.md

# README

This README would normally document whatever steps are necessary to get the
application up and running.

Things you may want to cover:

* Ruby version

* System dependencies

* Configuration

* Database creation

* Database initialization

* How to run the test suite

* Services (job queues, cache servers, search engines, etc.)

* Deployment instructions

* ...

rails/bsdports/app/Rakefile

# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative "config/application"

Rails.application.load_tasks

rails/bsdports/app/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Authentication
  include Pagy::Method
  allow_browser versions: :modern
end

rails/bsdports/app/app/controllers/categories_controller.rb

class CategoriesController < ApplicationController
  allow_unauthenticated_access only: %i[index show]

  def index
    @categories = Category.order(:name).includes(:ports)
  end

  def show
    @category = Category.find_by!(slug: params[:id])
    @pagy, @ports = pagy(@category.ports.order(:name))
  end
end

rails/bsdports/app/app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :require_authentication
  before_action :set_port

  def create
    @comment = @port.comments.build(comment_params.merge(user: Current.user))
    if @comment.save
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_to @port }
      end
    else
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    @comment = @port.comments.find(params[:id])
    @comment.destroy! if @comment.user == Current.user
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @port }
    end
  end

  private

  def set_port = @port = Port.find(params[:port_id])
  def comment_params = params.require(:comment).permit(:content, :parent_id)
end

rails/bsdports/app/app/controllers/concerns/authentication.rb

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

rails/bsdports/app/app/controllers/passwords_controller.rb

class PasswordsController < ApplicationController
  allow_unauthenticated_access
  before_action :set_user_by_token, only: %i[ edit update ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.find_by(email_address: params[:email_address])
      PasswordsMailer.reset(user).deliver_later
    end

    redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
  end

  def edit
  end

  def update
    if @user.update(params.permit(:password, :password_confirmation))
      @user.sessions.destroy_all
      redirect_to new_session_path, notice: "Password has been reset."
    else
      redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
    end
  end

  private
    def set_user_by_token
      @user = User.find_by_password_reset_token!(params[:token])
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
    end
end

rails/bsdports/app/app/controllers/ports_controller.rb

class PortsController < ApplicationController
  allow_unauthenticated_access only: %i[index show]
  before_action :set_port, only: %i[show watch unwatch]

  def index
    scope = Port.includes(:category)
    scope = scope.search(params[:q])       if params[:q].present?
    scope = scope.by_category(params[:category_id]) if params[:category_id].present?
    scope = scope.order(params[:sort] == "updated" ? "last_updated DESC" : :name)
    @pagy, @ports = pagy(scope)
    @categories   = Category.order(:name)
  end

  def show
    @updates  = @port.port_updates.order(committed_at: :desc).limit(10)
    @deps     = @port.depends_on.includes(:category)
    @rdeps    = @port.reverse_deps.includes(:category).limit(20)
    @comments = @port.comments.roots.includes(:user, replies: :user)
    @comment  = Comment.new
  end

  def watch
    require_authentication
    @port.watches.find_or_create_by!(user: Current.user)
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @port }
    end
  end

  def unwatch
    require_authentication
    @port.watches.find_by(user: Current.user)&.destroy!
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @port }
    end
  end

  private

  def set_port = @port = Port.find_by!(pkgpath: params[:id].gsub("-", "/")) rescue Port.find(params[:id])
end

rails/bsdports/app/app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  allow_unauthenticated_access only: %i[ new create ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

  def destroy
    terminate_session
    redirect_to new_session_path, status: :see_other
  end
end

rails/bsdports/app/app/helpers/application_helper.rb

module ApplicationHelper
end

rails/bsdports/app/app/javascript/application.js

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"

rails/bsdports/app/app/javascript/controllers/animated_number_controller.js

import AnimatedNumber from "@stimulus-components/animated-number"
export default class extends AnimatedNumber {}

rails/bsdports/app/app/javascript/controllers/application.js

import { Application } from "@hotwired/stimulus"

const application = Application.start()

// Configure Stimulus development experience
application.debug = false
window.Stimulus   = application

export { application }

rails/bsdports/app/app/javascript/controllers/auto_submit_controller.js

import AutoSubmit from "@stimulus-components/auto-submit"
export default class extends AutoSubmit {}

rails/bsdports/app/app/javascript/controllers/character_counter_controller.js

import CharacterCounter from "@stimulus-components/character-counter"
export default class extends CharacterCounter {}

rails/bsdports/app/app/javascript/controllers/clipboard_controller.js

import Clipboard from "@stimulus-components/clipboard"
export default class extends Clipboard {}

rails/bsdports/app/app/javascript/controllers/dialog_controller.js

import Dialog from "@stimulus-components/dialog"
export default class extends Dialog {}

rails/bsdports/app/app/javascript/controllers/dropdown_controller.js

import Dropdown from "@stimulus-components/dropdown"
export default class extends Dropdown {}

rails/bsdports/app/app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.textContent = "Hello World!"
  }
}

rails/bsdports/app/app/javascript/controllers/index.js

// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)

rails/bsdports/app/app/javascript/controllers/notification_controller.js

import Notification from "@stimulus-components/notification"
export default class extends Notification {}

rails/bsdports/app/app/javascript/controllers/sortable_controller.js

import Sortable from "@stimulus-components/sortable"
export default class extends Sortable {}

rails/bsdports/app/app/javascript/controllers/textarea_autogrow_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.resize()
    this.element.addEventListener("input", this.resize)
  }

  disconnect() {
    this.element.removeEventListener("input", this.resize)
  }

  resize = () => {
    this.element.style.height = "auto"
    this.element.style.height = `${this.element.scrollHeight}px`
  }
}

rails/bsdports/app/app/javascript/controllers/timeago_controller.js

import TimeAgo from "@stimulus-components/timeago"
export default class extends TimeAgo {}

rails/bsdports/app/app/jobs/application_job.rb

class ApplicationJob < ActiveJob::Base
  # Automatically retry jobs that encountered a deadlock
  # retry_on ActiveRecord::Deadlocked

  # Most jobs are safe to ignore if the underlying records are no longer available
  # discard_on ActiveJob::DeserializationError
end

rails/bsdports/app/app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout "mailer"
end

rails/bsdports/app/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
end

rails/bsdports/app/app/models/category.rb

class Category < ApplicationRecord
  has_many :ports, dependent: :nullify

  validates :name, :slug, presence: true
  validates :slug, uniqueness: true, format: { with: /\A[a-z0-9-]+\z/ }

  before_validation :generate_slug, on: :create

  def to_param = slug

  private

  def generate_slug
    self.slug ||= name.to_s.parameterize
  end
end

rails/bsdports/app/app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :port
  belongs_to :parent, class_name: "Comment", optional: true
  has_many :replies, class_name: "Comment", foreign_key: :parent_id, dependent: :destroy

  validates :content, presence: true, length: { maximum: 5000 }

  scope :roots, -> { where(parent_id: nil).order(created_at: :asc) }

  after_create_commit -> { broadcast_append_to [port, "comments"] }
end

rails/bsdports/app/app/models/current.rb

class Current < ActiveSupport::CurrentAttributes
  attribute :session
  delegate :user, to: :session, allow_nil: true
end

rails/bsdports/app/app/models/dependency.rb

class Dependency < ApplicationRecord
  belongs_to :port
  belongs_to :depends_on, class_name: "Port"

  TYPES = %w[build run test lib].freeze

  validates :dep_type, inclusion: { in: TYPES }, allow_nil: true
  validates :port_id, uniqueness: { scope: %i[depends_on_id dep_type] }
end

rails/bsdports/app/app/models/port.rb

class Port < ApplicationRecord
  belongs_to :category
  has_many :dependencies, dependent: :destroy
  has_many :depends_on, through: :dependencies, source: :depends_on
  has_many :dependents, class_name: "Dependency", foreign_key: :depends_on_id
  has_many :reverse_deps, through: :dependents, source: :port
  has_many :port_updates, dependent: :destroy
  has_many :watches, dependent: :destroy
  has_many :watchers, through: :watches, source: :user
  has_many :comments, dependent: :destroy

  validates :name, :version, :pkgpath, presence: true
  validates :pkgpath, uniqueness: true

  scope :recent_updates, -> { joins(:port_updates).order("port_updates.committed_at DESC").distinct }
  scope :by_category,    ->(cat) { where(category: cat) }
  scope :search,         ->(q) { where("name LIKE ? OR comment LIKE ?", "%#{q}%", "%#{q}%") }

  def watched_by?(user)
    watches.exists?(user: user)
  end

  def latest_update
    port_updates.order(committed_at: :desc).first
  end
end

rails/bsdports/app/app/models/port_update.rb

class PortUpdate < ApplicationRecord
  belongs_to :port

  validates :new_version, presence: true

  scope :recent, -> { order(committed_at: :desc) }
end

rails/bsdports/app/app/models/session.rb

class Session < ApplicationRecord
  belongs_to :user
end

rails/bsdports/app/app/models/user.rb

class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy
  has_many :watches, dependent: :destroy
  has_many :watched_ports, through: :watches, source: :port
  has_many :comments, dependent: :nullify

  normalizes :email_address, with: ->(e) { e.strip.downcase }
end

rails/bsdports/app/app/models/watch.rb

class Watch < ApplicationRecord
  belongs_to :user
  belongs_to :port

  validates :user_id, uniqueness: { scope: :port_id }
end

rails/bsdports/app/app/views/categories/index.html.erb

<% content_for :title, "Categories" %>
<h1>Categories</h1>
<ul id="categories">
  <% @categories.each do |cat| %>
    <li>
      <%= link_to cat.name, category_path(cat) %>
      <small><%= cat.description %></small>
    </li>
  <% end %>
</ul>

rails/bsdports/app/app/views/categories/show.html.erb

<% content_for :title, @category.name %>
<header>
  <h1><%= @category.name %></h1>
  <p><%= @category.description %></p>
</header>
<%= render "ports/index" %>

rails/bsdports/app/app/views/comments/_comment.html.erb

<article id="<%= dom_id(comment) %>">
  <small><%= comment.user.email_address.split("@").first %></small>
  <p><%= comment.content %></p>
  <% if authenticated? && comment.user == Current.user %>
    <%= button_to "Delete", port_comment_path(@port, comment), method: :delete %>
  <% end %>
</article>

rails/bsdports/app/app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for(:title) || "App" %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="application-name" content="App">
    <meta name="mobile-web-app-capable" content="yes">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= yield :head %>

    <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
    <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>

    <link rel="icon" href="/icon.png" type="image/png">
    <link rel="icon" href="/icon.svg" type="image/svg+xml">
    <link rel="apple-touch-icon" href="/icon.png">

    <%# Includes all stylesheet files in app/assets/stylesheets %>
    <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

rails/bsdports/app/app/views/layouts/mailer.html.erb

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

rails/bsdports/app/app/views/layouts/mailer.text.erb

<%= yield %>

rails/bsdports/app/app/views/ports/index.html.erb

<% content_for :title, "Ports" %>
<header>
  <h1>OpenBSD ports</h1>
  <%= form_with url: ports_path, method: :get do |f| %>
    <%= f.search_field :q, value: params[:q], placeholder: "Search ports…" %>
  <% end %>
</header>
<% if @category %>
  <p><mark><%= @category.name %></mark><%= @category.description %></p>
<% end %>
<ul id="ports">
  <% @ports.each do |port| %>
    <li>
      <%= link_to port.name, port %>
      <small><%= port.version %></small>
      <small><%= link_to port.category.name, category_path(port.category) %></small>
      <small><%= port.comment %></small>
    </li>
  <% end %>
</ul>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/bsdports/app/app/views/ports/show.html.erb

<% content_for :title, @port.name %>
<article>
  <header>
    <h1><%= @port.name %> <small><%= @port.version %></small></h1>
    <small><%= link_to @port.category.name, category_path(@port.category) %></small>
  </header>
  <p><%= @port.comment %></p>
  <p><%= @port.description %></p>
  <dl>
    <dt>Maintainer</dt><dd><%= @port.maintainer %></dd>
    <dt>Pkgpath</dt><dd><code><%= @port.pkgpath %></code></dd>
    <% if @port.homepage.present? %><dt>Homepage</dt><dd><%= link_to @port.homepage, @port.homepage %></dd><% end %>
    <dt>Updated</dt><dd><%= @port.last_updated&.strftime("%Y-%m-%d") %></dd>
  </dl>

  <% if @dependencies.any? %>
    <h2>Dependencies</h2>
    <ul>
      <% @dependencies.each do |dep| %>
        <li>
          <%= link_to dep.depends_on.name, port_path(dep.depends_on) %>
          <small><%= dep.dep_type %></small>
        </li>
      <% end %>
    </ul>
  <% end %>

  <% if @updates.any? %>
    <h2>Version history</h2>
    <ul>
      <% @updates.each do |update| %>
        <li>
          <small><%= update.old_version %><%= update.new_version %></small>
          <small><%= update.commit_message %></small>
          <small><%= update.committed_at&.strftime("%Y-%m-%d") %></small>
        </li>
      <% end %>
    </ul>
  <% end %>

  <nav>
    <% if authenticated? %>
      <% if @watching %>
        <%= button_to "Unwatch", unwatch_port_path(@port), method: :delete %>
      <% else %>
        <%= button_to "Watch", watch_port_path(@port), method: :post %>
      <% end %>
    <% end %>
  </nav>
</article>

<section id="comments">
  <h2>Comments</h2>
  <%= render @comments %>
  <% if authenticated? %>
    <%= form_with url: port_comments_path(@port) do |f| %>
      <p><%= f.text_area :content, rows: 3, placeholder: "Add a comment…" %></p>
      <p><%= f.submit "Comment" %></p>
    <% end %>
  <% end %>
</section>

rails/bsdports/app/app/views/pwa/manifest.json.erb

{
  "name": "App",
  "icons": [
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512"
    },
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "maskable"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "scope": "/",
  "description": "App.",
  "theme_color": "red",
  "background_color": "red"
}

rails/bsdports/app/app/views/pwa/service-worker.js

// Add a service worker for processing Web Push notifications:
//
// self.addEventListener("push", async (event) => {
//   const { title, options } = await event.data.json()
//   event.waitUntil(self.registration.showNotification(title, options))
// })
//
// self.addEventListener("notificationclick", function(event) {
//   event.notification.close()
//   event.waitUntil(
//     clients.matchAll({ type: "window" }).then((clientList) => {
//       for (let i = 0; i < clientList.length; i++) {
//         let client = clientList[i]
//         let clientPath = (new URL(client.url)).pathname
//
//         if (clientPath == event.notification.data.path && "focus" in client) {
//           return client.focus()
//         }
//       }
//
//       if (clients.openWindow) {
//         return clients.openWindow(event.notification.data.path)
//       }
//     })
//   )
// })

rails/bsdports/app/config/application.rb

require_relative "boot"

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module App
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 8.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w[assets tasks])

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Don't generate system test files.
    config.generators.system_tests = nil
  end
end

rails/bsdports/app/config/boot.rb

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

require "bundler/setup" # Set up gems listed in the Gemfile.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.

rails/bsdports/app/config/bundler-audit.yml

# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit.
# CVEs that are not relevant to the application can be enumerated on the ignore list below.

ignore:
  - CVE-THAT-DOES-NOT-APPLY

rails/bsdports/app/config/cable.yml

development:
  adapter: async

test:
  adapter: test

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: app_production

rails/bsdports/app/config/ci.rb

# Run using bin/ci

CI.run do
  step "Setup", "bin/setup --skip-server"

  step "Style: Ruby", "bin/rubocop"

  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Importmap vulnerability audit", "bin/importmap audit"
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"


  # Optional: set a green GitHub commit status to unblock PR merge.
  # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
  # if success?
  #   step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
  # else
  #   failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
  # end
end

rails/bsdports/app/config/database.yml

# SQLite. Versions 3.8.0 and up are supported.
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem "sqlite3"
#
default: &default
  adapter: sqlite3
  max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: storage/development.sqlite3

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: storage/test.sqlite3

# Store production database in the storage/ directory, which by default
# is mounted as a persistent Docker volume in config/deploy.yml.
production:
  primary:
    <<: *default
    database: storage/production.sqlite3
  cache:
    <<: *default
    database: storage/production_cache.sqlite3
    migrations_paths: db/cache_migrate
  queue:
    <<: *default
    database: storage/production_queue.sqlite3
    migrations_paths: db/queue_migrate
  cable:
    <<: *default
    database: storage/production_cable.sqlite3
    migrations_paths: db/cable_migrate

rails/bsdports/app/config/deploy.yml

# Name of your application. Used to uniquely configure containers.
service: app

# Name of the container image (use your-user/app-name on external registries).
image: app

# Deploy to these servers.
servers:
  web:
    - 192.168.0.1
  # job:
  #   hosts:
  #     - 192.168.0.1
  #   cmd: bin/jobs

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
#
# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb!
#
# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer).
#
# proxy:
#   ssl: true
#   host: app.example.com

# Where you keep your container images.
registry:
  # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ...
  server: localhost:5555

  # Needed for authenticated registries.
  # username: your-user

  # Always use an access token rather than real password when possible.
  # password:
  #   - KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
  secret:
    - RAILS_MASTER_KEY
  clear:
    # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
    # When you start using multiple servers, you should split out job processing to a dedicated machine.
    SOLID_QUEUE_IN_PUMA: true

    # Set number of processes dedicated to Solid Queue (default: 1)
    # JOB_CONCURRENCY: 3

    # Set number of cores available to the application on each server (default: 1).
    # WEB_CONCURRENCY: 2

    # Match this to any external database server to configure Active Record correctly
    # Use app-db for a db accessory server on same machine via local kamal docker network.
    # DB_HOST: 192.168.0.2

    # Log everything from Rails
    # RAILS_LOG_LEVEL: debug

# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
  console: app exec --interactive --reuse "bin/rails console"
  shell: app exec --interactive --reuse "bash"
  logs: app logs -f
  dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"

# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
  - "app_storage:/rails/storage"

# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets

# Configure the image builder.
builder:
  arch: amd64

  # # Build image via remote server (useful for faster amd64 builds on arm64 computers)
  # remote: ssh://docker@docker-builder-server
  #
  # # Pass arguments and secrets to the Docker build process
  # args:
  #   RUBY_VERSION: ruby-3.4.9
  # secrets:
  #   - GITHUB_TOKEN
  #   - RAILS_MASTER_KEY

# Use a different ssh user than root
# ssh:
#   user: app

# Use accessory services (secrets come from .kamal/secrets).
# accessories:
#   db:
#     image: mysql:8.0
#     host: 192.168.0.2
#     # Change to 3306 to expose port to the world instead of just local network.
#     port: "127.0.0.1:3306:3306"
#     env:
#       clear:
#         MYSQL_ROOT_HOST: '%'
#       secret:
#         - MYSQL_ROOT_PASSWORD
#     files:
#       - config/mysql/production.cnf:/etc/mysql/my.cnf
#       - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
#     directories:
#       - data:/var/lib/mysql
#   redis:
#     image: valkey/valkey:8
#     host: 192.168.0.2
#     port: 6379
#     directories:
#       - data:/data

rails/bsdports/app/config/environment.rb

# Load the Rails application.
require_relative "application"

# Initialize the Rails application.
Rails.application.initialize!

rails/bsdports/app/config/environments/development.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Make code changes take effect immediately without server restart.
  config.enable_reloading = true

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports.
  config.consider_all_requests_local = true

  # Enable server timing.
  config.server_timing = true

  # Enable/disable Action Controller caching. By default Action Controller caching is disabled.
  # Run rails dev:cache to toggle Action Controller caching.
  if Rails.root.join("tmp/caching-dev.txt").exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true
    config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
  else
    config.action_controller.perform_caching = false
  end

  # Change to :null_store to avoid any caching.
  config.cache_store = :memory_store

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  # Make template changes take effect immediately.
  config.action_mailer.perform_caching = false

  # Set localhost to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Highlight code that triggered database queries in logs.
  config.active_record.verbose_query_logs = true

  # Append comments with runtime information tags to SQL queries in logs.
  config.active_record.query_log_tags_enabled = true

  # Highlight code that enqueued background job in logs.
  config.active_job.verbose_enqueue_logs = true

  # Highlight code that triggered redirect in logs.
  config.action_dispatch.verbose_redirect_logs = true

  # Suppress logger output for asset requests.
  config.assets.quiet = true

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  config.action_view.annotate_rendered_view_with_filenames = true

  # Uncomment if you wish to allow Action Cable access from any origin.
  # config.action_cable.disable_request_forgery_protection = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true

  # Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
  # config.generators.apply_rubocop_autocorrect_after_generate!
end

rails/bsdports/app/config/environments/production.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Code is not reloaded between requests.
  config.enable_reloading = false

  # Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
  config.eager_load = true

  # Full error reports are disabled.
  config.consider_all_requests_local = false

  # Turn on fragment caching in view templates.
  config.action_controller.perform_caching = true

  # Cache assets for far-future expiry since they are all digest stamped.
  config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }

  # Enable serving of images, stylesheets, and JavaScripts from an asset server.
  # config.asset_host = "http://assets.example.com"

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Assume all access to the app is happening through a SSL-terminating reverse proxy.
  # config.assume_ssl = true

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  # config.force_ssl = true

  # Skip http-to-https redirect for the default health check endpoint.
  # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }

  # Log to STDOUT with the current request id as a default log tag.
  config.log_tags = [ :request_id ]
  config.logger   = ActiveSupport::TaggedLogging.logger(STDOUT)

  # Change to "debug" to log everything (including potentially personally-identifiable information!).
  config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")

  # Prevent health checks from clogging up the logs.
  config.silence_healthcheck_path = "/up"

  # Don't log any deprecations.
  config.active_support.report_deprecations = false

  # Replace the default in-process memory cache store with a durable alternative.
  # config.cache_store = :mem_cache_store

  # Replace the default in-process and non-durable queuing backend for Active Job.
  # config.active_job.queue_adapter = :resque

  # Ignore bad email addresses and do not raise email delivery errors.
  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
  # config.action_mailer.raise_delivery_errors = false

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
  # config.action_mailer.smtp_settings = {
  #   user_name: Rails.application.credentials.dig(:smtp, :user_name),
  #   password: Rails.application.credentials.dig(:smtp, :password),
  #   address: "smtp.example.com",
  #   port: 587,
  #   authentication: :plain
  # }

  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
  # the I18n.default_locale when a translation cannot be found).
  config.i18n.fallbacks = true

  # Do not dump schema after migrations.
  config.active_record.dump_schema_after_migration = false

  # Only use :id for inspections in production.
  config.active_record.attributes_for_inspect = [ :id ]

  # Enable DNS rebinding protection and other `Host` header attacks.
  # config.hosts = [
  #   "example.com",     # Allow requests from example.com
  #   /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
  # ]
  #
  # Skip DNS rebinding protection for the default health check endpoint.
  # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end

rails/bsdports/app/config/environments/test.rb

# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # While tests run files are not watched, reloading is not necessary.
  config.enable_reloading = false

  # Eager loading loads your entire application. When running a single test locally,
  # this is usually not necessary, and can slow down your test suite. However, it's
  # recommended that you enable it in continuous integration systems to ensure eager
  # loading is working properly before deploying your code.
  config.eager_load = ENV["CI"].present?

  # Configure public file server for tests with cache-control for performance.
  config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }

  # Show full error reports.
  config.consider_all_requests_local = true
  config.cache_store = :null_store

  # Render exception templates for rescuable exceptions and raise for other exceptions.
  config.action_dispatch.show_exceptions = :rescuable

  # Disable request forgery protection in test environment.
  config.action_controller.allow_forgery_protection = false

  # Store uploaded files on the local file system in a temporary directory.
  config.active_storage.service = :test

  # Tell Action Mailer not to deliver emails to the real world.
  # The :test delivery method accumulates sent emails in the
  # ActionMailer::Base.deliveries array.
  config.action_mailer.delivery_method = :test

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Print deprecation notices to the stderr.
  config.active_support.deprecation = :stderr

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  # config.action_view.annotate_rendered_view_with_filenames = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true
end

rails/bsdports/app/config/importmap.rb

# Pin npm packages by running ./bin/importmap

pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "@stimulus-components/dialog", to: "@stimulus-components--dialog.js" # @1.0.1
pin "@stimulus-components/auto-submit", to: "@stimulus-components--auto-submit.js" # @6.0.0
pin "@stimulus-components/character-counter", to: "@stimulus-components--character-counter.js" # @5.1.0
pin "@stimulus-components/dropdown", to: "@stimulus-components--dropdown.js" # @3.0.0
pin "stimulus-use" # @0.52.3
pin "@stimulus-components/clipboard", to: "@stimulus-components--clipboard.js" # @5.0.0
pin "@stimulus-components/notification", to: "@stimulus-components--notification.js" # @3.0.0
pin "@stimulus-components/timeago", to: "@stimulus-components--timeago.js" # @5.0.2
pin "date-fns" # @4.1.0
pin "@stimulus-components/animated-number", to: "@stimulus-components--animated-number.js" # @5.0.0
pin "@stimulus-components/sortable", to: "@stimulus-components--sortable.js" # @5.0.3
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_request", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_request.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_response", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_response.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/lib/utils", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--lib--utils.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/request_interceptor", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--request_interceptor.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/verbs", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--verbs.js" # @0.0.13
pin "@rails/request.js", to: "@rails--request.js.js" # @0.0.13
pin "sortablejs" # @1.15.7

rails/bsdports/app/config/initializers/assets.rb

# Be sure to restart your server when you modify this file.

# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = "1.0"

# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path

rails/bsdports/app/config/initializers/content_security_policy.rb

# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header

# Rails.application.configure do
#   config.content_security_policy do |policy|
#     policy.default_src :self, :https
#     policy.font_src    :self, :https, :data
#     policy.img_src     :self, :https, :data
#     policy.object_src  :none
#     policy.script_src  :self, :https
#     policy.style_src   :self, :https
#     # Specify URI for violation reports
#     # policy.report_uri "/csp-violation-report-endpoint"
#   end
#
#   # Generate session nonces for permitted importmap, inline scripts, and inline styles.
#   config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
#   config.content_security_policy_nonce_directives = %w(script-src style-src)
#
#   # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
#   # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
#   # config.content_security_policy_nonce_auto = true
#
#   # Report violations without enforcing the policy.
#   # config.content_security_policy_report_only = true
# end

rails/bsdports/app/config/initializers/filter_parameter_logging.rb

# Be sure to restart your server when you modify this file.

# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
  :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
]

rails/bsdports/app/config/initializers/inflections.rb

# Be sure to restart your server when you modify this file.

# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.plural /^(ox)$/i, "\\1en"
#   inflect.singular /^(ox)en/i, "\\1"
#   inflect.irregular "person", "people"
#   inflect.uncountable %w( fish sheep )
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.acronym "RESTful"
# end

rails/bsdports/app/config/locales/en.yml

# Files in the config/locales directory are used for internationalization and
# are automatically loaded by Rails. If you want to use locales other than
# English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
#     I18n.t "hello"
#
# In views, this is aliased to just `t`:
#
#     <%= t("hello") %>
#
# To use a different locale, set it with `I18n.locale`:
#
#     I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# To learn more about the API, please read the Rails Internationalization guide
# at https://guides.rubyonrails.org/i18n.html.
#
# Be aware that YAML interprets the following case-insensitive strings as
# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
# must be quoted to be interpreted as strings. For example:
#
#     en:
#       "yes": yup
#       enabled: "ON"

en:
  hello: "Hello world"

rails/bsdports/app/config/puma.rb

threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count
port ENV.fetch("PORT") { 10003 }
environment ENV.fetch("RAILS_ENV") { "production" }
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
plugin :tmp_restart

rails/bsdports/app/config/routes.rb

Rails.application.routes.draw do
  resource  :session
  resources :passwords, param: :token

  root "ports#index"

  resources :categories, only: %i[index show]

  resources :ports, only: %i[index show] do
    member do
      post   :watch
      delete :unwatch
    end
    resources :comments, only: %i[create destroy]
  end

  get "up", to: "rails/health#show", as: :rails_health_check
end

rails/bsdports/app/config/storage.yml

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket-<%= Rails.env %>

# Remember not to checkin your GCS keyfile to a repository
# google:
#   service: GCS
#   project: your_project
#   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
#   bucket: your_own_bucket-<%= Rails.env %>

# mirror:
#   service: Mirror
#   primary: local
#   mirrors: [ amazon, google, microsoft ]

rails/bsdports/app/db/migrate/20260501020807_create_users.rb

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users do |t|
      t.string :email_address, null: false
      t.string :password_digest, null: false

      t.timestamps
    end
    add_index :users, :email_address, unique: true
  end
end

rails/bsdports/app/db/migrate/20260501020818_create_sessions.rb

class CreateSessions < ActiveRecord::Migration[8.1]
  def change
    create_table :sessions do |t|
      t.references :user, null: false, foreign_key: true
      t.string :ip_address
      t.string :user_agent

      t.timestamps
    end
  end
end

rails/bsdports/app/db/migrate/20260507120001_create_categories.rb

class CreateCategories < ActiveRecord::Migration[8.1]
  def change
    create_table :categories do |t|
      t.string :name
      t.string :slug
      t.text :description
      t.timestamps
    end
    add_index :categories, :slug, unique: true
  end
end

rails/bsdports/app/db/migrate/20260507120002_create_ports.rb

class CreatePorts < ActiveRecord::Migration[8.1]
  def change
    create_table :ports do |t|
      t.string :name
      t.string :version
      t.references :category, foreign_key: true
      t.string :maintainer
      t.text :comment
      t.text :description
      t.string :homepage
      t.string :pkgpath
      t.boolean :permit_file_distfiles, default: false
      t.date :last_updated
      t.timestamps
    end
    add_index :ports, :name, unique: true
    add_index :ports, :pkgpath, unique: true
  end
end

rails/bsdports/app/db/migrate/20260507120003_create_dependencies.rb

class CreateDependencies < ActiveRecord::Migration[8.1]
  def change
    create_table :dependencies do |t|
      t.references :port, foreign_key: true
      t.references :depends_on, foreign_key: { to_table: :ports }
      t.string :dep_type
      t.timestamps
    end
  end
end

rails/bsdports/app/db/migrate/20260507120004_create_port_updates.rb

class CreatePortUpdates < ActiveRecord::Migration[8.1]
  def change
    create_table :port_updates do |t|
      t.references :port, foreign_key: true
      t.string :old_version
      t.string :new_version
      t.string :commit_id
      t.text :commit_message
      t.datetime :committed_at
      t.timestamps
    end
  end
end

rails/bsdports/app/db/migrate/20260507120005_create_watches.rb

class CreateWatches < ActiveRecord::Migration[8.1]
  def change
    create_table :watches do |t|
      t.references :user, foreign_key: true
      t.references :port, foreign_key: true
      t.boolean :notify_on_update, default: true
      t.timestamps
    end
  end
end

rails/bsdports/app/db/migrate/20260507120006_create_comments.rb

class CreateComments < ActiveRecord::Migration[8.1]
  def change
    create_table :comments do |t|
      t.references :user, foreign_key: true
      t.references :port, foreign_key: true
      t.integer :parent_id
      t.text :content
      t.timestamps
    end
  end
end

rails/bsdports/app/db/seeds.rb

# This file should ensure the existence of records required to run the application in every environment (production,
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
#
# Example:
#
#   ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
#     MovieGenre.find_or_create_by!(name: genre_name)
#   end

rails/bsdports/app/public/robots.txt

# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file

rails/bsdports/bsdports.sh

#!/usr/bin/env zsh
# bsdports.sh — deploys tracked Rails tree at app/ as %APP_NAME%
set -euo pipefail

APP_NAME=%APP_NAME%
APP_DIR=/home/${APP_NAME}/app
APP_PORT=47312
APP_DOMAIN=bsdports.org
SCRIPT_DIR=${0:a:h}
SRC_DIR=${SCRIPT_DIR}/app

. "${SCRIPT_DIR:h}/@shared_functions.sh"

need_cmd ruby34 bundle doas

[[ -d $SRC_DIR ]] || { log_err "missing source tree: $SRC_DIR"; exit 1 }

log "${APP_NAME} — deploying tracked tree → ${APP_DIR}"

id "$APP_NAME" >/dev/null 2>&1 || doas useradd -m -L daemon -s /bin/ksh "$APP_NAME"
doas mkdir -p "$APP_DIR"

doas cp -R "${SRC_DIR}/." "${APP_DIR}/"
doas chown -R "${APP_NAME}:${APP_NAME}" "$APP_DIR"

cd "$APP_DIR"

typeset bundle_home="/home/${APP_NAME}/.bundle"
if [[ ! -d ${bundle_home}/gems ]]; then
  log "Bootstrapping gems from amber"
  doas mkdir -p "$bundle_home"
  doas cp -R /home/amber/.bundle/gems "$bundle_home/"
  doas chown -R "${APP_NAME}:${APP_NAME}" "$bundle_home"
fi
print "---\nBUNDLE_PATH: \"${bundle_home}/gems\"" | doas tee "${APP_DIR}/.bundle/config" >/dev/null

doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bundle install --deployment --without development:test"
doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:create db:migrate"
[[ -f ${APP_DIR}/db/seeds.rb ]] && doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:seed" || true

install_rcd "$APP_NAME" "$APP_DIR" "$APP_PORT" "$APP_NAME"
[[ -n $APP_DOMAIN ]] && relayd_add_relay "$APP_DOMAIN" "$APP_PORT"

doas rcctl restart "$APP_NAME" || doas rcctl start "$APP_NAME"
log_ok "$APP_NAME live on :$APP_PORT"

rails/bsdports/bsdports_test.sh

rails/check_ports.sh

#!/usr/bin/env sh
set -eu

# Port consistency checker for DEPLOY/rails
# 0 → success, non‑zero → validation failure.

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
MASTER_JSON=${MASTER_JSON:-"$SCRIPT_DIR/../master.json"}

log()   { printf '%s\n' "$*"; }
error() { printf '❌ %s\n' "$*" >&2; }

require_command() {
    command -v "$1" >/dev/null 2>&1 || { error "Missing required command: $1"; exit 1; }
}

validate_master_json() {
    [ -f "$MASTER_JSON" ] || { error "master.json not found at: $MASTER_JSON"; return 1; }
    jq -e '.apps | type == "array" and length > 0' "$MASTER_JSON" >/dev/null 2>&1 ||
        { error "master.json must contain a non‑empty .apps array"; return 1; }
}

# Load app→port mapping into associative arrays.
load_ports() {
    declare -A port_of
    apps_list=''

    jq -r '.apps[] | "\(.name)\t\(.port)"' "$MASTER_JSON" |
    while IFS=$'\t' read -r app port; do
        [ -n "$app" ] && [ -n "$port" ] || { error "App with missing name or port"; return 1; }

        case "$port" in
            ''|*[!0-9]*) error "Invalid port '$port' for app '$app'"; return 1;;
        esac
        [ "$port" -ge 1 ] && [ "$port" -le 65535 ] || { error "Port out of range '$port' for app '$app'"; return 1; }

        if [ -n "${port_of[$app]+x}" ]; then
            error "Duplicate app name '$app' in master.json"
            return 1
        fi

        port_of[$app]=$port
        apps_list="${apps_list}${app} "
    done

    # Export for later stages.
    export APPS="$apps_list"
    export PORT_OF_JSON="$(printf '%s\n' "${!port_of[@]}" | while read -r k; do printf '%s=%s\n' "$k" "${port_of[$k]}"; done)"
    # Keep the associative array in a temporary file for subshells that need it.
    export PORT_MAP_FILE=$(mktemp)
    for k in "${!port_of[@]}"; do
        printf '%s=%s\n' "$k" "${port_of[$k]}" >>"$PORT_MAP_FILE"
    done
}

check_duplicate_ports() {
    duplicate=0
    # Build reverse map: port → apps
    declare -A seen
    for app in $APPS; do
        port=$(awk -F= -v a="$app" 'a==$1{print $2}' "$PORT_MAP_FILE")
        if [ -n "${seen[$port]+x}" ]; then
            error "Port collision on $port: ${seen[$port]} and $app"
            duplicate=1
        else
            seen[$port]=$app
        fi
    done
    return $duplicate
}

check_expected_port_constants() {
    mismatch=0
    i=1
    for app in $APPS; do
        installer="$SCRIPT_DIR/$app/$app.sh"
        if [ -f "$installer" ]; then
            installer_port=$(sed -nE 's/^[[:space:]]*(readonly[[:space:]]+)?PORT=([0-9]+).*/\2/p' "$installer" | head -n1)
            expected=$(awk -F= -v a="$app" 'a==$1{print $2}' "$PORT_MAP_FILE")
            if [ -n "$installer_port" ] && [ "$installer_port" != "$expected" ]; then
                error "$installer sets PORT=${installer_port}, expected ${expected}"
                mismatch=1
            fi
        fi
        i=$((i + 1))
    done
    return $mismatch
}

main() {
    log "=== Port Consistency Check ==="
    require_command jq

    validate_master_json
    load_ports
    check_duplicate_ports

    log ""
    log "Ports from ${MASTER_JSON}:"
    for app in $APPS; do
        port=$(awk -F= -v a="$app" 'a==$1{print $2}' "$PORT_MAP_FILE")
        log "  - $app: $port"
    done

    if check_expected_port_constants; then
        log ""
        log "✅ Port checks passed"
    else
        exit 1
    fi
}

main "$@"

rails/demo.sh

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

# Demo Rails 8 app generator – Simple CRUD with Hotwire
# Port: 10008
# Domain: demo.local (or configure as needed)

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly APP_NAME="demo"
readonly PORT=10008

die() {
  printf 'Error: %s\n' "$*" >&2
  exit 1
}

require_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}

check_prereqs() {
  require_cmd rails
  require_cmd bundler
  require_cmd lsof
  require_cmd sed
  require_cmd cp
  require_cmd cat
}

port_in_use() {
  lsof -i :"$PORT" -sTCP:LISTEN -t >/dev/null 2>&1
}

backup_file() {
  local src=$1
  [[ -f $src ]] && cp -f "$src" "${src}.backup"
}

append_once() {
  local file=$1 marker=$2 content=$3
  grep -qF "$marker" "$file" || printf '%s\n' "$content" >>"$file"
}

create_app() {
  rails new "$APP_NAME" \
    --database=postgresql \
    --css=tailwind \
    --javascript=importmap ||
    die "Failed to create Rails app"
}

write_database_yml() {
  cat > config/database.yml <<EOF
default: &default
  adapter: postgresql
  encoding: unicode
  pool: 5

development:
  <<: *default
  database: ${APP_NAME}_development

test:
  <<: *default
  database: ${APP_NAME}_test

production:
  <<: *default
  database: ${APP_NAME}_production
  username: ${APP_NAME}
  password: <%= ENV["${APP_NAME^^}_DATABASE_PASSWORD"] %>
EOF
}

configure_solid_gems() {
  local application_rb="config/application.rb"
  backup_file "$application_rb"

  append_once "$application_rb" "config.solid_queue" $'\n# Solid Queue configuration\nconfig.solid_queue.connects_to = { database: { writing: :primary } }'
  append_once "$application_rb" "config.solid_cache" $'\n# Solid Cache configuration\nconfig.solid_cache.connects_to = { database: { writing: :primary } }'
  append_once "$application_rb" "config.solid_cable" $'\n# Solid Cable configuration\nconfig.solid_cable.connects_to = { database: { writing: :primary } }'
}

inject_layout_wrapper() {
  local layout_file="app/views/layouts/application.html.erb"
  [[ -f $layout_file ]] || return

  sed -i.bak '/<body>/a\
    <div class="container mx-auto px-4 py-8">\
      <h1 class="text-3xl font-bold mb-6">Demo App</h1>\
      <%= yield %>\
    </div>' "$layout_file"
  rm -f "${layout_file}.bak"
}

start_server() {
  bin/rails server -p "$PORT" -d ||
    die "Failed to start Rails server"
  printf 'Demo app created successfully!\nApp is running on http://localhost:%s\nStop the server with: bin/rails server -p %s -d -s\n' "$PORT" "$PORT"
}

main() {
  check_prereqs

  local app_dir="${SCRIPT_DIR}/${APP_NAME}"
  [[ -d $app_dir ]] && die "Directory $app_dir already exists"
  port_in_use && die "Port $PORT is already in use"

  create_app
  cd "$APP_NAME"

  backup_file config/database.yml
  write_database_yml

  bin/rails db:create || die "Failed to create databases"

  bundle add solid_queue solid_cache solid_cable || die "Failed to add Solid* gems"
  configure_solid_gems

  bin/rails generate scaffold Post title:string content:text --no-jbuilder
  bin/rails db:migrate || die "Failed to run migrations"

  cat > config/routes.rb <<'EOF'
Rails.application.routes.draw do
  resources :posts
  root 'posts#index'
end
EOF

  inject_layout_wrapper
  start_server
}

main "$@"

rails/hjerterom/README.md

# hjerterom

Community care and support platform ("heart room"). Rails 8. PostgreSQL.

## Deploy

```zsh
cd ~/pub4/MASTER/DEPLOY/rails/hjerterom
doas zsh hjerterom.sh

## `rails/hjerterom/app/Dockerfile`
```text
# syntax=docker/dockerfile:1
# check=error=true

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t app .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name app app

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.9
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
    ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment variables and enable jemalloc for reduced memory usage and latency.
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development" \
    LD_PRELOAD="/usr/local/lib/libjemalloc.so"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY vendor/* ./vendor/
COPY Gemfile Gemfile.lock ./

RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
    bundle exec bootsnap precompile -j 1 --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times.
# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
RUN bundle exec bootsnap precompile -j 1 app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile




# Final stage for app image
FROM base

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
USER 1000:1000

# Copy built artifacts: gems, application
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --chown=rails:rails --from=build /rails /rails

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]

rails/hjerterom/app/Gemfile

source "https://rubygems.org"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.1.2"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Use sqlite3 as the database for Active Record
gem "sqlite3", ">= 2.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]

# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable
gem "solid_cache"
gem "solid_queue"
gem "solid_cable"

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
gem "kamal", require: false

# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
gem "thruster", require: false

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
gem "image_processing", "~> 1.2"

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

  # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues)
  gem "bundler-audit", require: false

  # Static analysis for security vulnerabilities [https://brakemanscanner.org/]
  gem "brakeman", require: false

  # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
  gem "rubocop-rails-omakase", require: false
end

group :development do
  # Use console on exceptions pages [https://github.com/rails/web-console]
  gem "web-console"
end
gem "pagy"

gem "falcon"

rails/hjerterom/app/README.md

# README

This README would normally document whatever steps are necessary to get the
application up and running.

Things you may want to cover:

* Ruby version

* System dependencies

* Configuration

* Database creation

* Database initialization

* How to run the test suite

* Services (job queues, cache servers, search engines, etc.)

* Deployment instructions

* ...

rails/hjerterom/app/Rakefile

# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative "config/application"

Rails.application.load_tasks

rails/hjerterom/app/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Authentication
  include Pagy::Method
  allow_browser versions: :modern
end

rails/hjerterom/app/app/controllers/community_controller.rb

class CommunityController < ApplicationController
  allow_unauthenticated_access only: %i[index show]

  def index
    @categories = Category.order(:name)
    @pagy, @posts = pagy(Post.recent.includes(:user, :category))
  end

  def show
    @post     = Post.find(params[:id])
    @post.increment!(:views_count)
    @comments = @post.comments.roots.includes(:user, replies: :user)
    @comment  = Comment.new
  end

  def new
    @post = Post.new
  end

  def create
    @post = Current.user.posts.build(post_params)
    @post.save ? redirect_to(community_show_path(@post), notice: "Posted") : render(:new, status: :unprocessable_entity)
  end

  private

  def post_params
    params.require(:post).permit(:title, :body, :category_id, :anonymous)
  end
end

rails/hjerterom/app/app/controllers/concerns/authentication.rb

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

rails/hjerterom/app/app/controllers/food_listings_controller.rb

class FoodListingsController < ApplicationController
  allow_unauthenticated_access only: %i[index show]
  before_action :set_listing, only: %i[show edit update destroy]
  before_action :authorize!, only: %i[edit update destroy]

  def index
    @pagy, @listings = pagy(FoodListing.available.order(created_at: :desc))
  end

  def show
    @request = FoodRequest.new
  end

  def new
    @listing = Current.user.food_listings.build
  end

  def create
    @listing = Current.user.food_listings.build(listing_params)
    @listing.save ? redirect_to(@listing, notice: "Food listing created") : render(:new, status: :unprocessable_entity)
  end

  def edit; end

  def update
    @listing.update(listing_params) ? redirect_to(@listing, notice: "Updated") : render(:edit, status: :unprocessable_entity)
  end

  def destroy
    @listing.destroy
    redirect_to food_listings_path, notice: "Listing removed"
  end

  private

  def set_listing  = @listing = FoodListing.find(params[:id])
  def authorize!   = redirect_to(food_listings_path, alert: "Unauthorized") unless @listing.user == Current.user

  def listing_params
    params.require(:food_listing).permit(
      :title, :description, :quantity, :unit,
      :available_from, :available_until,
      :pickup_address, :city, :dietary_info
    )
  end
end

rails/hjerterom/app/app/controllers/food_requests_controller.rb

class FoodRequestsController < ApplicationController
  def create
    listing  = FoodListing.find(params[:food_listing_id])
    @request = listing.food_requests.build(request_params.merge(user: Current.user, status: "pending"))
    @request.save ? redirect_to(listing, notice: "Request sent") : render(:new, status: :unprocessable_entity)
  end

  def update
    @request = FoodRequest.find(params[:id])
    authorize_owner!
    @request.update!(status: params[:status]) if params[:status].in?(%w[approved declined])
    redirect_to @request.food_listing
  end

  private

  def authorize_owner! = redirect_to(root_path, alert: "Unauthorized") unless @request.food_listing.user == Current.user
  def request_params   = params.require(:food_request).permit(:message, :pickup_time)
end

rails/hjerterom/app/app/controllers/home_controller.rb

class HomeController < ApplicationController
  allow_unauthenticated_access only: :index

  def index
    @crisis_lines  = Crisis.where(available_24h: true).limit(5)
    @food_listings = FoodListing.available.order(created_at: :desc).limit(6)
    @recent_posts  = Post.recent.includes(:user, :category).limit(5)
    @resources     = Resource.verified.limit(8)
  end
end

rails/hjerterom/app/app/controllers/passwords_controller.rb

class PasswordsController < ApplicationController
  allow_unauthenticated_access
  before_action :set_user_by_token, only: %i[ edit update ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.find_by(email_address: params[:email_address])
      PasswordsMailer.reset(user).deliver_later
    end

    redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
  end

  def edit
  end

  def update
    if @user.update(params.permit(:password, :password_confirmation))
      @user.sessions.destroy_all
      redirect_to new_session_path, notice: "Password has been reset."
    else
      redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
    end
  end

  private
    def set_user_by_token
      @user = User.find_by_password_reset_token!(params[:token])
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
    end
end

rails/hjerterom/app/app/controllers/resources_controller.rb

class ResourcesController < ApplicationController
  allow_unauthenticated_access only: %i[index show]
  before_action :set_resource, only: %i[show edit update destroy]
  before_action :authorize!, only: %i[edit update destroy]

  def index
    scope = Resource.includes(:category)
    scope = scope.by_type(params[:type]) if params[:type].present?
    scope = scope.where("title LIKE ?", "%#{params[:q]}%") if params[:q].present?
    @pagy, @resources = pagy(scope.verified.order(:title))
    @crisis_lines = Crisis.all
  end

  def show; end

  def new
    @resource = Current.user.resources.build
  end

  def create
    @resource = Current.user.resources.build(resource_params)
    @resource.save ? redirect_to(@resource, notice: "Resource submitted for review") : render(:new, status: :unprocessable_entity)
  end

  def edit; end

  def update
    @resource.update(resource_params) ? redirect_to(@resource, notice: "Updated") : render(:edit, status: :unprocessable_entity)
  end

  def destroy
    @resource.destroy
    redirect_to resources_path, notice: "Removed"
  end

  private

  def set_resource  = @resource = Resource.find(params[:id])
  def authorize!    = redirect_to(resources_path, alert: "Unauthorized") unless @resource.user == Current.user

  def resource_params
    params.require(:resource).permit(
      :title, :description, :url, :address, :city, :postal_code,
      :phone, :email, :resource_type, :opening_hours, :category_id
    )
  end
end

rails/hjerterom/app/app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  allow_unauthenticated_access only: %i[ new create ]
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }

  def new
  end

  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

  def destroy
    terminate_session
    redirect_to new_session_path, status: :see_other
  end
end

rails/hjerterom/app/app/helpers/application_helper.rb

module ApplicationHelper
end

rails/hjerterom/app/app/javascript/application.js

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"

rails/hjerterom/app/app/javascript/controllers/animated_number_controller.js

import AnimatedNumber from "@stimulus-components/animated-number"
export default class extends AnimatedNumber {}

rails/hjerterom/app/app/javascript/controllers/application.js

import { Application } from "@hotwired/stimulus"

const application = Application.start()

// Configure Stimulus development experience
application.debug = false
window.Stimulus   = application

export { application }

rails/hjerterom/app/app/javascript/controllers/auto_submit_controller.js

import AutoSubmit from "@stimulus-components/auto-submit"
export default class extends AutoSubmit {}

rails/hjerterom/app/app/javascript/controllers/character_counter_controller.js

import CharacterCounter from "@stimulus-components/character-counter"
export default class extends CharacterCounter {}

rails/hjerterom/app/app/javascript/controllers/clipboard_controller.js

import Clipboard from "@stimulus-components/clipboard"
export default class extends Clipboard {}

rails/hjerterom/app/app/javascript/controllers/dialog_controller.js

import Dialog from "@stimulus-components/dialog"
export default class extends Dialog {}

rails/hjerterom/app/app/javascript/controllers/dropdown_controller.js

import Dropdown from "@stimulus-components/dropdown"
export default class extends Dropdown {}

rails/hjerterom/app/app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.textContent = "Hello World!"
  }
}

rails/hjerterom/app/app/javascript/controllers/index.js

// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)

rails/hjerterom/app/app/javascript/controllers/notification_controller.js

import Notification from "@stimulus-components/notification"
export default class extends Notification {}

rails/hjerterom/app/app/javascript/controllers/sortable_controller.js

import Sortable from "@stimulus-components/sortable"
export default class extends Sortable {}

rails/hjerterom/app/app/javascript/controllers/textarea_autogrow_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.resize()
    this.element.addEventListener("input", this.resize)
  }

  disconnect() {
    this.element.removeEventListener("input", this.resize)
  }

  resize = () => {
    this.element.style.height = "auto"
    this.element.style.height = `${this.element.scrollHeight}px`
  }
}

rails/hjerterom/app/app/javascript/controllers/timeago_controller.js

import TimeAgo from "@stimulus-components/timeago"
export default class extends TimeAgo {}

rails/hjerterom/app/app/jobs/application_job.rb

class ApplicationJob < ActiveJob::Base
  # Automatically retry jobs that encountered a deadlock
  # retry_on ActiveRecord::Deadlocked

  # Most jobs are safe to ignore if the underlying records are no longer available
  # discard_on ActiveJob::DeserializationError
end

rails/hjerterom/app/app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout "mailer"
end

rails/hjerterom/app/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
end

rails/hjerterom/app/app/models/category.rb

class Category < ApplicationRecord
  has_many :resources, dependent: :nullify
  has_many :posts, dependent: :nullify

  TYPES = %w[mental_health food housing legal community other].freeze

  validates :name, :slug, presence: true
  validates :slug, uniqueness: true
  validates :type_of, inclusion: { in: TYPES }, allow_nil: true

  scope :of_type, ->(t) { where(type_of: t) }
end

rails/hjerterom/app/app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :post
  belongs_to :parent, class_name: "Comment", optional: true
  has_many :replies, class_name: "Comment", foreign_key: :parent_id, dependent: :destroy

  validates :content, presence: true, length: { maximum: 3000 }

  scope :roots, -> { where(parent_id: nil).order(created_at: :asc) }

  after_create_commit -> { broadcast_append_to [post, "comments"] }

  def display_author
    anonymous? ? "Anonym" : user.email_address.split("@").first
  end
end

rails/hjerterom/app/app/models/crisis.rb

class Crisis < ApplicationRecord
  validates :title, :phone, presence: true

  scope :around_clock, -> { where(available_24h: true) }
  scope :for_country,  ->(c) { where(country: c) }
end

rails/hjerterom/app/app/models/current.rb

class Current < ActiveSupport::CurrentAttributes
  attribute :session
  delegate :user, to: :session, allow_nil: true
end

rails/hjerterom/app/app/models/food_listing.rb

class FoodListing < ApplicationRecord
  belongs_to :user
  has_many :food_requests, dependent: :destroy

  STATUSES = %w[available reserved taken expired].freeze
  UNITS    = %w[kg portions bags boxes items].freeze

  validates :title, :quantity, :available_until, presence: true
  validates :quantity, numericality: { greater_than: 0 }
  validates :status, inclusion: { in: STATUSES }
  validates :unit, inclusion: { in: UNITS }

  attribute :status, :string, default: "available"

  geocoded_by :pickup_address
  after_validation :geocode, if: :pickup_address_changed?

  scope :available, -> { where(status: "available").where("available_until > ?", Time.current) }
  scope :nearby, ->(lat, lng, km = 20) {
    where("((latitude - ?) * (latitude - ?) + (longitude - ?) * (longitude - ?)) < ?",
      lat, lat, lng, lng, (km / 111.0)**2)
  }

  before_save :expire_if_past_date

  private

  def expire_if_past_date
    self.status = "expired" if available_until < Time.current && status == "available"
  end
end

rails/hjerterom/app/app/models/food_request.rb

class FoodRequest < ApplicationRecord
  belongs_to :food_listing
  belongs_to :user

  STATUSES = %w[pending accepted declined picked_up cancelled].freeze

  validates :status, inclusion: { in: STATUSES }
  validates :message, length: { maximum: 1000 }, allow_blank: true

  attribute :status, :string, default: "pending"

  after_create_commit -> { broadcast_prepend_to [food_listing, "requests"] }

  scope :pending,  -> { where(status: "pending") }
  scope :accepted, -> { where(status: "accepted") }
end

rails/hjerterom/app/app/models/post.rb

class Post < ApplicationRecord
  include ActionText::RichText

  belongs_to :user
  belongs_to :category
  has_rich_text :body
  has_many :comments, dependent: :destroy

  validates :title, presence: true, length: { maximum: 200 }

  scope :pinned,  -> { where(pinned: true) }
  scope :recent,  -> { order(created_at: :desc) }

  def display_author
    anonymous? ? "Anonym" : user.email_address.split("@").first
  end
end

rails/hjerterom/app/app/models/resource.rb

class Resource < ApplicationRecord
  belongs_to :user
  belongs_to :category

  RESOURCE_TYPES = %w[crisis_line support_group therapist hotline community_center other].freeze

  validates :title, presence: true
  validates :resource_type, inclusion: { in: RESOURCE_TYPES }

  geocoded_by :address
  after_validation :geocode, if: :address_changed?

  scope :verified,   -> { where(verified: true) }
  scope :nearby,     ->(lat, lng, km = 50) {
    where("((latitude - ?) * (latitude - ?) + (longitude - ?) * (longitude - ?)) < ?",
      lat, lat, lng, lng, (km / 111.0)**2)
  }
  scope :by_type,    ->(t) { where(resource_type: t) }
end

rails/hjerterom/app/app/models/session.rb

class Session < ApplicationRecord
  belongs_to :user
end

rails/hjerterom/app/app/models/support_request.rb

class SupportRequest < ApplicationRecord
  belongs_to :user

  STATUSES   = %w[open in_progress resolved closed].freeze
  PRIORITIES = %w[low normal high urgent].freeze

  validates :subject, presence: true, length: { maximum: 200 }
  validates :status, inclusion: { in: STATUSES }
  validates :priority, inclusion: { in: PRIORITIES }

  attribute :status,   :string, default: "open"
  attribute :priority, :string, default: "normal"

  scope :open,    -> { where(status: %w[open in_progress]) }
  scope :urgent,  -> { where(priority: "urgent") }
end

rails/hjerterom/app/app/models/user.rb

class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy
  has_many :resources, dependent: :nullify
  has_many :posts, dependent: :nullify
  has_many :comments, dependent: :nullify
  has_many :food_listings, dependent: :nullify
  has_many :food_requests, dependent: :destroy
  has_many :support_requests, dependent: :destroy

  normalizes :email_address, with: ->(e) { e.strip.downcase }
end

rails/hjerterom/app/app/views/community/index.html.erb

<% content_for :title, "Community" %>
<header>
  <h1>Community</h1>
  <% if authenticated? %><%= link_to "New post", new_community_post_path %><% end %>
</header>
<section id="community_posts">
  <% @posts.each do |post| %>
    <article>
      <%= link_to post.title, community_show_path(post) %>
      <small><%= post.anonymous? ? "Anonymous" : post.user.email_address.split("@").first %></small>
    </article>
  <% end %>
</section>

rails/hjerterom/app/app/views/community/new.html.erb

<% content_for :title, "New post" %>
<h1>New post</h1>
<%= form_with url: community_path do |f| %>
  <p><%= f.label :title %><%= f.text_field :title, name: "post[title]", autofocus: true %></p>
  <p><%= f.label :body %><%= f.rich_text_area :body, name: "post[body]" %></p>
  <p>
    <%= label_tag :anonymous, "Post anonymously" %>
    <%= check_box_tag "post[anonymous]" %>
  </p>
  <p><%= f.submit "Post" %> <%= link_to "Cancel", community_path %></p>
<% end %>

rails/hjerterom/app/app/views/community/show.html.erb

<% content_for :title, @post.title %>
<article>
  <h1><%= @post.title %></h1>
  <small><%= @post.anonymous? ? "Anonymous" : @post.user.email_address.split("@").first %></small>
  <%= @post.body %>
</article>
<section id="comments">
  <h2>Comments</h2>
  <%= render @comments %>
  <% if authenticated? %>
    <%= form_with url: community_comments_path do |f| %>
      <%= f.hidden_field :post_id, value: @post.id %>
      <p><%= f.text_area :content, rows: 3, placeholder: "Comment…" %></p>
      <p><%= f.submit "Comment" %></p>
    <% end %>
  <% end %>
</section>

rails/hjerterom/app/app/views/food_listings/_form.html.erb

<%= form_with model: listing, url: listing.new_record? ? food_listings_path : food_listing_path(listing) do |f| %>
  <%= render "shared/errors", object: listing %>
  <p><%= f.label :title %><%= f.text_field :title, autofocus: true %></p>
  <p><%= f.label :description %><%= f.text_area :description, rows: 2 %></p>
  <p><%= f.label :quantity %><%= f.number_field :quantity, min: 1 %></p>
  <p><%= f.label :unit %><%= f.text_field :unit, placeholder: "kg, portions, bags…" %></p>
  <p><%= f.label :pickup_address %><%= f.text_field :pickup_address %></p>
  <p><%= f.label :city %><%= f.text_field :city %></p>
  <p><%= f.label :available_from %><%= f.datetime_local_field :available_from %></p>
  <p><%= f.label :available_until %><%= f.datetime_local_field :available_until %></p>
  <p><%= f.label :dietary_info %><%= f.text_field :dietary_info, placeholder: "vegan, nut-free…" %></p>
  <p><%= f.submit %> <%= link_to "Cancel", food_listings_path %></p>
<% end %>

rails/hjerterom/app/app/views/food_listings/edit.html.erb

<% content_for :title, "Edit listing" %>
<h1>Edit listing</h1>
<%= render "form", listing: @listing %>

rails/hjerterom/app/app/views/food_listings/index.html.erb

<% content_for :title, "Food" %>
<header>
  <h1>Available food</h1>
  <% if authenticated? %><%= link_to "List food", new_food_listing_path %><% end %>
</header>
<section id="food_listings">
  <% @food_listings.each do |listing| %>
    <article>
      <%= link_to listing.title, food_listing_path(listing) %>
      <p><%= listing.quantity %> <%= listing.unit %> · <%= listing.city %></p>
      <p>Until <%= listing.available_until&.strftime("%b %-d, %H:%M") %></p>
      <% if listing.dietary_info.present? %>
        <small><%= listing.dietary_info %></small>
      <% end %>
    </article>
  <% end %>
</section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/hjerterom/app/app/views/food_listings/new.html.erb

<% content_for :title, "List food" %>
<h1>List food</h1>
<%= render "form", listing: @listing %>

rails/hjerterom/app/app/views/food_listings/show.html.erb

<% content_for :title, @listing.title %>
<article>
  <h1><%= @listing.title %></h1>
  <p><%= @listing.description %></p>
  <dl>
    <dt>Quantity</dt><dd><%= @listing.quantity %> <%= @listing.unit %></dd>
    <dt>Pickup</dt><dd><%= @listing.pickup_address %>, <%= @listing.city %></dd>
    <dt>Available</dt><dd><%= @listing.available_from&.strftime("%b %-d, %H:%M") %><%= @listing.available_until&.strftime("%b %-d, %H:%M") %></dd>
    <% if @listing.dietary_info.present? %><dt>Dietary</dt><dd><%= @listing.dietary_info %></dd><% end %>
  </dl>
  <% if authenticated? && @listing.user != Current.user && @listing.status == "available" %>
    <%= form_with url: food_listing_food_requests_path(@listing) do |f| %>
      <p><%= f.text_area :message, rows: 2, placeholder: "Message (optional)…" %></p>
      <p><%= f.datetime_local_field :pickup_time %></p>
      <p><%= f.submit "Request pickup" %></p>
    <% end %>
  <% end %>
  <% if @listing.user == Current.user %>
    <%= link_to "Edit", edit_food_listing_path(@listing) %>
    <%= button_to "Delete", @listing, method: :delete, data: { turbo_confirm: "Delete?" } %>
  <% end %>
</article>

rails/hjerterom/app/app/views/home/index.html.erb

<% content_for :title, "Hjerterom" %>
<% if @crisis_lines.any? %>
  <aside>
    <strong>Crisis support:</strong>
    <% @crisis_lines.each do |c| %>
      <%= c.title %> <strong><%= c.phone %></strong><%= " · " unless c == @crisis_lines.last %>
    <% end %>
  </aside>
<% end %>

<section>
  <h2>Food available</h2>
  <% @food_listings.each do |listing| %>
    <article>
      <%= link_to listing.title, food_listing_path(listing) %>
      <p><%= listing.city %> · available until <%= listing.available_until&.strftime("%b %-d") %></p>
    </article>
  <% end %>
  <%= link_to "All food listings →", food_listings_path %>
</section>

<section>
  <h2>Community</h2>
  <% @posts.each do |post| %>
    <article>
      <%= link_to post.title, community_show_path(post) %>
    </article>
  <% end %>
  <%= link_to "All posts →", community_path %>
  <% if authenticated? %><%= link_to "New post", new_community_post_path %><% end %>
</section>

<section>
  <h2>Resources</h2>
  <% @resources.each do |resource| %>
    <article>
      <%= link_to resource.title, resource_path(resource) %>
      <p><%= resource.resource_type %><%= " · #{resource.city}" if resource.city.present? %></p>
    </article>
  <% end %>
  <%= link_to "All resources →", resources_path %>
</section>

rails/hjerterom/app/app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for(:title) || "App" %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="application-name" content="App">
    <meta name="mobile-web-app-capable" content="yes">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= yield :head %>

    <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
    <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>

    <link rel="icon" href="/icon.png" type="image/png">
    <link rel="icon" href="/icon.svg" type="image/svg+xml">
    <link rel="apple-touch-icon" href="/icon.png">

    <%# Includes all stylesheet files in app/assets/stylesheets %>
    <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

rails/hjerterom/app/app/views/layouts/mailer.html.erb

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

rails/hjerterom/app/app/views/layouts/mailer.text.erb

<%= yield %>

rails/hjerterom/app/app/views/pwa/manifest.json.erb

{
  "name": "App",
  "icons": [
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512"
    },
    {
      "src": "/icon.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "maskable"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "scope": "/",
  "description": "App.",
  "theme_color": "red",
  "background_color": "red"
}

rails/hjerterom/app/app/views/pwa/service-worker.js

// Add a service worker for processing Web Push notifications:
//
// self.addEventListener("push", async (event) => {
//   const { title, options } = await event.data.json()
//   event.waitUntil(self.registration.showNotification(title, options))
// })
//
// self.addEventListener("notificationclick", function(event) {
//   event.notification.close()
//   event.waitUntil(
//     clients.matchAll({ type: "window" }).then((clientList) => {
//       for (let i = 0; i < clientList.length; i++) {
//         let client = clientList[i]
//         let clientPath = (new URL(client.url)).pathname
//
//         if (clientPath == event.notification.data.path && "focus" in client) {
//           return client.focus()
//         }
//       }
//
//       if (clients.openWindow) {
//         return clients.openWindow(event.notification.data.path)
//       }
//     })
//   )
// })

rails/hjerterom/app/app/views/resources/_form.html.erb

<%= form_with model: resource do |f| %>
  <%= render "shared/errors", object: resource %>
  <p><%= f.label :title %><%= f.text_field :title, autofocus: true %></p>
  <p><%= f.label :description %><%= f.text_area :description, rows: 3 %></p>
  <p>
    <%= f.label :resource_type %>
    <%= f.select :resource_type, %w[mental_health food shelter legal medical], include_blank: "Select…" %>
  </p>
  <p><%= f.label :address %><%= f.text_field :address %></p>
  <p><%= f.label :city %><%= f.text_field :city %></p>
  <p><%= f.label :phone %><%= f.telephone_field :phone %></p>
  <p><%= f.label :email %><%= f.email_field :email %></p>
  <p><%= f.label :url, "Website" %><%= f.url_field :url %></p>
  <p><%= f.label :opening_hours %><%= f.text_field :opening_hours %></p>
  <p><%= f.submit %> <%= link_to "Cancel", resources_path %></p>
<% end %>

rails/hjerterom/app/app/views/resources/edit.html.erb

<% content_for :title, "Edit resource" %>
<h1>Edit resource</h1>
<%= render "form", resource: @resource %>

rails/hjerterom/app/app/views/resources/index.html.erb

<% content_for :title, "Resources" %>
<header>
  <h1>Resources</h1>
  <% if authenticated? %><%= link_to "Add resource", new_resource_path %><% end %>
</header>
<section id="resources">
  <% @resources.each do |resource| %>
    <article data-resource-type="<%= resource.resource_type %>">
      <%= link_to resource.title, resource %>
      <small><%= resource.resource_type %></small>
      <p><%= resource.description %></p>
      <p><%= [resource.city, resource.phone].compact.join(" · ") %></p>
    </article>
  <% end %>
</section>
<%= @pagy.series_nav if @pagy.pages > 1 %>

rails/hjerterom/app/app/views/resources/new.html.erb

<% content_for :title, "Add resource" %>
<h1>Add resource</h1>
<%= render "form", resource: @resource %>

rails/hjerterom/app/app/views/resources/show.html.erb

<% content_for :title, @resource.title %>
<article data-resource-type="<%= @resource.resource_type %>">
  <h1><%= @resource.title %></h1>
  <small><%= @resource.resource_type %></small>
  <p><%= @resource.description %></p>
  <dl>
    <% if @resource.address.present? %><dt>Address</dt><dd><%= @resource.address %>, <%= @resource.city %></dd><% end %>
    <% if @resource.phone.present? %><dt>Phone</dt><dd><%= @resource.phone %></dd><% end %>
    <% if @resource.email.present? %><dt>Email</dt><dd><%= mail_to @resource.email %></dd><% end %>
    <% if @resource.url.present? %><dt>Website</dt><dd><%= link_to @resource.url, @resource.url %></dd><% end %>
    <% if @resource.opening_hours.present? %><dt>Hours</dt><dd><%= @resource.opening_hours %></dd><% end %>
  </dl>
  <% if @resource.user == Current.user %>
    <%= link_to "Edit", edit_resource_path(@resource) %>
    <%= button_to "Delete", @resource, method: :delete, data: { turbo_confirm: "Delete?" } %>
  <% end %>
</article>

rails/hjerterom/app/config/application.rb

require_relative "boot"

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module App
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 8.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w[assets tasks])

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Don't generate system test files.
    config.generators.system_tests = nil
  end
end

rails/hjerterom/app/config/boot.rb

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

require "bundler/setup" # Set up gems listed in the Gemfile.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.

rails/hjerterom/app/config/bundler-audit.yml

# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit.
# CVEs that are not relevant to the application can be enumerated on the ignore list below.

ignore:
  - CVE-THAT-DOES-NOT-APPLY

rails/hjerterom/app/config/cable.yml

development:
  adapter: async

test:
  adapter: test

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: app_production

rails/hjerterom/app/config/ci.rb

# Run using bin/ci

CI.run do
  step "Setup", "bin/setup --skip-server"

  step "Style: Ruby", "bin/rubocop"

  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Importmap vulnerability audit", "bin/importmap audit"
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"


  # Optional: set a green GitHub commit status to unblock PR merge.
  # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
  # if success?
  #   step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
  # else
  #   failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
  # end
end

rails/hjerterom/app/config/database.yml

# SQLite. Versions 3.8.0 and up are supported.
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem "sqlite3"
#
default: &default
  adapter: sqlite3
  max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: storage/development.sqlite3

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: storage/test.sqlite3

# Store production database in the storage/ directory, which by default
# is mounted as a persistent Docker volume in config/deploy.yml.
production:
  primary:
    <<: *default
    database: storage/production.sqlite3
  cache:
    <<: *default
    database: storage/production_cache.sqlite3
    migrations_paths: db/cache_migrate
  queue:
    <<: *default
    database: storage/production_queue.sqlite3
    migrations_paths: db/queue_migrate
  cable:
    <<: *default
    database: storage/production_cable.sqlite3
    migrations_paths: db/cable_migrate

rails/hjerterom/app/config/deploy.yml

# Name of your application. Used to uniquely configure containers.
service: app

# Name of the container image (use your-user/app-name on external registries).
image: app

# Deploy to these servers.
servers:
  web:
    - 192.168.0.1
  # job:
  #   hosts:
  #     - 192.168.0.1
  #   cmd: bin/jobs

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
#
# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb!
#
# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer).
#
# proxy:
#   ssl: true
#   host: app.example.com

# Where you keep your container images.
registry:
  # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ...
  server: localhost:5555

  # Needed for authenticated registries.
  # username: your-user

  # Always use an access token rather than real password when possible.
  # password:
  #   - KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
  secret:
    - RAILS_MASTER_KEY
  clear:
    # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
    # When you start using multiple servers, you should split out job processing to a dedicated machine.
    SOLID_QUEUE_IN_PUMA: true

    # Set number of processes dedicated to Solid Queue (default: 1)
    # JOB_CONCURRENCY: 3

    # Set number of cores available to the application on each server (default: 1).
    # WEB_CONCURRENCY: 2

    # Match this to any external database server to configure Active Record correctly
    # Use app-db for a db accessory server on same machine via local kamal docker network.
    # DB_HOST: 192.168.0.2

    # Log everything from Rails
    # RAILS_LOG_LEVEL: debug

# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
  console: app exec --interactive --reuse "bin/rails console"
  shell: app exec --interactive --reuse "bash"
  logs: app logs -f
  dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"

# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
  - "app_storage:/rails/storage"

# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets

# Configure the image builder.
builder:
  arch: amd64

  # # Build image via remote server (useful for faster amd64 builds on arm64 computers)
  # remote: ssh://docker@docker-builder-server
  #
  # # Pass arguments and secrets to the Docker build process
  # args:
  #   RUBY_VERSION: ruby-3.4.9
  # secrets:
  #   - GITHUB_TOKEN
  #   - RAILS_MASTER_KEY

# Use a different ssh user than root
# ssh:
#   user: app

# Use accessory services (secrets come from .kamal/secrets).
# accessories:
#   db:
#     image: mysql:8.0
#     host: 192.168.0.2
#     # Change to 3306 to expose port to the world instead of just local network.
#     port: "127.0.0.1:3306:3306"
#     env:
#       clear:
#         MYSQL_ROOT_HOST: '%'
#       secret:
#         - MYSQL_ROOT_PASSWORD
#     files:
#       - config/mysql/production.cnf:/etc/mysql/my.cnf
#       - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
#     directories:
#       - data:/var/lib/mysql
#   redis:
#     image: valkey/valkey:8
#     host: 192.168.0.2
#     port: 6379
#     directories:
#       - data:/data

rails/hjerterom/app/config/environment.rb

# Load the Rails application.
require_relative "application"

# Initialize the Rails application.
Rails.application.initialize!

rails/hjerterom/app/config/environments/development.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Make code changes take effect immediately without server restart.
  config.enable_reloading = true

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports.
  config.consider_all_requests_local = true

  # Enable server timing.
  config.server_timing = true

  # Enable/disable Action Controller caching. By default Action Controller caching is disabled.
  # Run rails dev:cache to toggle Action Controller caching.
  if Rails.root.join("tmp/caching-dev.txt").exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true
    config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
  else
    config.action_controller.perform_caching = false
  end

  # Change to :null_store to avoid any caching.
  config.cache_store = :memory_store

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  # Make template changes take effect immediately.
  config.action_mailer.perform_caching = false

  # Set localhost to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Highlight code that triggered database queries in logs.
  config.active_record.verbose_query_logs = true

  # Append comments with runtime information tags to SQL queries in logs.
  config.active_record.query_log_tags_enabled = true

  # Highlight code that enqueued background job in logs.
  config.active_job.verbose_enqueue_logs = true

  # Highlight code that triggered redirect in logs.
  config.action_dispatch.verbose_redirect_logs = true

  # Suppress logger output for asset requests.
  config.assets.quiet = true

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  config.action_view.annotate_rendered_view_with_filenames = true

  # Uncomment if you wish to allow Action Cable access from any origin.
  # config.action_cable.disable_request_forgery_protection = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true

  # Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
  # config.generators.apply_rubocop_autocorrect_after_generate!
end

rails/hjerterom/app/config/environments/production.rb

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Code is not reloaded between requests.
  config.enable_reloading = false

  # Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
  config.eager_load = true

  # Full error reports are disabled.
  config.consider_all_requests_local = false

  # Turn on fragment caching in view templates.
  config.action_controller.perform_caching = true

  # Cache assets for far-future expiry since they are all digest stamped.
  config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }

  # Enable serving of images, stylesheets, and JavaScripts from an asset server.
  # config.asset_host = "http://assets.example.com"

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Assume all access to the app is happening through a SSL-terminating reverse proxy.
  # config.assume_ssl = true

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  # config.force_ssl = true

  # Skip http-to-https redirect for the default health check endpoint.
  # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }

  # Log to STDOUT with the current request id as a default log tag.
  config.log_tags = [ :request_id ]
  config.logger   = ActiveSupport::TaggedLogging.logger(STDOUT)

  # Change to "debug" to log everything (including potentially personally-identifiable information!).
  config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")

  # Prevent health checks from clogging up the logs.
  config.silence_healthcheck_path = "/up"

  # Don't log any deprecations.
  config.active_support.report_deprecations = false

  # Replace the default in-process memory cache store with a durable alternative.
  # config.cache_store = :mem_cache_store

  # Replace the default in-process and non-durable queuing backend for Active Job.
  # config.active_job.queue_adapter = :resque

  # Ignore bad email addresses and do not raise email delivery errors.
  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
  # config.action_mailer.raise_delivery_errors = false

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
  # config.action_mailer.smtp_settings = {
  #   user_name: Rails.application.credentials.dig(:smtp, :user_name),
  #   password: Rails.application.credentials.dig(:smtp, :password),
  #   address: "smtp.example.com",
  #   port: 587,
  #   authentication: :plain
  # }

  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
  # the I18n.default_locale when a translation cannot be found).
  config.i18n.fallbacks = true

  # Do not dump schema after migrations.
  config.active_record.dump_schema_after_migration = false

  # Only use :id for inspections in production.
  config.active_record.attributes_for_inspect = [ :id ]

  # Enable DNS rebinding protection and other `Host` header attacks.
  # config.hosts = [
  #   "example.com",     # Allow requests from example.com
  #   /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
  # ]
  #
  # Skip DNS rebinding protection for the default health check endpoint.
  # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end

rails/hjerterom/app/config/environments/test.rb

# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # While tests run files are not watched, reloading is not necessary.
  config.enable_reloading = false

  # Eager loading loads your entire application. When running a single test locally,
  # this is usually not necessary, and can slow down your test suite. However, it's
  # recommended that you enable it in continuous integration systems to ensure eager
  # loading is working properly before deploying your code.
  config.eager_load = ENV["CI"].present?

  # Configure public file server for tests with cache-control for performance.
  config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }

  # Show full error reports.
  config.consider_all_requests_local = true
  config.cache_store = :null_store

  # Render exception templates for rescuable exceptions and raise for other exceptions.
  config.action_dispatch.show_exceptions = :rescuable

  # Disable request forgery protection in test environment.
  config.action_controller.allow_forgery_protection = false

  # Store uploaded files on the local file system in a temporary directory.
  config.active_storage.service = :test

  # Tell Action Mailer not to deliver emails to the real world.
  # The :test delivery method accumulates sent emails in the
  # ActionMailer::Base.deliveries array.
  config.action_mailer.delivery_method = :test

  # Set host to be used by links generated in mailer templates.
  config.action_mailer.default_url_options = { host: "example.com" }

  # Print deprecation notices to the stderr.
  config.active_support.deprecation = :stderr

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  # config.action_view.annotate_rendered_view_with_filenames = true

  # Raise error when a before_action's only/except options reference missing actions.
  config.action_controller.raise_on_missing_callback_actions = true
end

rails/hjerterom/app/config/importmap.rb

# Pin npm packages by running ./bin/importmap

pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "@stimulus-components/dialog", to: "@stimulus-components--dialog.js" # @1.0.1
pin "@stimulus-components/auto-submit", to: "@stimulus-components--auto-submit.js" # @6.0.0
pin "@stimulus-components/character-counter", to: "@stimulus-components--character-counter.js" # @5.1.0
pin "@stimulus-components/dropdown", to: "@stimulus-components--dropdown.js" # @3.0.0
pin "stimulus-use" # @0.52.3
pin "@stimulus-components/clipboard", to: "@stimulus-components--clipboard.js" # @5.0.0
pin "@stimulus-components/notification", to: "@stimulus-components--notification.js" # @3.0.0
pin "@stimulus-components/timeago", to: "@stimulus-components--timeago.js" # @5.0.2
pin "date-fns" # @4.1.0
pin "@stimulus-components/animated-number", to: "@stimulus-components--animated-number.js" # @5.0.0
pin "@stimulus-components/sortable", to: "@stimulus-components--sortable.js" # @5.0.3
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_request", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_request.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/fetch_response", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--fetch_response.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/lib/utils", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--lib--utils.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/request_interceptor", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--request_interceptor.js" # @0.0.13
pin "https://cdn.jsdelivr.net/npm/@rails/request.js@0.0.13/src/verbs", to: "https:----cdn.jsdelivr.net--npm--@rails--request.js@0.0.13--src--verbs.js" # @0.0.13
pin "@rails/request.js", to: "@rails--request.js.js" # @0.0.13
pin "sortablejs" # @1.15.7

rails/hjerterom/app/config/initializers/assets.rb

# Be sure to restart your server when you modify this file.

# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = "1.0"

# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path

rails/hjerterom/app/config/initializers/content_security_policy.rb

# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header

# Rails.application.configure do
#   config.content_security_policy do |policy|
#     policy.default_src :self, :https
#     policy.font_src    :self, :https, :data
#     policy.img_src     :self, :https, :data
#     policy.object_src  :none
#     policy.script_src  :self, :https
#     policy.style_src   :self, :https
#     # Specify URI for violation reports
#     # policy.report_uri "/csp-violation-report-endpoint"
#   end
#
#   # Generate session nonces for permitted importmap, inline scripts, and inline styles.
#   config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
#   config.content_security_policy_nonce_directives = %w(script-src style-src)
#
#   # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
#   # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
#   # config.content_security_policy_nonce_auto = true
#
#   # Report violations without enforcing the policy.
#   # config.content_security_policy_report_only = true
# end

rails/hjerterom/app/config/initializers/filter_parameter_logging.rb

# Be sure to restart your server when you modify this file.

# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
  :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
]

rails/hjerterom/app/config/initializers/inflections.rb

# Be sure to restart your server when you modify this file.

# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.plural /^(ox)$/i, "\\1en"
#   inflect.singular /^(ox)en/i, "\\1"
#   inflect.irregular "person", "people"
#   inflect.uncountable %w( fish sheep )
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.acronym "RESTful"
# end

rails/hjerterom/app/config/locales/en.yml

# Files in the config/locales directory are used for internationalization and
# are automatically loaded by Rails. If you want to use locales other than
# English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
#     I18n.t "hello"
#
# In views, this is aliased to just `t`:
#
#     <%= t("hello") %>
#
# To use a different locale, set it with `I18n.locale`:
#
#     I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# To learn more about the API, please read the Rails Internationalization guide
# at https://guides.rubyonrails.org/i18n.html.
#
# Be aware that YAML interprets the following case-insensitive strings as
# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
# must be quoted to be interpreted as strings. For example:
#
#     en:
#       "yes": yup
#       enabled: "ON"

en:
  hello: "Hello world"

rails/hjerterom/app/config/puma.rb

threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count
port ENV.fetch("PORT") { 10004 }
environment ENV.fetch("RAILS_ENV") { "production" }
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
plugin :tmp_restart

rails/hjerterom/app/config/routes.rb

Rails.application.routes.draw do
  resource  :session
  resources :passwords, param: :token

  root "home#index"

  resources :resources
  resources :food_listings do
    resources :food_requests, only: %i[create update]
  end

  scope :community do
    get  "/",       to: "community#index", as: :community
    get  "/:id",    to: "community#show",  as: :community_show
    get  "/new",    to: "community#new",   as: :new_community_post
    post "/",       to: "community#create"
    resources :comments, only: %i[create destroy]
  end

  resources :users, only: %i[show]

  get "up", to: "rails/health#show", as: :rails_health_check
end

rails/hjerterom/app/config/storage.yml

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket-<%= Rails.env %>

# Remember not to checkin your GCS keyfile to a repository
# google:
#   service: GCS
#   project: your_project
#   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
#   bucket: your_own_bucket-<%= Rails.env %>

# mirror:
#   service: Mirror
#   primary: local
#   mirrors: [ amazon, google, microsoft ]

rails/hjerterom/app/db/migrate/20260501020807_create_users.rb

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users do |t|
      t.string :email_address, null: false
      t.string :password_digest, null: false

      t.timestamps
    end
    add_index :users, :email_address, unique: true
  end
end

rails/hjerterom/app/db/migrate/20260501020818_create_sessions.rb

class CreateSessions < ActiveRecord::Migration[8.1]
  def change
    create_table :sessions do |t|
      t.references :user, null: false, foreign_key: true
      t.string :ip_address
      t.string :user_agent

      t.timestamps
    end
  end
end

rails/hjerterom/app/db/migrate/20260507120001_create_categories.rb

class CreateCategories < ActiveRecord::Migration[8.1]
  def change
    create_table :categories do |t|
      t.string :name
      t.string :slug
      t.text :description
      t.string :type_of
      t.timestamps
    end
    add_index :categories, :slug, unique: true
  end
end

rails/hjerterom/app/db/migrate/20260507120002_create_resources.rb

class CreateResources < ActiveRecord::Migration[8.1]
  def change
    create_table :resources do |t|
      t.references :user, foreign_key: true
      t.references :category, foreign_key: true
      t.string :title
      t.text :description
      t.string :url
      t.string :address
      t.string :city
      t.string :postal_code
      t.float :latitude
      t.float :longitude
      t.string :phone
      t.string :email
      t.boolean :verified, default: false
      t.string :resource_type
      t.text :opening_hours
      t.timestamps
    end
  end
end

rails/hjerterom/app/db/migrate/20260507120003_create_crises.rb

class CreateCrises < ActiveRecord::Migration[8.1]
  def change
    create_table :crises do |t|
      t.string :title
      t.text :description
      t.string :phone
      t.string :sms
      t.string :chat_url
      t.boolean :available_24h, default: false
      t.string :languages
      t.string :country
      t.timestamps
    end
  end
end

rails/hjerterom/app/db/migrate/20260507120004_create_food_listings.rb

class CreateFoodListings < ActiveRecord::Migration[8.1]
  def change
    create_table :food_listings do |t|
      t.references :user, foreign_key: true
      t.string :title
      t.text :description
      t.integer :quantity
      t.string :unit
      t.datetime :available_from
      t.datetime :available_until
      t.string :pickup_address
      t.string :city
      t.float :latitude
      t.float :longitude
      t.string :status
      t.string :dietary_info
      t.timestamps
    end
  end
end

rails/hjerterom/app/db/migrate/20260507120005_create_food_requests.rb

class CreateFoodRequests < ActiveRecord::Migration[8.1]
  def change
    create_table :food_requests do |t|
      t.references :food_listing, foreign_key: true
      t.references :user, foreign_key: true
      t.text :message
      t.string :status
      t.datetime :pickup_time
      t.timestamps
    end
  end
end

rails/hjerterom/app/db/migrate/20260507120006_create_posts.rb

class CreatePosts < ActiveRecord::Migration[8.1]
  def change
    create_table :posts do |t|
      t.references :user, foreign_key: true
      t.references :category, foreign_key: true
      t.string :title
      t.boolean :anonymous, default: false
      t.boolean :pinned, default: false
      t.integer :views_count, default: 0
      t.timestamps
    end
  end
end

rails/hjerterom/app/db/migrate/20260507120007_create_comments.rb

class CreateComments < ActiveRecord::Migration[8.1]
  def change
    create_table :comments do |t|
      t.references :user, foreign_key: true
      t.references :post, foreign_key: true
      t.integer :parent_id
      t.text :content
      t.boolean :anonymous, default: false
      t.timestamps
    end
  end
end

rails/hjerterom/app/db/migrate/20260507120008_create_support_requests.rb

class CreateSupportRequests < ActiveRecord::Migration[8.1]
  def change
    create_table :support_requests do |t|
      t.references :user, foreign_key: true
      t.string :subject
      t.string :status
      t.string :priority
      t.datetime :resolved_at
      t.timestamps
    end
  end
end

rails/hjerterom/app/db/seeds.rb

admin = User.find_or_create_by!(email_address: "admin@hjerterom.no") do |u|
  u.password = u.password_confirmation = "password123"
end

crisis_lines = [
  { title: "Mental Helse Hjelpelinjen", phone: "116 123", available_24h: true, languages: "Norsk", country: "NO" },
  { title: "Kirkens SOS", phone: "22 40 00 40", available_24h: true, languages: "Norsk", country: "NO" },
  { title: "Røde Kors Besøkstjeneste", phone: "800 33 321", available_24h: false, languages: "Norsk", country: "NO" },
]
crisis_lines.each { |c| Crisis.find_or_create_by!(title: c[:title]) { |cr| cr.update(c) } }

cats = [
  { name: "Angst",     slug: "angst",    type_of: "mental_health" },
  { name: "Depresjon", slug: "depresjon", type_of: "mental_health" },
  { name: "Ensomhet",  slug: "ensomhet",  type_of: "mental_health" },
  { name: "Mat",       slug: "mat",       type_of: "food" },
]
cats.each { |c| Category.find_or_create_by!(slug: c[:slug]) { |cat| cat.update(c) } }

puts "Seeded crisis lines and categories"

rails/hjerterom/app/public/robots.txt

# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file

rails/hjerterom/hjerterom.sh

#!/usr/bin/env zsh
# hjerterom.sh — deploys tracked Rails tree at app/ as %APP_NAME%
set -euo pipefail

APP_NAME=%APP_NAME%
APP_DIR=/home/${APP_NAME}/app
APP_PORT=38891
APP_DOMAIN=
SCRIPT_DIR=${0:a:h}
SRC_DIR=${SCRIPT_DIR}/app

. "${SCRIPT_DIR:h}/@shared_functions.sh"

need_cmd ruby34 bundle doas

[[ -d $SRC_DIR ]] || { log_err "missing source tree: $SRC_DIR"; exit 1 }

log "${APP_NAME} — deploying tracked tree → ${APP_DIR}"

id "$APP_NAME" >/dev/null 2>&1 || doas useradd -m -L daemon -s /bin/ksh "$APP_NAME"
doas mkdir -p "$APP_DIR"

doas cp -R "${SRC_DIR}/." "${APP_DIR}/"
doas chown -R "${APP_NAME}:${APP_NAME}" "$APP_DIR"

cd "$APP_DIR"

typeset bundle_home="/home/${APP_NAME}/.bundle"
if [[ ! -d ${bundle_home}/gems ]]; then
  log "Bootstrapping gems from amber"
  doas mkdir -p "$bundle_home"
  doas cp -R /home/amber/.bundle/gems "$bundle_home/"
  doas chown -R "${APP_NAME}:${APP_NAME}" "$bundle_home"
fi
print "---\nBUNDLE_PATH: \"${bundle_home}/gems\"" | doas tee "${APP_DIR}/.bundle/config" >/dev/null

doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bundle install --deployment --without development:test"
doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:create db:migrate"
[[ -f ${APP_DIR}/db/seeds.rb ]] && doas -u "$APP_NAME" sh -c "cd ${APP_DIR} && RAILS_ENV=production bin/rails db:seed" || true

install_rcd "$APP_NAME" "$APP_DIR" "$APP_PORT" "$APP_NAME"
[[ -n $APP_DOMAIN ]] && relayd_add_relay "$APP_DOMAIN" "$APP_PORT"

doas rcctl restart "$APP_NAME" || doas rcctl start "$APP_NAME"
log_ok "$APP_NAME live on :$APP_PORT"

rails/modernize_zsh.sh

#!/usr/bin/env zsh
setopt err_return no_unset pipe_fail extended_glob warn_create_g
set -euo pipefail

# Gather target files
typeset -a files errors
files=(**/*.sh)

# Default sed patterns (override by exporting sed_patterns)
if (( ${#sed_patterns[@]} == 0 )); then
  sed_patterns=(
    's/\r$//g'               # strip CR
    's/[[:space:]]+$//g'     # trim trailing whitespace
    's/^[[:space:]]+//'      # trim leading whitespace
  )
fi

# Choose correct -i syntax for sed
case $(uname) in
  Darwin) sed_in_place=(-i '') ;;
  *)      sed_in_place=(-i) ;;
esac

for file in $files; do
  [[ $file == */modernize_zsh.sh ]] && continue

  [[ -f $file && -r $file ]] || {
    errors+=("$file: not found or unreadable")
    continue
  }

  # Resolve symlink to real file
  if [[ -L $file ]]; then
    real=$(readlink -f $file 2>/dev/null) || {
      errors+=("$file: cannot resolve symlink")
      continue
    }
    [[ -f $real ]] && file=$real || {
      errors+=("$file: symlink target missing")
      continue
    }
  fi

  # Make a backup if none exists
  if [[ ! -f ${file}.bak ]]; then
    cp --preserve=mode,timestamps "$file" "${file}.bak" || {
      errors+=("$file: backup failed")
      continue
    }
  fi

  # Dry‑run all patterns
  for pat in "${sed_patterns[@]}"; do
    if ! sed -n "${sed_in_place[@]}" -e "$pat" -e 'q' "$file" >/dev/null 2>&1; then
      errors+=("$file: dry‑run failed for $pat")
      continue 2
    fi
  done

  # Apply patterns
  for pat in "${sed_patterns[@]}"; do
    if ! sed "${sed_in_place[@]}" -e "$pat" "$file"; then
      errors+=("$file: transformation failed for $pat")
      continue 2
    fi
  done
done

if (( ${#errors[@]} )); then
  printf "Errors:\n%s\n" "${errors[@]}"
  exit 1
fi

rails/rich_editor_system.sh

#!/usr/bin/env sh
set -euo pipefail

log() {
  printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$(basename "${0##*/}")" "$*" >&2
}

require_file() {
  if [ ! -f "$1" ]; then
    log "Error: required file not found: $1"
    return 1
  fi
}

install_tiptap_packages() {
  require_file package.json || return 1

  if command -v yarn >/dev/null 2>&1; then
    yarn add @tiptap/core @tiptap/starter-kit @tiptap/extension-link
  elif command -v npm >/dev/null 2>&1; then
    npm install --save @tiptap/core @tiptap/starter-kit @tiptap/extension-link
  else
    log "Error: neither yarn nor npm is available"
    return 1
  fi
}

create_tiptap_controller() {
  target="app/javascript/controllers/rich_text_controller.js"
  if [ -e "$target" ]; then
    log "Skipping controller creation; $target already exists"
    return 0
  fi

  mkdir -p "$(dirname "$target")"
  cat >"$target" <<'EOF'
import { Controller } from "@hotwired/stimulus"
import { Editor } from "@tiptap/core"
import StarterKit from "@tiptap/starter-kit"
import Link from "@tiptap/extension-link"

export default class extends Controller {
  static targets = ["input", "editor"]

  connect() {
    this.editor = new Editor({
      element: this.editorTarget,
      extensions: [StarterKit, Link],
      content: this.inputTarget.value || "",
      onUpdate: ({ editor }) => {
        this.inputTarget.value = editor.getHTML()
      }
    })
  }

  disconnect() {
    this.editor && this.editor.destroy()
  }
}
EOF
}

create_editor_styles() {
  target="app/assets/stylesheets/rich_editor.css"
  if [ -e "$target" ]; then
    log "Skipping stylesheet creation; $target already exists"
    return 0
  fi

  mkdir -p "$(dirname "$target")"
  cat >"$target" <<'EOF'
.rich-editor {
  border: 1px solid #d1d5db;
  border-radius: 12px;
  background: #ffffff;
  min-height: 14rem;
  padding: 0.875rem;
}

.rich-editor:focus-within {
  border-color: #2563eb;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
}
EOF
}

add_rich_editor() {
  app_name="${1:-$(basename "$(pwd)")}"
  log "Installing Tiptap rich editor into ${app_name}"
  install_tiptap_packages
  create_tiptap_controller
  create_editor_styles
  log "Rich editor scaffolding completed for ${app_name}"
}

# Execute only when run directly, not when sourced
case "$0" in
  *sh) add_rich_editor "${1:-}" ;;
esac

repligen.rb

# frozen_string_literal: true

module Bridges
  module Repligen
    MODEL_CATALOG = [
      {
        key: "repligen/krosflo-kr2i",
        name: "KrosFlo KR2i Tangential Flow Filtration System",
        manufacturer: "Repligen",
        category: "tff_system",
        tags: %w[tff filtration].freeze,
        url: "https://www.repligen.com/products/krosflo-kr2i"
      }.freeze,
      {
        key: "repligen/krosflo-kr2s",
        name: "KrosFlo KR2s Tangential Flow Filtration System",
        manufacturer: "Repligen",
        category: "tff_system",
        tags: %w[tff filtration].freeze,
        url: "https://www.repligen.com/products/krosflo-kr2s"
      }.freeze,
      {
        key: "repligen/xcell-atf",
        name: "XCell ATF System",
        manufacturer: "Repligen",
        category: "cell_retention",
        tags: %w[atf perfusion].freeze,
        url: "https://www.repligen.com/products/xcell-atf"
      }.freeze
    ].freeze

    def self.model_catalog = MODEL_CATALOG
  end
end

files: 790 / lines: 21095 / truncated: 5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment