Compare commits

..

2 Commits

Author SHA1 Message Date
Marcel
74cc4c8722 fix(admin): drop processed count from RUNNING import card
Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / OCR Service Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Semgrep Security Scan (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
The whole document load commits in one transaction, so a live counter
sits at 0 for the entire run and only jumps to the final number on
completion. Showing "0" next to the spinner read as "nothing happening"
and prompted repeated retriggers. Render just the spinner + running
label until the DONE branch displays the final processed count.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:56:00 +02:00
Marcel
548bc60747 fix(admin): include CSRF token on admin trigger/backfill POSTs
The four admin actions (trigger-import, generate-thumbnails,
backfill-versions, backfill-file-hashes) were posting bare fetches, so
the backend's CSRF filter would reject them once the protection is on.
Wrap each init with withCsrf() so the X-XSRF-TOKEN header is attached
from the cookie — same pattern other admin actions use.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:55:34 +02:00
3 changed files with 13 additions and 15 deletions

View File

@@ -3,6 +3,7 @@ import { onDestroy } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
import ImportStatusCard from './ImportStatusCard.svelte';
import type { ImportStatus } from './types.js';
import { withCsrf } from '$lib/shared/cookies';
let backfillResult: number | null = $state(null);
let backfillLoading = $state(false);
@@ -61,7 +62,7 @@ async function fetchImportStatus() {
}
async function triggerImport() {
const res = await fetch('/api/admin/trigger-import', { method: 'POST' });
const res = await fetch('/api/admin/trigger-import', withCsrf({ method: 'POST' }));
if (res.ok) {
importStatus = await res.json();
if (importStatus!.state === 'RUNNING') {
@@ -83,7 +84,7 @@ async function fetchThumbnailStatus() {
}
async function triggerThumbnails() {
const res = await fetch('/api/admin/generate-thumbnails', { method: 'POST' });
const res = await fetch('/api/admin/generate-thumbnails', withCsrf({ method: 'POST' }));
if (res.ok) {
thumbnailStatus = await res.json();
if (thumbnailStatus!.state === 'RUNNING') {
@@ -106,7 +107,7 @@ async function backfillVersions() {
backfillLoading = true;
backfillResult = null;
try {
const res = await fetch('/api/admin/backfill-versions', { method: 'POST' });
const res = await fetch('/api/admin/backfill-versions', withCsrf({ method: 'POST' }));
if (res.ok) {
const data = await res.json();
backfillResult = data.count;
@@ -120,7 +121,7 @@ async function backfillFileHashes() {
backfillHashesLoading = true;
backfillHashesResult = null;
try {
const res = await fetch('/api/admin/backfill-file-hashes', { method: 'POST' });
const res = await fetch('/api/admin/backfill-file-hashes', withCsrf({ method: 'POST' }));
if (res.ok) {
const data = await res.json();
backfillHashesResult = data.count;

View File

@@ -42,14 +42,9 @@ function reasonLabel(code: string): string {
aria-label={m.admin_system_import_status_running()}
class="inline-block h-5 w-5 animate-spin rounded-full border-2 border-ink-3 border-t-brand-mint motion-reduce:animate-none"
></span>
<div>
<p data-testid="processed-count" class="text-base font-bold text-ink">
{importStatus.processed}
</p>
<p class="font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
{m.admin_system_import_status_running()}
</p>
</div>
<p class="font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
{m.admin_system_import_status_running()}
</p>
</div>
{:else if importStatus?.state === 'DONE'}
<div class="mb-4 rounded-sm border border-green-200 bg-green-50 p-4 text-green-700">

View File

@@ -26,15 +26,17 @@ describe('ImportStatusCard', () => {
await expect.element(getByTestId('spinner')).toBeInTheDocument();
});
it('shows processed count at text-base while RUNNING', async () => {
it('shows no processed count while RUNNING (spinner only, no misleading 0)', async () => {
// The whole document load commits in one transaction, so a live count would sit at 0
// until the end. Show just the spinner + "running" label instead of a stuck "0".
const { getByTestId } = render(ImportStatusCard, {
props: {
importStatus: makeStatus({ state: 'RUNNING', statusCode: 'IMPORT_RUNNING', processed: 7 }),
importStatus: makeStatus({ state: 'RUNNING', statusCode: 'IMPORT_RUNNING', processed: 0 }),
ontrigger: () => {}
}
});
await expect.element(getByTestId('processed-count')).toHaveTextContent('7');
await expect.element(getByTestId('processed-count')).not.toBeInTheDocument();
});
it('shows processed count while DONE', async () => {