diff --git a/ui/src/lib/server/pocketbase.ts b/ui/src/lib/server/pocketbase.ts index 3a41c82..51413ba 100644 --- a/ui/src/lib/server/pocketbase.ts +++ b/ui/src/lib/server/pocketbase.ts @@ -248,6 +248,14 @@ const RATINGS_CACHE_TTL = 5 * 60; // 5 minutes const HOME_STATS_CACHE_KEY = 'home:stats'; const HOME_STATS_CACHE_TTL = 10 * 60; // 10 minutes — counts don't need to be exact +const SCRAPING_TASKS_CACHE_KEY = 'admin:scraping_tasks'; +const AUDIO_JOBS_CACHE_KEY = 'admin:audio_jobs'; +const TRANSLATION_JOBS_CACHE_KEY = 'admin:translation_jobs'; +const ADMIN_JOBS_CACHE_TTL = 30; // 30 seconds — admin views poll frequently + +const BOOK_SLUGS_CACHE_KEY = 'books:slugs'; +const BOOK_SLUGS_CACHE_TTL = 10 * 60; // 10 minutes — slugs change rarely + async function getAllRatings(): Promise { const cached = await cache.get(RATINGS_CACHE_KEY); if (cached) return cached; @@ -272,6 +280,31 @@ export async function listBooks(): Promise { return books; } +export interface BookSlug { + slug: string; + title: string; +} + +/** + * Returns only the slug and title of every book. Cheaper than listBooks() — + * used for datalist autocomplete in admin forms. Cached for 10 minutes. + */ +export async function listBookSlugs(): Promise { + const cached = await cache.get(BOOK_SLUGS_CACHE_KEY); + if (cached) return cached; + // Re-use full books cache if already warm — avoids a second PocketBase call. + const fullCached = await cache.get(BOOKS_CACHE_KEY); + if (fullCached) { + const slugs = fullCached.map((b) => ({ slug: b.slug, title: b.title })); + await cache.set(BOOK_SLUGS_CACHE_KEY, slugs, BOOK_SLUGS_CACHE_TTL); + return slugs; + } + const items = await listAll('books', '', '+title').catch(() => [] as BookSlug[]); + const slugs = items.map((b) => ({ slug: b.slug, title: b.title })); + await cache.set(BOOK_SLUGS_CACHE_KEY, slugs, BOOK_SLUGS_CACHE_TTL); + return slugs; +} + /** * Fetch only the books whose slugs are in the given set. * Uses PocketBase filter `slug IN (...)` — a single request regardless of how @@ -1052,16 +1085,6 @@ export interface AudioCacheEntry { updated: string; } -export async function listAudioCache(): Promise { - const jobs = await listAll('audio_jobs', 'status="done"', '-finished'); - return jobs.map((j) => ({ - id: j.id, - cache_key: j.cache_key, - filename: `${j.cache_key}.mp3`, - updated: j.finished - })); -} - // ─── Scraping tasks ─────────────────────────────────────────────────────────── export interface ScrapingTask { @@ -1081,7 +1104,11 @@ export interface ScrapingTask { } export async function listScrapingTasks(): Promise { - return listAll('scraping_tasks', '', '-started'); + const cached = await cache.get(SCRAPING_TASKS_CACHE_KEY); + if (cached) return cached; + const tasks = await listN('scraping_tasks', 500, '', '-started'); + await cache.set(SCRAPING_TASKS_CACHE_KEY, tasks, ADMIN_JOBS_CACHE_TTL); + return tasks; } export async function getScrapingTask(id: string): Promise { @@ -1103,7 +1130,11 @@ export interface AudioJob { } export async function listAudioJobs(): Promise { - return listAll('audio_jobs', '', '-started'); + const cached = await cache.get(AUDIO_JOBS_CACHE_KEY); + if (cached) return cached; + const jobs = await listN('audio_jobs', 500, '', '-started'); + await cache.set(AUDIO_JOBS_CACHE_KEY, jobs, ADMIN_JOBS_CACHE_TTL); + return jobs; } /** @@ -1130,7 +1161,11 @@ export interface TranslationJob { } export async function listTranslationJobs(): Promise { - return listAll('translation_jobs', '', '-started'); + const cached = await cache.get(TRANSLATION_JOBS_CACHE_KEY); + if (cached) return cached; + const jobs = await listN('translation_jobs', 500, '', '-started'); + await cache.set(TRANSLATION_JOBS_CACHE_KEY, jobs, ADMIN_JOBS_CACHE_TTL); + return jobs; } export async function getAudioTime( diff --git a/ui/src/routes/admin/audio/+page.server.ts b/ui/src/routes/admin/audio/+page.server.ts index 5d4d818..2dbeaf3 100644 --- a/ui/src/routes/admin/audio/+page.server.ts +++ b/ui/src/routes/admin/audio/+page.server.ts @@ -1,6 +1,6 @@ import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -import { listAudioCache, listAudioJobs, type AudioCacheEntry, type AudioJob } from '$lib/server/pocketbase'; +import { listAudioJobs, type AudioCacheEntry, type AudioJob } from '$lib/server/pocketbase'; import { log } from '$lib/server/logger'; export const load: PageServerLoad = async ({ locals }) => { @@ -8,16 +8,20 @@ export const load: PageServerLoad = async ({ locals }) => { redirect(302, '/'); } - const [entries, jobs] = await Promise.all([ - listAudioCache().catch((e): AudioCacheEntry[] => { - log.warn('admin/audio', 'failed to load audio cache', { err: String(e) }); - return []; - }), - listAudioJobs().catch((e): AudioJob[] => { - log.warn('admin/audio', 'failed to load audio jobs', { err: String(e) }); - return []; - }) - ]); + const jobs = await listAudioJobs().catch((e): AudioJob[] => { + log.warn('admin/audio', 'failed to load audio jobs', { err: String(e) }); + return []; + }); + + // Derive cache entries from done jobs — no second query needed. + const entries: AudioCacheEntry[] = jobs + .filter((j) => j.status === 'done') + .map((j) => ({ + id: j.id, + cache_key: j.cache_key, + filename: `${j.cache_key}.mp3`, + updated: j.finished + })); return { entries, jobs }; }; diff --git a/ui/src/routes/admin/audio/+page.svelte b/ui/src/routes/admin/audio/+page.svelte index 73fef48..04325d2 100644 --- a/ui/src/routes/admin/audio/+page.svelte +++ b/ui/src/routes/admin/audio/+page.svelte @@ -1,6 +1,5 @@