fix(research): surface errors to UI + proceed without pages when all fetches fail

This commit is contained in:
2026-04-25 13:59:05 +02:00
parent 0bff6771ce
commit bde41be767
3 changed files with 34 additions and 24 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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}