feat(admin/system): add system maintenance page under /admin/system

Moves the system maintenance panel out of the old tab-based admin page
and into a dedicated route. Renders maintenance cards with spinner state
and success message on completion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-30 01:39:09 +02:00
committed by marcel
parent 010904d6e1
commit ff3ea70826
2 changed files with 112 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
let backfillResult: number | null = $state(null);
let backfillLoading = $state(false);
let backfillHashesResult: number | null = $state(null);
let backfillHashesLoading = $state(false);
async function backfillVersions() {
backfillLoading = true;
backfillResult = null;
try {
const res = await fetch('/api/admin/backfill-versions', { method: 'POST' });
if (res.ok) {
const data = await res.json();
backfillResult = data.count;
}
} finally {
backfillLoading = false;
}
}
async function backfillFileHashes() {
backfillHashesLoading = true;
backfillHashesResult = null;
try {
const res = await fetch('/api/admin/backfill-file-hashes', { method: 'POST' });
if (res.ok) {
const data = await res.json();
backfillHashesResult = data.count;
}
} finally {
backfillHashesLoading = false;
}
}
</script>
<div class="flex-1 overflow-y-auto p-6">
<div class="mx-auto max-w-2xl space-y-5">
<!-- Backfill versions -->
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
<h2 class="mb-1 font-sans text-sm font-bold text-ink">{m.admin_system_backfill_heading()}</h2>
<p class="mb-4 text-sm text-ink-2">{m.admin_system_backfill_description()}</p>
<button
onclick={backfillVersions}
disabled={backfillLoading}
class="rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-opacity hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
>
{backfillLoading ? '…' : m.admin_system_backfill_btn()}
</button>
{#if backfillResult !== null}
<p class="mt-4 rounded-sm border border-green-200 bg-green-50 p-3 text-sm text-green-700">
{m.admin_system_backfill_success({ count: backfillResult })}
</p>
{/if}
</div>
<!-- Backfill file hashes -->
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
<h2 class="mb-1 font-sans text-sm font-bold text-ink">
{m.admin_system_backfill_hashes_heading()}
</h2>
<p class="mb-4 text-sm text-ink-2">{m.admin_system_backfill_hashes_description()}</p>
<button
onclick={backfillFileHashes}
disabled={backfillHashesLoading}
class="rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-opacity hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
>
{backfillHashesLoading ? '…' : m.admin_system_backfill_hashes_btn()}
</button>
{#if backfillHashesResult !== null}
<p class="mt-4 rounded-sm border border-green-200 bg-green-50 p-3 text-sm text-green-700">
{m.admin_system_backfill_hashes_success({ count: backfillHashesResult })}
</p>
{/if}
</div>
</div>
</div>

View File

@@ -0,0 +1,34 @@
import { afterEach, describe, expect, it } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import Page from './+page.svelte';
afterEach(cleanup);
describe('Admin system page', () => {
it('renders the backfill versions heading', async () => {
render(Page, {});
await expect.element(page.getByText(/Verlaufsdaten auffüllen/i)).toBeInTheDocument();
});
it('renders the backfill versions button', async () => {
render(Page, {});
await expect
.element(page.getByRole('button', { name: /jetzt auffüllen/i }))
.toBeInTheDocument();
});
it('renders the backfill file hashes heading', async () => {
render(Page, {});
await expect
.element(page.getByRole('heading', { name: /Datei-Hashes berechnen/i }))
.toBeInTheDocument();
});
it('renders the file hashes button', async () => {
render(Page, {});
await expect
.element(page.getByRole('button', { name: /Datei-Hashes berechnen/i }))
.toBeInTheDocument();
});
});