fix: audit log pagination + full-width table, board card button alignment, light sidebar dither

- Audit log: backend only returns a next cursor when the page is full, so the
  "Load more" button no longer needs two clicks (and hides at the last page).
  Table now spans the full screen width and long detail JSON wraps instead of
  overflowing.
- Bounty board: pin each card's action row to the bottom so "Request
  assignment" lines up across equal-height cards.
- Light theme: restore the Y2K dither texture on the sidebar nav panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
etalon
2026-06-13 11:36:32 +02:00
parent 420f045033
commit df08682bed
3 changed files with 26 additions and 3 deletions
+8 -2
View File
@@ -102,14 +102,20 @@ func (s *Server) handleAdminPatchSettings(w http.ResponseWriter, r *http.Request
func (s *Server) handleAdminAuditLog(w http.ResponseWriter, r *http.Request) { func (s *Server) handleAdminAuditLog(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query() q := r.URL.Query()
limit := atoiDefault(q.Get("limit"), 50)
if limit <= 0 || limit > 200 {
limit = 50
}
entries, err := s.store.ListAudit(r.Context(), q.Get("actorId"), q.Get("entityId"), entries, err := s.store.ListAudit(r.Context(), q.Get("actorId"), q.Get("entityId"),
q.Get("cursor"), atoiDefault(q.Get("limit"), 50)) q.Get("cursor"), limit)
if err != nil { if err != nil {
s.internalError(w, r, "list audit", err) s.internalError(w, r, "list audit", err)
return return
} }
// Only advertise a next cursor when the page is full; otherwise this is the
// last page and a further fetch would return nothing (the "click twice" bug).
next := "" next := ""
if len(entries) > 0 { if len(entries) == limit {
next = entries[len(entries)-1].ID next = entries[len(entries)-1].ID
} }
writeJSON(w, http.StatusOK, map[string]any{"entries": entries, "nextCursor": next}) writeJSON(w, http.StatusOK, map[string]any{"entries": entries, "nextCursor": next})
+16 -1
View File
@@ -326,7 +326,6 @@ body[data-nav=side] .topnav .links {
overflow-y: auto; overflow-y: auto;
z-index: 40; z-index: 40;
} }
:root[data-theme=light] body[data-nav=side] .topnav .links { background-image: none; }
body[data-nav=side] .topnav .links a { body[data-nav=side] .topnav .links a {
border-bottom: none; border-bottom: none;
border-left: 3px solid transparent; border-left: 3px solid transparent;
@@ -503,6 +502,22 @@ table.list tbody tr:nth-child(even) { background: color-mix(in srgb, var(--surfa
.grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } .grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.grid > .card { display: flex; flex-direction: column; } /* equal-height rows */ .grid > .card { display: flex; flex-direction: column; } /* equal-height rows */
/* pin the action row to the bottom so buttons line up across cards */
.grid > .card > .spread:last-child { margin-top: auto; }
/* Audit log: span the full screen width; long detail values wrap, never overflow */
#tab-audit {
width: calc(100vw - 32px);
margin-left: calc(50% - 50vw + 16px);
margin-right: calc(50% - 50vw + 16px);
}
body[data-nav=side] #tab-audit {
width: calc(100vw - 214px - 32px);
margin-left: 0;
margin-right: 0;
}
#audit-table td, #audit-table th { white-space: normal; overflow-wrap: anywhere; vertical-align: top; }
#audit-table td:last-child { font-family: var(--font-mono); font-size: 0.82rem; }
/* ---- toolbar: filter rows boxed and baseline-aligned ---- */ /* ---- toolbar: filter rows boxed and baseline-aligned ---- */
.toolbar { .toolbar {
+2
View File
@@ -362,6 +362,7 @@ async function loadAudit(reset = true) {
try { try {
const entity = encodeURIComponent(document.getElementById('audit-entity').value.trim()); const entity = encodeURIComponent(document.getElementById('audit-entity').value.trim());
if (reset) auditCursor = ''; if (reset) auditCursor = '';
else if (!auditCursor) return; // already at the last page
const res = await api('GET', `/api/v1/admin/audit-log?entityId=${entity}&cursor=${auditCursor}`); const res = await api('GET', `/api/v1/admin/audit-log?entityId=${entity}&cursor=${auditCursor}`);
auditCursor = res.nextCursor; auditCursor = res.nextCursor;
const rows = res.entries.map((e) => { const rows = res.entries.map((e) => {
@@ -380,6 +381,7 @@ async function loadAudit(reset = true) {
const tbody = document.querySelector('#audit-table tbody'); const tbody = document.querySelector('#audit-table tbody');
if (reset) tbody.replaceChildren(...rows); if (reset) tbody.replaceChildren(...rows);
else tbody.append(...rows); else tbody.append(...rows);
document.getElementById('audit-more').hidden = !auditCursor;
} catch (e) { showErr(e); } } catch (e) { showErr(e); }
} }
document.getElementById('audit-apply').addEventListener('click', () => loadAudit(true)); document.getElementById('audit-apply').addEventListener('click', () => loadAudit(true));