- 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}
110 lines
3.3 KiB
Go
110 lines
3.3 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
|
|
"github.com/libnovel/backend/internal/domain"
|
|
)
|
|
|
|
// runTranslationTask executes one machine-translation task end-to-end and
|
|
// reports the result back to PocketBase.
|
|
func (r *Runner) runTranslationTask(ctx context.Context, task domain.TranslationTask) {
|
|
ctx, span := otel.Tracer("runner").Start(ctx, "runner.translation_task")
|
|
defer span.End()
|
|
span.SetAttributes(
|
|
attribute.String("task.id", task.ID),
|
|
attribute.String("book.slug", task.Slug),
|
|
attribute.Int("chapter.number", task.Chapter),
|
|
attribute.String("translation.lang", task.Lang),
|
|
)
|
|
|
|
log := r.deps.Log.With("task_id", task.ID, "slug", task.Slug, "chapter", task.Chapter, "lang", task.Lang)
|
|
log.Info("runner: translation task starting")
|
|
|
|
// Heartbeat goroutine — keeps the task alive while translation runs.
|
|
hbCtx, hbCancel := context.WithCancel(ctx)
|
|
defer hbCancel()
|
|
go func() {
|
|
tick := time.NewTicker(r.cfg.HeartbeatInterval)
|
|
defer tick.Stop()
|
|
for {
|
|
select {
|
|
case <-hbCtx.Done():
|
|
return
|
|
case <-tick.C:
|
|
if err := r.deps.Consumer.HeartbeatTask(ctx, task.ID); err != nil {
|
|
log.Warn("runner: heartbeat failed", "err", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
fail := func(msg string) {
|
|
log.Error("runner: translation task failed", "reason", msg)
|
|
r.tasksFailed.Add(1)
|
|
span.SetStatus(codes.Error, msg)
|
|
result := domain.TranslationResult{ErrorMessage: msg}
|
|
if err := r.deps.Consumer.FinishTranslationTask(ctx, task.ID, result); err != nil {
|
|
log.Error("runner: FinishTranslationTask failed", "err", err)
|
|
}
|
|
if r.deps.Notifier != nil {
|
|
_ = r.deps.Notifier.CreateNotification(ctx, "admin",
|
|
"Translation Failed",
|
|
fmt.Sprintf("Ch.%d of %s (%s): %s", task.Chapter, task.Slug, task.Lang, msg),
|
|
fmt.Sprintf("/books/%s", task.Slug))
|
|
}
|
|
}
|
|
|
|
// Guard: LibreTranslate must be configured.
|
|
if r.deps.LibreTranslate == nil {
|
|
fail("libretranslate client not configured (LIBRETRANSLATE_URL is empty)")
|
|
return
|
|
}
|
|
|
|
// 1. Read raw markdown chapter.
|
|
raw, err := r.deps.BookReader.ReadChapter(ctx, task.Slug, task.Chapter)
|
|
if err != nil {
|
|
fail(fmt.Sprintf("read chapter: %v", err))
|
|
return
|
|
}
|
|
if raw == "" {
|
|
fail("chapter text is empty")
|
|
return
|
|
}
|
|
|
|
// 2. Translate (chunked, concurrent).
|
|
translated, err := r.deps.LibreTranslate.Translate(ctx, raw, "en", task.Lang)
|
|
if err != nil {
|
|
fail(fmt.Sprintf("translate: %v", err))
|
|
return
|
|
}
|
|
|
|
// 3. Store translated markdown in MinIO.
|
|
key := r.deps.TranslationStore.TranslationObjectKey(task.Lang, task.Slug, task.Chapter)
|
|
if err := r.deps.TranslationStore.PutTranslation(ctx, key, []byte(translated)); err != nil {
|
|
fail(fmt.Sprintf("put translation: %v", err))
|
|
return
|
|
}
|
|
|
|
// 4. Report success.
|
|
r.tasksCompleted.Add(1)
|
|
span.SetStatus(codes.Ok, "")
|
|
result := domain.TranslationResult{ObjectKey: key}
|
|
if err := r.deps.Consumer.FinishTranslationTask(ctx, task.ID, result); err != nil {
|
|
log.Error("runner: FinishTranslationTask failed", "err", err)
|
|
}
|
|
if r.deps.Notifier != nil {
|
|
_ = r.deps.Notifier.CreateNotification(ctx, "admin",
|
|
"Translation Ready",
|
|
fmt.Sprintf("Ch.%d of %s translated to %s", task.Chapter, task.Slug, task.Lang),
|
|
fmt.Sprintf("/books/%s", task.Slug))
|
|
}
|
|
log.Info("runner: translation task finished", "key", key)
|
|
}
|