feat: import review step + admin notifications
Some checks failed
Release / Test backend (push) Failing after 16s
Release / Check ui (push) Failing after 33s
Release / Docker (push) Has been skipped
Release / Gitea Release (push) Has been skipped

- Import page: add file upload with review step before committing
- Backend: add analyze endpoint to preview chapters before import
- Add notifications collection + API for admin alerts
- Add bell icon in header with notification dropdown (admin only)
- Runner creates notification on import completion
- Notifications link to /admin/import for easy review
This commit is contained in:
root
2026-04-09 10:30:36 +05:00
parent bb55afb562
commit ab92bf84bb
7 changed files with 297 additions and 34 deletions

View File

@@ -15,11 +15,20 @@
finished: string;
}
interface PendingImport {
file: File;
title: string;
preview: { chapters: number; firstLines: string[] };
}
let tasks = $state<ImportTask[]>([]);
let loading = $state(true);
let uploading = $state(false);
let title = $state('');
let error = $state('');
let selectedFile = $state<File | null>(null);
let pendingImport = $state<PendingImport | null>(null);
let analyzing = $state(false);
async function loadTasks() {
loading = true;
@@ -36,35 +45,82 @@
}
}
async function handleSubmit(e: Event) {
e.preventDefault();
if (!title.trim()) return;
async function handleFileSelect(e: Event) {
const input = e.target as HTMLInputElement;
if (!input.files?.length) return;
const file = input.files[0];
const ext = file.name.split('.').pop()?.toLowerCase() || '';
if (ext !== 'pdf' && ext !== 'epub') {
error = 'Please select a PDF or EPUB file';
return;
}
selectedFile = file;
title = file.name.replace(/\.(pdf|epub)$/i, '').replace(/[-_]/g, ' ');
}
uploading = true;
async function analyzeFile() {
if (!selectedFile || !title.trim()) return;
analyzing = true;
error = '';
try {
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('title', title);
formData.append('analyze', 'true');
const res = await fetch('/api/admin/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title })
body: formData
});
if (res.ok) {
const data = await res.json();
pendingImport = {
file: selectedFile,
title: title,
preview: data.preview || { chapters: 0, firstLines: [] }
};
} else {
const d = await res.json().catch(() => ({}));
error = d.error || 'Failed to analyze file';
}
} catch (e) {
error = 'Failed to analyze file';
} finally {
analyzing = false;
}
}
async function startImport() {
if (!pendingImport) return;
uploading = true;
error = '';
try {
const formData = new FormData();
formData.append('file', pendingImport.file);
formData.append('title', pendingImport.title);
const res = await fetch('/api/admin/import', {
method: 'POST',
body: formData
});
if (res.ok) {
pendingImport = null;
selectedFile = null;
title = '';
await loadTasks();
} else {
const data = await res.json();
error = data.error || 'Upload failed';
const d = await res.json().catch(() => ({}));
error = d.error || 'Import failed';
}
} catch (e) {
error = 'Upload failed';
error = 'Import failed';
} finally {
uploading = false;
}
}
function cancelReview() {
pendingImport = null;
}
function formatDate(dateStr: string) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
@@ -88,30 +144,82 @@
<div class="max-w-4xl">
<h1 class="text-2xl font-bold mb-6">Import PDF/EPUB</h1>
<!-- Upload Form -->
<form onsubmit={handleSubmit} class="mb-8 p-4 bg-(--color-surface-2) rounded-lg">
<div class="flex gap-4">
<input
type="text"
bind:value={title}
placeholder="Book title"
class="flex-1 px-3 py-2 rounded bg-(--color-surface) border border-(--color-border) text-(--color-text)"
/>
{#if pendingImport}
<!-- Review Step -->
<div class="mb-8 p-6 bg-(--color-surface-2) rounded-lg border border-(--color-brand)/30">
<h2 class="text-lg font-semibold mb-4">Review Import</h2>
<div class="space-y-3 mb-6">
<div class="flex justify-between">
<span class="text-(--color-muted)">Title:</span>
<span class="font-medium">{pendingImport.title}</span>
</div>
<div class="flex justify-between">
<span class="text-(--color-muted)">File:</span>
<span>{pendingImport.file.name}</span>
</div>
<div class="flex justify-between">
<span class="text-(--color-muted)">Size:</span>
<span>{(pendingImport.file.size / 1024 / 1024).toFixed(2)} MB</span>
</div>
{#if pendingImport.preview.chapters > 0}
<div class="flex justify-between">
<span class="text-(--color-muted)">Detected chapters:</span>
<span class="text-green-400">{pendingImport.preview.chapters}</span>
</div>
{/if}
</div>
<div class="flex gap-3">
<button
onclick={startImport}
disabled={uploading}
class="px-4 py-2 bg-green-600 text-white rounded font-medium disabled:opacity-50"
>
{uploading ? 'Starting...' : 'Start Import'}
</button>
<button
onclick={cancelReview}
class="px-4 py-2 border border-(--color-border) rounded font-medium"
>
Cancel
</button>
</div>
</div>
{:else}
<!-- Upload Form -->
<form onsubmit={(e) => { e.preventDefault(); analyzeFile(); }} class="mb-8 p-4 bg-(--color-surface-2) rounded-lg">
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Select File (PDF or EPUB)</label>
<input
type="file"
accept=".pdf,.epub"
onchange={handleFileSelect}
class="w-full px-3 py-2 rounded bg-(--color-surface) border border-(--color-border) text-(--color-text)"
/>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Book Title</label>
<input
type="text"
bind:value={title}
placeholder="Enter book title"
class="w-full px-3 py-2 rounded bg-(--color-surface) border border-(--color-border) text-(--color-text)"
/>
</div>
{#if error}
<p class="mb-4 text-sm text-red-400">{error}</p>
{/if}
<button
type="submit"
disabled={uploading || !title.trim()}
disabled={analyzing || !selectedFile || !title.trim()}
class="px-4 py-2 bg-(--color-brand) text-(--color-surface) rounded font-medium disabled:opacity-50"
>
{uploading ? 'Creating...' : 'Import'}
{analyzing ? 'Analyzing...' : 'Review & Import'}
</button>
</div>
{#if error}
<p class="mt-2 text-sm text-red-400">{error}</p>
{/if}
<p class="mt-2 text-xs text-(--color-muted)">
Upload a PDF or EPUB file to import chapters. The runner will process the file.
</p>
</form>
<p class="mt-2 text-xs text-(--color-muted)">
Select a file to preview chapter count before importing.
</p>
</form>
{/if}
<!-- Task List -->
<h2 class="text-lg font-semibold mb-4">Import Tasks</h2>