Add a second public site to the same process: a 5x5 card of the 25 largest Russian datacenters where a cell is crossed out once an incident with a public source is recorded (Yandex Sasovo and Kaluga drone strikes on 2026-10-08/09 are seeded). config.ru dispatches by Host, so it rides the existing container, DB and bucket instead of a new deploy. The card is localized ru/en/uk with per-locale columns falling back to ru. Content is managed from /admin/bingo on asxp.io, reusing AdminAuth and CSRF. Photos go to the existing bucket under bingo/ and are proxied by the app, so the bucket stays private and storage needs no change. The seed data was fact-checked against operator sites and press on 2026-10-10; several entries in the original list were wrong or vague. Assisted-by: Claude:opus-5
59 lines
1.9 KiB
Ruby
59 lines
1.9 KiB
Ruby
# Public datacenter bingo, served on BINGO_HOST from the same process as
|
|
# AsxpioWeb (config.ru dispatches by Host). Read-only: no session, no CSRF.
|
|
# Content is managed from asxp.io/admin/bingo (lib/bingo_admin.rb).
|
|
class BingoWeb < Sinatra::Base
|
|
configure do
|
|
set :root, $root
|
|
set :views, File.join($root, 'views', 'bingo')
|
|
set :erb, layout: :layout, escape_html: true
|
|
set :show_exceptions, $env == 'development'
|
|
set :host_authorization,
|
|
{ permitted_hosts: $env == 'production' && ENV['BINGO_HOST'] ? [ENV['BINGO_HOST']] : [] }
|
|
end
|
|
|
|
helpers do
|
|
def t(key) = Bingo::T[@locale][key]
|
|
def kind_label(kind) = Bingo::T[@locale][:kinds][kind] || Bingo::T[@locale][:kinds]['other']
|
|
def kind_icon(kind) = Bingo::ICONS[kind] || Bingo::ICONS['other']
|
|
def photo_path(dc) = dc.photo_file && "/p/#{dc.photo_file}"
|
|
def fmt_date(d) = d.strftime('%d.%m.%Y')
|
|
|
|
def mw(dc)
|
|
dc.mw.to_s.strip.empty? ? nil : "#{dc.mw} #{t(:mw)}"
|
|
end
|
|
|
|
def preferred_locale
|
|
langs = request.env['HTTP_ACCEPT_LANGUAGE'].to_s.downcase.scan(/([a-z]{2})(?:-[a-z]+)?(?:;q=([\d.]+))?/)
|
|
langs.sort_by { |_, q| -(q || '1').to_f }.map(&:first).find { |l| Bingo::LOCALES.include?(l) } || 'en'
|
|
end
|
|
end
|
|
|
|
before do
|
|
halt 503, 'Bingo not configured (DATABASE_URL missing).' unless defined?(Bingo::Datacenter)
|
|
end
|
|
|
|
get '/' do
|
|
redirect "/#{preferred_locale}", 302
|
|
end
|
|
|
|
get %r{/(ru|en|uk)/?} do |locale|
|
|
@locale = locale
|
|
@board = Bingo::Board.load
|
|
cache_control :public, max_age: 60
|
|
erb :index
|
|
end
|
|
|
|
# Photos are immutable per key (a new upload gets a new key).
|
|
get '/p/:file' do
|
|
bytes, type = Bingo::Photo.read(params[:file])
|
|
content_type type
|
|
cache_control :public, :immutable, max_age: 31_536_000
|
|
bytes
|
|
rescue Bingo::Photo::Error, Aws::S3::Errors::NoSuchKey, Aws::S3::Errors::NotFound
|
|
halt 404
|
|
end
|
|
|
|
not_found do
|
|
'Not found'
|
|
end
|
|
end
|