fix(research): surface errors to UI + proceed without pages when all fetches fail
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"marktvogt.de/backend/internal/domain/market/research"
|
||||
"marktvogt.de/backend/internal/pkg/ai"
|
||||
"marktvogt.de/backend/internal/pkg/apierror"
|
||||
"marktvogt.de/backend/internal/pkg/scrape"
|
||||
"marktvogt.de/backend/internal/pkg/search"
|
||||
)
|
||||
@@ -41,17 +42,17 @@ func (h *ResearchHandler) Research(c *gin.Context) {
|
||||
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid market ID"})
|
||||
c.JSON(http.StatusBadRequest, apierror.NewResponse(apierror.BadRequest("invalid_id", "invalid market ID")))
|
||||
return
|
||||
}
|
||||
|
||||
m, err := h.service.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrMarketNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "market not found"})
|
||||
c.JSON(http.StatusNotFound, apierror.NewResponse(apierror.NotFound("market")))
|
||||
} else {
|
||||
slog.ErrorContext(ctx, "research: get market failed", "market_id", id, "err", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
c.JSON(http.StatusInternalServerError, apierror.NewResponse(apierror.Internal("failed to load market")))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -69,25 +70,29 @@ func (h *ResearchHandler) Research(c *gin.Context) {
|
||||
if errors.As(err, &pe) {
|
||||
switch pe.Code {
|
||||
case ai.ErrRateLimited:
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "rate limited"})
|
||||
c.JSON(http.StatusServiceUnavailable, apierror.NewResponse(apierror.BadRequest("rate_limited", "KI rate limit erreicht, bitte kurz warten")))
|
||||
return
|
||||
case ai.ErrSchemaViolation:
|
||||
slog.ErrorContext(ctx, "research schema violation", "market_id", id, "raw", pe.RawOutput, "inner", pe.Inner)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "model returned invalid output"})
|
||||
c.JSON(http.StatusInternalServerError, apierror.NewResponse(apierror.Internal("Modell hat ungültige Ausgabe geliefert")))
|
||||
return
|
||||
case ai.ErrInternal, ai.ErrQuotaExceeded, ai.ErrTimeout, ai.ErrInvalidRequest, ai.ErrUnavailable:
|
||||
// fall through to generic 500
|
||||
case ai.ErrInvalidRequest:
|
||||
slog.ErrorContext(ctx, "research invalid request", "market_id", id, "err", pe.Message)
|
||||
c.JSON(http.StatusInternalServerError, apierror.NewResponse(apierror.Internal("KI-Anfrage ungültig: "+pe.Message)))
|
||||
return
|
||||
case ai.ErrInternal, ai.ErrQuotaExceeded, ai.ErrTimeout, ai.ErrUnavailable:
|
||||
// fall through to generic message
|
||||
}
|
||||
}
|
||||
slog.ErrorContext(ctx, "research failed", "market_id", id, "err", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "research failed"})
|
||||
c.JSON(http.StatusInternalServerError, apierror.NewResponse(apierror.Internal("llm enrich: "+err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
var raw llmOutput
|
||||
if err := json.Unmarshal(out.Raw, &raw); err != nil {
|
||||
slog.ErrorContext(ctx, "research: unmarshal LLM output failed", "market_id", id, "err", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
c.JSON(http.StatusInternalServerError, apierror.NewResponse(apierror.Internal("Ausgabe konnte nicht verarbeitet werden")))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package research
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
@@ -44,7 +43,7 @@ func FetchAll(ctx context.Context, sc Scraper, urls []string, concurrency int) (
|
||||
_ = g.Wait()
|
||||
|
||||
if len(pages) == 0 {
|
||||
return nil, errors.New("all candidate URLs failed to fetch")
|
||||
slog.Warn("no pages fetched; proceeding without page content")
|
||||
}
|
||||
return pages, nil
|
||||
}
|
||||
|
||||
@@ -120,19 +120,25 @@
|
||||
</a>
|
||||
<h1 class="mt-1 text-2xl font-bold">Markt bearbeiten</h1>
|
||||
</div>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/research"
|
||||
use:enhance={() => {
|
||||
researching = true;
|
||||
return async ({ update }) => {
|
||||
researching = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Button type="submit" variant="secondary" loading={researching}>Mit KI recherchieren</Button>
|
||||
</form>
|
||||
<div class="flex flex-col items-end gap-1">
|
||||
<form
|
||||
method="POST"
|
||||
action="?/research"
|
||||
use:enhance={() => {
|
||||
researching = true;
|
||||
return async ({ update }) => {
|
||||
researching = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Button type="submit" variant="secondary" loading={researching}>Mit KI recherchieren</Button
|
||||
>
|
||||
</form>
|
||||
{#if form?.error && !form?.research}
|
||||
<p class="text-xs text-red-500 dark:text-red-400">{form.error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if researchResult}
|
||||
|
||||
Reference in New Issue
Block a user