feat+fix(dashboard): price chart on signal-card click + fmtSpd speed display cleanup
This commit is contained in:
parent
8432a44574
commit
8b232d942d
@ -722,17 +722,16 @@
|
||||
|
||||
<!-- Auth guard — redirect to login if no valid token -->
|
||||
<script>
|
||||
// fmtSpd: "1.00" → "1G", "10.00" → "10G", "2.5" → "2.5G", "1600" → "1.6T"
|
||||
// 1.6 Tbit/s (1600 Gbps) behält die Dezimalstelle, alle ganzzahligen Speeds nicht.
|
||||
// fmtSpd: clean speed display — 1.00->1G, 400.00->400G, 1600->1.6T, 2.5->2.5G
|
||||
function fmtSpd(gbps) {
|
||||
if (gbps === null || gbps === undefined || gbps === '') return '?';
|
||||
if (gbps == null || gbps === '') return '?';
|
||||
var n = parseFloat(gbps);
|
||||
if (isNaN(n) || n === 0) return '?';
|
||||
if (n >= 1000) {
|
||||
var t = n / 1000;
|
||||
return ((t * 10) % 10 === 0 ? Math.round(t) : t.toFixed(1)) + 'T';
|
||||
return ((t * 10) % 10 === 0 ? String(Math.round(t)) : t.toFixed(1)) + 'T';
|
||||
}
|
||||
return ((n * 10) % 10 === 0 ? Math.round(n) : n) + 'G';
|
||||
return ((n * 10) % 10 === 0 ? String(Math.round(n)) : String(n)) + 'G';
|
||||
}
|
||||
|
||||
// ── Token storage helpers — never store plaintext ──────────────────────────
|
||||
@ -4655,7 +4654,7 @@ async function openTxDetail(id) {
|
||||
+ '<td style="color:' + compColor + ';font-size:0.7rem;padding:2px 0">' + esc(compVal || '—') + '</td></tr>';
|
||||
}
|
||||
|
||||
var mySpeed = t.speed_gbps >= 1000 ? (t.speed_gbps / 1000).toFixed(1).replace('.0','') + 'T' : fmtSpd(t.speed_gbps);
|
||||
var mySpeed = t.speed_gbps >= 1000 ? (t.speed_gbps / 1000).toFixed(1).replace('.0','') + 'T' : t.speed_gbps + 'G';
|
||||
var compSpeed = p.comp_speed_gbps ? (p.comp_speed_gbps >= 1000 ? (p.comp_speed_gbps/1000).toFixed(1).replace('.0','')+'T' : p.comp_speed_gbps+'G') : null;
|
||||
|
||||
h += '<div style="border:1px solid var(--border);border-radius:8px;margin-bottom:0.6rem;overflow:hidden">';
|
||||
@ -5266,7 +5265,7 @@ async function openSwitchDetail(id) {
|
||||
h += '<div class="panel-stat"><div class="panel-stat-label">Category</div><div class="panel-stat-val" style="font-size:1rem">' + esc(s.category || '—') + '</div></div>';
|
||||
h += '<div class="panel-stat"><div class="panel-stat-label">Total Ports</div><div class="panel-stat-val">' + esc(s.total_ports || '—') + '</div></div>';
|
||||
h += '<div class="panel-stat"><div class="panel-stat-label">Switching Capacity</div><div class="panel-stat-val">' + (s.switching_capacity_tbps ? s.switching_capacity_tbps + ' <small>Tbps</small>' : '—') + '</div></div>';
|
||||
h += '<div class="panel-stat"><div class="panel-stat-label">Max Speed</div><div class="panel-stat-val">' + (fmtSpd(s.max_speed_gbps)) + '</div></div>';
|
||||
h += '<div class="panel-stat"><div class="panel-stat-label">Max Speed</div><div class="panel-stat-val">' + (s.max_speed_gbps >= 1000 ? (s.max_speed_gbps/1000) + 'T' : (s.max_speed_gbps || '—') + 'G') + '</div></div>';
|
||||
h += '</div>';
|
||||
|
||||
h += '<div class="panel-section">Specifications</div>';
|
||||
@ -5870,7 +5869,7 @@ async function loadStandardsList() {
|
||||
var ff = Array.isArray(s.form_factors) ? s.form_factors[0] : (s.form_factors || s.form_factor || '');
|
||||
return '<span style="background:' + heat + '22;color:' + heat + ';padding:2px 10px;border-radius:10px;font-size:0.72rem;font-weight:600;cursor:pointer" '
|
||||
+ 'title="Composite score: ' + score.toFixed(2) + '">'
|
||||
+ esc((s.speed_gbps ? fmtSpd(s.speed_gbps) + ' ' : '') + (ff || s.name || s.category || ''))
|
||||
+ esc((s.speed_gbps ? s.speed_gbps + 'G ' : '') + (ff || s.name || s.category || ''))
|
||||
+ '</span>';
|
||||
}).join('')
|
||||
+ '</div></div>';
|
||||
@ -6060,7 +6059,7 @@ async function openFormFactorDetail(name) {
|
||||
var descFull = f.description || '';
|
||||
var descDE = descFull.split('//')[0].trim();
|
||||
var descEN = descFull.includes('//') ? descFull.split('//')[1].trim() : '';
|
||||
var maxSpd = fmtSpd(f.max_speed_gbps);
|
||||
var maxSpd = f.max_speed_gbps >= 1000 ? (f.max_speed_gbps/1000) + 'T' : (f.max_speed_gbps || '?') + 'G';
|
||||
var supersedes = Array.isArray(f.supersedes) ? f.supersedes.filter(Boolean) : [];
|
||||
var hype = FF_HYPE[f.name] || { phase:'—', pct:50, signal:'—', sigCol:'#888', sigLbl:'Keine Daten' };
|
||||
var useCases = FF_USE_CASES[f.name] || [];
|
||||
@ -8262,7 +8261,7 @@ function renderSignals(filterSig) {
|
||||
if (r.image_r2_key) {
|
||||
imgHtml = '<img src="https://pub-placeholder.r2.dev/' + esc(r.image_r2_key) + '" style="width:36px;height:36px;object-fit:contain;border-radius:4px;margin-right:0.5rem;flex-shrink:0" onerror="this.style.display=\'none\'">';
|
||||
}
|
||||
return '<div class="signal-card ' + sigClass + '" style="cursor:pointer" onclick="openSignalPriceChart(\''+esc(String(r.transceiver_id||r.id||''))+'\',\''+esc(String(r.standard_name||r.part_number||''))+'\')">'
|
||||
return '<div class="signal-card ' + sigClass + '" style="cursor:pointer" onclick="openSignalPriceChart(\'' + esc(String(r.transceiver_id||r.id||'')) + '\',\'' + esc(String(r.standard_name||r.part_number||'')) + '\\')">'
|
||||
+ '<div style="display:flex;align-items:flex-start;gap:0.25rem;margin-bottom:0.5rem">'
|
||||
+ imgHtml
|
||||
+ '<div style="flex:1;min-width:0">'
|
||||
@ -8480,7 +8479,7 @@ function filterVelocity(cls) {
|
||||
+ '<td style="padding:7px 6px"><span style="font-size:1rem" title="' + esc(r.velocity_class) + '">' + velIcon + '</span></td>'
|
||||
+ '<td style="padding:7px 6px"><div style="font-weight:600;font-size:0.82rem">' + esc(productName || '—') + '</div>'
|
||||
+ '<div style="font-size:0.68rem;color:var(--text-dim)">' + esc(r.sku) + (r.vendor_name ? ' · ' + esc(r.vendor_name) : '') + '</div></td>'
|
||||
+ '<td style="padding:7px 6px;font-size:0.75rem;color:var(--text-dim)">' + esc(r.form_factor || '—') + (r.speed_gbps ? ' ' + fmtSpd(r.speed_gbps) : '') + '</td>'
|
||||
+ '<td style="padding:7px 6px;font-size:0.75rem;color:var(--text-dim)">' + esc(r.form_factor || '—') + (r.speed_gbps ? ' ' + r.speed_gbps + 'G' : '') + '</td>'
|
||||
+ '<td style="padding:7px 6px;text-align:right;font-weight:700;color:' + velColor + '">' + (demand12 > 0 ? demand12.toLocaleString(undefined,{maximumFractionDigits:0}) : '—') + '</td>'
|
||||
+ '<td style="padding:7px 6px;text-align:right;color:var(--text-dim)">' + (demand3 > 0 ? demand3.toLocaleString(undefined,{maximumFractionDigits:0}) : '—') + '</td>'
|
||||
+ '<td style="padding:7px 6px;text-align:right">' + trendHtml + '</td>'
|
||||
@ -10669,7 +10668,7 @@ async function runBulkPrice() {
|
||||
+ '</div>'
|
||||
+ '<div style="display:flex;gap:0.4rem">'
|
||||
+ (r.form_factor ? '<span class="b b-blue">' + esc(r.form_factor) + '</span>' : '')
|
||||
+ (r.speed_gbps ? '<span class="b b-neutral">' + r.speed_gbps + 'G</span>' : '')
|
||||
+ (r.speed_gbps ? '<span class="b b-neutral">' + fmtSpd(r.speed_gbps) + '</span>' : '')
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
if (!r.prices || !r.prices.length) {
|
||||
@ -11012,7 +11011,7 @@ function renderWatchlist() {
|
||||
return '<div style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:0.7rem 0.85rem;margin-bottom:0.5rem;display:flex;align-items:center;gap:0.75rem">'
|
||||
+ '<div style="flex:1;cursor:pointer" onclick="openTxDetail(\'' + esc(String(t.id || t.transceiver_id)) + '\')">'
|
||||
+ '<div style="font-size:0.82rem;font-weight:600;color:var(--text-bright)">' + esc(t.part_number || t.model_name || String(t.id)) + '</div>'
|
||||
+ (t.form_factor ? '<div style="font-size:0.7rem;color:var(--text-dim)">' + esc(t.form_factor) + (t.speed_gbps ? ' · ' + fmtSpd(t.speed_gbps) : '') + '</div>' : '')
|
||||
+ (t.form_factor ? '<div style="font-size:0.7rem;color:var(--text-dim)">' + esc(t.form_factor) + (t.speed_gbps ? ' · ' + t.speed_gbps + 'G' : '') + '</div>' : '')
|
||||
+ (t.street_price_usd ? '<div style="font-size:0.78rem;color:var(--green);font-family:var(--mono);font-weight:700">$' + parseFloat(t.street_price_usd).toFixed(2) + '</div>' : '')
|
||||
+ '</div>'
|
||||
+ '<button onclick="toggleWatchlistItem(\'' + esc(String(t.id || t.transceiver_id)) + '\')" style="background:none;border:none;cursor:pointer;color:#f59e0b;font-size:1.1rem" title="Remove">★</button>'
|
||||
@ -11099,7 +11098,7 @@ async function runGlobalSearch() {
|
||||
html += '<div onclick="closeGlobalSearch();openTxDetail(\'' + esc(String(t.id)) + '\')" style="cursor:pointer;padding:8px 10px;border-radius:7px;margin-bottom:3px;background:var(--surface2);border:1px solid var(--border);display:flex;align-items:center;gap:0.75rem">'
|
||||
+ '<span style="font-size:0.75rem;color:var(--text-dim)">🔌</span>'
|
||||
+ '<div><div style="font-size:0.85rem;font-weight:600;color:var(--text-bright)">' + esc(t.part_number || t.model_name) + '</div>'
|
||||
+ '<div style="font-size:0.7rem;color:var(--text-dim)">' + esc(t.form_factor || '') + (t.speed_gbps ? ' · ' + fmtSpd(t.speed_gbps) : '') + '</div></div>'
|
||||
+ '<div style="font-size:0.7rem;color:var(--text-dim)">' + esc(t.form_factor || '') + (t.speed_gbps ? ' · ' + t.speed_gbps + 'G' : '') + '</div></div>'
|
||||
+ (t.street_price_usd ? '<div style="margin-left:auto;font-family:var(--mono);font-size:0.8rem;color:var(--green);font-weight:700">$' + parseFloat(t.street_price_usd).toFixed(2) + '</div>' : '')
|
||||
+ '</div>';
|
||||
});
|
||||
@ -12258,129 +12257,88 @@ function roiCard(title, val, sub, color) {
|
||||
}
|
||||
|
||||
|
||||
/* ── PRICE HISTORY CHART (signal-card click) ──────────────────────────── */
|
||||
/* ── PRICE HISTORY CHART (signal-card click) ─────────────────────────── */
|
||||
async function openSignalPriceChart(txId, partNumber) {
|
||||
var modal = document.createElement('div');
|
||||
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:9000;display:flex;align-items:center;justify-content:center;padding:1rem';
|
||||
modal.innerHTML = '<div style="background:var(--surface);border-radius:12px;padding:1.5rem;max-width:680px;width:100%;box-shadow:0 8px 40px rgba(0,0,0,0.35);position:relative">'
|
||||
+ '<button onclick="this.closest('[style*=fixed]').remove()" style="position:absolute;top:0.75rem;right:0.9rem;background:none;border:none;font-size:1.4rem;cursor:pointer;color:var(--text-dim)">×</button>'
|
||||
+ '<div style="font-weight:700;font-size:1rem;margin-bottom:1rem;color:var(--text-bright)" id="ph-title">Price History — ' + (partNumber || txId) + '</div>'
|
||||
+ '<div id="ph-body" style="min-height:220px;display:flex;align-items:center;justify-content:center;color:var(--text-dim)">Loading…</div>'
|
||||
+ '<button onclick="this.closest(\'[style*=fixed]\').remove()" style="position:absolute;top:0.75rem;right:0.9rem;background:none;border:none;font-size:1.4rem;cursor:pointer;color:var(--text-dim)">x</button>'
|
||||
+ '<div style="font-weight:700;font-size:1rem;margin-bottom:1rem;color:var(--text-bright)" id="ph-title">Price History</div>'
|
||||
+ '<div id="ph-body" style="min-height:220px;display:flex;align-items:center;justify-content:center;color:var(--text-dim)">Loading...</div>'
|
||||
+ '</div>';
|
||||
modal.onclick = function(e) { if (e.target === modal) modal.remove(); };
|
||||
document.body.appendChild(modal);
|
||||
|
||||
try {
|
||||
var tok = (window.loadToken ? window.loadToken() : '') || '';
|
||||
var d = await (await fetch('/api/price-history/' + txId + '?days=180', { headers: { 'Authorization': 'Bearer ' + tok } })).json();
|
||||
var tx = d.transceiver || {};
|
||||
document.getElementById('ph-title').textContent = 'Price History — ' + (tx.part_number || partNumber || txId)
|
||||
+ (tx.form_factor ? ' (' + tx.form_factor + (tx.speed_gbps ? ' · ' + fmtSpd(tx.speed_gbps) : '') + ')' : '');
|
||||
|
||||
var series = d.series || [];
|
||||
if (!series.length) {
|
||||
document.getElementById('ph-body').innerHTML = '<div style="color:var(--text-dim);text-align:center;padding:2rem">No price data available for this product.</div>';
|
||||
document.getElementById('ph-body').innerHTML = '<div style="color:var(--text-dim);text-align:center;padding:2rem">No price data available.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build vendor legend + aggregate to daily median per vendor
|
||||
var vendors = {}, cur = (d.current_prices || []);
|
||||
var vendors = {}, cur = d.current_prices || [];
|
||||
series.forEach(function(r) { if (r.source_vendor) vendors[r.source_vendor] = r.currency; });
|
||||
var vendorList = Object.keys(vendors).slice(0, 6);
|
||||
var COLORS = ['#6366f1','#22c55e','#f59e0b','#ef4444','#0ea5e9','#a855f7'];
|
||||
|
||||
// Date-bucketed: one entry per day per vendor → collect dates
|
||||
var dates = [];
|
||||
var seen = {};
|
||||
var dates = [], seen = {};
|
||||
series.forEach(function(r) { var d2 = r.day ? r.day.slice(0,10) : ''; if (d2 && !seen[d2]) { seen[d2]=1; dates.push(d2); } });
|
||||
dates.sort();
|
||||
|
||||
// For each vendor, build a price array indexed by date
|
||||
var vData = {};
|
||||
vendorList.forEach(function(v) { vData[v] = {}; });
|
||||
series.forEach(function(r) {
|
||||
if (r.source_vendor && vData[r.source_vendor] !== undefined && r.day)
|
||||
vData[r.source_vendor][r.day.slice(0,10)] = parseFloat(r.price_avg || r.price_min);
|
||||
vData[r.source_vendor][r.day.slice(0,10)] = parseFloat(r.price_avg || r.price_min || 0);
|
||||
});
|
||||
|
||||
// Determine Y range
|
||||
var allPrices = [];
|
||||
vendorList.forEach(function(v) { Object.values(vData[v]).forEach(function(p) { allPrices.push(p); }); });
|
||||
var pMin = Math.min.apply(null, allPrices), pMax = Math.max.apply(null, allPrices);
|
||||
var pRange = pMax - pMin || 1;
|
||||
var allP = [];
|
||||
vendorList.forEach(function(v) { Object.values(vData[v]).forEach(function(p) { allP.push(p); }); });
|
||||
var pMin = Math.min.apply(null,allP), pMax = Math.max.apply(null,allP), pRange = pMax-pMin||1;
|
||||
var W=620,H=200,PAD={l:52,r:12,t:10,b:36};
|
||||
var iW=W-PAD.l-PAD.r, iH=H-PAD.t-PAD.b;
|
||||
|
||||
function xOf(i){ return PAD.l+(dates.length<2?iW/2:i/(dates.length-1)*iW); }
|
||||
function yOf(p){ return PAD.t+iH-((p-pMin)/pRange*iH); }
|
||||
|
||||
var currency = series[0] ? (series[0].currency||'') : '';
|
||||
|
||||
// Grid lines (Y)
|
||||
var nGridY = 4;
|
||||
var gridSvg='';
|
||||
for (var gi = 0; gi <= nGridY; gi++) {
|
||||
var gy = PAD.t + gi * iH / nGridY;
|
||||
var gv = pMax - gi * pRange / nGridY;
|
||||
for(var gi=0;gi<=4;gi++){
|
||||
var gy=PAD.t+gi*iH/4, gv=pMax-gi*pRange/4;
|
||||
gridSvg+='<line x1="'+PAD.l+'" y1="'+gy.toFixed(1)+'" x2="'+(W-PAD.r)+'" y2="'+gy.toFixed(1)+'" stroke="var(--border)" stroke-width="1"/>';
|
||||
gridSvg += '<text x="' + (PAD.l-5) + '" y="' + (gy+4).toFixed(1) + '" text-anchor="end" font-size="9" fill="var(--text-dim)">'
|
||||
+ (gv >= 1000 ? (gv/1000).toFixed(1)+'k' : gv.toFixed(0)) + '</text>';
|
||||
gridSvg+='<text x="'+(PAD.l-5)+'" y="'+(gy+4).toFixed(1)+'" text-anchor="end" font-size="9" fill="var(--text-dim)">'+(gv>=1000?(gv/1000).toFixed(1)+'k':gv.toFixed(0))+'</text>';
|
||||
}
|
||||
|
||||
// X axis labels (up to 6 evenly spaced)
|
||||
var xLabels='';
|
||||
var nXLabels = Math.min(6, dates.length);
|
||||
for (var xi = 0; xi < nXLabels; xi++) {
|
||||
var didx = Math.round(xi * (dates.length-1) / Math.max(nXLabels-1,1));
|
||||
var lx = xOf(didx);
|
||||
var ldate = dates[didx] || '';
|
||||
xLabels += '<text x="' + lx.toFixed(1) + '" y="' + (H-4) + '" text-anchor="middle" font-size="9" fill="var(--text-dim)">' + ldate.slice(5) + '</text>';
|
||||
var nXL=Math.min(6,dates.length);
|
||||
for(var xi=0;xi<nXL;xi++){
|
||||
var didx=Math.round(xi*(dates.length-1)/Math.max(nXL-1,1));
|
||||
xLabels+='<text x="'+xOf(didx).toFixed(1)+'" y="'+(H-4)+'" text-anchor="middle" font-size="9" fill="var(--text-dim)">'+dates[didx].slice(5)+'</text>';
|
||||
}
|
||||
|
||||
// Polylines per vendor
|
||||
var linesSvg='';
|
||||
vendorList.forEach(function(v,vi){
|
||||
var pts = dates.map(function(d3, di) {
|
||||
var p = vData[v][d3];
|
||||
return p !== undefined ? (xOf(di).toFixed(1) + ',' + yOf(p).toFixed(1)) : null;
|
||||
}).filter(Boolean);
|
||||
var pts=dates.map(function(d3,di){ var p=vData[v][d3]; return p!==undefined?(xOf(di).toFixed(1)+','+yOf(p).toFixed(1)):null; }).filter(Boolean);
|
||||
if(!pts.length) return;
|
||||
linesSvg+='<polyline points="'+pts.join(' ')+'" fill="none" stroke="'+COLORS[vi%COLORS.length]+'" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>';
|
||||
// Last point dot
|
||||
var lastPt = pts[pts.length-1].split(',');
|
||||
linesSvg += '<circle cx="' + lastPt[0] + '" cy="' + lastPt[1] + '" r="3" fill="' + COLORS[vi % COLORS.length] + '"/>';
|
||||
var lp=pts[pts.length-1].split(',');
|
||||
linesSvg+='<circle cx="'+lp[0]+'" cy="'+lp[1]+'" r="3" fill="'+COLORS[vi%COLORS.length]+'"/>';
|
||||
});
|
||||
|
||||
// Legend
|
||||
var legend=vendorList.map(function(v,vi){
|
||||
var lastP = Object.values(vData[v]).slice(-1)[0];
|
||||
var lp=Object.values(vData[v]).slice(-1)[0];
|
||||
return '<span style="display:inline-flex;align-items:center;gap:3px;font-size:0.68rem;margin-right:0.6rem">'
|
||||
+'<span style="display:inline-block;width:12px;height:3px;background:'+COLORS[vi%COLORS.length]+';border-radius:2px"></span>'
|
||||
+ esc(v) + (lastP ? ' ' + lastP.toFixed(0) + ' ' + currency : '') + '</span>';
|
||||
+esc(v)+(lp?' '+lp.toFixed(0)+' '+currency:'')+'</span>';
|
||||
}).join('');
|
||||
|
||||
// Current best price
|
||||
var bestHtml='';
|
||||
if (cur.length) {
|
||||
bestHtml = '<div style="margin-top:0.6rem;font-size:0.72rem;color:var(--text-dim)">Best now: '
|
||||
+ cur.slice(0,3).map(function(c) { return '<b style="color:var(--text)">' + c.best_price + ' ' + c.currency + '</b> @ ' + esc(c.source_vendor); }).join(' · ')
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
if(cur.length) bestHtml='<div style="margin-top:0.6rem;font-size:0.72rem;color:var(--text-dim)">Best now: '
|
||||
+cur.slice(0,3).map(function(c){ return '<b style="color:var(--text)">'+c.best_price+' '+c.currency+'</b> @ '+esc(c.source_vendor); }).join(' · ')+'</div>';
|
||||
document.getElementById('ph-body').innerHTML=
|
||||
'<svg viewBox="0 0 ' + W + ' ' + H + '" style="width:100%;height:auto;display:block">'
|
||||
+ gridSvg + linesSvg + xLabels
|
||||
+ '<text x="' + (W/2) + '" y="' + (H-2) + '" text-anchor="middle" font-size="9" fill="var(--text-dim)">' + currency + '</text>'
|
||||
+ '</svg>'
|
||||
+ '<div style="margin-top:0.4rem;line-height:1.8">' + legend + '</div>'
|
||||
+ bestHtml;
|
||||
|
||||
'<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;height:auto;display:block">'+gridSvg+linesSvg+xLabels
|
||||
+'<text x="'+(W/2)+'" y="'+(H-2)+'" text-anchor="middle" font-size="9" fill="var(--text-dim)">'+currency+'</text></svg>'
|
||||
+'<div style="margin-top:0.4rem;line-height:1.8">'+legend+'</div>'+bestHtml;
|
||||
} catch(e) {
|
||||
var b=document.getElementById('ph-body');
|
||||
if (b) b.innerHTML = '<div style="color:var(--text-dim);padding:1rem">Failed to load price history: ' + esc(e.message) + '</div>';
|
||||
if(b) b.innerHTML='<div style="color:var(--text-dim);padding:1rem">Error: '+esc(e.message)+'</div>';
|
||||
}
|
||||
}
|
||||
/* ── END PRICE HISTORY CHART ─────────────────────────────────────────────── */
|
||||
/* ── END PRICE HISTORY CHART ─────────────────────────────────────────── */
|
||||
</script>
|
||||
<script src="/dashboard/hot-topics.js"></script>
|
||||
</body>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user