Fix SolidQueue Worker, Add Welcome Mail, Tests Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Reaktiviere die SolidQueue-Job-Verarbeitung in Production/Preview, füge eine Welcome-Mail nach Email-Verifizierung hinzu, recovere die hängenden Mail-Jobs, und sichere alles mit Tests ab, die das Problem reproduzieren würden, wenn der Worker wieder ausfällt.
Architecture: Puma lädt das solid_queue-Plugin → der Job-Worker läuft im selben Container-Prozess wie der Webserver (Rails-8-Standard für kleine bis mittlere Setups, vermeidet zusätzlichen Container). Welcome-Mail wird im EmailVerificationsController#verify getriggert, nachdem verify_email! erfolgreich war. Tests bestehen aus drei Schichten: (1) Mailer-Spec (Inhalt/Headers), (2) Request-Spec mit perform_enqueued_jobs (Trigger-Logik), (3) Post-Deploy Smoketest-Spec, die gegen das laufende System prüft, dass SolidQueue::Process-Heartbeats existieren.
Tech Stack: Rails 8.1, SolidQueue, Puma 6, RSpec, FactoryBot, ActiveJob TestHelper
File Structure
Modify:
web/config/puma.rb— Plugin:solid_queueaktivierenweb/config/environments/production.rb— wirkungslosesolid_queue.supervisor-Zeile entfernenweb/config/environments/preview.rb— ditoweb/app/mailers/user_mailer.rb— neuewelcome-Methodeweb/app/controllers/email_verifications_controller.rb— Welcome-Mail nach Verifizierung enqueuenweb/spec/rails_helper.rb—ActiveJob::TestHelpereinbinden
Create:
web/app/views/user_mailer/welcome.html.erbweb/app/views/user_mailer/welcome.text.erbweb/spec/mailers/user_mailer_spec.rbweb/spec/requests/email_verifications_spec.rbweb/spec/system/solid_queue_worker_spec.rb— bootet ein Puma in-process und prüft, dass Plugin geladen wirdweb/lib/tasks/mail_recovery.rake— One-Shot zum Aufräumen der hängenden Jobs + frische Codes für nicht-verifizierte User
Production-Hygiene (separates Skript, kein Code-File):
- Rails Console Snippet zum Verifizieren der Worker-Heartbeats nach Deployment
Task 1: Puma-Plugin für SolidQueue aktivieren
Files:
Modify:
web/config/puma.rbModify:
web/config/environments/production.rb:56-59Modify:
web/config/environments/preview.rb:46-48[ ] Step 1: Prüfen, dass
solid_queueGem im Gemfile ist
Run:
cd web && grep "solid_queue" Gemfile Gemfile.lock | head -5Expected: mindestens eine Zeile mit solid_queue (sonst muss erst bundle add solid_queue laufen — bei Rails 8 ist das aber Default).
- [ ] Step 2: Plugin in
web/config/puma.rbeinfügen
Existing content:
plugin :tmp_restartReplace with:
plugin :tmp_restart
# Run SolidQueue worker in the same process as Puma (Rails-8 standard for
# single-container deployments). Without this plugin, jobs enqueued via
# deliver_later / perform_later sit in solid_queue_ready_executions forever
# and no mail ever leaves the system.
plugin :solid_queue if %w[production preview].include?(ENV.fetch("RAILS_ENV", "development"))Rationale für die if-Condition: In Tests soll Puma den Worker nicht starten, weil RSpec den ActiveJob-Test-Adapter nutzt. In Development läuft das Bin-Script bin/dev mit foreman, das den Worker bei Bedarf separat hochzieht.
- [ ] Step 3: Wirkungslose Zeile aus production.rb entfernen
In web/config/environments/production.rb:56-59:
Replace:
# Jobs
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }
config.solid_queue.supervisor = true # Run worker in same processWith:
# Jobs — worker runs via the `solid_queue` Puma plugin (see config/puma.rb).
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }- [ ] Step 4: Dasselbe für preview.rb
In web/config/environments/preview.rb:46-48:
Replace:
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }
config.solid_queue.supervisor = true # Run worker in same processWith:
# Jobs — worker runs via the `solid_queue` Puma plugin (see config/puma.rb).
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }- [ ] Step 5: Bestätigen, dass Puma das Plugin lokal lädt
Run:
cd web && RAILS_ENV=production bundle exec puma -C config/puma.rb --dry-run 2>&1 | grep -iE "(plugin|solid_queue)" || echo "puma --dry-run hat keine plugin-Ausgabe"Falls --dry-run keine Plugin-Info druckt: stattdessen kurz mit RAILS_ENV=preview bundle exec puma -C config/puma.rb -p 4567 starten, in den Logs SolidQueue suchen, Strg-C. Expected: Log-Zeile wie SolidQueue-X.Y.Z started supervisor.
- [ ] Step 6: Commit
cd web
git add config/puma.rb config/environments/production.rb config/environments/preview.rb
git commit -m "fix: load solid_queue plugin in puma for production/preview
Without the plugin no worker process exists in the web container, so all
deliver_later / perform_later jobs accumulate in solid_queue_ready_executions
and no mail leaves the system. config.solid_queue.supervisor = true is not a
real Rails 8 option — removed.
Refs: 4 mail jobs pending since 2026-04-30 in prod."Task 2: WelcomeMailer-Methode und Views
Files:
Modify:
web/app/mailers/user_mailer.rbCreate:
web/app/views/user_mailer/welcome.html.erbCreate:
web/app/views/user_mailer/welcome.text.erbCreate:
web/spec/mailers/user_mailer_spec.rb[ ] Step 1: Failing Mailer-Spec schreiben
Create web/spec/mailers/user_mailer_spec.rb:
# frozen_string_literal: true
require "rails_helper"
RSpec.describe UserMailer, type: :mailer do
describe "welcome" do
let(:user) { create(:user, email: "new@example.com") }
let(:mail) { described_class.welcome(user) }
it "addresses the verified user" do
expect(mail.to).to eq(["new@example.com"])
expect(mail.from).to eq(["noreply@thelawin.dev"])
expect(mail.reply_to).to eq(["hello@thelawin.dev"])
end
it "uses a welcoming subject" do
expect(mail.subject).to match(/welcome/i)
expect(mail.subject).to include("thelawin.dev")
end
it "links to the dashboard and the docs in the HTML body" do
body = mail.body.parts.find { |p| p.content_type.start_with?("text/html") }.body.encoded
expect(body).to include("dashboard")
expect(body).to match(/docs\.thelawin\.dev|\/docs/)
end
it "renders a plain-text alternative" do
text = mail.body.parts.find { |p| p.content_type.start_with?("text/plain") }.body.encoded
expect(text).to include("thelawin.dev")
expect(text).to include(user.email.split("@").first)
end
it "mentions the first API call as the next step" do
body = mail.body.encoded
expect(body).to match(/api[ -]?key/i)
expect(body).to match(/curl|POST|\/v1\/generate/i)
end
end
describe "verification_code (regression)" do
let(:user) { create(:user, email: "verify@example.com") }
let(:mail) { described_class.verification_code(user, "123456") }
it "embeds the 6-digit code in the subject" do
expect(mail.subject).to include("123456")
expect(mail.to).to eq(["verify@example.com"])
end
end
end- [ ] Step 2: Run spec, confirm it fails
Run:
cd web && bundle exec rspec spec/mailers/user_mailer_spec.rbExpected: 5 Failures (alle welcome-Tests scheitern wegen NoMethodError: undefined method 'welcome'). Verification-Code-Test sollte schon grün sein.
- [ ] Step 3:
welcome-Methode im UserMailer ergänzen
In web/app/mailers/user_mailer.rb, vor dem end der Klasse, nach verification_code (line 37):
def welcome(user)
@user = user
@first_name = user.email.split("@").first
@dashboard_url = dashboard_url
@api_keys_url = api_keys_url
@docs_url = ENV.fetch("DOCS_BASE_URL", "https://docs.thelawin.dev")
mail(to: @user.email, subject: "Welcome to thelawin.dev — your first API call awaits")
end- [ ] Step 4: HTML-View erstellen
Create web/app/views/user_mailer/welcome.html.erb:
<!DOCTYPE html>
<html>
<body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 560px; margin: 0 auto; padding: 24px; color: #1f2937;">
<h1 style="font-size: 22px; margin: 0 0 16px;">Hey <%= @first_name %>,</h1>
<p>Your email is verified — your thelawin.dev account is live. Here is the 60-second tour:</p>
<ol style="line-height: 1.7;">
<li>Grab an API key from <a href="<%= @api_keys_url %>" style="color: #f59e0b;">your dashboard</a>. Sandbox keys are free; live keys need a plan.</li>
<li>Send your first invoice JSON to <code style="background: #f3f4f6; padding: 2px 6px; border-radius: 4px;">POST /v1/generate</code>. The response contains a ZUGFeRD/Factur-X PDF as Base64.</li>
<li>Read the format reference at <a href="<%= @docs_url %>" style="color: #f59e0b;"><%= @docs_url %></a> if you need XRechnung, Peppol, UBL, or FatturaPA.</li>
</ol>
<p style="margin-top: 24px;">
<a href="<%= @dashboard_url %>" style="display: inline-block; background: #f59e0b; color: #fff; padding: 10px 20px; border-radius: 6px; text-decoration: none;">Open dashboard</a>
</p>
<p style="margin-top: 32px; font-size: 14px; color: #6b7280;">
Stuck? Reply to this mail — it goes to a human.<br>
— Stephan
</p>
</body>
</html>- [ ] Step 5: Plain-Text-View erstellen
Create web/app/views/user_mailer/welcome.text.erb:
Hey <%= @first_name %>,
Your email is verified — your thelawin.dev account is live. Here is the
60-second tour:
1. Grab an API key in your dashboard: <%= @api_keys_url %>
Sandbox keys are free; live keys need a plan.
2. Send your first invoice JSON to POST /v1/generate. The response
contains a ZUGFeRD/Factur-X PDF as Base64.
Example:
curl -X POST https://api.thelawin.dev/v1/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d @invoice.json
3. Need XRechnung, Peppol, UBL, or FatturaPA? See the format reference:
<%= @docs_url %>
Open the dashboard: <%= @dashboard_url %>
Stuck? Reply to this mail — it goes to a human.
— Stephan- [ ] Step 6: Spec erneut laufen lassen
Run:
cd web && bundle exec rspec spec/mailers/user_mailer_spec.rbExpected: alle 6 Examples grün.
- [ ] Step 7: User-sichtbaren Text durch humanizer-Skill schicken
Nutzer-sichtbare Texte (Welcome-Mail HTML + Text + Subject) sind per User-Feedback feedback_ux_copy_humanizer.md durch das humanizer-Skill zu schicken. Skill aufrufen mit Argument: Pfade app/views/user_mailer/welcome.html.erb, app/views/user_mailer/welcome.text.erb, und das Subject aus user_mailer.rb. Vorgeschlagene Änderungen prüfen, anwenden, dann Spec erneut laufen.
Run:
cd web && bundle exec rspec spec/mailers/user_mailer_spec.rbExpected: weiterhin grün.
- [ ] Step 8: Commit
cd web
git add app/mailers/user_mailer.rb app/views/user_mailer/welcome.html.erb app/views/user_mailer/welcome.text.erb spec/mailers/user_mailer_spec.rb
git commit -m "feat(mailer): add welcome mail with first-API-call onboarding"Task 3: Welcome-Mail nach Verifizierung triggern
Files:
Modify:
web/app/controllers/email_verifications_controller.rb:11-18Create:
web/spec/requests/email_verifications_spec.rb[ ] Step 1: Failing Request-Spec schreiben
Create web/spec/requests/email_verifications_spec.rb:
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "Email verification flow", type: :request do
include ActiveJob::TestHelper
let(:user) { create(:user, email_verified_at: nil) }
let(:code) { user.generate_verification_code! }
before { sign_in user }
describe "POST /users/verify_email" do
context "with a valid code" do
it "verifies the user and enqueues a welcome mail" do
code # generate
expect {
post verify_email_path, params: { code: code }
}.to change { ActionMailer::Base.deliveries.size }.by(0) # deliver_later only enqueues
expect(response).to redirect_to(dashboard_path)
expect(user.reload).to be_email_verified
# The welcome job is the only one that should have been enqueued by /verify_email.
welcome_jobs = ActiveJob::Base.queue_adapter.enqueued_jobs.select do |j|
j[:args].first == "UserMailer" && j[:args].second == "welcome"
end
expect(welcome_jobs.size).to eq(1)
end
it "actually delivers the welcome mail when the queue runs" do
code
perform_enqueued_jobs do
post verify_email_path, params: { code: code }
end
sent = ActionMailer::Base.deliveries.map(&:subject)
expect(sent).to include(a_string_matching(/welcome/i))
end
end
context "with an invalid code" do
it "does not enqueue a welcome mail" do
code # generate the real one
expect {
post verify_email_path, params: { code: "000000" }
}.not_to(change { ActiveJob::Base.queue_adapter.enqueued_jobs.size })
expect(user.reload).not_to be_email_verified
end
end
context "with an expired code" do
it "does not verify and does not send a welcome mail" do
code
travel_to(15.minutes.from_now) do
expect {
post verify_email_path, params: { code: code }
}.not_to(change { ActiveJob::Base.queue_adapter.enqueued_jobs.size })
end
expect(user.reload).not_to be_email_verified
end
end
end
end- [ ] Step 2: Sicherstellen, dass der test-Adapter Queue-Inspection erlaubt
Open web/spec/rails_helper.rb and check that ActiveJob::TestHelper is included for request specs. If not present, add inside RSpec.configure do |config| block (around line 50, near other includes):
config.include ActiveJob::TestHelper, type: :request
config.include ActiveJob::TestHelper, type: :mailerAlso verify queue adapter for tests. In web/config/environments/test.rb (read it first; it should already say config.active_job.queue_adapter = :test). If it doesn't, add:
config.active_job.queue_adapter = :test- [ ] Step 3: Spec laufen lassen, Failure beobachten
Run:
cd web && bundle exec rspec spec/requests/email_verifications_spec.rbExpected: Mindestens das "enqueues a welcome mail"-Example failed mit expected size 1, got 0 (Controller triggert noch nichts).
- [ ] Step 4: Controller erweitern
In web/app/controllers/email_verifications_controller.rb, replace the verify method (lines 11-18):
def verify
if current_user.verify_email!(params[:code].to_s.strip)
UserMailer.welcome(current_user).deliver_later
redirect_to dashboard_path, notice: I18n.t("verification.success", default: "Email verified.")
else
flash.now[:alert] = I18n.t("verification.invalid_code", default: "Invalid or expired code. Please try again.")
render :show, status: :unprocessable_entity
end
end- [ ] Step 5: Spec erneut laufen lassen
Run:
cd web && bundle exec rspec spec/requests/email_verifications_spec.rbExpected: alle 4 Examples grün.
- [ ] Step 6: Commit
cd web
git add app/controllers/email_verifications_controller.rb spec/requests/email_verifications_spec.rb spec/rails_helper.rb config/environments/test.rb
git commit -m "feat(verification): send welcome mail after successful email verification"Task 4: E2E-Test: Puma-Plugin lädt SolidQueue
Files:
- Create:
web/spec/system/solid_queue_worker_spec.rb
Hintergrund: Reine Unit-Tests können nicht beweisen, dass das Puma-Plugin tatsächlich geladen wird, weil das erst beim Puma-Boot passiert. Wir booten in einem System-Spec einen echten Puma-Prozess in einem Subshell mit RAILS_ENV=preview, lesen die Logs, und prüfen auf die SolidQueue Boot-Zeile sowie auf einen SolidQueue::Process-Heartbeat-Eintrag.
- [ ] Step 1: Spec schreiben
Create web/spec/system/solid_queue_worker_spec.rb:
# frozen_string_literal: true
require "rails_helper"
require "socket"
require "timeout"
# This spec verifies that the puma config actually loads the solid_queue plugin
# in environments where we expect it to. We boot a real puma process pointed at
# the test database and watch its logs for the SolidQueue supervisor banner and
# a process heartbeat row in solid_queue_processes.
#
# It is deliberately tagged :slow so it can be excluded with `--tag '~slow'`
# during fast TDD loops.
RSpec.describe "SolidQueue Puma plugin", :slow, type: :system do
def free_port
server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]
server.close
port
end
it "starts a SolidQueue supervisor inside the Puma process when RAILS_ENV=preview" do
port = free_port
log = Tempfile.new(["puma", ".log"])
pid = nil
Dir.chdir(Rails.root) do
pid = Process.spawn(
{ "RAILS_ENV" => "preview", "PORT" => port.to_s, "DATABASE_URL" => ENV["DATABASE_URL"].to_s },
"bundle", "exec", "puma", "-C", "config/puma.rb",
out: log.path, err: log.path
)
end
# Wait up to 20s for the SolidQueue banner.
deadline = Time.now + 20
booted = false
while Time.now < deadline
output = File.read(log.path)
if output.match?(/SolidQueue.*started/i) || output.match?(/solid_queue.*supervisor/i)
booted = true
break
end
sleep 0.5
end
expect(booted).to eq(true), -> { "Puma never logged a SolidQueue boot line. Log:\n#{File.read(log.path)}" }
# Cross-check: SolidQueue should also register a heartbeat in the DB.
deadline = Time.now + 10
has_process = false
while Time.now < deadline
has_process = SolidQueue::Process.where("last_heartbeat_at > ?", 30.seconds.ago).exists?
break if has_process
sleep 0.5
end
expect(has_process).to eq(true)
ensure
if pid
Process.kill("TERM", pid) rescue nil
Process.wait(pid) rescue nil
end
log&.close
log&.unlink
end
end- [ ] Step 2: Sicherstellen, dass die preview-Konfig in Test-DB läuft
RAILS_ENV=preview braucht eine Datenbank. Check web/config/database.yml — der preview:-Block sollte eine url: <%= ENV["DATABASE_URL"] %>-Form haben. Wenn nicht, im Spec ein Stub-Setup verwenden: vor Process.spawn einfach ENV["DATABASE_URL"] ||= "postgresql://localhost/thelawin_test" setzen und auf db:test:prepare vertrauen.
Run:
cd web && bundle exec rspec spec/system/solid_queue_worker_spec.rbExpected: grün. Falls flaky wegen DB-Setup: das Spec mit :integration oder :slow taggen und in CI nur nightly laufen lassen — der eigentliche Schutz liegt bereits im Smoketest (Task 6).
- [ ] Step 3: Commit
cd web
git add spec/system/solid_queue_worker_spec.rb
git commit -m "test: verify puma actually boots the solid_queue supervisor"Task 5: One-Shot-Rake-Task für Mail-Recovery
Files:
- Create:
web/lib/tasks/mail_recovery.rake
Hintergrund: Production hat 4 hängende Jobs in solid_queue_ready_executions. Wenn der Worker wieder läuft, würden die alten Verification-Codes (Elena 30.04., Patrick 15.05.) ausgeführt — aber die Codes sind abgelaufen (10-Min-TTL), also nutzlos. Bessere Strategie: hängende Jobs droppen, frische Codes für die zwei nicht-verifizierten User generieren, und für die bereits verifizierten User (steviee77, deltacity) keine Welcome-Mail nachreichen (sie haben den Account schon seit Wochen aktiv).
- [ ] Step 1: Rake-Task schreiben
Create web/lib/tasks/mail_recovery.rake:
# frozen_string_literal: true
namespace :mail_recovery do
desc "Drop stuck verification-mail jobs from solid_queue and re-issue fresh codes to unverified users"
task purge_and_reissue: :environment do
pending = SolidQueue::ReadyExecution.joins(:job).where("solid_queue_jobs.class_name LIKE ?", "%MailDeliveryJob%")
puts "Pending mail-delivery executions: #{pending.count}"
pending.find_each do |exec|
job = exec.job
puts " - dropping job ##{job.id} created_at=#{job.created_at} args=#{job.arguments.to_s[0..120]}"
exec.destroy
job.destroy
end
unverified = User.where(email_verified_at: nil)
puts "Unverified users: #{unverified.count}"
unverified.find_each do |user|
code = user.generate_verification_code!
UserMailer.verification_code(user, code).deliver_later
puts " - reissued code for #{user.email}"
end
puts "Done. Run `SolidQueue::Process.where(\"last_heartbeat_at > ?\", 1.minute.ago).count` to confirm worker is alive."
end
end- [ ] Step 2: Task lokal testen (auf einer Test-DB)
Run:
cd web && RAILS_ENV=test bin/rails db:test:prepare
cd web && RAILS_ENV=test bin/rails mail_recovery:purge_and_reissueExpected: kein Crash, Ausgabe Pending mail-delivery executions: 0 und Unverified users: 0 (Test-DB ist leer).
- [ ] Step 3: Commit
cd web
git add lib/tasks/mail_recovery.rake
git commit -m "chore(mail): rake task to purge stuck mail jobs and reissue verification codes"Task 6: Post-Deploy Smoketest erweitern
Files:
Read: durchsuche das Repo nach existierender Smoketest-Logik
[ ] Step 1: Bestehenden Smoketest finden
Run:
grep -rn "smoke\|curl.*health\|status.*200" /Users/stephan/dev/thelawin.dev --include="*.sh" --include="*.rb" --include="*.yml" 2>/dev/null | grep -vE "(node_modules|tmp|\.git)" | head -10Erwartet: vermutlich ein Bash-Skript oder ein RSpec-File, das nach Preview/Prod-Deploy gegen die Endpoints curlt (gemäß feedback_post_deploy_smoketest.md).
- [ ] Step 2: Smoketest um Worker-Check erweitern
Wenn Smoketest in Bash existiert, einen zusätzlichen Schritt anfügen, der via SSH den Worker-Heartbeat prüft:
ssh debian@51.83.102.39 "docker exec prod-web-1 bin/rails runner '
age = (Time.current - SolidQueue::Process.maximum(:last_heartbeat_at).to_time) rescue 9999
if age > 60
puts \"WORKER STALE (heartbeat #{age.to_i}s old)\"
exit 1
else
puts \"worker ok, heartbeat ${age.to_i}s old\"
end
'"Wenn der Smoketest in Ruby/RSpec ist, ein zusätzliches Example hinzufügen, das via gh api oder direkt via Rails-Health-Endpoint prüft. Falls kein Health-Endpoint dafür existiert (vermutlich): einen /up/queue Health-Endpoint im Web-App ergänzen, der SolidQueue::Process.where("last_heartbeat_at > ?", 60.seconds.ago).count > 0 zurückgibt.
Pragmatisch: Erstmal das SSH-Bash-Check anhängen, weil das ohne neue Public-Endpoint funktioniert. Wenn der Smoketest schon ein Ruby-Skript ist, das Net::HTTP benutzt, dann ist eher ein neuer Health-Endpoint sauber. Entscheidung an dem Punkt nach Lesen des Files treffen.
- [ ] Step 3: Lokal manuell verifizieren
Run das SSH-Snippet oben gegen Preview (nach Preview-Deployment). Expected: worker ok, heartbeat <60s old.
- [ ] Step 4: Commit
cd web # oder Repo-Root, je nachdem wo der Smoketest liegt
git add <smoketest-files>
git commit -m "chore(ops): add solid_queue worker heartbeat check to post-deploy smoketest"Task 7: Deployment & Production-Recovery
- [ ] Step 1: Vor dem Push DEPLOYMENT.md konsultieren
Per User-Memory feedback_docs_maintenance.md. Falls Änderungen an Docker-Compose oder VPS-Pfaden nötig sind: dort prüfen und ggf. updaten.
Run:
cd /Users/stephan/dev/thelawin.dev && grep -iE "(solid_queue|worker|jobs)" DEPLOYMENT.md CLAUDE.mdWenn nichts gefunden wird, in CLAUDE.md einen kurzen Absatz unter "Wichtige Befehle" hinzufügen:
### SolidQueue Worker
Der Worker läuft als Puma-Plugin im `web`-Container. Status-Check:
\`\`\`bash
docker exec prod-web-1 bin/rails runner 'puts SolidQueue::Process.pluck(:kind, :last_heartbeat_at)'
\`\`\`- [ ] Step 2: Auf main pushen → Preview deployt automatisch
git push origin mainWatchtower aktualisiert den Preview-Stack innerhalb von 1-2 Minuten.
- [ ] Step 3: Preview-Smoketest
Per feedback_post_deploy_smoketest.md:
curl -sI https://preview.thelawin.dev | head -1
curl -sI https://api.preview.thelawin.dev/health | head -1
curl -sI https://docs.preview.thelawin.dev | head -1Plus Worker-Check:
ssh debian@51.83.102.39 "docker exec preview-web-1 bin/rails runner 'p SolidQueue::Process.pluck(:kind, :last_heartbeat_at)'"Expected: last_heartbeat_at < 60 Sekunden alt.
- [ ] Step 4: Mail-Recovery auf Preview testen
ssh debian@51.83.102.39 "docker exec preview-web-1 bin/rails mail_recovery:purge_and_reissue"Auf Preview ist die Anzahl Pending-Jobs vermutlich anders als auf Prod — das ist OK. Wichtig: kein Crash, Worker konsumiert die neu enqueuten Jobs sofort (heartbeat-DB-Eintrag bestätigt, dass Worker läuft).
- [ ] Step 5: Merge auf production
git checkout production
git merge --no-ff main
git push origin production
git checkout mainWatchtower zieht :latest Images, Prod-Stack aktualisiert.
- [ ] Step 6: Prod-Smoketest
curl -sI https://thelawin.dev | head -1
curl -sI https://api.thelawin.dev/health | head -1
curl -sI https://docs.thelawin.dev | head -1
ssh debian@51.83.102.39 "docker exec prod-web-1 bin/rails runner 'p SolidQueue::Process.pluck(:kind, :last_heartbeat_at)'"- [ ] Step 7: Production Mail-Recovery ausführen
ssh debian@51.83.102.39 "docker exec prod-web-1 bin/rails mail_recovery:purge_and_reissue"Expected-Output (laut prior Investigation):
Pending mail-delivery executions: 4(die Pre-Worker-Jobs, plus eventuell die Mattermost-Notification-Jobs)Unverified users: 2— Elena (elena@ultra-lab.net), Patrick (patrick.koelle@kspmaschinen.de)Beide kriegen frischen 6-Digit-Code und Welcome wird durch sie selbst nach Verifikation getriggert
[ ] Step 8: Bestätigung sammeln
ssh debian@51.83.102.39 "docker exec prod-web-1 bin/rails runner '
puts SolidQueue::Job.where(finished_at: nil).count.to_s + \" jobs pending\"
puts SolidQueue::Process.pluck(:kind, :last_heartbeat_at).inspect
puts User.where(email_verified_at: nil).pluck(:email, :email_verification_sent_at).inspect
'"Expected: 0 oder 1 pending (die frischen verification-mails werden in unter 30s konsumiert), heartbeat aktuell, beide unverified User haben email_verification_sent_at von gerade eben.
- [ ] Step 9: HISTORY.md updaten
Per CLAUDE.md-Konvention:
## 2026-05-19
- fix: solid_queue plugin in puma config — worker war seit Wochen tot, alle deliver_later/perform_later Jobs verpufft
- feat: welcome mail nach erfolgreicher Email-Verifizierung
- ops: mail_recovery rake task; reissue verification codes für Elena + Patrick (Sign-ups vom 30.04. und 15.05. die nie eine Mail erhalten hatten)
- test: mailer + request + system specs für Welcome-FlowRun:
cd /Users/stephan/dev/thelawin.dev
git add HISTORY.md
git commit -m "docs: history entry for mail worker fix + welcome mail"
git push origin mainSelf-Review
Spec coverage:
- ✅ Worker reparieren → Task 1
- ✅ Welcome-Mail bauen → Task 2 + 3
- ✅ E2E-Tests die das prüfen → Task 4 (Puma-Plugin lädt SolidQueue), Task 3 (Welcome-Trigger), Task 6 (Post-Deploy)
- ✅ Recovery der hängenden Mails → Task 5 + Task 7 Step 7
Placeholder scan: keine TBDs, alle Code-Snippets vollständig, kein "ähnlich wie Task N".
Type consistency: UserMailer.welcome(user) durchgängig; SolidQueue::Process.last_heartbeat_at durchgängig.
Mögliche Lücken:
- Mattermost-Notification-Jobs:
track_signupcallback (User-Model line 234) enqueuedMattermostNotificationJob.perform_later— d.h. auch Mattermost-Signups wurden nie versendet. Der Rake-Task in Task 5 zielt nur aufMailDeliveryJob. Workaround: nach Worker-Boot werden auch die hängenden Mattermost-Jobs automatisch konsumiert. Falls die als Sanity-Test nervig sind: zusätzlich in Task 5 dieMattermostNotificationJob-Entries droppen, da die Notifications mehrere Wochen alt sind. - Development-Mode: Das Puma-Plugin wird in Dev nicht geladen (mit Absicht —
bin/dev/foreman handhabt das). Falls Dev-Tests scheitern, weil Mails enqueued aber nie versendet werden: stattdessenconfig.active_job.queue_adapter = :inlineinconfig/environments/development.rbsetzen (separate Entscheidung, nicht in diesem Plan).
Plan complete and saved to docs/superpowers/plans/2026-05-19-fix-mail-worker-and-welcome.md.
Two execution options:
- Subagent-Driven (recommended) — fresh subagent per task, review between tasks, fast iteration
- Inline Execution — execute tasks in this session using executing-plans, batch with checkpoints
Which approach?