diff --git a/src/docs/openapi.js b/src/docs/openapi.js index a4a8dc7..a083cbb 100644 --- a/src/docs/openapi.js +++ b/src/docs/openapi.js @@ -157,6 +157,67 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au } } }, + '/stats/timeline': { + get: { + tags: ['System'], + summary: 'Activity timeline', + description: 'Returns daily activity timeline for probes, incidents, and reviews', + operationId: 'getStatsTimeline', + parameters: [{ + name: 'days', + in: 'query', + description: 'Number of days to include', + schema: { type: 'integer', default: 7, maximum: 30 } + }], + responses: { + 200: { + description: 'Activity timeline', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + probes: { + type: 'array', + items: { + type: 'object', + properties: { + date: { type: 'string', format: 'date' }, + total: { type: 'integer' }, + successful: { type: 'integer' } + } + } + }, + incidents: { + type: 'array', + items: { + type: 'object', + properties: { + date: { type: 'string', format: 'date' }, + count: { type: 'integer' } + } + } + }, + reviews: { + type: 'array', + items: { + type: 'object', + properties: { + date: { type: 'string', format: 'date' }, + count: { type: 'integer' } + } + } + }, + days: { type: 'integer' }, + computed_at: { type: 'string', format: 'date-time' } + } + } + } + } + } + } + } + }, '/reviews': { get: { tags: ['Reviews'], @@ -212,6 +273,45 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au } } }, + '/reviews/recent': { + get: { + tags: ['Reviews'], + summary: 'Recent ecosystem reviews', + description: 'Returns recent reviews across the entire ecosystem', + operationId: 'getRecentReviews', + parameters: [{ + name: 'limit', + in: 'query', + description: 'Maximum number of results', + schema: { type: 'integer', default: 20, maximum: 100 } + }, + { + name: 'since', + in: 'query', + description: 'Unix timestamp - only reviews after this time', + schema: { type: 'integer' } + } + ], + responses: { + 200: { + description: 'Recent reviews', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + reviews: { + type: 'array', + items: { $ref: '#/components/schemas/Review' } + } + } + } + } + } + } + } + } + }, '/analytics/uptime': { get: { tags: ['Analytics'], @@ -300,6 +400,113 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au } } }, + '/analytics/status-distribution': { + get: { + tags: ['Analytics'], + summary: 'Status distribution', + description: 'Returns the distribution of mint statuses across the ecosystem', + operationId: 'getStatusDistribution', + responses: { + 200: { + description: 'Status distribution', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + distribution: { + type: 'object', + properties: { + online: { type: 'integer' }, + degraded: { type: 'integer' }, + offline: { type: 'integer' }, + abandoned: { type: 'integer' }, + unknown: { type: 'integer' } + } + }, + total: { type: 'integer' }, + computed_at: { type: 'string', format: 'date-time' } + } + } + } + } + } + } + } + }, + '/analytics/networks': { + get: { + tags: ['Analytics'], + summary: 'Network breakdown', + description: 'Returns network type breakdown (clearnet, tor, dual-stack)', + operationId: 'getNetworkBreakdown', + responses: { + 200: { + description: 'Network breakdown', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + clearnet_only: { type: 'integer' }, + tor_only: { type: 'integer' }, + dual_stack: { type: 'integer' }, + total: { type: 'integer' }, + computed_at: { type: 'string', format: 'date-time' } + } + } + } + } + } + } + } + }, + '/analytics/metadata-quality': { + get: { + tags: ['Analytics'], + summary: 'Metadata quality leaderboard', + description: 'Returns mints ranked by metadata completeness', + operationId: 'getMetadataQuality', + parameters: [{ + name: 'limit', + in: 'query', + description: 'Maximum number of results', + schema: { type: 'integer', default: 50, maximum: 200 } + }], + responses: { + 200: { + description: 'Metadata quality rankings', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + mints: { + type: 'array', + items: { + type: 'object', + properties: { + mint_id: { type: 'string', format: 'uuid' }, + canonical_url: { type: 'string' }, + name: { type: 'string' }, + completeness_score: { type: 'number' }, + has_name: { type: 'boolean' }, + has_description: { type: 'boolean' }, + has_icon: { type: 'boolean' }, + has_contact: { type: 'boolean' }, + has_pubkey: { type: 'boolean' } + } + } + }, + total: { type: 'integer' } + } + } + } + } + } + } + } + }, '/wallets/{wallet_name}/recommended-mints': { get: { tags: ['Mints'], @@ -367,6 +574,145 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au } } }, + '/mints/activity': { + get: { + tags: ['Mints'], + summary: 'Mint ecosystem activity', + description: 'Get mint ecosystem activity overview including recent additions and updates', + operationId: 'getMintActivity', + responses: { + 200: { + description: 'Ecosystem activity', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + new_mints_24h: { type: 'integer' }, + new_mints_7d: { type: 'integer' }, + updated_24h: { type: 'integer' }, + status_changes_24h: { type: 'integer' }, + computed_at: { type: 'string', format: 'date-time' } + } + } + } + } + } + } + } + }, + '/mints/recent': { + get: { + tags: ['Mints'], + summary: 'Recently added mints', + description: 'Get mints that were recently added to the ecosystem', + operationId: 'getRecentMints', + parameters: [{ + name: 'window', + in: 'query', + description: 'Time window', + schema: { type: 'string', enum: ['24h', '7d', '30d'], default: '7d' } + }, + { + name: 'limit', + in: 'query', + description: 'Maximum number of results', + schema: { type: 'integer', default: 20, maximum: 100 } + } + ], + responses: { + 200: { + description: 'Recently added mints', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + window: { type: 'string' }, + mints: { + type: 'array', + items: { $ref: '#/components/schemas/MintCard' } + } + } + } + } + } + } + } + } + }, + '/mints/updated': { + get: { + tags: ['Mints'], + summary: 'Recently updated mints', + description: 'Get mints that were recently updated (metadata changes)', + operationId: 'getUpdatedMints', + parameters: [{ + name: 'limit', + in: 'query', + description: 'Maximum number of results', + schema: { type: 'integer', default: 20, maximum: 100 } + }], + responses: { + 200: { + description: 'Recently updated mints', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + mints: { + type: 'array', + items: { $ref: '#/components/schemas/MintCard' } + } + } + } + } + } + } + } + } + }, + '/mints/popular': { + get: { + tags: ['Mints'], + summary: 'Popular mints by views', + description: 'Get mints ranked by pageview count', + operationId: 'getPopularMints', + parameters: [{ + name: 'window', + in: 'query', + description: 'Time window', + schema: { type: 'string', enum: ['24h', '7d', '30d'], default: '7d' } + }, + { + name: 'limit', + in: 'query', + description: 'Maximum number of results', + schema: { type: 'integer', default: 20, maximum: 100 } + } + ], + responses: { + 200: { + description: 'Popular mints', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + window: { type: 'string' }, + mints: { + type: 'array', + items: { $ref: '#/components/schemas/MintCard' } + } + } + } + } + } + } + } + } + }, '/mints/cards': { get: { tags: ['Mints'], @@ -937,6 +1283,339 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au } } }, + '/mints/{mint_id}/views/timeseries': { + get: { + tags: ['Analytics'], + summary: 'Pageview timeseries', + description: 'Returns pageview history for adoption trend charts', + operationId: 'getMintViewsTimeseries', + parameters: [ + { $ref: '#/components/parameters/mintId' }, + { + name: 'window', + in: 'query', + description: 'Time window', + schema: { type: 'string', enum: ['7d', '30d'], default: '7d' } + }, + { + name: 'bucket', + in: 'query', + description: 'Time bucket size', + schema: { type: 'string', enum: ['1h', '1d'], default: '1d' } + } + ], + responses: { + 200: { + description: 'Pageview timeseries', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + window: { type: 'string' }, + bucket: { type: 'string' }, + data: { + type: 'array', + items: { + type: 'object', + properties: { + timestamp: { type: 'string', format: 'date-time' }, + views: { type: 'integer' }, + unique_sessions: { type: 'integer' } + } + } + } + } + } + } + } + }, + 404: { $ref: '#/components/responses/NotFound' } + } + } + }, + '/mints/{mint_id}/stats': { + get: { + tags: ['Mint Details'], + summary: 'Aggregated mint KPIs', + description: 'Returns key performance indicators in a single request for summary cards', + operationId: 'getMintStats', + parameters: [ + { $ref: '#/components/parameters/mintId' } + ], + responses: { + 200: { + description: 'Mint statistics', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + mint_id: { type: 'string', format: 'uuid' }, + uptime: { + type: 'object', + properties: { + uptime_24h: { type: 'number' }, + uptime_7d: { type: 'number' }, + uptime_30d: { type: 'number' } + } + }, + latency: { + type: 'object', + properties: { + avg_rtt_ms: { type: 'number' }, + p95_rtt_ms: { type: 'number' } + } + }, + trust_score: { type: 'integer' }, + trust_level: { type: 'string' }, + review_count: { type: 'integer' }, + avg_rating: { type: 'number', nullable: true }, + views_30d: { type: 'integer' }, + computed_at: { type: 'string', format: 'date-time' } + } + } + } + } + }, + 404: { $ref: '#/components/responses/NotFound' } + } + } + }, + '/mints/{mint_id}/card': { + get: { + tags: ['Mint Details'], + summary: 'Get mint card', + description: 'Optimized lightweight endpoint for grid/list views', + operationId: 'getMintCard', + parameters: [ + { $ref: '#/components/parameters/mintId' } + ], + responses: { + 200: { + description: 'Mint card data', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/MintCard' } + } + } + }, + 404: { $ref: '#/components/responses/NotFound' } + } + } + }, + '/mints/{mint_id}/latency/timeseries': { + get: { + tags: ['Uptime'], + summary: 'Response time history', + description: 'Returns latency timeseries for charting response time trends', + operationId: 'getLatencyTimeseries', + parameters: [ + { $ref: '#/components/parameters/mintId' }, + { + name: 'window', + in: 'query', + description: 'Time window', + schema: { type: 'string', enum: ['24h', '7d', '30d'], default: '24h' } + }, + { + name: 'bucket', + in: 'query', + description: 'Time bucket size', + schema: { type: 'string', enum: ['5m', '15m', '1h'], default: '1h' } + } + ], + responses: { + 200: { + description: 'Latency timeseries', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + window: { type: 'string' }, + bucket: { type: 'string' }, + data: { + type: 'array', + items: { + type: 'object', + properties: { + timestamp: { type: 'string', format: 'date-time' }, + avg_rtt_ms: { type: 'number' }, + min_rtt_ms: { type: 'number' }, + max_rtt_ms: { type: 'number' }, + p95_rtt_ms: { type: 'number' } + } + } + } + } + } + } + } + }, + 404: { $ref: '#/components/responses/NotFound' } + } + } + }, + '/mints/{mint_id}/availability': { + get: { + tags: ['Uptime'], + summary: 'Availability breakdown', + description: 'Returns availability breakdown showing online/degraded/offline percentages', + operationId: 'getMintAvailability', + parameters: [ + { $ref: '#/components/parameters/mintId' }, + { + name: 'window', + in: 'query', + description: 'Time window', + schema: { type: 'string', enum: ['24h', '7d', '30d'], default: '30d' } + } + ], + responses: { + 200: { + description: 'Availability breakdown', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + window: { type: 'string' }, + online_pct: { type: 'number' }, + degraded_pct: { type: 'number' }, + offline_pct: { type: 'number' }, + total_checks: { type: 'integer' }, + computed_at: { type: 'string', format: 'date-time' } + } + } + } + } + }, + 404: { $ref: '#/components/responses/NotFound' } + } + } + }, + '/mints/{mint_id}/trust/history': { + get: { + tags: ['Trust'], + summary: 'Trust score history', + description: 'Returns trust score changes over time with change reasons', + operationId: 'getTrustHistory', + parameters: [ + { $ref: '#/components/parameters/mintId' }, + { + name: 'limit', + in: 'query', + description: 'Maximum number of historical records', + schema: { type: 'integer', default: 30, maximum: 100 } + } + ], + responses: { + 200: { + description: 'Trust score history', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + history: { + type: 'array', + items: { + type: 'object', + properties: { + computed_at: { type: 'string', format: 'date-time' }, + score_total: { type: 'integer' }, + score_level: { type: 'string' }, + breakdown: { type: 'object' } + } + } + } + } + } + } + } + }, + 404: { $ref: '#/components/responses/NotFound' } + } + } + }, + '/mints/{mint_id}/trust/compare': { + get: { + tags: ['Trust'], + summary: 'Compare trust score', + description: 'Compare mint trust score against ecosystem benchmarks', + operationId: 'getTrustComparison', + parameters: [ + { $ref: '#/components/parameters/mintId' }, + { + name: 'against', + in: 'query', + description: 'Comparison benchmark', + schema: { type: 'string', enum: ['ecosystem', 'top10', 'median'], default: 'ecosystem' } + } + ], + responses: { + 200: { + description: 'Trust score comparison', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + mint_score: { type: 'integer' }, + benchmark_score: { type: 'number' }, + percentile: { type: 'number' }, + rank: { type: 'integer' }, + total_mints: { type: 'integer' } + } + } + } + } + }, + 404: { $ref: '#/components/responses/NotFound' } + } + } + }, + '/mints/{mint_id}/reviews/summary': { + get: { + tags: ['Reviews'], + summary: 'Review summary', + description: 'Quick review overview with rating distribution and averages', + operationId: 'getReviewSummary', + parameters: [ + { $ref: '#/components/parameters/mintId' } + ], + responses: { + 200: { + description: 'Review summary', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + total_reviews: { type: 'integer' }, + avg_rating: { type: 'number', nullable: true }, + unique_reviewers: { type: 'integer' }, + rating_distribution: { + type: 'object', + properties: { + '5': { type: 'integer' }, + '4': { type: 'integer' }, + '3': { type: 'integer' }, + '2': { type: 'integer' }, + '1': { type: 'integer' } + } + }, + last_review_at: { type: 'string', format: 'date-time', nullable: true } + } + } + } + } + }, + 404: { $ref: '#/components/responses/NotFound' } + } + } + }, '/mints/{mint_id}/profile': { get: { tags: ['Mint Details'], diff --git a/src/services/AnalyticsService.js b/src/services/AnalyticsService.js index 2fbd427..65edb8f 100644 --- a/src/services/AnalyticsService.js +++ b/src/services/AnalyticsService.js @@ -1569,7 +1569,13 @@ export function getRecommendedMints(useCase = 'general', limit = 10) { ur.avg_rtt_ms, ur.uptime_pct FROM mints m LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id - LEFT JOIN uptime_rollups ur ON m.mint_id = ur.mint_id AND ur.window = '30d' + LEFT JOIN ( + SELECT mint_id, avg_rtt_ms, uptime_pct + FROM uptime_rollups + WHERE window = '30d' + GROUP BY mint_id + HAVING computed_at = MAX(computed_at) + ) ur ON m.mint_id = ur.mint_id WHERE m.status = 'online' AND (m.visibility IS NULL OR m.visibility = 'public') ORDER BY ur.avg_rtt_ms ASC NULLS LAST, ur.uptime_pct DESC NULLS LAST @@ -1585,7 +1591,13 @@ export function getRecommendedMints(useCase = 'general', limit = 10) { ur.uptime_pct FROM mints m LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id - LEFT JOIN uptime_rollups ur ON m.mint_id = ur.mint_id AND ur.window = '30d' + LEFT JOIN ( + SELECT mint_id, uptime_pct + FROM uptime_rollups + WHERE window = '30d' + GROUP BY mint_id + HAVING computed_at = MAX(computed_at) + ) ur ON m.mint_id = ur.mint_id WHERE m.status = 'online' AND (m.visibility IS NULL OR m.visibility = 'public') AND ts.score_total >= 70 @@ -1618,7 +1630,13 @@ export function getRecommendedMints(useCase = 'general', limit = 10) { ur.uptime_pct FROM mints m LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id - LEFT JOIN uptime_rollups ur ON m.mint_id = ur.mint_id AND ur.window = '30d' + LEFT JOIN ( + SELECT mint_id, uptime_pct + FROM uptime_rollups + WHERE window = '30d' + GROUP BY mint_id + HAVING computed_at = MAX(computed_at) + ) ur ON m.mint_id = ur.mint_id WHERE m.status = 'online' AND (m.visibility IS NULL OR m.visibility = 'public') ORDER BY ts.score_total DESC NULLS LAST, ur.uptime_pct DESC NULLS LAST @@ -1741,7 +1759,13 @@ export function getWalletRecommendedMints(walletName, limit = 10) { FROM mints m LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id LEFT JOIN metadata_snapshots ms ON m.mint_id = ms.mint_id - LEFT JOIN uptime_rollups ur ON m.mint_id = ur.mint_id AND ur.window = '30d' + LEFT JOIN ( + SELECT mint_id, uptime_pct + FROM uptime_rollups + WHERE window = '30d' + GROUP BY mint_id + HAVING computed_at = MAX(computed_at) + ) ur ON m.mint_id = ur.mint_id WHERE m.status = 'online' AND (m.visibility IS NULL OR m.visibility = 'public') `);