Fix bugs found in local testing
- Dashboard auto-refresh rendered the full layout into its own refresh container, producing a duplicate sidebar every 60s; it now renders only the body partial. - 'Run Now' runs synchronously with a bounded timeout and returns refreshed results plus success/error feedback, instead of firing-and-forgetting with no signal. - Price-history chart data moved from a <script> block to a data- attribute: templ does not interpolate expressions inside <script> element content, so the JSON was emitted literally. - The htmx indicator spinner was permanently visible due to CSS source order; the indicator rules now follow .v-spinner. Also refreshes README for this session's changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,13 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"veola/internal/apify"
|
||||
"veola/internal/models"
|
||||
@@ -261,46 +261,16 @@ func (a *App) runPreview(ctx context.Context, it models.Item) ([]apify.UnifiedRe
|
||||
var merged []apify.UnifiedResult
|
||||
primarySource := ""
|
||||
for _, p := range plans {
|
||||
actorID := p.ActorID()
|
||||
if actorID == "" {
|
||||
continue
|
||||
}
|
||||
raw, err := a.Apify.Run(ctx, actorID, p.Input())
|
||||
decoded, err := a.Scheduler.ExecutePlan(ctx, p)
|
||||
if err != nil {
|
||||
slog.Warn("preview run failed", "actor", actorID, "query", p.Query(), "err", err)
|
||||
slog.Warn("preview plan failed",
|
||||
"provider", p.Provider(),
|
||||
"marketplace", p.Marketplace(),
|
||||
"query", p.Query(),
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
decoded, _ := apify.Decode(raw, p.Source())
|
||||
for i := range decoded {
|
||||
decoded[i].MatchedQuery = p.Query()
|
||||
}
|
||||
usable := 0
|
||||
for _, r := range decoded {
|
||||
if r.URL != "" && r.Price > 0 {
|
||||
usable++
|
||||
}
|
||||
}
|
||||
slog.Info("preview decoded",
|
||||
"marketplace", previewMarket,
|
||||
"actor", actorID,
|
||||
"query", p.Query(),
|
||||
"raw", len(raw),
|
||||
"decoded", len(decoded),
|
||||
"usable", usable,
|
||||
)
|
||||
if usable == 0 && len(raw) > 0 {
|
||||
var sample map[string]any
|
||||
if err := json.Unmarshal(raw[0], &sample); err == nil {
|
||||
ks := make([]string, 0, len(sample))
|
||||
for k := range sample {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
slog.Warn("preview decoded zero usable rows; raw item keys",
|
||||
"actor", actorID,
|
||||
"keys", ks,
|
||||
)
|
||||
}
|
||||
}
|
||||
merged = append(merged, decoded...)
|
||||
if primarySource == "" {
|
||||
primarySource = p.Source()
|
||||
@@ -431,9 +401,40 @@ func (a *App) PostRunItem(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
go a.Scheduler.RunPoll(context.Background(), *it)
|
||||
// Re-render the row immediately so HTMX has something to swap in.
|
||||
render(w, r, templates.ItemRow(*it, a.Auth.CSRFToken(r.Context())))
|
||||
|
||||
// Run synchronously so the response reflects the finished poll. Bounded so
|
||||
// a slow Apify actor run can't tie the request up indefinitely (eBay
|
||||
// Browse API polls finish in seconds, well within this). Detached from the
|
||||
// request context so a client disconnect mid-run doesn't abort DB writes.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
a.Scheduler.RunPoll(ctx, *it)
|
||||
|
||||
// RunPoll writes best price, last_polled_at, and last_poll_error; re-fetch
|
||||
// so the rendered partial shows the post-poll state.
|
||||
fresh, err := a.Store.GetItem(r.Context(), id)
|
||||
if err != nil || fresh == nil {
|
||||
http.Error(w, "could not reload item after run", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// The results page asks for a refreshed listing table; the items list
|
||||
// asks for a refreshed row. Both POST to this same endpoint.
|
||||
if r.PostFormValue("from") == "results" {
|
||||
d, err := a.buildItemResultsData(r, fresh, 1, "found_desc")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if fresh.LastPollError != "" {
|
||||
d.RunError = "Run finished with errors: " + fresh.LastPollError
|
||||
} else {
|
||||
d.RunMsg = fmt.Sprintf("Run complete. Showing %d listing(s).", len(d.Results))
|
||||
}
|
||||
render(w, r, templates.ItemResultsTable(d))
|
||||
return
|
||||
}
|
||||
render(w, r, templates.ItemRow(*fresh, a.Auth.CSRFToken(r.Context())))
|
||||
}
|
||||
|
||||
func (a *App) GetItemError(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user