asxpio/views/admin/bingo/monitor.erb
Sergei Poljanski 0cc3e1dece
All checks were successful
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 1m11s
Build and Deploy to Production / deploy (push) Successful in 37s
bingo: add a reachability monitor for the status page
datacenterbingo gets a public page showing whether each datacenter's
network answers. The probing lives here, next to the schema and the
admin, since the public site only has a read-only role.

bin/monitor runs as a second container from the same image. Once a
minute it opens one TCP connection to every active target (a handshake
or a refusal both count as answered) and folds the results into
5-minute rows per datacenter, plus a per-bucket row for control hosts in
Russia, so a lost vantage point isn't mistaken for dark sites. Rows are
kept 30 days. Targets are managed at /admin/bingo/monitor; only hosts a
public source ties to that one site belong there.

Migration 017 also grants the new tables to the bingo role, so the
public site can read them without a storage redeploy.

Assisted-by: Claude:opus-5-5
2026-10-18 10:31:00 +04:00

60 lines
3.5 KiB
Text

<%== erb :'admin/_nav', layout: false %>
<div class="header">
<h1>Bingo monitor</h1>
<p class="tagline">
<a href="/admin/bingo">← bingo</a>
· last round: <%= @last_round ? "#{@last_round[:bucket_at].strftime('%Y-%m-%d %H:%M')} UTC bucket, controls #{@last_round[:control_ok]}/#{@last_round[:control_checks]}" : 'never (is bin/monitor running?)' %>
</p>
</div>
<% if @flash %><div class="form-error-base"><div><%= @flash %></div></div><% end %>
<% if can?('bingo', 'write') %>
<section class="section">
<h2>Add targets</h2>
<p class="optional">One per line: <code>slug host[:port] source_url label…</code>. Port defaults to 443. Slug <code><%= Bingo::Monitor::CONTROL %></code> adds a control host (in Russia, not in any listed site). Only hosts a public source ties to that one datacenter; one TCP connect per minute each.</p>
<form class="invoice-form" method="post" action="/admin/bingo/monitor" autocomplete="off">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<div class="field"><textarea name="targets" rows="6" style="width: 100%; font-family: monospace" placeholder="selectel-dubrovka-1 203.0.113.10:80 https://example.com/test-ips Selectel test IP ru-3"></textarea></div>
<button type="submit" class="download-button">Add</button>
</form>
</section>
<% end %>
<section class="section">
<h2>Targets (<%= @targets.size %>)</h2>
<table class="invoice-list">
<thead><tr><th>Datacenter</th><th>Endpoint</th><th>Label</th><th>Last</th><th class="num">ms</th><th>Since</th><th></th></tr></thead>
<tbody>
<% @targets.each do |t| %>
<tr<%== ' style="opacity: .5"' unless t.active %>>
<td><%= t.control? ? 'control' : "##{t.datacenter.position} #{t.datacenter.name_ru}" %></td>
<td class="mono"><%= t.endpoint %></td>
<td><% if t.source_url %><a href="<%= t.source_url %>" target="_blank" rel="noopener"><%= t.label || 'source' %></a><% else %><%= t.label %><% end %></td>
<td><%= { true => '✅', false => '❌', nil => '—' }[t.last_ok] %></td>
<td class="num mono"><%= t.last_rtt_ms %></td>
<td class="mono"><%= t.last_change_at&.strftime('%m-%d %H:%M') %></td>
<td class="actions">
<% if can?('bingo', 'write') %>
<form method="post" action="/admin/bingo/monitor/<%= t.id %>/toggle">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<button type="submit" class="link-button"><%= t.active ? 'pause' : 'resume' %></button>
</form>
<% end %>
<% if can?('bingo', 'delete') %>
<form method="post" action="/admin/bingo/monitor/<%= t.id %>/delete" onsubmit="return confirm('Delete this target?')">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<button type="submit" class="link-button">delete</button>
</form>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</section>
<section class="section">
<h2>Operational, not monitored (<%= @unmonitored.size %>)</h2>
<p class="optional"><%= @unmonitored.map { |dc| "##{dc.position} #{dc.slug}" }.join(', ') %></p>
</section>