asxpio/views/admin/dashboard.erb
Sergei Poljanski 4c10221727
All checks were successful
Build and Deploy to Production / test (push) Successful in 27s
Build and Deploy to Production / build (push) Successful in 1m3s
Build and Deploy to Production / deploy (push) Successful in 40s
admin: publish files to the public bucket
There was no way to put a file up for public use short of running an
S3 client with the uploader key. Add a "files" area to the admin:
upload one or more files, copy the link, edit the note, replace the
contents under the same link, delete. It has its own read/write/delete
permission flags like the other areas, and every action is audited.

The objects go to the storage stack's public bucket and are served by
Garage at s3.asxp.io/public/<key>, not by this app. The app's key can
only put and delete there, so a public_files table is the record of
what was published. Keys get a random prefix: links are not guessable
and names never collide.

That host is same-site with the admin, so a file that can carry script
must not render there. Only a fixed list of types (images, PDF, plain
text, audio, video) is served inline; the rest, HTML and SVG included,
is stored as an attachment. Image metadata is stripped by default, as
for the bingo photos, because of GPS tags.

A per-file limit and a total quota keep the small disk on asxpio-ops
from filling up.

Assisted-by: Claude:fable-5-2
2026-11-12 05:52:10 +04:00

80 lines
3.7 KiB
Text

<%== erb :'admin/_nav', layout: false %>
<div class="header">
<h1>Dashboard</h1>
</div>
<div class="dash">
<% if @inv %>
<section class="section dash-tile">
<h2><a href="/admin/invoices">Invoices</a></h2>
<div class="stats">
<div><b><%= @inv[:count] %></b><span>total</span></div>
<div><b><%= @inv[:unpaid].size %></b><span>unpaid</span></div>
<div><b><%= @inv[:overdue] %></b><span>overdue</span></div>
</div>
<% if @inv[:unpaid_totals].any? %>
<p class="optional">Outstanding: <span class="mono"><%= @inv[:unpaid_totals].map { |cur, sum| "#{cur} #{'%.2f' % sum}" }.join(' · ') %></span></p>
<% end %>
<% if @inv[:recent].any? %>
<table class="invoice-list">
<tbody>
<% @inv[:recent].each do |inv| %>
<tr>
<td class="mono"><a href="/admin/invoices/<%= inv.uuid %>"><%= inv.number %></a></td>
<td><%= inv.client_name %></td>
<td class="num mono"><%= inv.currency %> <%= '%.2f' % inv.total %></td>
<td><span class="badge badge-<%= inv.status %>"><%= inv.status %></span></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<p><% if can?('invoices', 'write') %><a href="/admin/invoices/new">+ New invoice</a> · <% end %><a href="/admin/invoices">all invoices</a></p>
</section>
<% end %>
<% if @bingo %>
<section class="section dash-tile">
<h2><a href="/admin/bingo">Bingo</a></h2>
<div class="stats">
<div><b><%= @bingo[:hit] %>/<%= @bingo[:cells] %></b><span>crossed out</span></div>
<div><b><%= @bingo[:lines] %></b><span>lines</span></div>
<div><b><%= @bingo[:reserve] %></b><span>in reserve</span></div>
</div>
<% if (last = @bingo[:last_incident]) %>
<p>Latest: <span class="mono"><%= last.occurred_on %></span> <%= Bingo::ICONS[last.kind] %> <%= last.datacenter.name_ru %> — <%= last.title_ru %></p>
<% end %>
<% if @bingo[:no_photo].any? %>
<p class="optional">No photo on the card: <%= @bingo[:no_photo].map(&:name_ru).join(', ') %></p>
<% end %>
<p><a href="/admin/bingo">manage</a><% if bingo_public_url %> · <a href="<%= bingo_public_url %>" target="_blank" rel="noopener">public card ↗</a><% end %></p>
</section>
<% end %>
<% if @files %>
<section class="section dash-tile">
<h2><a href="/admin/files">Files</a></h2>
<div class="stats">
<div><b><%= @files[:count] %></b><span>published</span></div>
<div><b><%= fmt_bytes(@files[:bytes]) %></b><span>used</span></div>
<div><b><%= fmt_bytes(PublicFile.quota_bytes) %></b><span>quota</span></div>
</div>
<% if @files[:recent].any? %>
<table class="invoice-list">
<tbody>
<% @files[:recent].each do |f| %>
<tr>
<td class="mono"><a href="/admin/files/<%= f.id %>"><%= f.filename %></a></td>
<td class="num mono"><%= fmt_bytes(f.size) %></td>
<td class="mono"><%= f.updated_at.strftime('%Y-%m-%d') %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<p><a href="/admin/files">manage</a></p>
</section>
<% end %>
<% unless @inv || @bingo || @files %>
<section class="section"><p>No areas are assigned to your account yet. Ask an administrator for access.</p></section>
<% end %>
</div>