diff --git a/CLAUDE.md b/CLAUDE.md index 1060ed0..3144525 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,9 @@ renderings only for unbuilt sites) and credit format. degraded, down (nothing answered for two buckets), unknown (worker stale, or under half the controls answering). Not incidents: never cross out a cell from this alone. +- `/:locale/policy`: the no-unsolicited-messages policy, also in + `README.md`. Blocklists (SURBL) require a published one to delist; keep + both copies in step. - `config.ru`: `www.` → `BINGO_HOST` 301. - `views/`: `layout.erb` (CSS and SEO head), `index.erb` (card, provider filter, dialogs), `dc.erb`, partials. `assets/`: icons. diff --git a/README.md b/README.md index 53a732e..b22e51c 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,16 @@ of Claude (Anthropic). Part of the list came from the Credit "Datacenter Bingo (datacenterbingo.xyz)"; the full attribution line is in the file. +## No unsolicited messages + +datacenterbingo.xyz is never advertised by unsolicited email, SMS, +messenger, forum or social media messages. Neither we nor any third party +may promote the site this way. The site sends no email and has no mailing +list, newsletter or contact form, and it carries no ads, affiliate programs +or paid promotion. + +If you received a message linking to this site, we did not send it. Please +report it via [asxp.io/contact](https://asxp.io/contact). The same policy +is on the site at [/en/policy](https://datacenterbingo.xyz/en/policy). + Development: `nix develop`, then `bin/test`. See `CLAUDE.md`. diff --git a/app.rb b/app.rb index 8afc537..70ccd60 100644 --- a/app.rb +++ b/app.rb @@ -269,7 +269,8 @@ class BingoWeb < Sinatra::Base cache_control :public, max_age: 3600 dcs = Bingo::Datacenter.order(:position, :id).eager(:incidents).all @entries = [[->(l) { "/#{l}" }, Bingo::Board.load.updated_on, '/'], - [->(l) { "/#{l}/status" }, nil, '/en/status']] + + [->(l) { "/#{l}/status" }, nil, '/en/status'], + [->(l) { "/#{l}/policy" }, nil, '/en/policy']] + dcs.map { |dc| [->(l) { dc_path(dc, l) }, [dc.updated_at, *dc.incidents.map(&:updated_at)].compact.max] } erb :sitemap, layout: false end @@ -326,6 +327,20 @@ class BingoWeb < Sinatra::Base erb :status end + # Anti-spam policy; blocklists (SURBL) ask for a published one. + get %r{/(ru|en|uk)/policy} do |locale| + @locale = locale + @title = t(:policy_title) + @desc = t(:policy_desc) + @path_for = ->(l) { "/#{l}/policy" } + @x_default = '/en/policy' + url = "#{base_url}/#{locale}/policy" + @ld = JSON.generate('@context' => 'https://schema.org', '@type' => 'WebPage', '@id' => url, 'url' => url, + 'name' => @title, 'description' => @desc, 'inLanguage' => locale).gsub(' '*' diff --git a/lib/bingo.rb b/lib/bingo.rb index 33fc3d5..635df03 100644 --- a/lib/bingo.rb +++ b/lib/bingo.rb @@ -86,7 +86,12 @@ module Bingo status_cols: { 'dc' => 'ЦОД', 'state' => 'Сейчас', 'hours' => '24 часа', 'rtt' => 'мс', 'targets' => 'адр.' }, status_dc: 'Доступность сети', status_hour: '%{h} ч назад: %{p}', status_desc: 'Доступность крупнейших дата-центров России онлайн: ежеминутные сетевые проверки IXcellerate, Selectel, DataPro, Key Point и других, история за 24 часа и открытые данные.', - status_probes: 'Что проверяем', status_controls_list: 'Контрольные узлы в России', status_no_answer: 'не отвечает', status_source: 'почему этот адрес' + status_probes: 'Что проверяем', status_controls_list: 'Контрольные узлы в России', status_no_answer: 'не отвечает', status_source: 'почему этот адрес', + policy_link: 'Без спама', policy_title: 'Без нежелательных рассылок | Бинго ЦОДов', + policy_desc: 'Бинго ЦОДов никогда не рекламируется нежелательными сообщениями. Сайт не отправляет писем и не ведёт рассылок.', + policy_text: ['datacenterbingo.xyz никогда не рекламируется нежелательными письмами, SMS, сообщениями в мессенджерах, на форумах или в соцсетях. Ни мы, ни кто-либо другой не вправе продвигать сайт таким образом.', + 'Сайт не отправляет писем, у него нет рассылок, подписок и форм обратной связи. На нём нет рекламы, партнёрских программ и платного продвижения.', + 'Если вы получили сообщение со ссылкой на этот сайт, его отправили не мы. Пожалуйста, сообщите о нём через %{link}.'] }, 'en' => { seo_title: 'Datacenter Bingo — strikes, fires and outages at Russian datacenters', @@ -132,7 +137,12 @@ module Bingo status_cols: { 'dc' => 'Datacenter', 'state' => 'Now', 'hours' => '24 hours', 'rtt' => 'ms', 'targets' => 'addr.' }, status_dc: 'Network reachability', status_hour: '%{h} h ago: %{p}', status_desc: "Live reachability of Russia's largest datacenters: minute-by-minute network checks of IXcellerate, Selectel, DataPro, Key Point and others, 24-hour history and open data.", - status_probes: 'What we check', status_controls_list: 'Control hosts in Russia', status_no_answer: 'no answer', status_source: 'why this address' + status_probes: 'What we check', status_controls_list: 'Control hosts in Russia', status_no_answer: 'no answer', status_source: 'why this address', + policy_link: 'No-spam policy', policy_title: 'No unsolicited messages | Datacenter Bingo', + policy_desc: 'Datacenter Bingo is never advertised by unsolicited messages. The site sends no email and runs no mailing list.', + policy_text: ['datacenterbingo.xyz is never advertised by unsolicited email, SMS, messenger, forum or social media messages. Neither we nor any third party may promote the site this way.', + 'The site sends no email and has no mailing list, newsletter or contact form. It carries no ads, affiliate programs or paid promotion.', + 'If you received a message linking to this site, we did not send it. Please report it via %{link}.'] }, 'uk' => { seo_title: 'Бінго ЦОДів — атаки, пожежі та аварії дата-центрів Росії', @@ -178,7 +188,12 @@ module Bingo status_cols: { 'dc' => 'ЦОД', 'state' => 'Зараз', 'hours' => '24 години', 'rtt' => 'мс', 'targets' => 'адр.' }, status_dc: 'Доступність мережі', status_hour: '%{h} год тому: %{p}', status_desc: 'Доступність найбільших дата-центрів Росії онлайн: щохвилинні мережеві перевірки IXcellerate, Selectel, DataPro, Key Point та інших, історія за 24 години та відкриті дані.', - status_probes: 'Що перевіряємо', status_controls_list: 'Контрольні вузли в Росії', status_no_answer: 'не відповідає', status_source: 'чому ця адреса' + status_probes: 'Що перевіряємо', status_controls_list: 'Контрольні вузли в Росії', status_no_answer: 'не відповідає', status_source: 'чому ця адреса', + policy_link: 'Без спаму', policy_title: 'Без небажаних розсилок | Бінго ЦОДів', + policy_desc: 'Бінго ЦОДів ніколи не рекламується небажаними повідомленнями. Сайт не надсилає листів і не веде розсилок.', + policy_text: ['datacenterbingo.xyz ніколи не рекламується небажаними листами, SMS, повідомленнями в месенджерах, на форумах чи в соцмережах. Ні ми, ні будь-хто інший не має права просувати сайт у такий спосіб.', + 'Сайт не надсилає листів, не має розсилок, підписок і форм зворотного зв’язку. На ньому немає реклами, партнерських програм і платного просування.', + 'Якщо ви отримали повідомлення з посиланням на цей сайт, його надіслали не ми. Будь ласка, повідомте про нього через %{link}.'] } }.freeze diff --git a/test/bingo_test.rb b/test/bingo_test.rb index f3cf031..b6cc5a9 100644 --- a/test/bingo_test.rb +++ b/test/bingo_test.rb @@ -150,6 +150,17 @@ class BingoTest < Minitest::Test assert_includes last_response.body, 'hackyourmom' end + def test_policy_page_in_every_locale_and_footer + { 'en' => 'never advertised', 'ru' => 'никогда не рекламируется', 'uk' => 'ніколи не рекламується' }.each do |l, text| + get "/#{l}/policy" + assert last_response.ok? + assert_includes last_response.body, text + assert_includes last_response.body, 'asxp.io/contact' + end + get '/en' + assert_includes last_response.body, 'No-spam policy' + end + def test_robots_and_sitemap_are_the_bingos_own get '/robots.txt' assert last_response.ok? @@ -161,7 +172,8 @@ class BingoTest < Minitest::Test %w[ru en uk].each { |l| assert_includes last_response.body, "https://example.org/#{l}/dc/dc-25" } assert_includes last_response.body, '' %w[ru en uk].each { |l| assert_includes last_response.body, "https://example.org/#{l}/status" } - assert_equal 6, last_response.body.scan('hreflang="x-default"').size # card and status URLs only + %w[ru en uk].each { |l| assert_includes last_response.body, "https://example.org/#{l}/policy" } + assert_equal 9, last_response.body.scan('hreflang="x-default"').size # card, status and policy URLs only assert_includes last_response.body, 'hreflang="x-default"' refute_includes last_response.body, 'changefreq' assert_match %r{\n \d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ\n <%= t(:footer) %>

<%= t(:credits_ai) %> <%== format(Rack::Utils.escape_html(t(:credits_hym)), link: 'hackyourmom') %>

-

RSS · JSON Feed · <%= t(:open_data) %> · <%= t(:status_link) %>

+

RSS · JSON Feed · <%= t(:open_data) %> · <%= t(:status_link) %> · <%= t(:policy_link) %>

© <%= Date.today.year %> asxp.io · <%= t(:source_code) %>

diff --git a/views/policy.erb b/views/policy.erb new file mode 100644 index 0000000..193f9c2 --- /dev/null +++ b/views/policy.erb @@ -0,0 +1,17 @@ +
+
+

<%= t(:policy_link) %>

+ +
+ + + <% t(:policy_text).each do |para| %> +

<%== format(Rack::Utils.escape_html(para), link: 'asxp.io/contact') %>

+ <% end %> + + <%== erb :_footer, layout: false %> +