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.
This commit is contained in:
@@ -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.
|
// 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 {
|
if jobID != "" && s.deps.AIJobStore != nil {
|
||||||
status := domain.TaskStatusDone
|
status := domain.TaskStatusDone
|
||||||
if jobCtx.Err() != nil {
|
if jobCtx.Err() != nil {
|
||||||
@@ -321,7 +323,7 @@ func (s *Server) handleAdminTextGenChapterNames(w http.ResponseWriter, r *http.R
|
|||||||
resultsJSON, _ := json.Marshal(allResults)
|
resultsJSON, _ := json.Marshal(allResults)
|
||||||
finalPayload := fmt.Sprintf(`{"pattern":%q,"slug":%q,"results":%s}`,
|
finalPayload := fmt.Sprintf(`{"pattern":%q,"slug":%q,"results":%s}`,
|
||||||
req.Pattern, req.Slug, string(resultsJSON))
|
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),
|
"status": string(status),
|
||||||
"items_done": chaptersDone,
|
"items_done": chaptersDone,
|
||||||
"finished": time.Now().Format(time.RFC3339),
|
"finished": time.Now().Format(time.RFC3339),
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ export type { AIJob };
|
|||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
// Parent layout already guards admin role.
|
// Parent layout already guards admin role.
|
||||||
// Stream jobs so navigation is instant; list populates a moment later.
|
const jobs = await listAIJobs().catch((e): AIJob[] => {
|
||||||
const jobs = listAIJobs().catch((e): AIJob[] => {
|
|
||||||
log.warn('admin/ai-jobs', 'failed to load ai jobs', { err: String(e) });
|
log.warn('admin/ai-jobs', 'failed to load ai jobs', { err: String(e) });
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
let jobs = $state<AIJob[]>([]);
|
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(() => {
|
$effect(() => {
|
||||||
data.jobs.then((resolved) => { jobs = resolved; });
|
jobs = data.jobs;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Live-poll while any job is in-flight ─────────────────────────────────────
|
// ── Live-poll while any job is in-flight ─────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user