== the install window: the hold's own log line vs the stranger's first 200
2026/10/01 16:07:05 install_hold.go:106: [INFO] [stacks] dawarich: install HOLD before the first start — only the household reaches [timeline.enkisfelhom.hu] until the known first login is replaced
2026/10/01 16:09:24 after_install.go:181: [INFO] [stacks] after_install dawarich: dawarich [bin/rails runner u = User.find_by!(email: 'demo@dawarich.app'); u.update!(password: ARGV[0], password_confir
2026/10/01 16:09:24 install_hold.go:135: [INFO] [stacks] dawarich: install hold OPENED by after_install — the app is reached as without a hold

first 200 in the poll: 16:09:26 200 <!DOCTYPE html> <html lang= | last gate 401: 16:09:24 401 {"error":"this app is waiting for its first set
== Dawarich's own rate limits (config/initializers/rack_attack.rb)
21:# 400, so an unparseable request would otherwise escape these throttles as a 500.
38:# legitimate client and is rejected before any throttle reads the body.
47:# stack). Use safe_body_params for throttles that key on a field sent in a JSON
48:# request body, otherwise the discriminator returns nil for JSON clients and
49:# the throttle is silently bypassed. Deliberately not logged: a malformed-body
58:# ignores. Required for throttles keyed on a JSON body field (API login email,
61:# result memoised on env so multiple throttles in one request share a single
63:# keys, matching ActionDispatch::Request#parameters, so the throttle keys on the
81:# The query string can be malformed on its own; a throttle must not raise here.
89:# with the JSON parser, so the throttle must accept the same set or a one-header
92:# letting an unauthenticated header crash the throttle.
102:# buffer unbounded input; anything longer is handed to the size blocklist.
104:# memoises the parsed Hash on env because several throttles run per request
126:# The size guard must cover exactly the paths whose per-email/per-token throttle
127:# is live: the web sign-in throttle runs everywhere, the API ones are exempt on
129:def json_body_throttled_path?(request)
132:  path = throttle_path(request)
133:  return true if WEB_JSON_BODY_THROTTLED_PATHS.include?(path)
135:  API_JSON_BODY_THROTTLED_PATHS.include?(path) && !DawarichSettings.self_hosted?
139:# path before routing. Share counters across formats without matching child paths.
140:def throttle_path(request)
141:  request.path.sub(%r{\.[^/.]+\z}, '')
148:# Disabled in the test environment so request specs aren't throttled by
149:# accumulated counters across examples (login throttle is 5/min by IP,
172:# Execution order: rack-attack evaluates the discriminator block first (which sets
174:Rack::Attack.throttle('api/token',
175:                      limit: proc { |req| req.env['rack.attack.api_rate_limit'] || 1_000 },
176:                      period: 1.hour) do |req|
177:  next unless req.path.start_with?('/api/')
179:  # minutes; they run on their own api/tiles throttle below.

40:WEB_JSON_BODY_THROTTLED_PATHS = %w[/users/sign_in].freeze
48:# request body, otherwise the discriminator returns nil for JSON clients and
150:# 20/min by email — easy to trip when many specs hit /users/sign_in).
172:# Execution order: rack-attack evaluates the discriminator block first (which sets
174:Rack::Attack.throttle('api/token',
175:                      limit: proc { |req| req.env['rack.attack.api_rate_limit'] || 1_000 },
176:                      period: 1.hour) do |req|
197:Rack::Attack.throttle('api/tiles',
198:                      limit: proc { Rack::Attack.tiles_limit },
199:                      period: 1.hour) do |req|
209:Rack::Attack.throttle('api/tiles_burst',
210:                      limit: proc { Rack::Attack.tiles_burst_limit },
211:                      period: 30.seconds) do |req|
221:# Points creation rate limit: 10,000 req/hr per API key.
229:Rack::Attack.throttle('api/points_creation', limit: 10_000, period: 1.hour) do |req|
248:Rack::Attack.throttle('api/heavy_recompute', limit: 5, period: 1.hour) do |req|
269:Rack::Attack.throttle('logins/ip', limit: 20, period: 1.minute) do |req|
270:  next unless throttle_path(req) == '/users/sign_in' && req.post?
272:  req.ip
275:Rack::Attack.throttle('logins/email', limit: 5, period: 1.minute) do |req|
276:  next unless throttle_path(req) == '/users/sign_in' && req.post?
288:Rack::Attack.throttle('logins/api_ip', limit: 20, period: 1.minute) do |req|
292:  req.ip
295:Rack::Attack.throttle('logins/api_email', limit: 5, period: 1.minute) do |req|
302:Rack::Attack.throttle('signups/api_ip_burst', limit: 5, period: 1.minute) do |req|
305:  req.ip if throttle_path(req) == '/api/v1/auth/register' && req.post?
308:Rack::Attack.throttle('signups/api_ip_hourly', limit: 20, period: 1.hour) do |req|
311:  req.ip if throttle_path(req) == '/api/v1/auth/register' && req.post?
314:Rack::Attack.throttle('oauth/token_exchange', limit: 30, period: 1.minute) do |req|
319:  req.ip
322:Rack::Attack.throttle('apple_web_callback_per_ip', limit: 20, period: 1.minute) do |req|
326:  req.ip
329:Rack::Attack.throttle('users/exist', limit: 600, period: 1.hour) do |req|
340:Rack::Attack.throttle('api/auth/otp_challenge', limit: 5, period: 15.minutes) do |req|
343:  req.ip if throttle_path(req) == '/api/v1/auth/otp_challenge' && req.post?
348:Rack::Attack.throttle('api/auth/otp_challenge_token', limit: 5, period: 15.minutes) do |req|
357:Rack::Attack.throttle('users/otp_challenge_session', limit: 5, period: 15.minutes) do |req|
361:  session_id || req.ip
364:Rack::Attack.throttle('users/otp_challenge_ip', limit: 20, period: 15.minutes) do |req|
365:  req.ip if throttle_path(req) == '/users/otp_challenge' && req.post?
368:Rack::Attack.throttle('auth/account_link_challenge_session', limit: 5, period: 15.minutes) do |req|
372:  pending.is_a?(Hash) ? pending['user_id'] : req.ip
375:Rack::Attack.throttle('auth/account_link_challenge_ip', limit: 20, period: 15.minutes) do |req|
376:  req.ip if throttle_path(req) == '/auth/account_link/challenge' && req.post?
389:Rack::Attack.throttle('api/users/two_factor_sensitive', limit: 5, period: 15.minutes) do |req|
400:Rack::Attack.throttle('trial/welcome', limit: 30, period: 1.minute) do |req|
403:  req.ip
406:Rack::Attack.throttle('signups/ip_burst', limit: 5, period: 1.minute) do |req|
409:  req.ip
412:Rack::Attack.throttle('signups/ip_hourly', limit: 20, period: 1.hour) do |req|
415:  req.ip
420:Rack::Attack.throttle('admin/flipper', limit: 30, period: 5.minutes) do |req|
423:  req.ip if req.path.start_with?('/admin/flipper')
429:Rack::Attack.throttle('shared_links/viewer',
430:                      limit: proc { Rack::Attack.shared_links_viewer_limit },
431:                      period: 1.minute) do |req|
434:  req.ip if req.path.match?(%r{\A/s/[^/]+\z}) || req.path.start_with?('/api/v1/shared/')
439:Rack::Attack.throttle('shared_links/cable',
440:                      limit: proc { Rack::Attack.shared_links_viewer_limit },
441:                      period: 1.minute) do |req|

== measured: how long does the household wait after a stranger's burst? (one right-password try every 15 s)
Traceback (most recent call last):
  File "<stdin>", line 12, in <module>
  File "<stdin>", line 9, in web_login
  File "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts/box_walk.py", line 155, in app_curl
    gc = GATE[sub] if sub in GATE else gate_cookie(sub)
                                       ~~~~~~~~~~~^^^^^
  File "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts/box_walk.py", line 130, in gate_cookie
    sess = open(f"{SC}/sess{os.getpid()}.txt").read().strip()
           ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/ab4a031d-d768-4e08-8771-c6280233e5ed/scratchpad/sc/sess307463.txt'
== measured: a stranger's 8 wrong tries for the public name, then the household's right password every 15 s
  stranger: ['422', '422', '422', '422', '500', '429', '429', '429']
  +    0s right password -> 429
  +   15s right password -> 429
  +   30s right password -> 429
  +   45s right password -> 429
  +   61s right password -> 422
  +   76s right password -> 422
  +   91s right password -> 422
  +  107s right password -> 422
  +  122s right password -> 422
  +  137s right password -> 422
  +  153s right password -> 422
  +  168s right password -> 422
  Devise locked_at: 2026-10-01 18:14:13.572479000 CEST +02:00
