JairoDanielMT commited on
Commit
f5ca7d3
·
verified ·
1 Parent(s): 161780e

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +222 -114
index.html CHANGED
@@ -3,59 +3,75 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>Dashboard: Últimos 7 días (XRP, PAXG, WLD, USDC) + Earn Morpho (USDC)</title>
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
  <style>
9
  body { font-family: Inter, system-ui, Arial; margin: 18px; background:#f7f8fb; color:#111; }
10
  header { display:flex; gap:12px; align-items:center; margin-bottom:12px; flex-wrap:wrap; }
11
- .card { background:white; border-radius:8px; padding:12px; box-shadow:0 6px 18px rgba(20,20,40,0.06); }
12
  .small { font-size:0.9rem; color:#555; }
13
- #grid { display:grid; grid-template-columns: 1fr 300px; gap:12px; align-items:start; }
14
- #chartCard { min-height:360px; position:relative; }
 
15
  .banner {
16
- display:none;
17
- margin-bottom:10px;
18
- padding:10px 12px;
19
- background:#fff7ed;
20
- border:1px solid #fed7aa;
21
- color:#9a3412;
22
- border-radius:8px;
23
- font-size:0.95rem;
24
  }
25
- input[type="number"]{ width:120px; padding:6px; border-radius:6px; border:1px solid #ddd; }
26
- button { padding:8px 10px; border-radius:6px; border:none; background:#2563eb; color:white; cursor:pointer; }
 
27
  button.secondary { background:#e5e7eb; color:#111; }
28
- .asset-toggle { display:flex; gap:6px; flex-wrap:wrap; }
29
- .asset-toggle label { display:flex; gap:6px; align-items:center; font-size:0.9rem; }
30
  .muted { color:#6b7280; font-size:0.85rem; }
31
- @media (max-width: 900px){
32
  #grid { grid-template-columns: 1fr; }
 
 
33
  }
34
  </style>
35
  </head>
36
  <body>
37
  <header>
38
- <h2 style="margin:0;">Crypto 7d — XRP, PAXG, WLD, USDC</h2>
39
- <div class="small">Fuente de precios: CoinGecko · Est. rendimiento Morpho para USDC (ejemplo).</div>
40
  </header>
41
 
42
  <div id="grid">
43
- <div class="card" id="chartCard">
44
- <div id="errorBox" class="banner"></div>
45
- <canvas id="priceChart"></canvas>
46
- <div class="muted" style="margin-top:8px;">Tip: puedes ocultar/mostrar activos a la derecha.</div>
47
- </div>
48
 
49
  <div class="card">
50
- <h3 style="margin-top:0;">Earn Morpho — Estimador (USDC)</h3>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  <div class="small" style="margin-bottom:8px;">
52
- Tasa usada (30d APY de referencia): <span id="morphoRateDisplay">4.00%</span>
53
  </div>
54
 
55
- <label>Monto en USDC:
56
  <input id="amount" type="number" min="0" step="0.01" value="1000">
57
  </label>
58
- <div style="margin-top:8px; display:flex; gap:8px; flex-wrap:wrap;">
 
59
  <button id="calcBtn">Calcular (7 días)</button>
60
  <button id="setRateBtn" class="secondary">Usar otra tasa</button>
61
  </div>
@@ -63,30 +79,37 @@
63
  <div id="results" style="margin-top:12px;">
64
  <div class="small">Ganancia estimada (7 días): <strong id="gain7">-</strong></div>
65
  <div class="small">Total aproximado: <strong id="total7">-</strong></div>
66
- <div class="small" style="margin-top:6px;">(Cálculo: interés compuesto diario aplicado sobre APY)</div>
67
  </div>
68
 
69
- <hr style="margin:12px 0;" />
70
- <h4 style="margin:0 0 8px;">Visibilidad activos</h4>
71
- <div class="asset-toggle" id="toggles"></div>
72
-
73
- <div style="margin-top:12px;" class="small">
74
- Nota: CoinGecko API pública se usa para datos históricos. Ajusta la tasa Morpho según la plataforma o fuente que prefieras.
75
  </div>
76
  </div>
77
  </div>
78
 
79
  <script>
80
- const assets = [
81
- // IDs correctos en CoinGecko:
82
- { id: 'ripple', label: 'XRP' },
83
- { id: 'pax-gold', label: 'PAXG' },
84
- { id: 'worldcoin-wld', label: 'WLD' },
85
- { id: 'usd-coin', label: 'USDC' }
86
  ];
87
 
88
- // CoinGecko market_chart: /coins/{id}/market_chart?vs_currency=usd&days=7&interval=daily
89
- async function fetchPriceSeries(assetId) {
 
 
 
 
 
 
 
 
 
 
 
90
  const url = `https://api.coingecko.com/api/v3/coins/${assetId}/market_chart?vs_currency=usd&days=7&interval=daily`;
91
  const res = await fetch(url);
92
  if (!res.ok) throw new Error(`${assetId} -> ${res.status}`);
@@ -94,92 +117,159 @@ async function fetchPriceSeries(assetId) {
94
  return j.prices.map(p => ({ t: p[0], price: p[1] })); // [timestamp, price]
95
  }
96
 
97
- function tsToLabel(ts) {
98
- const d = new Date(ts);
99
- return d.toLocaleDateString();
 
 
 
100
  }
101
 
102
- function colorFor(label) {
103
- const map = { 'XRP':'#1e90ff','PAXG':'#bfa34a','WLD':'#7c3aed','USDC':'#16a34a' };
104
- return map[label] || '#888';
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
 
107
- function showError(msg) {
108
- const box = document.getElementById('errorBox');
 
109
  box.style.display = 'block';
110
  box.textContent = msg;
111
  }
112
 
113
- (async function init(){
114
- const results = await Promise.allSettled(assets.map(a => fetchPriceSeries(a.id)));
 
 
 
 
115
 
116
- // Separa OK vs errores
117
- const ok = [];
118
- const fails = [];
119
- results.forEach((r, i) => {
120
- if (r.status === 'fulfilled') ok.push({ asset: assets[i], series: r.value });
121
- else fails.push({ asset: assets[i], error: r.reason });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  });
 
 
123
 
124
- if (fails.length) {
125
- showError(
126
- 'Algunos activos no cargaron: ' +
127
- fails.map(f => `${f.asset.label} (${String(f.error.message || f.error)})`).join(' · ')
128
- );
 
 
 
 
 
 
129
  }
130
 
131
- if (!ok.length) {
132
- showError('No se pudo cargar ningún activo (revisa conectividad / límites de API).');
133
- return;
 
134
  }
135
 
136
- // Labels desde el primer activo exitoso
137
- const labels = ok[0].series.map(p => tsToLabel(p.t));
138
-
139
- const datasets = ok.map(({asset, series}) => ({
140
- label: asset.label,
141
- data: series.map(pt => Number(pt.price.toFixed(6))),
142
- borderColor: colorFor(asset.label),
143
- backgroundColor: colorFor(asset.label),
144
- tension: 0.2,
145
- fill: false,
146
- pointRadius: 3
147
- }));
148
 
149
- // Chart.js
150
- const ctx = document.getElementById('priceChart').getContext('2d');
151
- window.myChart = new Chart(ctx, {
152
- type: 'line',
153
- data: { labels, datasets },
154
- options: {
155
- responsive: true,
156
- interaction: { mode: 'index', intersect: false },
157
- plugins: { legend: { display: true } },
158
- scales: { y: { ticks: { callback: (v) => v } } }
159
- }
160
- });
161
 
162
- // toggles (solo para los datasets realmente cargados)
163
- const togglesDiv = document.getElementById('toggles');
164
- togglesDiv.innerHTML = '';
165
- datasets.forEach((ds, i) => {
166
- const id = 'chk_' + i;
167
- const wrapper = document.createElement('label');
168
- wrapper.innerHTML = `
169
- <input type="checkbox" id="${id}" checked data-idx="${i}">
170
- <span style="width:64px;display:inline-block">${ds.label}</span>
171
- `;
172
- togglesDiv.appendChild(wrapper);
173
- wrapper.querySelector('input').addEventListener('change', e => {
174
- const idx = Number(e.target.dataset.idx);
175
- myChart.data.datasets[idx].hidden = !e.target.checked;
176
- myChart.update();
177
- });
178
  });
179
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  // --- Morpho estimator ---
182
- let morphoAPY = 0.04; // ejemplo 4% APY
183
  document.getElementById('morphoRateDisplay').innerText = (morphoAPY * 100).toFixed(2) + '%';
184
 
185
  function estimateCompound(amount, apy, days) {
@@ -203,13 +293,31 @@ document.getElementById('setRateBtn').addEventListener('click', () => {
203
  morphoAPY = n;
204
  document.getElementById('morphoRateDisplay').innerText = (morphoAPY * 100).toFixed(2) + '%';
205
  document.getElementById('calcBtn').click();
206
- } else {
207
- alert('Valor inválido');
208
- }
209
  });
210
 
211
- // initial calc
212
- document.getElementById('calcBtn').click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  </script>
214
  </body>
215
  </html>
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Dashboard: 7d (XRP, PAXG, WLD, USDC) + Earn Morpho (USDC)</title>
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
  <style>
9
  body { font-family: Inter, system-ui, Arial; margin: 18px; background:#f7f8fb; color:#111; }
10
  header { display:flex; gap:12px; align-items:center; margin-bottom:12px; flex-wrap:wrap; }
11
+ .card { background:white; border-radius:10px; padding:12px; box-shadow:0 6px 18px rgba(20,20,40,0.06); }
12
  .small { font-size:0.9rem; color:#555; }
13
+ #grid { display:grid; grid-template-columns: 1fr 320px; gap:12px; align-items:start; }
14
+ .charts { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:12px; }
15
+ .chartWrap { height: 260px; position:relative; }
16
  .banner {
17
+ display:none; margin-bottom:10px; padding:10px 12px;
18
+ background:#fff7ed; border:1px solid #fed7aa; color:#9a3412;
19
+ border-radius:10px; font-size:0.95rem;
 
 
 
 
 
20
  }
21
+ input[type="number"]{ width:120px; padding:6px; border-radius:8px; border:1px solid #ddd; }
22
+ select { padding:6px; border-radius:8px; border:1px solid #ddd; background:white; }
23
+ button { padding:8px 10px; border-radius:8px; border:none; background:#2563eb; color:white; cursor:pointer; }
24
  button.secondary { background:#e5e7eb; color:#111; }
25
+ .row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
 
26
  .muted { color:#6b7280; font-size:0.85rem; }
27
+ @media (max-width: 980px){
28
  #grid { grid-template-columns: 1fr; }
29
+ .charts { grid-template-columns: 1fr; }
30
+ .chartWrap { height: 240px; }
31
  }
32
  </style>
33
  </head>
34
  <body>
35
  <header>
36
+ <h2 style="margin:0;">Crypto 7d — 4 gráficas (XRP, PAXG, WLD, USDC)</h2>
37
+ <div class="small">Precios: CoinGecko · Realtime por polling · Morpho estimator (ejemplo).</div>
38
  </header>
39
 
40
  <div id="grid">
41
+ <div class="charts" id="charts"></div>
 
 
 
 
42
 
43
  <div class="card">
44
+ <h3 style="margin-top:0;">Realtime (polling)</h3>
45
+ <div class="small" style="margin-bottom:8px;">
46
+ Última actualización: <strong id="lastUpdate">-</strong>
47
+ </div>
48
+
49
+ <div class="row" style="margin-bottom:10px;">
50
+ <label class="small">Intervalo:</label>
51
+ <select id="pollSelect">
52
+ <option value="10">10s</option>
53
+ <option value="20" selected>20s</option>
54
+ <option value="30">30s</option>
55
+ <option value="60">60s</option>
56
+ </select>
57
+
58
+ <button id="toggleLiveBtn">Iniciar</button>
59
+ <button id="refreshNowBtn" class="secondary">Actualizar ahora</button>
60
+ </div>
61
+ <div class="muted">Recomendado: 30–60s para máxima estabilidad (menos 429).</div>
62
+
63
+ <hr style="margin:12px 0;" />
64
+
65
+ <h3 style="margin:0 0 6px;">Earn Morpho — Estimador (USDC)</h3>
66
  <div class="small" style="margin-bottom:8px;">
67
+ Tasa usada (APY referencia): <span id="morphoRateDisplay">4.00%</span>
68
  </div>
69
 
70
+ <label class="small">Monto en USDC:
71
  <input id="amount" type="number" min="0" step="0.01" value="1000">
72
  </label>
73
+
74
+ <div class="row" style="margin-top:8px;">
75
  <button id="calcBtn">Calcular (7 días)</button>
76
  <button id="setRateBtn" class="secondary">Usar otra tasa</button>
77
  </div>
 
79
  <div id="results" style="margin-top:12px;">
80
  <div class="small">Ganancia estimada (7 días): <strong id="gain7">-</strong></div>
81
  <div class="small">Total aproximado: <strong id="total7">-</strong></div>
82
+ <div class="muted" style="margin-top:6px;">(Interés compuesto diario aplicado sobre APY)</div>
83
  </div>
84
 
85
+ <div class="muted" style="margin-top:12px;">
86
+ Nota: CoinGecko API pública se usa para datos históricos. Ajusta la tasa Morpho según tu fuente.
 
 
 
 
87
  </div>
88
  </div>
89
  </div>
90
 
91
  <script>
92
+ /** IDs correctos CoinGecko */
93
+ const ASSETS = [
94
+ { id: 'ripple', label: 'XRP', color: '#1e90ff' },
95
+ { id: 'pax-gold', label: 'PAXG', color: '#bfa34a' },
96
+ { id: 'worldcoin-wld', label: 'WLD', color: '#7c3aed' },
97
+ { id: 'usd-coin', label: 'USDC', color: '#16a34a' },
98
  ];
99
 
100
+ const chartsById = new Map(); // assetId -> { chart, labels, data, hasLivePoint }
101
+ let liveTimer = null;
102
+
103
+ function tsToDateLabel(ts) {
104
+ const d = new Date(ts);
105
+ return d.toLocaleDateString();
106
+ }
107
+ function timeLabelNow() {
108
+ const d = new Date();
109
+ return 'Ahora ' + d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit', second:'2-digit'});
110
+ }
111
+
112
+ async function fetchMarketChart7d(assetId) {
113
  const url = `https://api.coingecko.com/api/v3/coins/${assetId}/market_chart?vs_currency=usd&days=7&interval=daily`;
114
  const res = await fetch(url);
115
  if (!res.ok) throw new Error(`${assetId} -> ${res.status}`);
 
117
  return j.prices.map(p => ({ t: p[0], price: p[1] })); // [timestamp, price]
118
  }
119
 
120
+ async function fetchSimplePrices(ids) {
121
+ const qs = encodeURIComponent(ids.join(','));
122
+ const url = `https://api.coingecko.com/api/v3/simple/price?ids=${qs}&vs_currencies=usd&include_last_updated_at=true`;
123
+ const res = await fetch(url);
124
+ if (!res.ok) throw new Error(`simple/price -> ${res.status}`);
125
+ return res.json();
126
  }
127
 
128
+ function makeCard(asset) {
129
+ const el = document.createElement('div');
130
+ el.className = 'card';
131
+ el.innerHTML = `
132
+ <div style="display:flex; align-items:baseline; justify-content:space-between; gap:10px; flex-wrap:wrap;">
133
+ <div>
134
+ <div style="font-size:1.05rem; font-weight:700;">${asset.label}</div>
135
+ <div class="muted">${asset.id}</div>
136
+ </div>
137
+ <div class="small">USD</div>
138
+ </div>
139
+ <div class="banner" id="err_${asset.id}"></div>
140
+ <div class="chartWrap"><canvas id="c_${asset.id}"></canvas></div>
141
+ `;
142
+ return el;
143
  }
144
 
145
+ function showAssetError(assetId, msg) {
146
+ const box = document.getElementById(`err_${assetId}`);
147
+ if (!box) return;
148
  box.style.display = 'block';
149
  box.textContent = msg;
150
  }
151
 
152
+ function hideAssetError(assetId) {
153
+ const box = document.getElementById(`err_${assetId}`);
154
+ if (!box) return;
155
+ box.style.display = 'none';
156
+ box.textContent = '';
157
+ }
158
 
159
+ function createChart(asset, labels, data) {
160
+ const ctx = document.getElementById(`c_${asset.id}`).getContext('2d');
161
+ const chart = new Chart(ctx, {
162
+ type: 'line',
163
+ data: {
164
+ labels,
165
+ datasets: [{
166
+ label: asset.label,
167
+ data,
168
+ borderColor: asset.color,
169
+ backgroundColor: asset.color,
170
+ tension: 0.2,
171
+ fill: false,
172
+ pointRadius: 2.5
173
+ }]
174
+ },
175
+ options: {
176
+ responsive: true,
177
+ maintainAspectRatio: false,
178
+ interaction: { mode: 'index', intersect: false },
179
+ plugins: { legend: { display: false } },
180
+ scales: {
181
+ x: { ticks: { maxRotation: 0 } },
182
+ y: { ticks: { callback: (v) => v } }
183
+ }
184
+ }
185
  });
186
+ chartsById.set(asset.id, { chart, labels, data, hasLivePoint: false });
187
+ }
188
 
189
+ function upsertLivePoint(assetId, price) {
190
+ const entry = chartsById.get(assetId);
191
+ if (!entry) return;
192
+
193
+ if (!entry.hasLivePoint) {
194
+ entry.labels.push(timeLabelNow());
195
+ entry.data.push(price);
196
+ entry.hasLivePoint = true;
197
+ } else {
198
+ entry.labels[entry.labels.length - 1] = timeLabelNow();
199
+ entry.data[entry.data.length - 1] = price;
200
  }
201
 
202
+ // Mantener puntaje razonable (histórico ~8 + live)
203
+ while (entry.labels.length > 9) {
204
+ entry.labels.shift();
205
+ entry.data.shift();
206
  }
207
 
208
+ entry.chart.update();
209
+ }
 
 
 
 
 
 
 
 
 
 
210
 
211
+ function setLastUpdate(ts) {
212
+ const el = document.getElementById('lastUpdate');
213
+ if (!ts) { el.textContent = '-'; return; }
214
+ const d = new Date(ts * 1000);
215
+ el.textContent = d.toLocaleString();
216
+ }
217
+
218
+ async function refreshLiveOnce() {
219
+ const ids = ASSETS.map(a => a.id);
220
+ const j = await fetchSimplePrices(ids);
 
 
221
 
222
+ let newest = 0;
223
+ ASSETS.forEach(a => {
224
+ const row = j[a.id];
225
+ if (!row || typeof row.usd !== 'number') return;
226
+ hideAssetError(a.id);
227
+ upsertLivePoint(a.id, row.usd);
228
+ if (row.last_updated_at && row.last_updated_at > newest) newest = row.last_updated_at;
 
 
 
 
 
 
 
 
 
229
  });
230
+ if (newest) setLastUpdate(newest);
231
+ }
232
+
233
+ function getSafeIntervalSeconds() {
234
+ const allowed = new Set([10, 20, 30, 60]);
235
+ const raw = parseInt(document.getElementById('pollSelect').value, 10);
236
+ const chosen = allowed.has(raw) ? raw : 20;
237
+ // “Más estable”: mínimo recomendado 20s
238
+ return Math.max(20, chosen);
239
+ }
240
+
241
+ function stopLive() {
242
+ if (liveTimer) clearInterval(liveTimer);
243
+ liveTimer = null;
244
+ document.getElementById('toggleLiveBtn').textContent = 'Iniciar';
245
+ }
246
+
247
+ async function startLive() {
248
+ stopLive();
249
+ try { await refreshLiveOnce(); } catch (e) { console.warn(e); }
250
+ const s = getSafeIntervalSeconds();
251
+ liveTimer = setInterval(async () => {
252
+ try { await refreshLiveOnce(); } catch (e) { console.warn(e); }
253
+ }, s * 1000);
254
+ document.getElementById('toggleLiveBtn').textContent = 'Detener';
255
+ }
256
+
257
+ // Re-inicia automáticamente si cambias intervalo mientras está corriendo
258
+ document.getElementById('pollSelect').addEventListener('change', () => {
259
+ if (liveTimer) startLive();
260
+ });
261
+
262
+ document.getElementById('refreshNowBtn').addEventListener('click', async () => {
263
+ try { await refreshLiveOnce(); } catch (e) { console.warn(e); }
264
+ });
265
+
266
+ document.getElementById('toggleLiveBtn').addEventListener('click', () => {
267
+ if (liveTimer) stopLive();
268
+ else startLive();
269
+ });
270
 
271
  // --- Morpho estimator ---
272
+ let morphoAPY = 0.04;
273
  document.getElementById('morphoRateDisplay').innerText = (morphoAPY * 100).toFixed(2) + '%';
274
 
275
  function estimateCompound(amount, apy, days) {
 
293
  morphoAPY = n;
294
  document.getElementById('morphoRateDisplay').innerText = (morphoAPY * 100).toFixed(2) + '%';
295
  document.getElementById('calcBtn').click();
296
+ } else alert('Valor inválido');
 
 
297
  });
298
 
299
+ (async function init() {
300
+ const chartsDiv = document.getElementById('charts');
301
+ ASSETS.forEach(a => chartsDiv.appendChild(makeCard(a)));
302
+
303
+ // Cargar histórico por activo (independiente)
304
+ await Promise.allSettled(ASSETS.map(async (a) => {
305
+ try {
306
+ const series = await fetchMarketChart7d(a.id);
307
+ const labels = series.map(p => tsToDateLabel(p.t));
308
+ const data = series.map(p => Number(p.price.toFixed(6)));
309
+ createChart(a, labels, data);
310
+ } catch (e) {
311
+ showAssetError(a.id, `Error cargando histórico: ${String(e.message || e)}`);
312
+ }
313
+ }));
314
+
315
+ // Primer cálculo Morpho
316
+ document.getElementById('calcBtn').click();
317
+
318
+ // Primer refresh realtime (sin iniciar timer)
319
+ try { await refreshLiveOnce(); } catch (e) { console.warn(e); }
320
+ })();
321
  </script>
322
  </body>
323
  </html>