Give each user independent read, write and delete flags per area (invoices, bingo, users), so someone can e.g. review and remove bingo incidents without being able to add any. The request method and path decide the flag needed: GET is read except the new/duplicate forms, POST to .../delete is delete, any other POST is write. Unmapped admin paths are superuser-only, and a test enumerates every registered admin route so a new one cannot ship without a rule. Pages follow the same flags: nav and dashboard tiles show readable areas only, write forms and delete buttons are hidden, and the datacenter form is shown read-only. Assisted-by: Claude:opus-5
116 lines
6.5 KiB
Text
116 lines
6.5 KiB
Text
<%== 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 %>
|
||
</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>
|