add re-extract from Strava originals endpoint and improve diag
- POST /api/admin/users/{handle}/reextract-originals: reads stored
originals/strava/*.json and re-runs strava_to_parsed + ingest_parsed
without hitting the Strava API; streams SSE progress; calls merge_all
and rebuild on completion
- GET /api/admin/users/{handle}/diag: now shows _merged/activities/
file counts, a sample of filenames in activities/ (with symlink flag),
and lists pending_files by name
- Admin page: Re-extract button per user with live SSE progress modal
This commit is contained in:
@@ -31,6 +31,16 @@ import Base from '../../layouts/Base.astro';
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Re-extract progress modal -->
|
||||
<dialog id="reextract-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">Re-extract from Strava originals — <span id="reextract-handle" class="text-zinc-400 font-mono"></span></h3>
|
||||
<button id="reextract-close" class="text-zinc-500 hover:text-zinc-200 text-xs px-2 py-1 rounded bg-zinc-800" disabled>Close</button>
|
||||
</div>
|
||||
<div id="reextract-summary" class="text-xs text-zinc-400 mb-2"></div>
|
||||
<div class="bg-zinc-950 rounded p-3 h-64 overflow-y-auto" id="reextract-log"></div>
|
||||
</dialog>
|
||||
|
||||
<!-- 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">
|
||||
@@ -62,6 +72,13 @@ import Base from '../../layouts/Base.astro';
|
||||
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 reextractDialog = document.getElementById('reextract-dialog') as HTMLDialogElement;
|
||||
const reextractHandle = document.getElementById('reextract-handle')!;
|
||||
const reextractSummary = document.getElementById('reextract-summary')!;
|
||||
const reextractLog = document.getElementById('reextract-log')!;
|
||||
const reextractClose = document.getElementById('reextract-close') as HTMLButtonElement;
|
||||
reextractClose.addEventListener('click', () => { reextractDialog.close(); load(); });
|
||||
const confirmOk = document.getElementById('confirm-ok')!;
|
||||
const confirmCancel = document.getElementById('confirm-cancel')!;
|
||||
|
||||
@@ -136,6 +153,11 @@ import Base from '../../layouts/Base.astro';
|
||||
data-handle="${u.handle}"
|
||||
title="Show diagnostic snapshot of this user's data directory"
|
||||
>Diag</button>
|
||||
<button
|
||||
class="reextract-btn text-xs px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-amber-900 hover:text-amber-300 text-zinc-400 transition-colors"
|
||||
data-handle="${u.handle}"
|
||||
title="Re-extract activities from stored Strava originals (no API call)"
|
||||
>Re-extract</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}"
|
||||
@@ -157,6 +179,63 @@ import Base from '../../layouts/Base.astro';
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
tbodyEl.querySelectorAll<HTMLButtonElement>('.reextract-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const h = btn.dataset.handle!;
|
||||
reextractHandle.textContent = h;
|
||||
reextractLog.innerHTML = '';
|
||||
reextractSummary.textContent = 'Starting…';
|
||||
reextractClose.disabled = true;
|
||||
reextractDialog.showModal();
|
||||
|
||||
let imported = 0, skipped = 0, errors = 0;
|
||||
try {
|
||||
const es = new EventSource(`/api/admin/users/${h}/reextract-originals`);
|
||||
// EventSource only does GET; use fetch + ReadableStream instead
|
||||
es.close();
|
||||
|
||||
const r = await fetch(`/api/admin/users/${h}/reextract-originals`, {
|
||||
method: 'POST', credentials: 'include',
|
||||
});
|
||||
const reader = r.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = '';
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
const lines = buf.split('\n\n');
|
||||
buf = lines.pop() ?? '';
|
||||
for (const chunk of lines) {
|
||||
const dataLine = chunk.split('\n').find(l => l.startsWith('data: '));
|
||||
if (!dataLine) continue;
|
||||
const ev = JSON.parse(dataLine.slice(6));
|
||||
if (ev.type === 'progress') {
|
||||
const color = ev.status === 'imported' ? 'text-green-400'
|
||||
: ev.status === 'error' ? 'text-red-400'
|
||||
: 'text-zinc-500';
|
||||
const line = document.createElement('div');
|
||||
line.className = `text-xs font-mono ${color}`;
|
||||
line.textContent = `[${ev.n}/${ev.total}] ${ev.status.padEnd(8)} ${ev.name}${ev.detail ? ' — ' + ev.detail : ''}`;
|
||||
reextractLog.appendChild(line);
|
||||
reextractLog.scrollTop = reextractLog.scrollHeight;
|
||||
if (ev.status === 'imported') imported++;
|
||||
else if (ev.status === 'error') errors++;
|
||||
else skipped++;
|
||||
reextractSummary.textContent = `Processing… ${ev.n}/${ev.total} — imported: ${imported}, skipped: ${skipped}, errors: ${errors}`;
|
||||
} else if (ev.type === 'done') {
|
||||
reextractSummary.textContent = `Done — imported: ${ev.imported}, skipped: ${ev.skipped}, errors: ${ev.errors}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
reextractSummary.textContent = 'Error: ' + String(err);
|
||||
} finally {
|
||||
reextractClose.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tbodyEl.querySelectorAll<HTMLButtonElement>('.diag-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const h = btn.dataset.handle!;
|
||||
|
||||
Reference in New Issue
Block a user