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
This commit is contained in:
Michaël
2025-12-21 19:31:58 -03:00
parent c2a7267459
commit 48965a6f18
3 changed files with 108 additions and 39 deletions

View File

@@ -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',

View File

@@ -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
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 = ? AND rating IS NOT NULL
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
};
}

View File

@@ -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,10 +172,12 @@ 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
};
// 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);
@@ -177,6 +188,16 @@ function calculateReviewsScore(mintId, component) {
);
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 quantityScore;
}
/**