add structured logging and admin diagnostics to serve
- bincio.serve logger wired into uvicorn output: rebuild steps, upload
errors, strava-zip progress all now appear in the server log
- _trigger_rebuild: capture stdout/stderr, log errors instead of silently
discarding; exceptions logged with traceback instead of swallowed
- upload handler: log per-file errors with traceback; include error detail
in the SSE event sent back to the browser
- strava-zip handler: log imported/error counts on completion
- GET /api/admin/users/{handle}/diag: snapshot of a user's data dir
(file counts, sizes, index activity counts, pending uploads)
- POST /api/admin/users/{handle}/rebuild-sync: blocking rebuild that
returns full stdout/stderr — for debugging without SSH log access
- Admin page: Diag button per user opens a modal showing the diag JSON
This commit is contained in:
@@ -31,6 +31,15 @@ import Base from '../../layouts/Base.astro';
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Diag modal -->
|
||||
<dialog id="diag-dialog" class="rounded-xl bg-zinc-900 border border-zinc-700 p-6 text-white max-w-2xl w-full backdrop:bg-black/60">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-sm">Data directory snapshot — <span id="diag-handle" class="text-zinc-400 font-mono"></span></h3>
|
||||
<button id="diag-close" class="text-zinc-500 hover:text-zinc-200 text-xs px-2 py-1 rounded bg-zinc-800">Close</button>
|
||||
</div>
|
||||
<pre id="diag-output" class="text-xs font-mono bg-zinc-950 rounded p-4 overflow-auto max-h-96 text-green-300 whitespace-pre-wrap"></pre>
|
||||
</dialog>
|
||||
|
||||
<!-- 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-1">Reset all data for <strong id="confirm-handle" class="text-white"></strong>?</p>
|
||||
@@ -44,10 +53,15 @@ import Base from '../../layouts/Base.astro';
|
||||
</Base>
|
||||
|
||||
<script>
|
||||
const overviewEl = document.getElementById('disk-overview')!;
|
||||
const tbodyEl = document.getElementById('user-list')!;
|
||||
const dialog = document.getElementById('confirm-dialog') as HTMLDialogElement;
|
||||
const confirmH = document.getElementById('confirm-handle')!;
|
||||
const overviewEl = document.getElementById('disk-overview')!;
|
||||
const tbodyEl = document.getElementById('user-list')!;
|
||||
const dialog = document.getElementById('confirm-dialog') as HTMLDialogElement;
|
||||
const confirmH = document.getElementById('confirm-handle')!;
|
||||
const diagDialog = document.getElementById('diag-dialog') as HTMLDialogElement;
|
||||
const diagHandle = document.getElementById('diag-handle')!;
|
||||
const diagOutput = document.getElementById('diag-output')!;
|
||||
document.getElementById('diag-close')!.addEventListener('click', () => diagDialog.close());
|
||||
diagDialog.addEventListener('click', e => { if (e.target === diagDialog) diagDialog.close(); });
|
||||
const confirmOk = document.getElementById('confirm-ok')!;
|
||||
const confirmCancel = document.getElementById('confirm-cancel')!;
|
||||
|
||||
@@ -117,6 +131,11 @@ import Base from '../../layouts/Base.astro';
|
||||
<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
|
||||
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="rebuild-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}"
|
||||
@@ -138,6 +157,22 @@ import Base from '../../layouts/Base.astro';
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
tbodyEl.querySelectorAll<HTMLButtonElement>('.diag-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const h = btn.dataset.handle!;
|
||||
diagHandle.textContent = h;
|
||||
diagOutput.textContent = 'Loading…';
|
||||
diagDialog.showModal();
|
||||
try {
|
||||
const r = await fetch(`/api/admin/users/${h}/diag`, { credentials: 'include' });
|
||||
const d = await r.json();
|
||||
diagOutput.textContent = JSON.stringify(d, null, 2);
|
||||
} catch (err) {
|
||||
diagOutput.textContent = String(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tbodyEl.querySelectorAll<HTMLButtonElement>('.rebuild-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const h = btn.dataset.handle!;
|
||||
|
||||
Reference in New Issue
Block a user