asxpio/views/admin/bingo/index.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

116 lines
6.5 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<%== erb :'admin/_nav', layout: false %>
<div class="header">
<h1>Bingo</h1>
<p class="tagline">
<% if bingo_public_url %><a href="<%= bingo_public_url %>" target="_blank" rel="noopener">public card ↗</a> · <% end %><a href="/admin/bingo/monitor">monitor</a>
</p>
</div>
<% if @flash %><div class="form-error-base"><div><%= @flash %></div></div><% end %>
<% if can?('bingo', 'write') %>
<section class="section">
<h2>Record incident</h2>
<form class="invoice-form" method="post" action="/admin/bingo/incidents" autocomplete="off">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<div class="invoice-grid">
<div class="field">
<label for="datacenter_id">Datacenter</label>
<select id="datacenter_id" name="datacenter_id" required>
<% @dcs.each do |dc| %><option value="<%= dc.id %>">#<%= dc.position %> <%= dc.name_ru %></option><% end %>
</select>
</div>
<div class="field">
<label for="occurred_on">Date</label>
<input type="date" id="occurred_on" name="occurred_on" value="<%= Date.today %>" required />
</div>
<div class="field">
<label for="kind">Kind</label>
<select id="kind" name="kind">
<% Bingo::KINDS.each do |k| %><option value="<%= k %>"><%= Bingo::ICONS[k] %> <%= k %></option><% end %>
</select>
</div>
</div>
<div class="field"><label for="title_ru">Title (ru)</label><input type="text" id="title_ru" name="title_ru" required maxlength="300" /></div>
<div class="field"><label for="title_en">Title (en) <span class="optional">(falls back to ru)</span></label><input type="text" id="title_en" name="title_en" maxlength="300" /></div>
<div class="field"><label for="title_uk">Title (uk) <span class="optional">(falls back to ru)</span></label><input type="text" id="title_uk" name="title_uk" maxlength="300" /></div>
<div class="field"><label for="url">Source URL</label><input type="url" id="url" name="url" placeholder="https://" /></div>
<button type="submit" class="download-button">Cross it out</button>
</form>
</section>
<% end %>
<section class="section">
<h2>Incidents</h2>
<% if @incidents.empty? %>
<p>None yet.</p>
<% else %>
<table class="invoice-list">
<thead><tr><th>Date</th><th>Datacenter</th><th>Kind</th><th>Title</th><th></th></tr></thead>
<tbody>
<% @incidents.each do |i| %>
<tr>
<td class="mono"><%= i.occurred_on %></td>
<td><%= i.datacenter.name_ru %></td>
<td><%= Bingo::ICONS[i.kind] %> <%= i.kind %></td>
<td><% if i.url %><a href="<%= i.url %>" target="_blank" rel="noopener"><%= i.title_ru %></a><% else %><%= i.title_ru %><% end %></td>
<td class="actions">
<% if can?('bingo', 'delete') %>
<form method="post" action="/admin/bingo/incidents/<%= i.id %>/delete" onsubmit="return confirm('Delete this incident?')">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<button type="submit" class="link-button">delete</button>
</form>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
</section>
<section class="section">
<h2>Card</h2>
<% if can?('bingo', 'write') %>
<p><a href="/admin/bingo/dc/new">+ Add datacenter</a> · positions 1–<%= Bingo::SIZE**2 %> are on the card, the rest are reserve</p>
<form method="post" action="/admin/bingo/sort" onsubmit="return confirm('Re-rank every datacenter by MW? Manual ordering is lost.')">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<button type="submit" class="link-button">⇅ Sort by MW (unknown last)</button>
</form>
<form method="post" action="/admin/bingo/photos/import">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<button type="submit" class="link-button">↻ Import all missing photos from their source URLs</button>
</form>
<form method="post" action="/admin/bingo/photos/rebuild">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<button type="submit" class="link-button">⟳ Rebuild resized photos from stored originals</button>
</form>
<form method="post" action="/admin/bingo/photos/import" onsubmit="return confirm('Re-download every photo that has a source URL? Manually uploaded photos with a source URL get replaced too.')">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<input type="hidden" name="all" value="1" />
<button type="submit" class="link-button">↻ Re-import all photos from their source URLs</button>
</form>
<% else %>
<p class="optional">Positions 1–<%= Bingo::SIZE**2 %> are on the card, the rest are reserve.</p>
<% end %>
<table class="invoice-list">
<thead><tr><th>#</th><th>Photo</th><th>Name</th><th>Operator</th><th class="num">MW</th><th>Status</th><th class="num">Incidents</th><th></th></tr></thead>
<tbody>
<% @dcs.each do |dc| %>
<% if dc.position == Bingo::SIZE**2 + 1 %>
<tr><td colspan="8" class="optional">— reserve (not on the card) —</td></tr>
<% end %>
<tr>
<td class="mono"><%= dc.position %></td>
<td><% if dc.photo_file %><img src="/admin/bingo/p/<%= dc.photo_file(480) %>" alt="" width="64" height="40" style="object-fit: cover" /><% else %>—<% end %></td>
<td><%= dc.name_ru %><br /><span class="optional"><%= dc.city_ru %></span></td>
<td><%= dc.operator %></td>
<td class="num mono"><%= dc.mw %><%= " #{dc.mw_type}" if dc.mw_type %></td>
<td><%= dc.status %></td>
<td class="num mono"><%= dc.incidents.size %></td>
<td class="actions"><a href="/admin/bingo/dc/<%= dc.id %>"><%= can?('bingo', 'write') ? 'edit' : 'view' %></a></td>
</tr>
<% end %>
</tbody>
</table>
</section>