From 48965a6f188f5dc49f9804ef76b1467323b2a0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl?= Date: Sun, 21 Dec 2025 19:31:58 -0300 Subject: [PATCH] Fix reviews and trust score calculation, improve metadata API docs - Fix reviews/recent and getRecentReviews to return all reviews (not just those with parsed ratings) - Fix trust score calculation to parse ratings from content text (e.g., [5/5]) - Fix getReviewSummary and getReviewStats to parse ratings from content - Fix OpenAPI schema for contact field (should be array, not object) - Fix popular/trending mints endpoints to show results even without pageviews --- src/docs/openapi.js | 17 ++++++- src/services/ReviewService.js | 87 ++++++++++++++++++++++++----------- src/services/TrustService.js | 43 ++++++++++++----- 3 files changed, 108 insertions(+), 39 deletions(-) diff --git a/src/docs/openapi.js b/src/docs/openapi.js index c8e3337..8ba724f 100644 --- a/src/docs/openapi.js +++ b/src/docs/openapi.js @@ -1794,9 +1794,22 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au description: { type: 'string', nullable: true }, description_long: { type: 'string', nullable: true }, contact: { - type: 'object', + type: 'array', nullable: true, - additionalProperties: { type: 'string' } + description: 'Contact methods for the mint operator', + items: { + type: 'object', + properties: { + method: { + type: 'string', + description: 'Contact method (email, twitter, nostr, etc.)' + }, + info: { + type: 'string', + description: 'Contact identifier (email address, handle, npub, etc.)' + } + } + } }, motd: { type: 'string', diff --git a/src/services/ReviewService.js b/src/services/ReviewService.js index 6f43054..10f7b4c 100644 --- a/src/services/ReviewService.js +++ b/src/services/ReviewService.js @@ -214,20 +214,34 @@ export function getReviews(mintId, options = {}) { /** * Get review stats for a mint + * Parses ratings from content text if rating column is null */ export function getReviewStats(mintId) { return queryOne(` SELECT COUNT(*) as total_reviews, - AVG(rating) as average_rating, + AVG(parsed_rating) as average_rating, COUNT(DISTINCT pubkey) as unique_reviewers, - SUM(CASE WHEN rating = 5 THEN 1 ELSE 0 END) as five_star, - SUM(CASE WHEN rating = 4 THEN 1 ELSE 0 END) as four_star, - SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) as three_star, - SUM(CASE WHEN rating = 2 THEN 1 ELSE 0 END) as two_star, - SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) as one_star - FROM reviews - WHERE mint_id = ? AND rating IS NOT NULL + SUM(CASE WHEN parsed_rating = 5 THEN 1 ELSE 0 END) as five_star, + SUM(CASE WHEN parsed_rating = 4 THEN 1 ELSE 0 END) as four_star, + SUM(CASE WHEN parsed_rating = 3 THEN 1 ELSE 0 END) as three_star, + SUM(CASE WHEN parsed_rating = 2 THEN 1 ELSE 0 END) as two_star, + SUM(CASE WHEN parsed_rating = 1 THEN 1 ELSE 0 END) as one_star + FROM ( + SELECT + pubkey, + CASE + WHEN rating IS NOT NULL THEN rating + WHEN content LIKE '[5/5]%' OR content LIKE '%(5/5)%' THEN 5 + WHEN content LIKE '[4/5]%' OR content LIKE '%(4/5)%' THEN 4 + WHEN content LIKE '[3/5]%' OR content LIKE '%(3/5)%' THEN 3 + WHEN content LIKE '[2/5]%' OR content LIKE '%(2/5)%' THEN 2 + WHEN content LIKE '[1/5]%' OR content LIKE '%(1/5)%' THEN 1 + ELSE NULL + END as parsed_rating + FROM reviews + WHERE mint_id = ? + ) parsed `, [mintId]); } @@ -381,24 +395,20 @@ export function countReviews(options = {}) { /** * Get review summary for a mint (quick overview) + * Parses ratings from content text if rating column is null */ export function getReviewSummary(mintId) { - const stats = queryOne(` + // First get total count of all reviews + const totalStats = queryOne(` SELECT COUNT(*) as total_reviews, - AVG(rating) as avg_rating, COUNT(DISTINCT pubkey) as unique_reviewers, - SUM(CASE WHEN rating = 5 THEN 1 ELSE 0 END) as five_star, - SUM(CASE WHEN rating = 4 THEN 1 ELSE 0 END) as four_star, - SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) as three_star, - SUM(CASE WHEN rating = 2 THEN 1 ELSE 0 END) as two_star, - SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) as one_star, MAX(created_at) as last_review_at FROM reviews - WHERE mint_id = ? AND rating IS NOT NULL + WHERE mint_id = ? `, [mintId]); - if (!stats || stats.total_reviews === 0) { + if (!totalStats || totalStats.total_reviews === 0) { return { total_reviews: 0, avg_rating: null, @@ -407,18 +417,43 @@ export function getReviewSummary(mintId) { }; } + // Parse ratings from content if rating column is null + const ratingStats = queryOne(` + SELECT + AVG(parsed_rating) as avg_rating, + SUM(CASE WHEN parsed_rating = 5 THEN 1 ELSE 0 END) as five_star, + SUM(CASE WHEN parsed_rating = 4 THEN 1 ELSE 0 END) as four_star, + SUM(CASE WHEN parsed_rating = 3 THEN 1 ELSE 0 END) as three_star, + SUM(CASE WHEN parsed_rating = 2 THEN 1 ELSE 0 END) as two_star, + SUM(CASE WHEN parsed_rating = 1 THEN 1 ELSE 0 END) as one_star + FROM ( + SELECT CASE + WHEN rating IS NOT NULL THEN rating + WHEN content LIKE '[5/5]%' OR content LIKE '%(5/5)%' THEN 5 + WHEN content LIKE '[4/5]%' OR content LIKE '%(4/5)%' THEN 4 + WHEN content LIKE '[3/5]%' OR content LIKE '%(3/5)%' THEN 3 + WHEN content LIKE '[2/5]%' OR content LIKE '%(2/5)%' THEN 2 + WHEN content LIKE '[1/5]%' OR content LIKE '%(1/5)%' THEN 1 + ELSE NULL + END as parsed_rating + FROM reviews + WHERE mint_id = ? + ) parsed + WHERE parsed_rating IS NOT NULL + `, [mintId]); + return { - total_reviews: stats.total_reviews, - avg_rating: stats.avg_rating ? Math.round(stats.avg_rating * 10) / 10 : null, - unique_reviewers: stats.unique_reviewers, + total_reviews: totalStats.total_reviews, + avg_rating: ratingStats && ratingStats.avg_rating ? Math.round(ratingStats.avg_rating * 10) / 10 : null, + unique_reviewers: totalStats.unique_reviewers, rating_distribution: { - 5: stats.five_star || 0, - 4: stats.four_star || 0, - 3: stats.three_star || 0, - 2: stats.two_star || 0, - 1: stats.one_star || 0 + 5: (ratingStats && ratingStats.five_star) || 0, + 4: (ratingStats && ratingStats.four_star) || 0, + 3: (ratingStats && ratingStats.three_star) || 0, + 2: (ratingStats && ratingStats.two_star) || 0, + 1: (ratingStats && ratingStats.one_star) || 0 }, - last_review_at: stats.last_review_at ? unixToISO(stats.last_review_at) : null + last_review_at: totalStats.last_review_at ? unixToISO(totalStats.last_review_at) : null }; } diff --git a/src/services/TrustService.js b/src/services/TrustService.js index b9727ad..1919ca6 100644 --- a/src/services/TrustService.js +++ b/src/services/TrustService.js @@ -142,14 +142,23 @@ function calculateSpeedScore(mintId, component) { * Calculate reviews score (max 20 points) */ function calculateReviewsScore(mintId, component) { - // Get review stats + // Get review stats - include all reviews, not just those with parsed ratings + // Parse rating from content if rating column is null (ratings often in "[5/5]" format in content) const stats = queryOne(` SELECT COUNT(*) as count, - AVG(rating) as avg_rating, + AVG(CASE + WHEN rating IS NOT NULL THEN rating + WHEN content LIKE '[5/5]%' OR content LIKE '%(5/5)%' THEN 5 + WHEN content LIKE '[4/5]%' OR content LIKE '%(4/5)%' THEN 4 + WHEN content LIKE '[3/5]%' OR content LIKE '%(3/5)%' THEN 3 + WHEN content LIKE '[2/5]%' OR content LIKE '%(2/5)%' THEN 2 + WHEN content LIKE '[1/5]%' OR content LIKE '%(1/5)%' THEN 1 + ELSE NULL + END) as avg_rating, COUNT(DISTINCT pubkey) as unique_reviewers FROM reviews - WHERE mint_id = ? AND rating IS NOT NULL + WHERE mint_id = ? `, [mintId]); if (!stats || stats.count === 0) { @@ -163,20 +172,32 @@ function calculateReviewsScore(mintId, component) { component.details = { review_count: reviewCount, - average_rating: Math.round(avgRating * 10) / 10, + average_rating: avgRating ? Math.round(avgRating * 10) / 10 : null, unique_reviewers: uniqueReviewers }; - // Base score from average rating (1-5 scale) - const ratingScore = ((avgRating - 1) / 4) * (trustWeights.reviews * 0.7); + // If we have a calculated average rating, use it + if (avgRating) { + // Base score from average rating (1-5 scale) + const ratingScore = ((avgRating - 1) / 4) * (trustWeights.reviews * 0.7); - // Bonus for review quantity (max 30% of review score) - const quantityBonus = Math.min( - trustWeights.reviews * 0.3, - (Math.log10(reviewCount + 1) / 2) * trustWeights.reviews * 0.3 + // Bonus for review quantity (max 30% of review score) + const quantityBonus = Math.min( + trustWeights.reviews * 0.3, + (Math.log10(reviewCount + 1) / 2) * trustWeights.reviews * 0.3 + ); + + return ratingScore + quantityBonus; + } + + // Fallback: if we have reviews but no parseable ratings, give partial credit based on count + // This rewards having reviews even without explicit ratings + const quantityScore = Math.min( + trustWeights.reviews * 0.5, // Max 50% of review score without ratings + (Math.log10(reviewCount + 1) / 2) * trustWeights.reviews * 0.5 ); - return ratingScore + quantityBonus; + return quantityScore; } /**