Files
AtreidesAssetsFree/client/widgets/spot.js
T
2026-10-29 11:57:29 +01:00

173 lines
6.8 KiB
JavaScript

// Generated, do not edit. sha256:a0b282c1ed4d4e3e20c57f97c21ad932b410360a0adabc7ef61e694ce97a0ed9
import { listAssets } from '../data/assets.js';
import { getPref, setPref } from '../data/preferences.js';
import { baseCurrency } from '../app-settings.js';
import { listPriceHistory } from '../data/price-history.js';
import { t } from '../i18n/index.js';
import { toDailyGrid, dailyGridSources } from '../signals/data.js';
import { liveSpotFor, basePriceOf, lastSpotSource } from '../price/providers.js';
import { listProviders } from '../data/providers.js';
import { fmtMoney, colouredSigns } from '../ui/format.js';
import { assetLabelHtml } from '../ui/asset-label.js';
import { priceFromBase } from '../../shared/units.js';
import { secondCurrencyOf, secondAmount, renderSecondCurrencySetting } from './second-currency.js';
import { mountSparkline, changePctHtml, sliceTone } from '../ui/sparkline.js';
import { RANGE_DAYS, SPOT_RANGE_DAYS, rangeSeries, intradayStart, rangeLabel, rangeCycleButton, wireRangeCycle, rangePointLabel, rangePointSources, namedDaySources } from '../ui/range-cycle.js';
import { loadIntraday } from '../price/intraday.js';
import { momentumOn, momentumDotHtml, renderMomentumSetting } from './momentum-dot.js';
import { loadMomentum } from '../price/momentum.js';
const MS_PER_DAY = 86400000;
function fromDateStr(days) {
return new Date(Date.now() - (days + 5) * MS_PER_DAY).toISOString().slice(0, 10);
}
const liveHosts = new Set();
let spotBound = false;
async function refreshOneSpot(entry) {
try {
const spot = await liveSpotFor(entry.asset);
if (!entry.host.isConnected) { liveHosts.delete(entry); return; }
const live = basePriceOf(spot);
if (live == null) return;
entry.setLive(live);
} catch { }
}
async function refreshAllSpots() {
for (const entry of liveHosts) {
if (!entry.host.isConnected) { liveHosts.delete(entry); continue; }
refreshOneSpot(entry);
}
}
export async function renderSpotSettings(host, instance, commit) {
await renderSecondCurrencySetting(host, instance, commit);
renderMomentumSetting(host, instance, commit);
}
export async function renderSpotWidget(host, instance) {
const assetId = instance && instance.params ? instance.params.assetId : null;
const asset = (await listAssets()).find(a => a.id === assetId);
if (!asset) {
host.innerHTML = `<div class="card"><div class="t-body-sm t-muted">\u26A0 ${t('spot.unknownAsset')}</div></div>`;
return;
}
const cur = baseCurrency();
const headHtml = assetLabelHtml(asset);
const inUnit = p => (p == null ? null : priceFromBase(p, asset));
const second = secondCurrencyOf(instance);
const fmtOpts = p => ({ decimals: p >= 1000 ? 0 : 2 });
let range = await getPref('widget.spot', String(assetId), 'week');
if (!SPOT_RANGE_DAYS[range]) range = 'week';
host.innerHTML = `<div class="card spot-card">
<div class="flex-between spot-head-row">
<div class="t-label spot-head">${headHtml}</div>
<div class="spot-price"><span id="spot-price">-</span><span class="amount-second hidden" id="spot-second"></span></div>
</div>
<div class="flex-between spot-sub">
<div class="spot-change" id="spot-change"></div>
<div class="range-cycle" id="spot-range">${rangeCycleButton(range)}</div>
</div>
<div class="spot-spark-wrap" id="spot-spark"></div>
<div class="t-body-sm t-neg hidden" id="spot-err"></div>
</div>`;
let grid = null;
let live = null;
let intraday = null;
let intradaySource = null;
let intradaySources = null;
let daySources = new Map();
const priceEl = host.querySelector('#spot-price');
const changeEl = host.querySelector('#spot-change');
const sparkEl = host.querySelector('#spot-spark');
const secondEl = host.querySelector('#spot-second');
async function showSecond(price) {
if (!second) return;
const tekst = await secondAmount(price, second, fmtOpts(price));
secondEl.textContent = tekst || '';
secondEl.classList.toggle('hidden', !tekst);
}
function setLive(basePrice) {
const price = inUnit(basePrice);
if (!Number.isFinite(price)) return;
live = price;
priceEl.textContent = fmtMoney(price, cur, fmtOpts(price));
showSecond(price);
draw();
refreshIntraday();
}
async function refreshIntraday() {
if (range !== 'day' || !grid) return;
const r = await loadIntraday(asset.id, intradayStart(grid));
if (!host.isConnected) return;
intraday = r.points ? r.points.map(([ms, p]) => [ms, inUnit(p)]) : null;
intradaySource = r.source || null;
intradaySources = r.sources || null;
draw();
}
const entry = { host, asset, setLive };
liveHosts.add(entry);
if (!spotBound) { document.addEventListener('tick:spot', refreshAllSpots); spotBound = true; }
function draw() {
const rb = host.querySelector('.range-cycle-btn');
if (rb) rb.textContent = rangeLabel(range);
if (!grid) return;
const nowMs = Date.now();
const use = rangeSeries(grid, range, live, nowMs, range === 'day' ? intraday : null);
const sources = rangePointSources(use, { range, spot: live, nowMs, live: lastSpotSource(asset),
intraday: intradaySource, intradayAt: intradaySources, days: daySources });
mountSparkline(sparkEl, use, {
tone: sliceTone(use),
formatValue: (p, ms) => rangePointLabel(range, fmtMoney(p, cur, { decimals: p >= 1000 ? 0 : 2 }), ms, sources.get(ms)),
});
changeEl.innerHTML = changePctHtml(use, momentumDotHtml(live != null ? asset.id : null, momentumOn(instance)), { fine: range === 'day', signs: colouredSigns() });
}
draw();
try {
const [spot, rows, , providers] = await Promise.all([
liveSpotFor(asset).catch(() => null),
Promise.resolve(await listPriceHistory(assetId, fromDateStr(RANGE_DAYS.year))),
loadMomentum(asset.id),
listProviders().catch(() => []),
]);
try { grid = toDailyGrid(rows).map(([d, p]) => [d, inUnit(p)]); }
catch { grid = null; }
daySources = namedDaySources(dailyGridSources(rows), providers);
const spotNu = inUnit(basePriceOf(spot));
const lastClose = grid && grid.length ? grid[grid.length - 1][1] : null;
if (spotNu != null) live = spotNu;
const price = spotNu != null ? spotNu : lastClose;
priceEl.textContent = price != null ? fmtMoney(price, cur, fmtOpts(price)) : '-';
if (price != null) showSecond(price);
if (!grid) sparkEl.innerHTML = `<div class="spot-spark-empty">${t('spot.noHistory')}</div>`;
else { draw(); refreshIntraday(); }
} catch (err) {
console.error('[spot-widget]', err);
const e = host.querySelector('#spot-err');
if (e) { e.classList.remove('hidden'); e.textContent = '\u26A0 ' + err.message; }
}
wireRangeCycle(host.querySelector('#spot-range'), () => range, next => {
range = next;
setPref('widget.spot', String(assetId), range).catch(() => { });
draw();
refreshIntraday();
}, SPOT_RANGE_DAYS);
}