Rank by operational sites; show what each status check probes
All checks were successful
Build and Deploy to Production / test (push) Successful in 16s
Build and Deploy to Production / build (push) Successful in 47s
Build and Deploy to Production / deploy (push) Successful in 32s

The datacenter text said "No. 33 by power" for a site that is 25th
on the default card, because it counted construction projects. Rank
among operational datacenters only, matching the card, and leave the
rank out for sites not yet in service. data.json and status.json gain
an operational_rank next to the list position.

The status page now lists, under each site, the hosts it probes with
their label, last result and the source that ties them to the site,
folded away behind a click; the control hosts in Russia get the same
list, and datacenter pages show it under their reachability block.
status.json carries the same targets.

For search: a keyword title tagline and meta description, WebPage and
Dataset structured data (pointing at status.json), x-default, and the
status pages in the sitemap.

Assisted-by: Claude:opus-5-5
This commit is contained in:
Sergei Poljanski 2026-10-18 11:12:47 +04:00
commit 5e36cc6a95
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
8 changed files with 118 additions and 28 deletions

52
app.rb
View file

@ -121,7 +121,7 @@ class BingoWeb < Sinatra::Base
[format(t(:dc_intro), name: dc.l(:name, @locale), city: dc.l(:city, @locale), operator: dc.operator),
(format(t(:dc_capacity), mw: mw_prose(dc)) if mw(dc)),
{ 'building' => t(:dc_building), 'planned' => t(:dc_planned) }[dc.status] || (dc.year && format(t(:dc_since), dc.year)),
format(t(:dc_rank), pos: dc.position)].compact
(format(t(:dc_rank), pos: op_rank(dc)) if op_rank(dc))].compact
end
def dc_latest(dc)
@ -181,6 +181,41 @@ class BingoWeb < Sinatra::Base
def feed_time(i) = Time.utc(i.occurred_on.year, i.occurred_on.month, i.occurred_on.day)
# Rank by power among operational datacenters only, the same order as the
# default card; nil for sites not yet in service.
def op_rank(dc)
@op_ranks ||= Bingo::Datacenter.where(status: 'operational').order(:position, :id).select_map(:id)
.each_with_index.to_h { |id, i| [id, i + 1] }
@op_ranks[dc.id]
end
def probe_json(t)
{ host: t[:host], port: t[:port], label: t[:label], source_url: t[:source_url],
answered: t[:last_ok], rtt_ms: t[:last_rtt_ms] }
end
# WebPage + Dataset (status.json), so the page reads as live open data.
def status_ld(report)
url = "#{base_url}/#{@locale}/status"
JSON.generate(
'@context' => 'https://schema.org',
'@graph' => [
{ '@type' => 'WebPage', '@id' => url, 'url' => url, 'name' => t(:status_title), 'description' => @desc,
'inLanguage' => @locale, 'dateModified' => report&.checked_at&.utc&.iso8601,
'isPartOf' => { '@id' => "#{base_url}/#website" },
'breadcrumb' => { '@type' => 'BreadcrumbList', 'itemListElement' => [
{ '@type' => 'ListItem', 'position' => 1, 'name' => t(:title), 'item' => "#{base_url}/#{@locale}" },
{ '@type' => 'ListItem', 'position' => 2, 'name' => t(:status_link), 'item' => url }
] } }.compact,
{ '@type' => 'Dataset', 'name' => t(:status_link), 'description' => t(:status_lead), 'url' => url,
'license' => LICENSE_URL, 'isAccessibleForFree' => true, 'temporalCoverage' => 'P1D',
'creator' => { '@type' => 'Person', 'name' => 'asxpi', 'url' => 'https://asxp.io' },
'distribution' => [{ '@type' => 'DataDownload', 'encodingFormat' => 'application/json',
'contentUrl' => "#{base_url}/status.json" }] }
]
).gsub('</', '<\/')
end
# Bar colour for an answered share (nil = no data).
def avail_class(a)
return '' if a.nil?
@ -193,9 +228,11 @@ class BingoWeb < Sinatra::Base
{ checked_at: report&.checked_at&.utc&.iso8601, monitor: report&.state,
controls: report&.controls && { ok: report.controls[0], checked: report.controls[1] },
datacenters: (report&.sites || []).map do |s|
{ slug: s.dc.slug, rank: s.dc.position, state: s.state, availability: s.availability&.round(3),
rtt_ms: s.rtt_ms, targets: s.targets, hourly: s.history.map { |a| a&.round(3) } }
{ slug: s.dc.slug, rank: s.dc.position, operational_rank: op_rank(s.dc), state: s.state,
availability: s.availability&.round(3), rtt_ms: s.rtt_ms, hourly: s.history.map { |a| a&.round(3) },
targets: s.targets.map { |t| probe_json(t) } }
end,
control_hosts: (report&.control_hosts || []).map { |t| probe_json(t) },
unmonitored: (report&.unmonitored || []).map(&:slug) }
end
@ -231,7 +268,8 @@ class BingoWeb < Sinatra::Base
content_type 'application/xml'
cache_control :public, max_age: 3600
dcs = Bingo::Datacenter.order(:position, :id).eager(:incidents).all
@entries = [[->(l) { "/#{l}" }, Bingo::Board.load.updated_on, '/']] +
@entries = [[->(l) { "/#{l}" }, Bingo::Board.load.updated_on, '/'],
[->(l) { "/#{l}/status" }, nil, '/en/status']] +
dcs.map { |dc| [->(l) { dc_path(dc, l) }, [dc.updated_at, *dc.incidents.map(&:updated_at)].compact.max] }
erb :sitemap, layout: false
end
@ -280,8 +318,10 @@ class BingoWeb < Sinatra::Base
@locale = locale
@report = status_report
@title = t(:status_title)
@desc = snippet(t(:status_lead))
@desc = t(:status_desc)
@path_for = ->(l) { "/#{l}/status" }
@x_default = '/en/status'
@ld = status_ld(@report)
cache_control :public, max_age: 60
erb :status
end
@ -345,7 +385,7 @@ class BingoWeb < Sinatra::Base
'part of the list from the hackyourmom datacenter map (data by Cat Eyes OSINT), re-verified.',
updated_at: Bingo::Board.load.updated_on&.utc&.iso8601,
datacenters: dcs.map do |dc|
{ slug: dc.slug, rank: dc.position, name: loc.(dc, :name), city: loc.(dc, :city),
{ slug: dc.slug, rank: dc.position, operational_rank: op_rank(dc), name: loc.(dc, :name), city: loc.(dc, :city),
operator: dc.operator, provider: dc.provider, status: dc.status,
mw: dc.mw, mw_value: Bingo.mw_value(dc.mw), mw_type: dc.mw_type, year: dc.year,
info_url: dc.info_url,

View file

@ -70,11 +70,11 @@ module Bingo
dc_intro: '%{name} — дата-центр в городе %{city}, оператор — %{operator}.',
dc_capacity: 'Мощность: %{mw}.', dc_building: 'Строится.', dc_planned: 'Планируется.',
dc_since: 'Работает с %d года.',
dc_rank: '№ %{pos} по мощности среди крупнейших дата-центров России.',
dc_rank: '№ %{pos} по мощности среди действующих крупнейших дата-центров России.',
dc_latest: 'Последний инцидент (%{date}): %{title}', incidents: 'Инциденты',
back: '← К карточке', dc_page: 'Страница ЦОДа →',
status_link: 'Доступность ЦОДов', status_title: 'Доступность ЦОДов России — сетевой мониторинг | Бинго ЦОДов',
status_h1: 'Доступность', status_accent: 'ЦОДов',
status_h1: 'Доступность', status_accent: 'ЦОДов', status_tagline: 'Сетевые проверки крупнейших дата-центров России раз в минуту',
status_lead: 'Раз в минуту мы открываем TCP-соединение с публичными адресами, которые открытые источники связывают с конкретной площадкой. Проверка идёт из Финляндии: российская фильтрация и отключения внешних каналов могут выглядеть как авария, поэтому параллельно проверяются контрольные узлы в России.',
status_note: 'Это не подтверждённые инциденты: клетка на карточке зачёркивается только по публичному источнику.',
status_states: { 'up' => 'доступен', 'degraded' => 'частично', 'down' => 'не отвечает', 'unknown' => 'нет данных' },
@ -84,7 +84,9 @@ module Bingo
status_checked: 'Последняя проверка: %{time} UTC.', status_never: 'Проверок ещё не было.',
status_unmonitored: 'Без мониторинга', status_unmonitored_note: 'Действующие ЦОДы, для которых нет публичных адресов, надёжно привязанных к площадке (обычно собственные ЦОДы Яндекса, VK, Сбера).',
status_cols: { 'dc' => 'ЦОД', 'state' => 'Сейчас', 'hours' => '24 часа', 'rtt' => 'мс', 'targets' => 'адр.' },
status_dc: 'Доступность сети', status_hour: '%{h} ч назад: %{p}'
status_dc: 'Доступность сети', status_hour: '%{h} ч назад: %{p}',
status_desc: 'Доступность крупнейших дата-центров России онлайн: ежеминутные сетевые проверки IXcellerate, Selectel, DataPro, Key Point и других, история за 24 часа и открытые данные.',
status_probes: 'Что проверяем', status_controls_list: 'Контрольные узлы в России', status_no_answer: 'не отвечает', status_source: 'почему этот адрес'
},
'en' => {
seo_title: 'Datacenter Bingo — strikes, fires and outages at Russian datacenters',
@ -114,11 +116,11 @@ module Bingo
dc_intro: '%{name} is a datacenter in %{city} operated by %{operator}.',
dc_capacity: 'Capacity: %{mw}.', dc_building: 'Under construction.', dc_planned: 'Planned.',
dc_since: 'In service since %d.',
dc_rank: "No. %{pos} by power among Russia's largest datacenters.",
dc_rank: "No. %{pos} by power among Russia's largest operational datacenters.",
dc_latest: 'Latest reported incident (%{date}): %{title}', incidents: 'Incidents',
back: '← Back to the card', dc_page: 'Datacenter page →',
status_link: 'Datacenter reachability', status_title: "Russian datacenter reachability — network monitor | Datacenter Bingo",
status_h1: 'Datacenter', status_accent: 'reachability',
status_h1: 'Datacenter', status_accent: 'reachability', status_tagline: "Minute-by-minute network checks of Russia's largest datacenters",
status_lead: 'Once a minute we open a TCP connection to public addresses that open sources tie to a specific site. Probes run from Finland: Russian filtering and cuts to international links can look like an outage, so control hosts inside Russia are checked alongside.',
status_note: 'These are not confirmed incidents: a cell on the card is crossed out only on a public source.',
status_states: { 'up' => 'reachable', 'degraded' => 'partial', 'down' => 'not answering', 'unknown' => 'no data' },
@ -128,7 +130,9 @@ module Bingo
status_checked: 'Last check: %{time} UTC.', status_never: 'No checks yet.',
status_unmonitored: 'Not monitored', status_unmonitored_note: 'Operational datacenters with no public address reliably tied to the site (mostly Yandex, VK and Sber in-house sites).',
status_cols: { 'dc' => 'Datacenter', 'state' => 'Now', 'hours' => '24 hours', 'rtt' => 'ms', 'targets' => 'addr.' },
status_dc: 'Network reachability', status_hour: '%{h} h ago: %{p}'
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'
},
'uk' => {
seo_title: 'Бінго ЦОДів — атаки, пожежі та аварії дата-центрів Росії',
@ -158,11 +162,11 @@ module Bingo
dc_intro: '%{name} — дата-центр у місті %{city}, оператор — %{operator}.',
dc_capacity: 'Потужність: %{mw}.', dc_building: 'Будується.', dc_planned: 'Планується.',
dc_since: 'Працює з %d року.',
dc_rank: '№ %{pos} за потужністю серед найбільших дата-центрів Росії.',
dc_rank: '№ %{pos} за потужністю серед найбільших діючих дата-центрів Росії.',
dc_latest: 'Останній інцидент (%{date}): %{title}', incidents: 'Інциденти',
back: '← До картки', dc_page: 'Сторінка ЦОДу →',
status_link: 'Доступність ЦОДів', status_title: 'Доступність ЦОДів Росії — мережевий моніторинг | Бінго ЦОДів',
status_h1: 'Доступність', status_accent: 'ЦОДів',
status_h1: 'Доступність', status_accent: 'ЦОДів', status_tagline: 'Мережеві перевірки найбільших дата-центрів Росії щохвилини',
status_lead: 'Раз на хвилину ми відкриваємо TCP-з’єднання з публічними адресами, які відкриті джерела пов’язують із конкретним майданчиком. Перевірка йде з Фінляндії: російська фільтрація та відключення зовнішніх каналів можуть виглядати як аварія, тому паралельно перевіряються контрольні вузли в Росії.',
status_note: 'Це не підтверджені інциденти: клітинка на картці закреслюється лише за публічним джерелом.',
status_states: { 'up' => 'доступний', 'degraded' => 'частково', 'down' => 'не відповідає', 'unknown' => 'немає даних' },
@ -172,7 +176,9 @@ module Bingo
status_checked: 'Остання перевірка: %{time} UTC.', status_never: 'Перевірок ще не було.',
status_unmonitored: 'Без моніторингу', status_unmonitored_note: 'Діючі ЦОДи, для яких немає публічних адрес, надійно прив’язаних до майданчика (здебільшого власні ЦОДи Яндекса, VK, Сбера).',
status_cols: { 'dc' => 'ЦОД', 'state' => 'Зараз', 'hours' => '24 години', 'rtt' => 'мс', 'targets' => 'адр.' },
status_dc: 'Доступність мережі', status_hour: '%{h} год тому: %{p}'
status_dc: 'Доступність мережі', status_hour: '%{h} год тому: %{p}',
status_desc: 'Доступність найбільших дата-центрів Росії онлайн: щохвилинні мережеві перевірки IXcellerate, Selectel, DataPro, Key Point та інших, історія за 24 години та відкриті дані.',
status_probes: 'Що перевіряємо', status_controls_list: 'Контрольні вузли в Росії', status_no_answer: 'не відповідає', status_source: 'чому ця адреса'
}
}.freeze
@ -313,8 +319,10 @@ module Bingo
STALE = 900 # newest worker bucket older than this: nothing is current
HOURS = 24
# targets / control_hosts: the probed hosts (endpoint, label, source and
# last result), shown so readers can see and check what is measured.
Site = Struct.new(:dc, :state, :availability, :rtt_ms, :history, :targets)
Report = Struct.new(:sites, :unmonitored, :checked_at, :controls, :state) do
Report = Struct.new(:sites, :unmonitored, :checked_at, :controls, :state, :control_hosts) do
def counts = sites.map(&:state).tally
end
@ -355,11 +363,14 @@ module Bingo
bad_hours = buckets.reject { |b| vantage_ok?(b) }.map { |b| hour_index(b[:bucket_at], now, hours) }.to_set
samples = db[:bingo_monitor_samples].where { bucket_at >= since }
targets = db[:bingo_monitor_targets].where(active: true).exclude(datacenter_id: nil)
targets = db[:bingo_monitor_targets].where(active: true)
.select(:datacenter_id, :host, :port, :label, :source_url, :last_ok, :last_rtt_ms)
.order(:host, :port)
dcs = Datacenter.order(:position, :id)
samples, targets, dcs = [samples.where(datacenter_id: only), targets.where(datacenter_id: only), dcs.where(id: only)] if only
samples, targets, dcs = [samples.where(datacenter_id: only), targets.where(datacenter_id: [only, nil]), dcs.where(id: only)] if only
samples = samples.all.group_by { |r| r[:datacenter_id] }
targets = targets.group_and_count(:datacenter_id).to_hash(:datacenter_id, :count)
targets = targets.all.group_by { |r| r[:datacenter_id] }
controls = targets.delete(nil) || []
dcs = dcs.all
monitored, unmonitored = dcs.partition { |dc| targets.key?(dc.id) }
@ -373,7 +384,7 @@ module Bingo
end
Report.new(sites, unmonitored.select(&:operational?), latest && latest[:bucket_at],
latest && [latest[:control_ok], latest[:control_checks]],
!fresh ? 'stale' : (trusted ? 'ok' : 'vantage'))
!fresh ? 'stale' : (trusted ? 'ok' : 'vantage'), controls)
end
def hour_index(time, now, hours) = hours - 1 - ((now - time) / 3600).floor

View file

@ -160,7 +160,8 @@ class BingoTest < Minitest::Test
%w[ru en uk].each { |l| assert_includes last_response.body, "<loc>https://example.org/#{l}</loc>" }
%w[ru en uk].each { |l| assert_includes last_response.body, "<loc>https://example.org/#{l}/dc/dc-25</loc>" }
assert_includes last_response.body, '<xhtml:link rel="alternate" hreflang="uk" href="https://example.org/uk/dc/dc-7"/>'
assert_equal 3, last_response.body.scan('hreflang="x-default"').size # card URLs only
%w[ru en uk].each { |l| assert_includes last_response.body, "<loc>https://example.org/#{l}/status</loc>" }
assert_equal 6, last_response.body.scan('hreflang="x-default"').size # card and status URLs only
assert_includes last_response.body, 'hreflang="x-default"'
refute_includes last_response.body, 'changefreq'
assert_match %r{</loc>\n <lastmod>\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ</lastmod>\n <xhtml:link}, last_response.body

View file

@ -16,7 +16,10 @@ class StatusTest < Minitest::Test
Bingo::Datacenter.create(slug: "dc-#{s}", position: i + 1, name_ru: "ЦОД #{s}", city_ru: 'Москва',
operator: 'Op', status: 'operational')
end
[@up, @down, @part].each { |dc| @db[:bingo_monitor_targets].insert(datacenter_id: dc.id, host: "h-#{dc.slug}", port: 443, created_at: B) }
[@up, @down, @part].each do |dc|
@db[:bingo_monitor_targets].insert(datacenter_id: dc.id, host: "h-#{dc.slug}", port: 443, label: "probe #{dc.slug}",
source_url: 'https://example.com/src', last_ok: true, last_rtt_ms: 40, created_at: B)
end
@db[:bingo_monitor_targets].insert(datacenter_id: nil, host: 'ctl', port: 80, created_at: B)
end
@ -86,12 +89,26 @@ class StatusTest < Minitest::Test
data = JSON.parse(last_response.body)
assert_equal %w[dc-up dc-down dc-part], data['datacenters'].map { |d| d['slug'] }
assert_equal ['dc-none'], data['unmonitored']
assert_equal({ 'host' => 'h-dc-up', 'port' => 443, 'label' => 'probe dc-up', 'source_url' => 'https://example.com/src',
'answered' => true, 'rtt_ms' => 40 }, data['datacenters'][0]['targets'][0])
assert_equal ['ctl'], data['control_hosts'].map { |t| t['host'] }
get '/ru/status'
assert_includes last_response.body, 'h-dc-part:443'
assert_includes last_response.body, '"@type":"Dataset"'
get '/en/dc/dc-up'
assert_includes last_response.body, 'Network reachability'
get '/en/dc/dc-none'
refute_includes last_response.body, 'Network reachability'
end
def test_rank_counts_operational_sites_only
@part.update(status: 'building') # position 3 is not in service
get '/en/dc/dc-none' # position 4
assert_includes last_response.body, 'No. 3 by power among'
get '/en/dc/dc-part'
refute_includes last_response.body, 'by power among'
end
def test_status_page_before_any_round
get '/en/status'
assert last_response.ok?

13
views/_probes.erb Normal file
View file

@ -0,0 +1,13 @@
<details class="probes">
<summary><%= title %> (<%= probes.size %>)</summary>
<ul>
<% probes.each do |p| %>
<li>
<span class="st <%= { true => 'up', false => 'down', nil => 'unknown' }[p[:last_ok]] %>"></span>
<code><%= p[:host] %>:<%= p[:port] %></code><% if p[:label] %> — <%= p[:label] %><% end %>
<% if p[:last_ok] %> · <%= p[:last_rtt_ms] %> <%= t(:status_cols)['rtt'] %><% elsif p[:last_ok] == false %> · <%= t(:status_no_answer) %><% end %>
<% if p[:source_url] %> · <a href="<%= p[:source_url] %>" target="_blank" rel="nofollow noopener"><%= t(:status_source) %></a><% end %>
</li>
<% end %>
</ul>
</details>

View file

@ -20,6 +20,7 @@
<h2><%= t(:status_dc) %></h2>
<p><span class="st <%= @status.state %>"><%= t(:status_states)[@status.state] %></span> · <a href="/<%= @locale %>/status"><%= t(:status_link) %></a></p>
<%== erb :_status_bars, layout: false, locals: { history: @status.history } %>
<%== erb :_probes, layout: false, locals: { probes: @status.targets, title: t(:status_probes) } %>
</section>
<% end %>
<section>
@ -29,8 +30,8 @@
</article>
<nav class="pager">
<% if @prev %><a href="<%= dc_path(@prev) %>" rel="prev">← #<%= @prev.position %> <%= @prev.l(:name, @locale) %></a><% else %><span></span><% end %>
<% if @next %><a href="<%= dc_path(@next) %>" rel="next">#<%= @next.position %> <%= @next.l(:name, @locale) %> →</a><% end %>
<% if @prev %><a href="<%= dc_path(@prev) %>" rel="prev">← <%= "##{op_rank(@prev)} " if op_rank(@prev) %><%= @prev.l(:name, @locale) %></a><% else %><span></span><% end %>
<% if @next %><a href="<%= dc_path(@next) %>" rel="next"><%= "##{op_rank(@next)} " if op_rank(@next) %><%= @next.l(:name, @locale) %> →</a><% end %>
</nav>
<p><a href="/<%= @locale %>"><%= t(:back) %></a></p>

View file

@ -168,6 +168,13 @@
.bars i.down { background: var(--hit); }
.status-banner { padding: 10px 14px; border-radius: 8px; background: var(--hit-bg); margin: 0 0 16px; max-width: 65ch; }
.unmonitored { color: var(--muted); font-size: 13px; max-width: 75ch; }
.probes { font-size: 13px; color: var(--muted); margin: 4px 0 8px; }
.probes summary { cursor: pointer; width: max-content; }
.probes ul { list-style: none; margin: 6px 0 0; padding: 0; }
.probes li { margin: 2px 0; overflow-wrap: anywhere; }
.probes code { color: var(--ink); }
.status-table .probe-row td { border-top: 0; padding-top: 0; }
.status-table tr:has(+ .probe-row) td { border-bottom: 0; padding-bottom: 2px; }
footer { margin-top: 32px; color: var(--muted); font-size: 13px; }
footer p { margin: 0 0 4px; }
@media (max-width: 560px) {

View file

@ -1,7 +1,7 @@
<% r = @report %>
<main>
<header>
<h1><%= t(:status_h1) %> <span><%= t(:status_accent) %></span></h1>
<h1><%= t(:status_h1) %> <span><%= t(:status_accent) %></span> <small><%= t(:status_tagline) %></small></h1>
<nav class="langs">
<% Bingo::LOCALES.each do |l| %>
<a href="<%= @path_for.(l) %>" hreflang="<%= l %>" lang="<%= l %>" <%== 'aria-current="page"' if l == @locale %>><%= l %></a>
@ -24,25 +24,25 @@
<% c = r.counts %>
<% %w[up degraded down unknown].each do |s| %><% if c[s] %> <span class="st <%= s %>"><%= t(:status_states)[s] %>: <%= c[s] %></span><% end %><% end %>
</p>
<% if r.control_hosts.any? %><%== erb :_probes, layout: false, locals: { probes: r.control_hosts, title: t(:status_controls_list) } %><% end %>
<table class="status-table">
<thead>
<tr>
<th>#</th><th><%= t(:status_cols)['dc'] %></th><th><%= t(:status_cols)['state'] %></th>
<th><%= t(:status_cols)['hours'] %></th><th class="num hide-sm"><%= t(:status_cols)['rtt'] %></th>
<th class="num hide-sm"><%= t(:status_cols)['targets'] %></th>
</tr>
</thead>
<tbody>
<% r.sites.each do |s| %>
<tr>
<td class="num"><%= s.dc.position %></td>
<td class="num"><%= op_rank(s.dc) %></td>
<td><a href="<%= dc_path(s.dc) %>"><%= s.dc.l(:name, @locale) %></a><span class="city"><%= s.dc.l(:city, @locale) %> · <%= s.dc.operator %></span></td>
<td><span class="st <%= s.state %>"><%= t(:status_states)[s.state] %></span></td>
<td><%== erb :_status_bars, layout: false, locals: { history: s.history } %></td>
<td class="num hide-sm"><%= s.rtt_ms %></td>
<td class="num hide-sm"><%= s.targets %></td>
</tr>
<tr class="probe-row"><td></td><td colspan="4"><%== erb :_probes, layout: false, locals: { probes: s.targets, title: t(:status_probes) } %></td></tr>
<% end %>
</tbody>
</table>