- svelte.config.js: paths.relative=false so CSS uses absolute /_app/ paths (fixes blank home page after redirect)
- ai-jobs: fix openReview() mutating stale alias r instead of $state review — was causing 'Loading results...' to never resolve for chapter-names/image-gen/description
- notifications bell: redesign with All/Unread tabs, per-item dismiss (×), mark-all-read, clear-all, 'View all' footer link
- /admin/notifications: new dedicated full-page notifications view
- api/notifications proxy: add PATCH (mark-all-read) and DELETE (clear-all, dismiss) handlers
- runner: add CreateNotification calls on success/failure in runScrapeTask, runAudioTask, runTranslationTask
- storage/import.go: real PDF (dslipak/pdf) and EPUB (archive/zip + x/net/html) parsing replacing stubs
- translation admin page: stream jobs Promise instead of blocking navigation
- store.go: DeleteNotification, ClearAllNotifications, MarkAllNotificationsRead methods
- handlers_notifications.go + server.go: PATCH /api/notifications, DELETE /api/notifications, DELETE /api/notifications/{id}
66 lines
2.3 KiB
TypeScript
66 lines
2.3 KiB
TypeScript
import { redirect } from '@sveltejs/kit';
|
|
import type { Actions, PageServerLoad } from './$types';
|
|
import { listBookSlugs, listTranslationJobs, type TranslationJob } from '$lib/server/pocketbase';
|
|
import { backendFetch } from '$lib/server/scraper';
|
|
import { log } from '$lib/server/logger';
|
|
|
|
export const load: PageServerLoad = async ({ locals }) => {
|
|
if (locals.user?.role !== 'admin') {
|
|
redirect(302, '/');
|
|
}
|
|
|
|
// Stream jobs — navigation is instant, list populates shortly after.
|
|
const jobs = listTranslationJobs().catch((e): TranslationJob[] => {
|
|
log.warn('admin/translation', 'failed to load translation jobs', { err: String(e) });
|
|
return [];
|
|
});
|
|
|
|
// Books list is needed immediately for the enqueue form, but use cache so
|
|
// it's fast on repeat visits.
|
|
const books = await listBookSlugs().catch((e): Awaited<ReturnType<typeof listBookSlugs>> => {
|
|
log.warn('admin/translation', 'failed to load book slugs', { err: String(e) });
|
|
return [];
|
|
});
|
|
|
|
return { books, jobs };
|
|
};
|
|
|
|
export const actions: Actions = {
|
|
bulk: async ({ request, locals }) => {
|
|
if (locals.user?.role !== 'admin') {
|
|
return { success: false, error: 'Unauthorized' };
|
|
}
|
|
|
|
const form = await request.formData();
|
|
const slug = form.get('slug')?.toString().trim() ?? '';
|
|
const lang = form.get('lang')?.toString().trim() ?? '';
|
|
const from = parseInt(form.get('from')?.toString() ?? '1', 10);
|
|
const to = parseInt(form.get('to')?.toString() ?? '1', 10);
|
|
|
|
if (!slug || !lang) {
|
|
return { success: false, error: 'slug and lang are required' };
|
|
}
|
|
if (isNaN(from) || isNaN(to) || from < 1 || to < from) {
|
|
return { success: false, error: 'Invalid chapter range' };
|
|
}
|
|
|
|
try {
|
|
const res = await backendFetch('/api/admin/translation/bulk', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ slug, lang, from, to })
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => '');
|
|
log.error('admin/translation', 'bulk enqueue failed', { status: res.status, body });
|
|
return { success: false, error: `Backend error ${res.status}: ${body}` };
|
|
}
|
|
const data = await res.json();
|
|
return { success: true, enqueued: data.enqueued as number };
|
|
} catch (e) {
|
|
log.error('admin/translation', 'bulk enqueue fetch error', { err: String(e) });
|
|
return { success: false, error: String(e) };
|
|
}
|
|
}
|
|
};
|