From 94417ada72caaef229fffb5e9d5d6e4e1c26fa5f Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 11 Jul 2026 17:43:18 +0200 Subject: [PATCH] fix(feed): repair 2 bugs found in the Phase 2 #2 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG-1 (search.py, high): the scrape search source logged one VIDEOS_SEARCH quota event PER continuation page inside the paging loop, but actions_today counts events — so a single user search that pages N times consumed N against search_daily_limit_per_user (its docstring even warns it only works for once-per-action logging). Log exactly once per request after the loop instead; the API source already logs once via record_usage (it never auto-pages). BUG-2 (Feed.tsx, medium): the optimistic-override reset effect keyed on query.dataUpdatedAt also fired on fetchNextPage (infinite scroll bumps dataUpdatedAt while page 1 is unchanged), so a just-hidden card flashed back mid-scroll. Guard with a page-count ref: only reset on a real refetch, not an append. The two override-reset effects legitimately differ now (resolves the would-be C-F1 "identical effects" cleanup). tsc green, ruff clean, localdev boots healthy. --- backend/app/routes/search.py | 10 ++++++++-- frontend/src/components/Feed.tsx | 13 +++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/backend/app/routes/search.py b/backend/app/routes/search.py index 139b6f7..d744714 100644 --- a/backend/app/routes/search.py +++ b/backend/app/routes/search.py @@ -253,8 +253,6 @@ def search_youtube( page = yt.search_videos(term, page_token=next_cursor) else: page = search_scrape.search(term, continuation=next_cursor) - # Zero-cost, but logged so the per-user daily cap above keeps counting it. - quota.log_action(db, user.id, quota.QuotaAction.VIDEOS_SEARCH) except (YouTubeError, search_scrape.ScrapeError) as exc: if collected: break # keep what we already gathered @@ -274,6 +272,14 @@ def search_youtube( if source == "api" or len(collected) >= limit or not next_cursor: break + # Count this as ONE per-user search against the daily cap. The scrape source spends no + # quota, so it never logs an event on its own; the API source already logged exactly one + # (via record_usage — it never auto-pages). Logging once here, after a search that produced + # at least one page (a total failure raises 502 above and never reaches this), keeps the cap + # counting user searches instead of internal continuation pages. + if source != "api": + quota.log_action(db, user.id, quota.QuotaAction.VIDEOS_SEARCH) + ordered = collected[:limit] if ordered: diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index 633df6d..37e0423 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -184,9 +184,18 @@ export default function Feed({ setOverrides({}); setSavedOverrides({}); }, [filters]); + // ...but NOT on infinite-scroll appends: fetchNextPage also bumps dataUpdatedAt while page 1's + // rows are unchanged, so clearing overrides there would make a just-hidden card flash back. + // Only a real refetch (page count doesn't grow) is authoritative. + const pagesLenRef = useRef(0); useEffect(() => { - setOverrides({}); - setSavedOverrides({}); + const len = query.data?.pages?.length ?? 0; + const appended = len > pagesLenRef.current; + pagesLenRef.current = len; + if (!appended) { + setOverrides({}); + setSavedOverrides({}); + } }, [query.dataUpdatedAt]); const countQuery = useQuery({