achievement badges added#147
Conversation
|
@ValayaDase is attempting to deploy a commit to the karan3431's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
🎉 Thank you for your Pull Request! We're thrilled to have your contribution to FreshScan AI. Before we review, please make sure you have:
A maintainer will review your code as soon as possible! |
There was a problem hiding this comment.
Pull request overview
Adds vendor achievement badges across the backend and frontend so vendor milestone achievements can be persisted, recalculated, and displayed in the Leaderboard and Market Map UI surfaces.
Changes:
- Introduces
public.vendor_achievementstable + RLS/grants for persisting awarded achievements. - Adds backend award/recalculation + API enrichment to return vendor achievement data alongside vendor/market responses.
- Adds UI rendering for achievement badge icons (Leaderboard and Market Map) and updates shared TS types.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| supabase/.temp/cli-latest | Adds a Supabase CLI artifact file (should likely be ignored). |
| supabase/.branches/_current_branch | Adds a Supabase branch-tracking artifact file (should likely be ignored). |
| src/pages/MarketMapPage.tsx | Renders up to 4 achievement icons in map popups and the selected market panel. |
| src/pages/Leaderboard.tsx | Renders achievement icons under vendor entries on the leaderboard. |
| src/lib/types.ts | Adds VendorAchievement type and attaches achievements to Market. |
| backend/vendors.py | Implements achievement definitions, awarding logic, lookup helper, and enriches vendor API responses with achievements. |
| backend/supabase/snippets/Untitled query 230.sql | Adds a manual SQL snippet for inserting achievements (likely dev-only). |
| backend/supabase/migrations/20240702001000_grant_app_table_permissions.sql | Grants required table privileges for Supabase API roles. |
| backend/supabase/migrations/20240702000000_add_vendor_achievements.sql | Creates vendor_achievements table with indexes + RLS policy. |
| backend/main.py | Adds scan-payload hardening, triggers achievement refresh after scans, and enriches vendor/market endpoints with achievements. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ts = row.get("timestamp") or "" | ||
| day = ts[:10] |
| recent_streak = _longest_recent_grade_a_streak(rows) | ||
| clean_days = _clean_grade_a_days(rows) |
| def _top_percent_vendor_ids(db) -> set[str]: | ||
| vendors = ( | ||
| db.table("vendors") | ||
| .select("id, avg_freshness_score, total_scans") | ||
| .order("avg_freshness_score", desc=True) | ||
| .execute() | ||
| ) |
| @@ -70,7 +233,11 @@ async def get_leaderboard(limit: int = Query(default=20, ge=1, le=100)): | |||
| .limit(limit) | |||
| .execute() | |||
| ) | |||
| return {"success": True, "leaderboard": resp.data or []} | |||
| rows = resp.data or [] | |||
| achievements = get_vendor_achievements(db_getter(), [str(v["id"]) for v in rows]) | |||
| for vendor in rows: | |||
| vendor["achievements"] = achievements.get(str(vendor["id"]), []) | |||
| return {"success": True, "leaderboard": rows} | |||
| @app.get("/api/v1/vendors/leaderboard") | ||
| async def get_vendor_leaderboard(): | ||
| """Get vendor leaderboard sorted by trust score""" | ||
| try: | ||
| resp = ( | ||
| _db() | ||
| .table("vendors") | ||
| .select("id, name, trust_score, total_scans, avg_freshness_score, lat, lng") | ||
| .select("id, name, address, trust_score, total_scans, avg_freshness_score, trust_badge, trend, lat, lng") | ||
| .order("trust_score", desc=True) | ||
| .limit(10) | ||
| .execute() | ||
| ) | ||
| return {"success": True, "leaderboard": resp.data} | ||
| rows = resp.data or [] | ||
| achievements = get_vendor_achievements(_db(), [str(v["id"]) for v in rows]) | ||
| for vendor in rows: | ||
| vendor["achievements"] = achievements.get(str(vendor["id"]), []) | ||
| return {"success": True, "leaderboard": rows} |
| insert into public.vendor_achievements | ||
| (vendor_id, code, title, description, icon, tier) | ||
| select |
| const ACHIEVEMENT_ICON = { | ||
| bolt: Zap, | ||
| calendar: CalendarCheck, | ||
| crown: Crown, | ||
| shield: ShieldCheck, | ||
| }; | ||
|
|
||
| function AchievementBadges({ achievements = [] }: { achievements?: VendorAchievement[] }) { | ||
| if (!achievements.length) return null; | ||
|
|
||
| return ( | ||
| <div className="mt-2 flex flex-wrap gap-1.5"> | ||
| {achievements.slice(0, 4).map((achievement) => { | ||
| const Icon = | ||
| ACHIEVEMENT_ICON[achievement.icon as keyof typeof ACHIEVEMENT_ICON] ?? ShieldCheck; | ||
| return ( | ||
| <span | ||
| key={achievement.code} | ||
| title={`${achievement.title}: ${achievement.description}`} | ||
| className="inline-flex h-6 w-6 items-center justify-center border border-neon/50 bg-surface-lowest text-neon shadow-[0_0_14px_rgba(195,244,0,0.22)]" | ||
| aria-label={achievement.title} | ||
| > | ||
| <Icon size={13} strokeWidth={2.4} /> | ||
| </span> | ||
| ); | ||
| })} | ||
| </div> | ||
| ); | ||
| } |
| const ACHIEVEMENT_ICON = { | ||
| bolt: Zap, | ||
| calendar: CalendarCheck, | ||
| crown: Crown, | ||
| shield: ShieldCheck, | ||
| }; | ||
|
|
||
| function AchievementBadges({ achievements = [] }: { achievements?: VendorAchievement[] }) { | ||
| if (!achievements.length) return null; | ||
|
|
||
| return ( | ||
| <div className="mt-2 flex flex-wrap gap-1.5"> | ||
| {achievements.slice(0, 4).map((achievement) => { | ||
| const Icon = | ||
| ACHIEVEMENT_ICON[achievement.icon as keyof typeof ACHIEVEMENT_ICON] ?? ShieldCheck; | ||
| return ( | ||
| <span | ||
| key={achievement.code} | ||
| title={`${achievement.title}: ${achievement.description}`} | ||
| className="inline-flex h-6 w-6 items-center justify-center border border-[#c3f400]/60 bg-[#071014] text-[#c3f400] shadow-[0_0_14px_rgba(195,244,0,0.24)]" | ||
| aria-label={achievement.title} | ||
| > | ||
| <Icon size={13} strokeWidth={2.4} /> | ||
| </span> | ||
| ); | ||
| })} | ||
| </div> | ||
| ); | ||
| } |
| <span | ||
| key={achievement.code} | ||
| title={`${achievement.title}: ${achievement.description}`} | ||
| className="inline-flex h-6 w-6 items-center justify-center border border-neon/50 bg-surface-lowest text-neon shadow-[0_0_14px_rgba(195,244,0,0.22)]" | ||
| aria-label={achievement.title} | ||
| > |
| <span | ||
| key={achievement.code} | ||
| title={`${achievement.title}: ${achievement.description}`} | ||
| className="inline-flex h-6 w-6 items-center justify-center border border-[#c3f400]/60 bg-[#071014] text-[#c3f400] shadow-[0_0_14px_rgba(195,244,0,0.24)]" | ||
| aria-label={achievement.title} | ||
| > |
|
Warning
|
| Layer / File(s) | Summary |
|---|---|
Database schema, permissions, and seed data backend/supabase/migrations/20240702000000_add_vendor_achievements.sql, backend/supabase/migrations/20240702001000_grant_app_table_permissions.sql, backend/supabase/snippets/Untitled query 230.sql |
Creates vendor_achievements table with RLS/policy/indexes, grants role permissions on vendors/scans/vendor_achievements, and adds a seed insert for the top vendor's badges. |
Achievement computation and vendor metrics recalculation backend/vendors.py |
Adds ACHIEVEMENTS mapping, streak/clean-day/top-percent helpers, recalculate_vendor_metrics_and_achievements, and get_vendor_achievements; wires these into leaderboard, trust-score, and recalculate endpoints. |
Scan and market API integration backend/main.py |
Reorganizes imports, normalizes scan payload fields, refreshes vendor metrics/achievements after scan creation, and attaches achievements to vendors, leaderboard, and market responses. |
Frontend achievement badges src/lib/types.ts, src/pages/Leaderboard.tsx, src/pages/MarketMapPage.tsx |
Adds VendorAchievement type and extends Market/Vendor types; renders achievement badge icons on the Leaderboard vendor rows and Market Map popups/panels. |
Estimated code review effort: 3 (Moderate) | ~30 minutes
Sequence Diagram(s)
sequenceDiagram
participant Client
participant ScanEndpoint
participant recalculate_vendor_metrics_and_achievements
participant Database
participant Cache
Client->>ScanEndpoint: POST /api/v1/scan
ScanEndpoint->>Database: insert scan record
ScanEndpoint->>recalculate_vendor_metrics_and_achievements: _refresh_vendor_after_scan(vendor_id)
recalculate_vendor_metrics_and_achievements->>Database: load scan history, compute metrics
recalculate_vendor_metrics_and_achievements->>Database: update vendor trust_score/badge/trend
recalculate_vendor_metrics_and_achievements->>Database: upsert vendor_achievements
recalculate_vendor_metrics_and_achievements->>Cache: clear markets namespace
ScanEndpoint-->>Client: scan payload
Possibly related issues
- Fullstack: Vendor Gamification (Achievement Badges) #59: Implements the vendor achievements/gamification feature end-to-end (schema, backend awarding logic, and leaderboard/market badge UI) matching this issue's objective.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The title clearly matches the main change: adding vendor achievement badges. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
✨ Finishing Touches
🧪 Generate unit tests (beta)
- Create PR with unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/main.py (1)
774-786: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winRemove the shadowed vendor leaderboard route and extract the achievement-attachment helper
backend/main.pyalready defines/api/v1/vendors/leaderboardbeforevendors_routeris included, so the/leaderboardhandler inbackend/vendors.pyis shadowed. The repeatedget_vendor_achievements→ attach loop across these vendor/map responses should be shared to avoid drift.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/main.py` around lines 774 - 786, The `/api/v1/vendors/leaderboard` handler in `vendors.py` is being shadowed by the earlier route defined in `main.py`, so remove or relocate the duplicate route registration and keep a single authoritative leaderboard endpoint. Also extract the repeated `get_vendor_achievements` plus per-vendor `achievements` attachment logic used by `get_vendors` and the other vendor/map responses into a shared helper so the behavior stays consistent. Use the existing `get_vendors`, `get_vendor_achievements`, and `vendors_router` symbols to locate and consolidate the affected code.
🧹 Nitpick comments (3)
src/pages/Leaderboard.tsx (1)
47-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract
AchievementBadges/ACHIEVEMENT_ICONinto a shared component.This component and icon map are duplicated verbatim in
src/pages/MarketMapPage.tsx(only styling classes differ). Extracting to a sharedsrc/components/AchievementBadges.tsxavoids divergence (e.g., icon mapping updates only applied in one place) and centralizes styling.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/Leaderboard.tsx` around lines 47 - 75, `AchievementBadges` and `ACHIEVEMENT_ICON` are duplicated in `Leaderboard.tsx` and `MarketMapPage.tsx`; extract them into a shared `AchievementBadges` component so icon mapping and rendering stay in sync. Move the shared logic (including the icon lookup and slice/map behavior) into a reusable component under `src/components`, then update both pages to import it and pass any page-specific styling or class overrides through props instead of keeping separate copies.backend/supabase/migrations/20240702000000_add_vendor_achievements.sql (1)
13-13: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueRedundant index on
vendor_id.The
UNIQUE (vendor_id, code)constraint at Line 13 already creates a composite index withvendor_idas the leading column, which Postgres can use forvendor_id-only lookups. The separatevendor_achievements_vendor_id_idx(Line 22-23) duplicates that coverage and adds unnecessary write/storage overhead.♻️ Proposed simplification
-CREATE INDEX IF NOT EXISTS vendor_achievements_vendor_id_idx - ON public.vendor_achievements (vendor_id); - CREATE INDEX IF NOT EXISTS vendor_achievements_code_idx ON public.vendor_achievements (code);Also applies to: 22-23
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/supabase/migrations/20240702000000_add_vendor_achievements.sql` at line 13, The migration adds a redundant standalone index on vendor_id because the UNIQUE (vendor_id, code) constraint in the vendor_achievements table already provides usable coverage for vendor_id lookups. Remove the extra vendor_achievements_vendor_id_idx creation from the migration and keep the unique composite constraint as the only index-related definition for this access pattern.backend/supabase/snippets/Untitled query 230.sql (1)
10-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueStatic analysis: duplicate table alias.
SQLFluff flags
public.vendorsas reused without an alias between the outer query and the correlated subquery. This is likely a false positive (separate scopes), but adding explicit aliases would remove the ambiguity and satisfy the linter.♻️ Proposed clarification
-from public.vendors +from public.vendors v cross join ( values ... ) as badge(code, title, description, icon, tier) -where public.vendors.id = ( +where v.id = ( select id from public.vendors order by avg_freshness_score desc limit 1 )Also applies to: 42-44
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/supabase/snippets/Untitled` query 230.sql at line 10, The query is using public.vendors in both the outer query and a correlated subquery without explicit aliases, which triggers the duplicate table alias lint warning. Update the SQL in the affected snippet to give each vendors reference a distinct alias in the outer query and subquery, and then adjust all column references to use those aliases consistently. This should be applied wherever the same pattern appears in the snippet, including the referenced repeated section.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/supabase/snippets/Untitled` query 230.sql:
- Around line 1-45: The seed SQL in the vendor achievements insert is creating
all badge rows for the top-ranked vendor without checking the real achievement
conditions. Update the script so it either derives eligibility from the same
metric rules used by recalculate_vendor_metrics_and_achievements in
backend/vendors.py, or clearly marks the data as non-production demo seed and
gates it from real environments. Keep the existing insert pattern with
vendor_achievements, vendors, and the badge values, but ensure only genuinely
qualified achievements are inserted.
In `@backend/vendors.py`:
- Around line 112-198: The vendor metrics refresh is doing unbounded
full-history and full-table reads on every scan, which makes the request path
grow linearly with data size. Update recalculate_vendor_metrics_and_achievements
and _top_percent_vendor_ids so they avoid fetching all scans/vendors each time,
using incremental counters and/or LIMIT/aggregate queries for the top-percent
lookup. Also move the _refresh_vendor_after_scan call off the synchronous
scan-processing path in backend/main.py into a background task or queue so scan
submission is not blocked.
---
Outside diff comments:
In `@backend/main.py`:
- Around line 774-786: The `/api/v1/vendors/leaderboard` handler in `vendors.py`
is being shadowed by the earlier route defined in `main.py`, so remove or
relocate the duplicate route registration and keep a single authoritative
leaderboard endpoint. Also extract the repeated `get_vendor_achievements` plus
per-vendor `achievements` attachment logic used by `get_vendors` and the other
vendor/map responses into a shared helper so the behavior stays consistent. Use
the existing `get_vendors`, `get_vendor_achievements`, and `vendors_router`
symbols to locate and consolidate the affected code.
---
Nitpick comments:
In `@backend/supabase/migrations/20240702000000_add_vendor_achievements.sql`:
- Line 13: The migration adds a redundant standalone index on vendor_id because
the UNIQUE (vendor_id, code) constraint in the vendor_achievements table already
provides usable coverage for vendor_id lookups. Remove the extra
vendor_achievements_vendor_id_idx creation from the migration and keep the
unique composite constraint as the only index-related definition for this access
pattern.
In `@backend/supabase/snippets/Untitled` query 230.sql:
- Line 10: The query is using public.vendors in both the outer query and a
correlated subquery without explicit aliases, which triggers the duplicate table
alias lint warning. Update the SQL in the affected snippet to give each vendors
reference a distinct alias in the outer query and subquery, and then adjust all
column references to use those aliases consistently. This should be applied
wherever the same pattern appears in the snippet, including the referenced
repeated section.
In `@src/pages/Leaderboard.tsx`:
- Around line 47-75: `AchievementBadges` and `ACHIEVEMENT_ICON` are duplicated
in `Leaderboard.tsx` and `MarketMapPage.tsx`; extract them into a shared
`AchievementBadges` component so icon mapping and rendering stay in sync. Move
the shared logic (including the icon lookup and slice/map behavior) into a
reusable component under `src/components`, then update both pages to import it
and pass any page-specific styling or class overrides through props instead of
keeping separate copies.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7aa97097-0723-488d-b34f-aabb6431cff7
⛔ Files ignored due to path filters (1)
supabase/.temp/cli-latestis excluded by!**/.temp/**
📒 Files selected for processing (9)
backend/main.pybackend/supabase/migrations/20240702000000_add_vendor_achievements.sqlbackend/supabase/migrations/20240702001000_grant_app_table_permissions.sqlbackend/supabase/snippets/Untitled query 230.sqlbackend/vendors.pysrc/lib/types.tssrc/pages/Leaderboard.tsxsrc/pages/MarketMapPage.tsxsupabase/.branches/_current_branch
| insert into public.vendor_achievements | ||
| (vendor_id, code, title, description, icon, tier) | ||
| select | ||
| id, | ||
| badge.code, | ||
| badge.title, | ||
| badge.description, | ||
| badge.icon, | ||
| badge.tier | ||
| from public.vendors | ||
| cross join ( | ||
| values | ||
| ( | ||
| 'fresh_streak_50', | ||
| '50x Grade A Streak', | ||
| 'Awarded for 50 consecutive Grade A scans.', | ||
| 'bolt', | ||
| 'legendary' | ||
| ), | ||
| ( | ||
| 'consistent_30d', | ||
| '30 Days Consistently Fresh', | ||
| 'Awarded after Grade A scans across 30 clean scan days.', | ||
| 'calendar', | ||
| 'neon' | ||
| ), | ||
| ( | ||
| 'trusted_volume_100', | ||
| '100 Verified Scans', | ||
| 'Awarded after 100 recorded freshness scans.', | ||
| 'shield', | ||
| 'trusted' | ||
| ), | ||
| ( | ||
| 'top_1_percent', | ||
| 'Top 1% Freshness', | ||
| 'Awarded to the highest freshness performers on the vendor leaderboard.', | ||
| 'crown', | ||
| 'elite' | ||
| ) | ||
| ) as badge(code, title, description, icon, tier) | ||
| where public.vendors.id = ( | ||
| select id from public.vendors order by avg_freshness_score desc limit 1 | ||
| ) | ||
| on conflict (vendor_id, code) do nothing; No newline at end of file |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Seed script awards achievements without validating criteria.
This unconditionally inserts all four achievement badges (fresh_streak_50, consistent_30d, trusted_volume_100, top_1_percent) for whichever vendor currently has the highest avg_freshness_score, with no check against the actual thresholds (50-scan streak, 30 clean days, 100 scans, top-1% ranking) enforced by recalculate_vendor_metrics_and_achievements in backend/vendors.py. If this is run against production, it will fabricate achievement data inconsistent with the computed logic elsewhere in the PR.
If this is intended purely as local/demo seed data, consider a comment header clarifying that, and/or gating it so it can't accidentally be applied to a real environment.
🧰 Tools
🪛 SQLFluff (4.2.2)
[error] 10-10: Duplicate table alias 'vendors'. Table aliases should be unique.
(AL04)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/supabase/snippets/Untitled` query 230.sql around lines 1 - 45, The
seed SQL in the vendor achievements insert is creating all badge rows for the
top-ranked vendor without checking the real achievement conditions. Update the
script so it either derives eligibility from the same metric rules used by
recalculate_vendor_metrics_and_achievements in backend/vendors.py, or clearly
marks the data as non-production demo seed and gates it from real environments.
Keep the existing insert pattern with vendor_achievements, vendors, and the
badge values, but ensure only genuinely qualified achievements are inserted.
| def _top_percent_vendor_ids(db) -> set[str]: | ||
| vendors = ( | ||
| db.table("vendors") | ||
| .select("id, avg_freshness_score, total_scans") | ||
| .order("avg_freshness_score", desc=True) | ||
| .execute() | ||
| ) | ||
| rows = [v for v in (vendors.data or []) if (v.get("total_scans") or 0) >= 5] | ||
| if not rows: | ||
| return set() | ||
| cutoff = max(1, int(len(rows) * 0.01)) | ||
| return {str(v["id"]) for v in rows[:cutoff]} | ||
|
|
||
|
|
||
| def _upsert_vendor_achievement(db, vendor_id: str, code: str, metadata: dict | None = None): | ||
| achievement = ACHIEVEMENTS[code] | ||
| db.table("vendor_achievements").upsert( | ||
| { | ||
| "vendor_id": vendor_id, | ||
| "code": code, | ||
| "title": achievement["title"], | ||
| "description": achievement["description"], | ||
| "icon": achievement["icon"], | ||
| "tier": achievement["tier"], | ||
| "metadata": metadata or {}, | ||
| }, | ||
| on_conflict="vendor_id,code", | ||
| ).execute() | ||
|
|
||
|
|
||
| def recalculate_vendor_metrics_and_achievements(db, vendor_id: str) -> dict: | ||
| """Update vendor trust metrics and award achievements from scan history.""" | ||
| scans = ( | ||
| db.table("scans") | ||
| .select("freshness_index, final_grade, timestamp") | ||
| .eq("vendor_id", vendor_id) | ||
| .order("timestamp", desc=True) | ||
| .execute() | ||
| ) | ||
| rows = [r for r in (scans.data or []) if r.get("freshness_index") is not None] | ||
| if not rows: | ||
| return {"total_scans": 0, "achievements": []} | ||
|
|
||
| scores = [r["freshness_index"] for r in rows] | ||
| total = len(scores) | ||
| avg = round(sum(scores) / total, 2) | ||
| badge = _compute_badge(avg, total) | ||
| trend = _compute_trend(db, vendor_id) | ||
|
|
||
| db.table("vendors").update( | ||
| { | ||
| "avg_freshness_score": avg, | ||
| "trust_score": avg, | ||
| "total_scans": total, | ||
| "trust_badge": badge, | ||
| "trend": trend, | ||
| } | ||
| ).eq("id", vendor_id).execute() | ||
|
|
||
| awarded = [] | ||
| recent_streak = _longest_recent_grade_a_streak(rows) | ||
| clean_days = _clean_grade_a_days(rows) | ||
|
|
||
| candidates = [] | ||
| if recent_streak >= 50: | ||
| candidates.append(("fresh_streak_50", {"streak": recent_streak})) | ||
| if clean_days >= 30: | ||
| candidates.append(("consistent_30d", {"clean_days": clean_days})) | ||
| if total >= 100: | ||
| candidates.append(("trusted_volume_100", {"total_scans": total})) | ||
| if vendor_id in _top_percent_vendor_ids(db): | ||
| candidates.append(("top_1_percent", {"avg_freshness_score": avg})) | ||
|
|
||
| for code, metadata in candidates: | ||
| try: | ||
| _upsert_vendor_achievement(db, vendor_id, code, metadata) | ||
| awarded.append(code) | ||
| except Exception as exc: | ||
| print(f"Achievement award failed for {vendor_id}/{code}: {exc}") | ||
|
|
||
| return { | ||
| "avg_score": avg, | ||
| "total_scans": total, | ||
| "trust_badge": badge, | ||
| "trend": trend, | ||
| "achievements": awarded, | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
Unbounded full-history recompute on every scan write.
recalculate_vendor_metrics_and_achievements fetches the vendor's entire scan history with no limit/pagination (Line 144-150), and _top_percent_vendor_ids (Line 112-123) fetches every vendor row on each call. Both run synchronously inside _refresh_vendor_after_scan, which is awaited directly in the scan-processing request path in backend/main.py (Lines 510, 547). As scan volume and vendor count grow, this adds unbounded, linearly-increasing latency to every single scan submission.
Consider either:
- Moving this recalculation to a background task (e.g.,
BackgroundTasks/task queue) so it doesn't block the scan response, and/or - Maintaining incremental counters (e.g., running streak, clean-day count) instead of recomputing from full history each time, and querying only the top-percent cutoff via
LIMIT/aggregate instead of pulling all vendor rows.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/vendors.py` around lines 112 - 198, The vendor metrics refresh is
doing unbounded full-history and full-table reads on every scan, which makes the
request path grow linearly with data size. Update
recalculate_vendor_metrics_and_achievements and _top_percent_vendor_ids so they
avoid fetching all scans/vendors each time, using incremental counters and/or
LIMIT/aggregate queries for the top-percent lookup. Also move the
_refresh_vendor_after_scan call off the synchronous scan-processing path in
backend/main.py into a background task or queue so scan submission is not
blocked.
Description
Adds vendor achievement badges to incentivize vendors based on scan-history milestones.
This PR introduces a new
vendor_achievementstable, backend award logic, and UI support for displaying achievement badges in the Vendor Leaderboard and Market Map surfaces.Achievements currently supported:
50x Grade A Streak30 Days Consistently Fresh100 Verified ScansTop 1% FreshnessBackend changes include:
UI changes include:
/leaderboard.Manual badge test SQL used:
insert into public.vendor_achievements
(vendor_id, code, title, description, icon, tier)
select
id,
badge.code,
badge.title,
badge.description,
badge.icon,
badge.tier
from public.vendors
cross join (
values
(
'fresh_streak_50',
'50x Grade A Streak',
'Awarded for 50 consecutive Grade A scans.',
'bolt',
'legendary'
),
(
'consistent_30d',
'30 Days Consistently Fresh',
'Awarded after Grade A scans across 30 clean scan days.',
'calendar',
'neon'
),
(
'trusted_volume_100',
'100 Verified Scans',
'Awarded after 100 recorded freshness scans.',
'shield',
'trusted'
),
(
'top_1_percent',
'Top 1% Freshness',
'Awarded to the highest freshness performers on the vendor leaderboard.',
'crown',
'elite'
)
) as badge(code, title, description, icon, tier)
where public.vendors.id = (
select id from public.vendors order by avg_freshness_score desc limit 1
)
on conflict (vendor_id, code) do nothing;
Checklist
npm run lintpasses with no errorsnpm run buildcompiles without TypeScript errorspython -m pytestpasses (including new tests I added).envfiles, API keys, secrets, model weights, or__pycache__in this diffmain, not mergedSummary by CodeRabbit
New Features
Bug Fixes
Database