admin: mark ghost users (no DB account) and add Delete dir button
- /api/admin/disk now includes in_db flag per user (true if account exists in DB)
- Ghost users (directory exists, no DB account) show amber 'ghost' badge and only
Diag + Delete dir buttons (no Re-extract, Rebuild, Reset pwd, Reset data)
- DELETE /api/admin/users/{handle}/directory wipes the entire directory and updates
the root manifest; refuses if the account still exists in the DB
- Wires up rmdir-btn with a window.confirm before calling the new endpoint
This commit is contained in:
@@ -123,32 +123,14 @@ import Base from '../../layouts/Base.astro';
|
||||
const leaked = u.leaked_zips_count > 0
|
||||
? `<span class="text-red-400 font-medium ml-2" title="${u.leaked_zips_count} orphaned temp ZIP(s)">⚠ ${fmt(u.leaked_zips_mb)} leaked</span>`
|
||||
: '';
|
||||
const ghostBadge = !u.in_db
|
||||
? `<span class="text-amber-500 text-xs ml-1" title="No account in database">ghost</span>`
|
||||
: '';
|
||||
const stravaNote = u.originals_strava_mb > 0
|
||||
? `<span class="text-zinc-600 text-xs ml-1">(${fmt(u.originals_strava_mb)} Strava)</span>`
|
||||
: '';
|
||||
return `
|
||||
<tr class="border-b border-zinc-800/50 hover:bg-zinc-900/40" data-handle="${u.handle}">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<a href="/u/${u.handle}/" class="text-white hover:text-zinc-300">@${u.handle}</a>
|
||||
${leaked}
|
||||
</div>
|
||||
${bar(rowPct)}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-zinc-300 font-medium tabular-nums">${fmt(u.total_mb)}</td>
|
||||
<td class="px-4 py-3 text-right text-zinc-400 tabular-nums">
|
||||
${fmt(u.activities_mb)}
|
||||
<span class="text-zinc-600 text-xs block">${u.activities_count} files</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-zinc-400 tabular-nums">
|
||||
${u.originals_mb > 0 ? fmt(u.originals_mb) : '—'}
|
||||
${stravaNote}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-zinc-400 tabular-nums">${u.merged_mb > 0 ? fmt(u.merged_mb) : '—'}</td>
|
||||
<td class="px-4 py-3 text-right text-zinc-400 tabular-nums">${u.images_mb > 0 ? fmt(u.images_mb) : '—'}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button
|
||||
const actionButtons = u.in_db
|
||||
? `<button
|
||||
class="diag-btn text-xs px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-zinc-200 transition-colors"
|
||||
data-handle="${u.handle}"
|
||||
title="Show diagnostic snapshot of this user's data directory"
|
||||
@@ -172,7 +154,40 @@ import Base from '../../layouts/Base.astro';
|
||||
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}"
|
||||
title="Wipe all activities, originals, edits and images — account is kept"
|
||||
>Reset data</button>
|
||||
>Reset data</button>`
|
||||
: `<button
|
||||
class="diag-btn text-xs px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-zinc-200 transition-colors"
|
||||
data-handle="${u.handle}"
|
||||
title="Show diagnostic snapshot of this user's data directory"
|
||||
>Diag</button>
|
||||
<button
|
||||
class="rmdir-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}"
|
||||
title="Delete the entire directory for this ghost user (no DB account)"
|
||||
>Delete dir</button>`;
|
||||
return `
|
||||
<tr class="border-b border-zinc-800/50 hover:bg-zinc-900/40" data-handle="${u.handle}">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<a href="/u/${u.handle}/" class="text-white hover:text-zinc-300">@${u.handle}</a>
|
||||
${ghostBadge}${leaked}
|
||||
</div>
|
||||
${bar(rowPct)}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-zinc-300 font-medium tabular-nums">${fmt(u.total_mb)}</td>
|
||||
<td class="px-4 py-3 text-right text-zinc-400 tabular-nums">
|
||||
${fmt(u.activities_mb)}
|
||||
<span class="text-zinc-600 text-xs block">${u.activities_count} files</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-zinc-400 tabular-nums">
|
||||
${u.originals_mb > 0 ? fmt(u.originals_mb) : '—'}
|
||||
${stravaNote}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-zinc-400 tabular-nums">${u.merged_mb > 0 ? fmt(u.merged_mb) : '—'}</td>
|
||||
<td class="px-4 py-3 text-right text-zinc-400 tabular-nums">${u.images_mb > 0 ? fmt(u.images_mb) : '—'}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex gap-2 justify-end">
|
||||
${actionButtons}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -320,6 +335,35 @@ import Base from '../../layouts/Base.astro';
|
||||
dialog.showModal();
|
||||
});
|
||||
});
|
||||
|
||||
tbodyEl.querySelectorAll<HTMLButtonElement>('.rmdir-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const h = btn.dataset.handle!;
|
||||
if (!confirm(`Delete entire directory for ghost user "${h}"? This cannot be undone.`)) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Deleting…';
|
||||
try {
|
||||
const r = await fetch(`/api/admin/users/${h}/directory`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
const d = await r.json();
|
||||
if (r.ok) {
|
||||
btn.textContent = 'Deleted';
|
||||
btn.classList.add('text-green-500');
|
||||
setTimeout(() => load(), 1500);
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Error: ' + (d.detail ?? 'failed');
|
||||
btn.classList.add('text-red-400');
|
||||
}
|
||||
} catch {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Error';
|
||||
btn.classList.add('text-red-400');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
confirmCancel.addEventListener('click', () => dialog.close());
|
||||
|
||||
Reference in New Issue
Block a user