- Update all route documentation comments - Update README, env.example, and starter-docs - Update install.sh
909 lines
27 KiB
JavaScript
909 lines
27 KiB
JavaScript
/**
|
|
* Mint Routes
|
|
*
|
|
* Core mint discovery and management endpoints.
|
|
*
|
|
* IMPORTANT: Route order matters!
|
|
* Static routes (like /by-url/*) must come BEFORE parameterized routes (/:mint_id/*)
|
|
*/
|
|
|
|
import { Router } from 'express';
|
|
import {
|
|
getMintById,
|
|
getMintByUrl,
|
|
getMints,
|
|
getMintUrls,
|
|
submitMint,
|
|
countMintsByStatus,
|
|
resolveMint
|
|
} from '../services/MintService.js';
|
|
import { getMetadataSnapshot, getMetadataHistory, deriveFeatures } from '../services/MetadataService.js';
|
|
import { getUptime, getUptimeTimeseries, getUptimeSummary, getLatencyTimeseries } from '../services/UptimeService.js';
|
|
import { getIncidents, countIncidents, getLatestProbe } from '../services/ProbeService.js';
|
|
import { getTrustScore, getTrustScoreHistory, getTrustComparison, getTrustScoreHistoryDetailed } from '../services/TrustService.js';
|
|
import { getReviews, getReviewSummary } from '../services/ReviewService.js';
|
|
import { getPageviewStats, recordPageview, getTrendingMints, getViewsTimeseries } from '../services/PageviewService.js';
|
|
import { daysAgo } from '../utils/time.js';
|
|
import { getAllReviews, countReviews } from '../services/ReviewService.js';
|
|
import {
|
|
getMintActivity,
|
|
getRecentMints,
|
|
getUpdatedMints,
|
|
getPopularMints,
|
|
getMintStats,
|
|
getMintAvailability,
|
|
getMintCard
|
|
} from '../services/AnalyticsService.js';
|
|
|
|
const router = Router();
|
|
|
|
// ==========================================
|
|
// HELPER: Resolve mint by URL query param
|
|
// ==========================================
|
|
|
|
function getMintFromUrl(req, res) {
|
|
const { url } = req.query;
|
|
if (!url) {
|
|
res.status(400).json({ error: 'URL parameter required' });
|
|
return null;
|
|
}
|
|
|
|
const mint = getMintByUrl(url);
|
|
if (!mint) {
|
|
res.status(404).json({ error: 'Mint not found' });
|
|
return null;
|
|
}
|
|
|
|
return mint;
|
|
}
|
|
|
|
// ==========================================
|
|
// HELPER: Resolve mint middleware for ID routes
|
|
// ==========================================
|
|
|
|
function resolveMintMiddleware(req, res, next) {
|
|
const { mint_id } = req.params;
|
|
|
|
if (!mint_id) {
|
|
return res.status(400).json({ error: 'Mint ID required' });
|
|
}
|
|
|
|
const mint = resolveMint(mint_id);
|
|
if (!mint) {
|
|
return res.status(404).json({ error: 'Mint not found' });
|
|
}
|
|
|
|
req.mint = mint;
|
|
next();
|
|
}
|
|
|
|
// ==========================================
|
|
// LIST & SUBMIT (no param conflicts)
|
|
// ==========================================
|
|
|
|
/**
|
|
* GET /mints
|
|
* List all mints with optional filters
|
|
*/
|
|
router.get('/', (req, res) => {
|
|
try {
|
|
const { status, limit = 100, offset = 0, sort_by, sort_order } = req.query;
|
|
|
|
const mints = getMints({
|
|
status,
|
|
limit: Math.min(parseInt(limit) || 100, 500),
|
|
offset: parseInt(offset) || 0,
|
|
sortBy: sort_by,
|
|
sortOrder: sort_order
|
|
});
|
|
|
|
// Enrich with uptime summary
|
|
const enrichedMints = mints.map(mint => ({
|
|
...mint,
|
|
...getUptimeSummary(mint.mint_id),
|
|
incidents_7d: countIncidents(mint.mint_id, daysAgo(7)),
|
|
incidents_30d: countIncidents(mint.mint_id, daysAgo(30))
|
|
}));
|
|
|
|
res.json({
|
|
mints: enrichedMints,
|
|
total: enrichedMints.length,
|
|
limit: Math.min(parseInt(limit) || 100, 500),
|
|
offset: parseInt(offset) || 0
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error listing mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /mints/submit
|
|
*/
|
|
router.post('/submit', (req, res) => {
|
|
try {
|
|
const { mint_url } = req.body;
|
|
|
|
if (!mint_url) {
|
|
return res.status(400).json({ error: 'mint_url required' });
|
|
}
|
|
|
|
const result = submitMint(mint_url);
|
|
res.status(result.success ? 201 : 400).json(result);
|
|
} catch (error) {
|
|
console.error('[API] Error submitting mint:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// HOMEPAGE & DISCOVERY ENDPOINTS
|
|
// ==========================================
|
|
|
|
/**
|
|
* GET /mints/activity
|
|
* Get mint ecosystem activity overview
|
|
*/
|
|
router.get('/activity', (req, res) => {
|
|
try {
|
|
const activity = getMintActivity();
|
|
res.json(activity);
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint activity:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/recent
|
|
* Get recently added mints
|
|
*/
|
|
router.get('/recent', (req, res) => {
|
|
try {
|
|
const { window = '7d', limit = 20 } = req.query;
|
|
const validWindows = ['24h', '7d', '30d'];
|
|
const timeWindow = validWindows.includes(window) ? window : '7d';
|
|
|
|
const mints = getRecentMints(timeWindow, Math.min(parseInt(limit) || 20, 100));
|
|
res.json({
|
|
window: timeWindow,
|
|
mints
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting recent mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/updated
|
|
* Get recently updated mints
|
|
*/
|
|
router.get('/updated', (req, res) => {
|
|
try {
|
|
const { limit = 20 } = req.query;
|
|
const mints = getUpdatedMints(Math.min(parseInt(limit) || 20, 100));
|
|
res.json({ mints });
|
|
} catch (error) {
|
|
console.error('[API] Error getting updated mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/popular
|
|
* Get popular mints by views
|
|
*/
|
|
router.get('/popular', async(req, res) => {
|
|
try {
|
|
const { window = '7d', limit = 20 } = req.query;
|
|
const validWindows = ['24h', '7d', '30d'];
|
|
const timeWindow = validWindows.includes(window) ? window : '7d';
|
|
|
|
const mints = await getPopularMints(timeWindow, Math.min(parseInt(limit) || 20, 100));
|
|
res.json({
|
|
window: timeWindow,
|
|
mints
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting popular mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/trending
|
|
* Get trending mints by view velocity
|
|
*/
|
|
router.get('/trending', async(req, res) => {
|
|
try {
|
|
const { window = '7d', limit = 10 } = req.query;
|
|
|
|
// Validate window
|
|
const validWindows = ['24h', '7d'];
|
|
const timeWindow = validWindows.includes(window) ? window : '7d';
|
|
|
|
const trending = await getTrendingMints(
|
|
Math.min(parseInt(limit) || 10, 50),
|
|
timeWindow
|
|
);
|
|
|
|
res.json({
|
|
window: timeWindow,
|
|
mints: trending.map(m => ({
|
|
mint_id: m.mint_id,
|
|
canonical_url: m.canonical_url,
|
|
name: m.name,
|
|
icon_url: m.icon_url,
|
|
status: m.status,
|
|
trust_score: m.trust_score,
|
|
trust_level: m.trust_level,
|
|
view_count: m.view_count,
|
|
view_velocity: m.view_velocity,
|
|
unique_sessions: m.unique_sessions
|
|
}))
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting trending mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// BY-URL ROUTES (must come BEFORE :mint_id routes)
|
|
// ==========================================
|
|
|
|
/**
|
|
* GET /mints/by-url
|
|
*/
|
|
router.get('/by-url', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const uptime = getUptimeSummary(mint.mint_id);
|
|
res.json({
|
|
mint_id: mint.mint_id,
|
|
canonical_url: mint.canonical_url,
|
|
urls: getMintUrls(mint.mint_id).map(u => u.url),
|
|
name: mint.name,
|
|
icon_url: mint.icon_url,
|
|
status: mint.status,
|
|
offline_since: mint.offline_since,
|
|
last_success_at: mint.last_success_at,
|
|
last_failure_at: mint.last_failure_at,
|
|
...uptime,
|
|
incidents_7d: countIncidents(mint.mint_id, daysAgo(7)),
|
|
incidents_30d: countIncidents(mint.mint_id, daysAgo(30)),
|
|
trust_score: mint.trust_score,
|
|
trust_level: mint.trust_level
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint by URL:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/urls
|
|
*/
|
|
router.get('/by-url/urls', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const urls = getMintUrls(mint.mint_id);
|
|
res.json({
|
|
canonical_url: mint.canonical_url,
|
|
urls
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint URLs:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/metadata
|
|
*/
|
|
router.get('/by-url/metadata', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const metadata = getMetadataSnapshot(mint.mint_id);
|
|
if (!metadata) {
|
|
return res.json({ error: 'No metadata available', mint_id: mint.mint_id });
|
|
}
|
|
|
|
res.json({
|
|
name: metadata.name,
|
|
pubkey: metadata.pubkey,
|
|
version: metadata.version,
|
|
description: metadata.description,
|
|
description_long: metadata.description_long,
|
|
contact: metadata.contact,
|
|
motd: metadata.motd,
|
|
icon_url: metadata.icon_url,
|
|
urls: metadata.urls,
|
|
tos_url: metadata.tos_url,
|
|
nuts: metadata.nuts,
|
|
server_time: metadata.server_time,
|
|
last_fetched_at: metadata.last_fetched_at
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting metadata:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/metadata/history
|
|
*/
|
|
router.get('/by-url/metadata/history', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const { limit = 50 } = req.query;
|
|
const history = getMetadataHistory(mint.mint_id, { limit: parseInt(limit) });
|
|
res.json({ history });
|
|
} catch (error) {
|
|
console.error('[API] Error getting metadata history:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/status
|
|
*/
|
|
router.get('/by-url/status', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const latestProbe = getLatestProbe(mint.mint_id);
|
|
res.json({
|
|
status: mint.status,
|
|
offline_since: mint.offline_since,
|
|
last_checked_at: latestProbe ? latestProbe.probed_at : null,
|
|
current_rtt_ms: latestProbe ? latestProbe.rtt_ms : null
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting status:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/uptime
|
|
*/
|
|
router.get('/by-url/uptime', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const { window = '24h' } = req.query;
|
|
const uptime = getUptime(mint.mint_id, window);
|
|
if (!uptime) {
|
|
return res.json({ error: 'No uptime data available' });
|
|
}
|
|
|
|
res.json(uptime);
|
|
} catch (error) {
|
|
console.error('[API] Error getting uptime:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/uptime/timeseries
|
|
*/
|
|
router.get('/by-url/uptime/timeseries', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const { window = '24h', bucket = '1h' } = req.query;
|
|
const timeseries = getUptimeTimeseries(mint.mint_id, window, bucket);
|
|
res.json({ data: timeseries });
|
|
} catch (error) {
|
|
console.error('[API] Error getting uptime timeseries:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/incidents
|
|
*/
|
|
router.get('/by-url/incidents', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const { limit = 50 } = req.query;
|
|
const incidents = getIncidents(mint.mint_id, { limit: parseInt(limit) });
|
|
res.json({ incidents });
|
|
} catch (error) {
|
|
console.error('[API] Error getting incidents:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/trust
|
|
*/
|
|
router.get('/by-url/trust', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const trust = getTrustScore(mint.mint_id);
|
|
if (!trust) {
|
|
return res.json({
|
|
score_total: null,
|
|
score_level: 'unknown',
|
|
breakdown: null,
|
|
computed_at: null
|
|
});
|
|
}
|
|
|
|
res.json(trust);
|
|
} catch (error) {
|
|
console.error('[API] Error getting trust score:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/reviews
|
|
*/
|
|
router.get('/by-url/reviews', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const { limit = 50, offset = 0 } = req.query;
|
|
const reviews = getReviews(mint.mint_id, {
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({ reviews });
|
|
} catch (error) {
|
|
console.error('[API] Error getting reviews:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/views
|
|
*/
|
|
router.get('/by-url/views', async(req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const stats = await getPageviewStats(mint.mint_id);
|
|
res.json(stats);
|
|
} catch (error) {
|
|
console.error('[API] Error getting views:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/features
|
|
*/
|
|
router.get('/by-url/features', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const features = deriveFeatures(mint.mint_id);
|
|
res.json(features);
|
|
} catch (error) {
|
|
console.error('[API] Error getting features:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// PARAMETERIZED ROUTES (:mint_id)
|
|
// These must come AFTER /by-url/* routes
|
|
// ==========================================
|
|
|
|
/**
|
|
* GET /mints/:mint_id
|
|
*/
|
|
router.get('/:mint_id', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { mint } = req;
|
|
|
|
// Enrich with computed fields
|
|
const uptime = getUptimeSummary(mint.mint_id);
|
|
const response = {
|
|
mint_id: mint.mint_id,
|
|
canonical_url: mint.canonical_url,
|
|
urls: getMintUrls(mint.mint_id).map(u => u.url),
|
|
name: mint.name,
|
|
icon_url: mint.icon_url,
|
|
status: mint.status,
|
|
offline_since: mint.offline_since,
|
|
last_success_at: mint.last_success_at,
|
|
last_failure_at: mint.last_failure_at,
|
|
...uptime,
|
|
incidents_7d: countIncidents(mint.mint_id, daysAgo(7)),
|
|
incidents_30d: countIncidents(mint.mint_id, daysAgo(30)),
|
|
trust_score: mint.trust_score,
|
|
trust_level: mint.trust_level
|
|
};
|
|
|
|
// Record pageview
|
|
recordPageview(mint.mint_id, {
|
|
sessionId: req.headers['x-session-id'],
|
|
userAgent: req.headers['user-agent'],
|
|
referer: req.headers['referer']
|
|
});
|
|
|
|
res.json(response);
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/urls
|
|
*/
|
|
router.get('/:mint_id/urls', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const urls = getMintUrls(req.mint.mint_id);
|
|
|
|
res.json({
|
|
canonical_url: req.mint.canonical_url,
|
|
urls
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint URLs:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/metadata
|
|
*/
|
|
router.get('/:mint_id/metadata', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const metadata = getMetadataSnapshot(req.mint.mint_id);
|
|
|
|
if (!metadata) {
|
|
return res.json({ error: 'No metadata available', mint_id: req.mint.mint_id });
|
|
}
|
|
|
|
res.json({
|
|
name: metadata.name,
|
|
pubkey: metadata.pubkey,
|
|
version: metadata.version,
|
|
description: metadata.description,
|
|
description_long: metadata.description_long,
|
|
contact: metadata.contact,
|
|
motd: metadata.motd,
|
|
icon_url: metadata.icon_url,
|
|
urls: metadata.urls,
|
|
tos_url: metadata.tos_url,
|
|
nuts: metadata.nuts,
|
|
server_time: metadata.server_time,
|
|
last_fetched_at: metadata.last_fetched_at
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting metadata:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/metadata/history
|
|
*/
|
|
router.get('/:mint_id/metadata/history', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { limit = 50 } = req.query;
|
|
const history = getMetadataHistory(req.mint.mint_id, { limit: parseInt(limit) });
|
|
|
|
res.json({ history });
|
|
} catch (error) {
|
|
console.error('[API] Error getting metadata history:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/status
|
|
*/
|
|
router.get('/:mint_id/status', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { mint } = req;
|
|
const latestProbe = getLatestProbe(mint.mint_id);
|
|
|
|
res.json({
|
|
status: mint.status,
|
|
offline_since: mint.offline_since,
|
|
last_checked_at: latestProbe ? latestProbe.probed_at : null,
|
|
current_rtt_ms: latestProbe ? latestProbe.rtt_ms : null
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting status:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/uptime
|
|
*/
|
|
router.get('/:mint_id/uptime', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { window = '24h' } = req.query;
|
|
const uptime = getUptime(req.mint.mint_id, window);
|
|
|
|
if (!uptime) {
|
|
return res.json({ error: 'No uptime data available' });
|
|
}
|
|
|
|
res.json(uptime);
|
|
} catch (error) {
|
|
console.error('[API] Error getting uptime:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/uptime/timeseries
|
|
*/
|
|
router.get('/:mint_id/uptime/timeseries', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { window = '24h', bucket = '1h' } = req.query;
|
|
const timeseries = getUptimeTimeseries(req.mint.mint_id, window, bucket);
|
|
|
|
res.json({ data: timeseries });
|
|
} catch (error) {
|
|
console.error('[API] Error getting uptime timeseries:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/incidents
|
|
*/
|
|
router.get('/:mint_id/incidents', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { limit = 50 } = req.query;
|
|
const incidents = getIncidents(req.mint.mint_id, { limit: parseInt(limit) });
|
|
|
|
res.json({ incidents });
|
|
} catch (error) {
|
|
console.error('[API] Error getting incidents:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/trust
|
|
*/
|
|
router.get('/:mint_id/trust', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const trust = getTrustScore(req.mint.mint_id);
|
|
|
|
if (!trust) {
|
|
return res.json({
|
|
score_total: null,
|
|
score_level: 'unknown',
|
|
breakdown: null,
|
|
computed_at: null
|
|
});
|
|
}
|
|
|
|
res.json(trust);
|
|
} catch (error) {
|
|
console.error('[API] Error getting trust score:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/reviews
|
|
*/
|
|
router.get('/:mint_id/reviews', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { limit = 50, offset = 0 } = req.query;
|
|
const reviews = getReviews(req.mint.mint_id, {
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({ reviews });
|
|
} catch (error) {
|
|
console.error('[API] Error getting reviews:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/views
|
|
*/
|
|
router.get('/:mint_id/views', resolveMintMiddleware, async(req, res) => {
|
|
try {
|
|
const stats = await getPageviewStats(req.mint.mint_id);
|
|
res.json(stats);
|
|
} catch (error) {
|
|
console.error('[API] Error getting views:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/features
|
|
*/
|
|
router.get('/:mint_id/features', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const features = deriveFeatures(req.mint.mint_id);
|
|
res.json(features);
|
|
} catch (error) {
|
|
console.error('[API] Error getting features:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// MINT DETAIL AGGREGATED ENDPOINTS
|
|
// ==========================================
|
|
|
|
/**
|
|
* GET /mints/:mint_id/stats
|
|
* Aggregated mint KPIs (single endpoint for summary cards)
|
|
*/
|
|
router.get('/:mint_id/stats', resolveMintMiddleware, async(req, res) => {
|
|
try {
|
|
const stats = await getMintStats(req.mint.mint_id);
|
|
res.json(stats);
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint stats:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/latency/timeseries
|
|
* Response time history for charting
|
|
*/
|
|
router.get('/:mint_id/latency/timeseries', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { window = '24h', bucket = '1h' } = req.query;
|
|
const validWindows = ['24h', '7d', '30d'];
|
|
const validBuckets = ['5m', '15m', '1h'];
|
|
|
|
const timeWindow = validWindows.includes(window) ? window : '24h';
|
|
const timeBucket = validBuckets.includes(bucket) ? bucket : '1h';
|
|
|
|
const data = getLatencyTimeseries(req.mint.mint_id, timeWindow, timeBucket);
|
|
res.json({
|
|
window: timeWindow,
|
|
bucket: timeBucket,
|
|
data
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting latency timeseries:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/availability
|
|
* Availability breakdown (online/degraded/offline percentages)
|
|
*/
|
|
router.get('/:mint_id/availability', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { window = '30d' } = req.query;
|
|
const validWindows = ['24h', '7d', '30d'];
|
|
const timeWindow = validWindows.includes(window) ? window : '30d';
|
|
|
|
const availability = getMintAvailability(req.mint.mint_id, timeWindow);
|
|
res.json(availability);
|
|
} catch (error) {
|
|
console.error('[API] Error getting availability:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/trust/history
|
|
* Trust score history with change reasons
|
|
*/
|
|
router.get('/:mint_id/trust/history', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { limit = 30 } = req.query;
|
|
const history = getTrustScoreHistoryDetailed(
|
|
req.mint.mint_id,
|
|
Math.min(parseInt(limit) || 30, 100)
|
|
);
|
|
res.json({ history });
|
|
} catch (error) {
|
|
console.error('[API] Error getting trust history:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/trust/compare
|
|
* Compare trust score against ecosystem benchmarks
|
|
*/
|
|
router.get('/:mint_id/trust/compare', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { against = 'ecosystem' } = req.query;
|
|
const validBenchmarks = ['ecosystem', 'top10', 'median'];
|
|
const benchmark = validBenchmarks.includes(against) ? against : 'ecosystem';
|
|
|
|
const comparison = getTrustComparison(req.mint.mint_id, benchmark);
|
|
res.json(comparison);
|
|
} catch (error) {
|
|
console.error('[API] Error getting trust comparison:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/reviews/summary
|
|
* Quick review overview (rating distribution, averages)
|
|
*/
|
|
router.get('/:mint_id/reviews/summary', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const summary = getReviewSummary(req.mint.mint_id);
|
|
res.json(summary);
|
|
} catch (error) {
|
|
console.error('[API] Error getting review summary:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/views/timeseries
|
|
* Pageview history for adoption trends
|
|
*/
|
|
router.get('/:mint_id/views/timeseries', resolveMintMiddleware, async(req, res) => {
|
|
try {
|
|
const { window = '7d', bucket = '1d' } = req.query;
|
|
const validWindows = ['7d', '30d'];
|
|
const validBuckets = ['1h', '1d'];
|
|
|
|
const timeWindow = validWindows.includes(window) ? window : '7d';
|
|
const timeBucket = validBuckets.includes(bucket) ? bucket : '1d';
|
|
|
|
const data = await getViewsTimeseries(req.mint.mint_id, timeWindow, timeBucket);
|
|
res.json({
|
|
window: timeWindow,
|
|
bucket: timeBucket,
|
|
data
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting views timeseries:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/card
|
|
* Optimized endpoint for grid/list views
|
|
*/
|
|
router.get('/:mint_id/card', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const card = getMintCard(req.mint.mint_id);
|
|
if (!card) {
|
|
return res.status(404).json({ error: 'Mint not found' });
|
|
}
|
|
res.json(card);
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint card:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
export default router; |