- DELETE /api/admin/users/{handle}/activities — deletes all activities/*.json, wipes _merged/ and

index.json, then triggers a rebuild. Admin-only.
  - /admin/ page — lists all users, each with a "Delete activities" button. Clicking asks for
  confirmation in a <dialog> before firing the request. Button shows "Deleted (N)" or an error inline.
  - "Admin" nav link — appears in the top-right for admins only, hidden for everyone else.
This commit is contained in:
Davide Scaini
2026-04-12 17:46:28 +02:00
parent 2774f436d8
commit d659b90cd9
3 changed files with 134 additions and 1 deletions
+10 -1
View File
@@ -200,6 +200,13 @@ try {
title=""
class="text-xs px-2 py-0.5 rounded-full bg-amber-900/60 text-amber-300 border border-amber-700/50 animate-pulse cursor-default"
></span>
<!-- Admin link — hidden until confirmed admin -->
<a
id="nav-admin"
href={`${baseUrl}admin/`}
style="display:none"
class="text-xs text-zinc-500 hover:text-white transition-colors px-1"
>Admin</a>
<!-- Logout button — hidden until logged in -->
<button
@@ -484,8 +491,10 @@ try {
const chk = document.getElementById('upload-keep-original');
if (chk && user.store_originals_default) chk.checked = true;
// Admin: poll for active jobs and show a badge in the nav
// Admin: show admin link and poll for active jobs
if (user.is_admin) {
const adminLink = document.getElementById('nav-admin');
if (adminLink) adminLink.style.display = '';
const badge = document.getElementById('admin-jobs-badge');
async function pollJobs() {
try {
+93
View File
@@ -0,0 +1,93 @@
---
import Base from '../../layouts/Base.astro';
---
<Base title="Admin — BincioActivity">
<div class="max-w-2xl mx-auto px-4 py-10">
<h1 class="text-2xl font-bold text-white mb-8">Admin</h1>
<h2 class="text-sm font-semibold text-zinc-400 uppercase tracking-wider mb-3">Users</h2>
<div id="user-list" class="space-y-2">
<p class="text-zinc-500 text-sm">Loading…</p>
</div>
<!-- Confirmation dialog -->
<dialog id="confirm-dialog" class="rounded-xl bg-zinc-900 border border-zinc-700 p-6 text-white max-w-sm w-full backdrop:bg-black/60">
<p class="text-sm text-zinc-300 mb-5">Delete all activities for <strong id="confirm-handle" class="text-white"></strong>? This cannot be undone.</p>
<div class="flex gap-3 justify-end">
<button id="confirm-cancel" class="px-4 py-2 rounded-lg text-sm bg-zinc-800 hover:bg-zinc-700 text-zinc-300 transition-colors">Cancel</button>
<button id="confirm-ok" class="px-4 py-2 rounded-lg text-sm bg-red-700 hover:bg-red-600 text-white font-medium transition-colors">Delete</button>
</div>
</dialog>
</div>
</Base>
<script>
const listEl = document.getElementById('user-list')!;
const dialog = document.getElementById('confirm-dialog') as HTMLDialogElement;
const confirmH = document.getElementById('confirm-handle')!;
const confirmOk = document.getElementById('confirm-ok')!;
const confirmCancel = document.getElementById('confirm-cancel')!;
let pendingHandle = '';
async function load() {
const r = await fetch('/api/admin/users', { credentials: 'include' });
if (!r.ok) {
listEl.innerHTML = '<p class="text-red-400 text-sm">Not authorised or server unavailable.</p>';
return;
}
const users = await r.json();
if (!users.length) {
listEl.innerHTML = '<p class="text-zinc-500 text-sm">No users.</p>';
return;
}
listEl.innerHTML = users.map((u: { handle: string; display_name: string; is_admin: boolean }) => `
<div class="flex items-center justify-between px-4 py-3 rounded-lg bg-zinc-900 border border-zinc-800" data-handle="${u.handle}">
<div>
<span class="text-sm font-medium text-white">@${u.handle}</span>
${u.display_name ? `<span class="text-zinc-500 text-sm ml-2">${u.display_name}</span>` : ''}
${u.is_admin ? '<span class="ml-2 text-xs text-amber-400">admin</span>' : ''}
</div>
<button
class="delete-btn text-xs px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-red-900 hover:text-red-300 text-zinc-400 transition-colors"
data-handle="${u.handle}"
>Delete activities</button>
</div>
`).join('');
listEl.querySelectorAll<HTMLButtonElement>('.delete-btn').forEach(btn => {
btn.addEventListener('click', () => {
pendingHandle = btn.dataset.handle!;
confirmH.textContent = pendingHandle;
dialog.showModal();
});
});
}
confirmCancel.addEventListener('click', () => dialog.close());
dialog.addEventListener('click', e => { if (e.target === dialog) dialog.close(); });
confirmOk.addEventListener('click', async () => {
dialog.close();
const row = listEl.querySelector(`[data-handle="${pendingHandle}"]`);
const btn = row?.querySelector<HTMLButtonElement>('.delete-btn');
if (btn) { btn.disabled = true; btn.textContent = 'Deleting…'; }
try {
const r = await fetch(`/api/admin/users/${pendingHandle}/activities`, {
method: 'DELETE',
credentials: 'include',
});
const d = await r.json();
if (r.ok) {
if (btn) { btn.textContent = `Deleted (${d.deleted})`; btn.classList.add('text-green-500'); }
} else {
if (btn) { btn.disabled = false; btn.textContent = 'Error: ' + (d.detail ?? 'failed'); btn.classList.add('text-red-400'); }
}
} catch (e: any) {
if (btn) { btn.disabled = false; btn.textContent = 'Error'; btn.classList.add('text-red-400'); }
}
});
load();
</script>