mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* feat(market): add crypto sectors heatmap and token panels (DeFi, AI, Other) backend - Add shared/crypto-sectors.json, defi-tokens.json, ai-tokens.json, other-tokens.json configs - Add scripts/seed-crypto-sectors.mjs and seed-token-panels.mjs seed scripts - Add proto messages for ListCryptoSectors, ListDefiTokens, ListAiTokens, ListOtherTokens - Add change7d field (field 6) to CryptoQuote proto message - Run buf generate to produce updated TypeScript bindings - Add server handlers for all 4 new RPCs reading from seeded Redis cache - Wire handlers into marketHandler and register cache keys with BOOTSTRAP_TIERS=slow - Wire seedCryptoSectors and seedTokenPanels into ais-relay.cjs seedAllMarketData loop * feat(panels): add crypto sectors heatmap and token panels (DeFi, AI, Other) - Add TokenData interface to src/types/index.ts - Wire ListCryptoSectorsResponse/ListDefiTokensResponse/ListAiTokensResponse/ListOtherTokensResponse into market service with circuit breakers and hydration fallbacks - Add CryptoHeatmapPanel, TokenListPanel, DefiTokensPanel, AiTokensPanel, OtherTokensPanel to MarketPanel.ts - Register 4 new panels in panels.ts FINANCE_PANELS and cryptoDigital category - Instantiate new panels in panel-layout.ts - Load data in data-loader.ts loadMarkets() alongside existing crypto fetch * fix(crypto-panels): resolve test failures and type errors post-review - Add @ts-nocheck to regenerated market service_server/client (matches repo convention) - Add 4 new RPC routes to RPC_CACHE_TIER in gateway.ts (route-cache-tier test) - Sync scripts/shared/ with shared/ for new token/sector JSON configs - Restore non-market generated files to origin/main state (avoid buf version diff) * fix(crypto-panels): address code review findings (P1-P3) - ais-relay seedTokenPanels: add empty-guard before Redis write to prevent overwriting cached data when all IDs are unresolvable - server _feeds.ts: sync 4 missing crypto feeds (Wu Blockchain, Messari, NFT News, Stablecoin Policy) with client-side feeds.ts - data-loader: expose panel refs outside try block so catch can call showRetrying(); log error instead of swallowing silently - MarketPanel: replace hardcoded English error strings with t() calls (failedSectorData / failedCryptoData) to honour user locale - seed-token-panels.mjs: remove unused getRedisCredentials import - cache-keys.ts: one BOOTSTRAP_TIERS entry per line for consistency * fix(crypto-panels): three correctness fixes for RSS proxy, refresh, and Redis write visibility - api/_rss-allowed-domains.js: add 7 new crypto domains (decrypt.co, blockworks.co, thedefiant.io, bitcoinmagazine.com, www.dlnews.com, cryptoslate.com, unchainedcrypto.com) so rss-proxy.js accepts the new finance feeds instead of rejecting them as disallowed hosts - src/App.ts: add crypto-heatmap/defi-tokens/ai-tokens/other-tokens to the periodic markets refresh viewport condition so panels on screen continue receiving live updates, not just the initial load - ais-relay seedTokenPanels: capture upstashSet return values and log PARTIAL if any Redis write fails, matching seedCryptoSectors pattern
68 lines
2.5 KiB
JavaScript
68 lines
2.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { loadEnvFile, loadSharedConfig, CHROME_UA, runSeed, sleep } from './_seed-utils.mjs';
|
|
|
|
const sectorsConfig = loadSharedConfig('crypto-sectors.json');
|
|
|
|
loadEnvFile(import.meta.url);
|
|
|
|
const CANONICAL_KEY = 'market:crypto-sectors:v1';
|
|
const CACHE_TTL = 3600;
|
|
|
|
const SECTORS = sectorsConfig.sectors;
|
|
|
|
async function fetchWithRateLimitRetry(url, maxAttempts = 5, headers = { Accept: 'application/json', 'User-Agent': CHROME_UA }) {
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
const resp = await fetch(url, { headers, signal: AbortSignal.timeout(15_000) });
|
|
if (resp.status === 429) {
|
|
const wait = Math.min(10_000 * (i + 1), 60_000);
|
|
console.warn(` CoinGecko 429 — waiting ${wait / 1000}s (attempt ${i + 1}/${maxAttempts})`);
|
|
await sleep(wait);
|
|
continue;
|
|
}
|
|
if (!resp.ok) throw new Error(`CoinGecko HTTP ${resp.status}`);
|
|
return resp;
|
|
}
|
|
throw new Error('CoinGecko rate limit exceeded after retries');
|
|
}
|
|
|
|
async function fetchSectorData() {
|
|
const allIds = [...new Set(SECTORS.flatMap(s => s.tokens))];
|
|
|
|
const apiKey = process.env.COINGECKO_API_KEY;
|
|
const baseUrl = apiKey ? 'https://pro-api.coingecko.com/api/v3' : 'https://api.coingecko.com/api/v3';
|
|
const url = `${baseUrl}/coins/markets?vs_currency=usd&ids=${allIds.join(',')}&order=market_cap_desc&sparkline=false&price_change_percentage=24h`;
|
|
const headers = { Accept: 'application/json', 'User-Agent': CHROME_UA };
|
|
if (apiKey) headers['x-cg-pro-api-key'] = apiKey;
|
|
|
|
const resp = await fetchWithRateLimitRetry(url, 5, headers);
|
|
const data = await resp.json();
|
|
if (!Array.isArray(data) || data.length === 0) throw new Error('CoinGecko returned no data');
|
|
|
|
const byId = new Map(data.map(c => [c.id, c.price_change_percentage_24h]));
|
|
|
|
const sectors = SECTORS.map(sector => {
|
|
const changes = sector.tokens
|
|
.map(id => byId.get(id))
|
|
.filter(v => typeof v === 'number' && isFinite(v));
|
|
const change = changes.length > 0 ? changes.reduce((a, b) => a + b, 0) / changes.length : 0;
|
|
return { id: sector.id, name: sector.name, change };
|
|
});
|
|
|
|
return { sectors };
|
|
}
|
|
|
|
function validate(data) {
|
|
return Array.isArray(data?.sectors) && data.sectors.length > 0;
|
|
}
|
|
|
|
runSeed('market', 'crypto-sectors', CANONICAL_KEY, fetchSectorData, {
|
|
validateFn: validate,
|
|
ttlSeconds: CACHE_TTL,
|
|
sourceVersion: 'coingecko-sectors',
|
|
}).catch(err => {
|
|
const _cause = err.cause ? ` (cause: ${err.cause.message || err.cause.code || err.cause})` : '';
|
|
console.error('FATAL:', (err.message || err) + _cause);
|
|
process.exit(1);
|
|
});
|