Діагностика + виправлення даних прод-Rails: kamal exec, повноширинний `?`, Base64, неперервність треду.
Продакшн-400/500 зовсім не те, що локальні. Локально перезапускаєш тести, правиш, знову запускаєш. У проді користувач уже натиснув «Сплатити» і дивиться на білий екран.
Три шляхи:
rails console, зміни без аудиту, одна помилка — і ти в бідіЦя стаття про третій шлях. Два реальні випадки: повноширинний ?, що заліз у credentials і поклав x402-платежі 500-ми, і твіт посередині треду зі status: :failed, який треба повторити, зберігши цілісність треду. Дорогою Claude намагатиметься звернути в 4 неправильні боки за замовчуванням — кожен треба перехопити.
kamal app exec --reuseKamal — інструмент деплою Rails від 37signals. Має команду:
kamal app exec --reuse 'bin/rails runner "..."'
--reuse означає: не піднімати новий контейнер, а виконати всередині вже працюючого web-контейнера. Жодного нового build, жодного docker pull, жодного рестарту, жодної повторної ін'єкції ENV. Команда виконується, контейнер повертається до обробки запитів.
Вивід тече через stdout у твій термінал — фактично puts у продакшн-Rails-консолі, без SSH, tmux або виходу з ноута.
Типова сесія:
$ kamal app exec --reuse 'bin/rails runner "puts User.count"'
Launching command with version abc123 from existing container...
INFO [ok] Finished in 3.8 seconds with exit status 0
App Host: deploy.how2claude.com
12847
Туди-назад 3-6 секунд. На два порядки швидше передеплою.
?, що спричинив прод-500Commit eba9ac9.
Симптом: Через кілька годин після запуску x402-платежів кожен платіжний запит валився у 500. Логи повні Net::HTTPBadResponse від HTTP-клієнта. Локально — ідеально.
Діагностика: Спершу попроси Claude надрукувати x402-конфіг у проді:
kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'
Вивід:
"0xAbC123...def?"
На хвості зайвий ? — повноширинний знак запитання (U+FF1F), а не напівширинний ?. У когось під час правки config/credentials/production.yml.enc перемкнувся IME, і цей символ проліз.
Локальний config/credentials.yml.enc (розшифрований dev/test master.key) цього не містить — у Rails 8 production і dev — окремі encrypted credentials, уміст не спільний.
Фікс: Не можна SSH і редагувати файл напряму (зашифрований), не можна стягнути й редагувати локально (master.key не на ноуті). Правильний хід — дати Claude написати одноразовий Ruby-скрипт і заінжектити через EDITOR= у credentials:edit:
# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# Прибрати повноширинний знак запитання з хвоста
content.gsub!(/(wallet_address: 0x[0-9a-fA-F]+)?\s*$/, '\1')
File.write(ARGV[0], content)
EDITOR="ruby script/fix_prod_wallet.rb" \
bin/rails credentials:edit --environment production
Потік credentials:edit: розшифрувати → записати у тимчасовий файл → викликати $EDITOR → перезашифрувати → видалити тимчасовий. Заміна EDITOR на наш Ruby-скрипт — і редагування автоматизовано, не треба вдивлятися в шифртекст локально.
Після того git commit + kamal deploy один раз — цей деплой обов'язковий, бо production.yml.enc змінився. Але діагностика деплою не коштувала.
Правило: Коли прод ламається, спершу читай через kamal app exec --reuse. Не вгадуй, не передеплой першим.
Симптом: Після публікації статті твіти йдуть у таблицю x_queue_tweets. Один із них — 2-й із 4-твітового треду — застрягає зі status: :failed (рейт-ліміт X API, валідація контенту, що завгодно). Повтор потребує збереження цілісності треду з 1-м твітом.
Знайти неуспішний:
kamal app exec --reuse 'bin/rails runner "
XQueue::Tweet.where(status: :failed).each do |t|
puts \"#{t.id}: thread=#{t.thread_id} pos=#{t.thread_position} content=#{t.content.inspect}\"
end
"'
Виявляється id=87, thread_id=15, thread_position=2.
Пастка 1: shell-екранування. У вмісті твітів часто лапки, переноси рядків, backtick. Якщо написати:
# вибухне — shell з'їсть лапки та зворотні слеші
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'
Танець Base64 — кодуй локально, передавай base64-рядок, декодуй усередині runner:
# Кодуємо локально
echo -n 'Переписаний вміст твіту...' | base64
# => 0J/QtdGA0LXQv9C40YHQsNC90LjQuSDQstC80ZbRgdGCINGC0LLRltGC0YMuLi4=
# Передаємо
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
t.update!(content: Base64.decode64('0J/QtdGA0LXQv9C40YHQsNC90LjQuSDQstC80ZbRgdGCINGC0LLRltGC0YMuLi4='), status: :scheduled)
puts t.status
\""
Base64-рядки — чистий ASCII, для shell безпечні.
Пастка 2: цілісність треду. XQueue::PostTweetJob.perform_later(87) публікує твіт сам по собі — не чіпляється до твіту #1 — бо X API потрібен reply_to_tweet_id, а Job за замовчуванням такого значення не несе.
Знайти x_tweet_id попереднього твіту (успішні відправки заповнюють це поле):
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
prev = XQueue::Tweet.where(thread_id: t.thread_id, thread_position: t.thread_position - 1).first
puts 'prev x_tweet_id: ' + prev&.x_tweet_id.to_s
\""
# => prev x_tweet_id: 1834567890123456789
Поставити в чергу з reply-ціллю:
kamal app exec --reuse 'bin/rails runner "
XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
puts \"enqueued\"
"'
polling_interval у воркера — 0,1 секунди — він підхоплює job майже одразу. Через пару секунд kamal app exec дивиться, що status перейшов із scheduled у posted, а x_tweet_id заповнився — тред неперервний.
Правило: Операції з даними у проді мають поважати обмеження бізнес-рівня, а не лише «запис оновлено успішно». Цілісність треду — бізнес-обмеження; Rails runner її за тебе не перевіряє.
При такій роботі перший рефлекс Claude часто хибний. Ловимо кожен:
«Я зайду по SSH і подивлюся...»
Перенаправ: kamal app exec --reuse кращий за SSH — усередині контейнера, оточення Rails завантажене, аудіюється (kamal-логи зберігають запис), не чіпає shell хоста, немає контейнерного дрейфу (reuse гарантує поточну прод-версію).
«Напишу migration, щоб прибрати повноширинний
?з wallet_address...»
Перенаправ: Зміна одного значення credentials не потребує migration (БД не чіпали). Одноразовий Rails runner робить це за 10 секунд; migration потребує деплою і залишається в схемі назавжди. Migration — лише якщо фікс може знадобитися знову; друкарська помилка — разова річ.
«Просто викличу
update!(content: "...")...»
Перенаправ: Будь-який контент, згенерований користувачем (твіти, коментарі, уведений markdown), має йти через Base64. Парсинг shell лапок, зворотних слешів, $ та backtick — класичне мінне поле — прод не місце для тренувань.
perform_later без бізнес-параметрів«Просто перезапущу
PostTweetJob.perform_later(87)...»
Перенаправ: Спершу запитай: «чи пов'язаний цей запис з іншими?» У тредів — reply_to, у батч-джобів — batch_id, у пагінованих — cursor. Список аргументів джоба — носій тих бізнес-зв'язків. Викинеш аргумент — розірвеш ланцюг.
Налагодження + модифікація даних прод-Rails з Claude — 6 правил:
kamal app exec --reuse 'bin/rails runner "puts X"'. Локалізуй проблему, перш ніж щось змінювати.kamal app exec --reuse — інструмент за замовчуванням, не SSH, не передеплой. Усередині контейнера, Rails завантажений, туди-назад 3-6 секунд.EDITOR=ruby-script bin/rails credentials:edit --environment production. Ruby-скрипт робить правку, не вдивляємося в шифртекст локально.echo -n 'X' | base64 локально, Base64.decode64 у runner.Налагодження у проді — не пересадка dev-флоу в прод — у тебе немає простору для ітерації та немає допусків помилок. Насправді ти використовуєш поверхню інтроспекції, яку прод уже надає (Rails runner + kamal exec + credentials:edit), кожен крок — мінімально можлива зміна. Claude вміє писати коректний Ruby — але знання «що можна робити напряму, а що треба спершу діагностувати» — рішення, яке він за тебе не прийме. Це твоя продова дисципліна.