bingo: add, delete and reorder datacenters from admin
All checks were successful
Build and Deploy to Production / test (push) Successful in 22s
Build and Deploy to Production / build (push) Successful in 52s
Build and Deploy to Production / deploy (push) Successful in 38s

The card was fixed to the 25 seeded rows, so a new site such as
IXcellerate MOS7 (34 MW, under construction) could only be shoehorned in
by overwriting an existing row. Add create and delete (cascading to
incidents), make position an insert that shifts the rest, and keep
positions dense. More than 25 rows are allowed: the card shows the first
25 and the rest wait in reserve. "Sort by MW" re-ranks by the parsed
power figure, unknown last.

Assisted-by: Claude:opus-5
This commit is contained in:
Sergei Poljanski 2026-10-10 06:00:46 +04:00
commit e725395825
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
6 changed files with 197 additions and 14 deletions

View file

@ -1,10 +1,12 @@
<% fresh = @dc.new? %>
<div class="header">
<h1>#<%= @dc.position %> <%= @dc.name_ru %></h1>
<p class="tagline"><a href="/admin/bingo">← bingo</a></p>
<h1><%= fresh ? 'New datacenter' : "##{@dc.position} #{@dc.name_ru}" %></h1>
<p class="tagline"><a href="/admin/bingo">← bingo</a><% if !fresh && @dc.position > Bingo::SIZE**2 %> · off card (reserve)<% end %></p>
</div>
<% if @flash %><div class="form-error-base"><div><%= @flash %></div></div><% end %>
<% unless fresh %>
<section class="section">
<h2>Photo</h2>
<% if @dc.photo_file %>
@ -21,11 +23,15 @@
<button type="submit" class="download-button">Store photo</button>
</form>
</section>
<% end %>
<section class="section">
<h2>Details</h2>
<form class="invoice-form" method="post" action="/admin/bingo/dc/<%= @dc.id %>" autocomplete="off">
<form class="invoice-form" method="post" action="<%= fresh ? '/admin/bingo/dc' : "/admin/bingo/dc/#{@dc.id}" %>" autocomplete="off">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<% if fresh %>
<div class="field"><label for="slug">Slug <span class="optional">(permanent; used in photo names, e.g. ix-mos7)</span></label><input type="text" id="slug" name="slug" value="<%= @dc.slug %>" required pattern="[a-z0-9]+(-[a-z0-9]+)*" /></div>
<% end %>
<div class="invoice-grid">
<% %w[ru en uk].each do |l| %>
<div class="field"><label for="name_<%= l %>">Name (<%= l %>)</label><input type="text" id="name_<%= l %>" name="name_<%= l %>" value="<%= @dc[:"name_#{l}"] %>" <%= 'required' if l == 'ru' %> /></div>
@ -42,11 +48,21 @@
</select>
</div>
<div class="field"><label for="year">Year</label><input type="text" id="year" name="year" value="<%= @dc.year %>" inputmode="numeric" /></div>
<div class="field"><label for="position">Position (1–25)</label><input type="text" id="position" name="position" value="<%= @dc.position %>" inputmode="numeric" /></div>
<div class="field"><label for="position">Position <span class="optional">(inserts here; 1–<%= Bingo::SIZE**2 %> are on the card<%= ', blank = last' if fresh %>)</span></label><input type="text" id="position" name="position" value="<%= @dc.position %>" inputmode="numeric" /></div>
</div>
<div class="field"><label for="photo_credit">Photo credit</label><input type="text" id="photo_credit" name="photo_credit" value="<%= @dc.photo_credit %>" /></div>
<div class="field"><label for="photo_source_url">Photo source URL</label><input type="url" id="photo_source_url" name="photo_source_url" value="<%= @dc.photo_source_url %>" /></div>
<div class="field"><label for="info_url">Info / fact source URL</label><input type="url" id="info_url" name="info_url" value="<%= @dc.info_url %>" /></div>
<button type="submit" class="download-button">Save</button>
<button type="submit" class="download-button"><%= fresh ? 'Add datacenter' : 'Save' %></button>
</form>
</section>
<% unless fresh %>
<section class="section">
<h2>Danger zone</h2>
<form method="post" action="/admin/bingo/dc/<%= @dc.id %>/delete" onsubmit="return confirm('Delete this datacenter and all its incidents? This cannot be undone.')">
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
<button type="submit" class="link-button">delete this datacenter<%= " (and #{@dc.incidents.size} incident(s))" if @dc.incidents.any? %></button>
</form>
</section>
<% end %>

View file

@ -67,6 +67,11 @@
<section class="section">
<h2>Card</h2>
<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>
@ -80,6 +85,9 @@
<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 %>" alt="" width="64" height="40" style="object-fit: cover" /><% else %>—<% end %></td>