Compare commits

...

2 Commits

Author SHA1 Message Date
root
734ba68eed perf: cache translated chapter responses for 1 hour
Translation content is immutable once generated — add Cache-Control: public,
max-age=3600, stale-while-revalidate=86400 to handleTranslationRead so the
browser and any CDN reuse the response without hitting MinIO on repeat views.
Forward the header through the SvelteKit /api/translation/[slug]/[n] proxy
which previously stripped it by constructing a bare Content-Type-only Response.
2026-04-13 21:34:02 +05:00
root
708f8bcd6f fix: ai-jobs page empty list + missing Review button + no results in modal
Three bugs:

1. +page.server.ts returned an unawaited Promise — SvelteKit awaits it on
   the server anyway so data.jobs arrived as a plain AIJob[] on the client,
   not a Promise. The $effect calling .then() on an array silently failed,
   leaving jobs=[] and the table empty. Fixed by awaiting in the load fn.

2. +page.svelte $effect updated to assign data.jobs directly (plain array)
   instead of calling .then() on it.

3. handlers_textgen.go: final UpdateAIJob (payload+status write) used
   r.Context() which is cancelled when the SSE client disconnects. If the
   browser navigated away mid-job, results were silently dropped and the
   payload stayed as the initial {pattern} stub with no results array.
   Fixed by using context.Background() for the final write, matching the
   pattern already used in handlers_image.go.
2026-04-13 21:25:52 +05:00
5 changed files with 16 additions and 8 deletions

View File

@@ -1279,6 +1279,10 @@ func (s *Server) handleTranslationRead(w http.ResponseWriter, r *http.Request) {
return
}
// Translated chapter content is immutable once generated — cache aggressively.
// The browser and any intermediary (CDN, SvelteKit fetch cache) can reuse this
// response for 1 hour without hitting MinIO again.
w.Header().Set("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400")
writeJSON(w, 0, map[string]string{"html": buf.String(), "lang": lang})
}

View File

@@ -313,6 +313,8 @@ func (s *Server) handleAdminTextGenChapterNames(w http.ResponseWriter, r *http.R
}
// Mark job as done in PB, persisting results so the Review button works.
// Use context.Background() — r.Context() may be cancelled if the SSE client
// disconnected before processing finished, which would silently drop results.
if jobID != "" && s.deps.AIJobStore != nil {
status := domain.TaskStatusDone
if jobCtx.Err() != nil {
@@ -321,7 +323,7 @@ func (s *Server) handleAdminTextGenChapterNames(w http.ResponseWriter, r *http.R
resultsJSON, _ := json.Marshal(allResults)
finalPayload := fmt.Sprintf(`{"pattern":%q,"slug":%q,"results":%s}`,
req.Pattern, req.Slug, string(resultsJSON))
_ = s.deps.AIJobStore.UpdateAIJob(r.Context(), jobID, map[string]any{
_ = s.deps.AIJobStore.UpdateAIJob(context.Background(), jobID, map[string]any{
"status": string(status),
"items_done": chaptersDone,
"finished": time.Now().Format(time.RFC3339),

View File

@@ -6,8 +6,7 @@ export type { AIJob };
export const load: PageServerLoad = async () => {
// Parent layout already guards admin role.
// Stream jobs so navigation is instant; list populates a moment later.
const jobs = listAIJobs().catch((e): AIJob[] => {
const jobs = await listAIJobs().catch((e): AIJob[] => {
log.warn('admin/ai-jobs', 'failed to load ai jobs', { err: String(e) });
return [];
});

View File

@@ -9,9 +9,9 @@
let jobs = $state<AIJob[]>([]);
// Resolve streamed promise on load and on server reloads (invalidateAll)
// data.jobs is a plain AIJob[] (resolved on server); re-sync on invalidateAll
$effect(() => {
data.jobs.then((resolved) => { jobs = resolved; });
jobs = data.jobs;
});
// ── Live-poll while any job is in-flight ─────────────────────────────────────

View File

@@ -23,9 +23,12 @@ export const GET: RequestHandler = async ({ params, url }) => {
return new Response(null, { status: res.status });
}
const data = await res.json();
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
});
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
// Forward the immutable cache header from the backend so browsers and CDNs
// can cache translated chapter content without hitting MinIO on every load.
const cc = res.headers.get('Cache-Control');
if (cc) headers['Cache-Control'] = cc;
return new Response(JSON.stringify(data), { headers });
};
export const POST: RequestHandler = async ({ params, url, locals }) => {