upstream: integrar Oasis 1.3.1

Base del merge a tres bandas: git 0.9.5 de epsylon, esta rama y el src de la
APK oficial 1.3.1 (la variante movil: OASIS_MOBILE, regex sin \p{}, audio y
video de Android). Corre en el mismo nodejs-mobile 18.20.4 que ya usabamos.

Se conserva de la rama: Karvan, identidades y multicuenta, topbar y barra
inferior, borrado de EXIF sin sharp, filtro hot del foro, RTC sin STUN y los
modulos apagados en movil (wallet, banking, torrents).

Se toma de upstream lo que ya hace de serie: respuesta citada en chats, ssb-lan
en movil, configuracion en ~/.ssb/oasis (sobrevive a reinstalar el backend).
El asistente de bienvenida deja elegir workflow tambien en movil.
This commit is contained in:
oasis-bot 2026-10-18 02:52:17 +02:00
parent 8d838dd8b1
commit c332afd6ea
265 changed files with 80970 additions and 29167 deletions

View file

@ -1,9 +1,11 @@
const { div, h2, h3, p, section, button, form, a, input, span, pre, table, thead, tbody, tr, td, th, br, strong, label } = require("../server/node_modules/hyperaxe");
const { template, i18n, userLink, formatCarbon } = require("../views/main_views");
const { div, h2, h3, p, section, button, form, a, input, span, pre, table, thead, tbody, tr, td, th, br, strong, label, ul, li } = require("../server/node_modules/hyperaxe");
const RAW_HTML = Symbol.for('oasis.rawHtml');
const { template, i18n, userLink, formatCarbon, renderStateChip, renderModuleStats, renderWalletChip, paged } = require("../views/main_views");
const moment = require("../server/node_modules/moment");
const FILTER_LABELS = {
overview: i18n.bankOverview,
ubi: i18n.bankUbiTab,
exchange: i18n.bankExchange,
taxes: i18n.bankTaxes || "Taxes",
mine: i18n.mine,
@ -63,79 +65,254 @@ const escAttr = (s) => String(s)
.replace(/"/g, '"')
.replace(/'/g, ''');
const buildEcoValueChartSvg = (history, labels) => {
const arr = Array.isArray(history) ? history.slice(-120) : [];
const W = 720, H = 320;
const padL = 56, padR = 16, padT = 16, padB = 70;
const CHART_LINE_CLASSES = ["value", "supply", "inflation"];
const legendLayout = (labels, x0, maxX) => {
const rows = [[]];
let x = x0;
for (const label of labels) {
const width = 18 + String(label).length * 6.6 + 24;
if (x + width > maxX && rows[rows.length - 1].length) { rows.push([]); x = x0; }
rows[rows.length - 1].push({ label, x });
x += width;
}
return rows;
};
const axisDecimals = (max, fallback) => {
const m = Math.abs(Number(max) || 0);
if (m >= 10000) return 0;
if (m >= 100) return 2;
return fallback;
};
const buildSeriesChartSvg = ({ series = [], xLabels = [], empty = "", yDecimals = 4, yDomain = null }) => {
const valid = series.filter(s => Array.isArray(s.points) && s.points.length >= 2);
const W = 720;
const padR = 16, padT = 16;
const first = valid[0] ? valid[0].points.map(v => Number(v) || 0) : [0];
const yMax = yDomain ? Number(yDomain.max) : Math.max(...first);
const yMin = yDomain ? Number(yDomain.min) : Math.min(...first);
const decimals = axisDecimals(Math.max(Math.abs(yMax), Math.abs(yMin)), yDecimals);
const widestLabel = Math.max(...[yMax, yMin].map(v => v.toFixed(decimals).length));
const padL = Math.max(56, Math.round(widestLabel * 6.2) + 14);
const legendRows = legendLayout(valid.map(s => s.label || ""), padL, W - padR);
const padB = 54 + 16 * Math.max(1, legendRows.length);
const H = 250 + padB;
const plotW = W - padL - padR;
const plotH = H - padT - padB;
if (arr.length < 2) {
if (!valid.length) {
return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" class="bank-eco-chart-svg" preserveAspectRatio="xMidYMid meet">`
+ `<rect x="0" y="0" width="${W}" height="${H}" class="bank-eco-chart-bg" />`
+ `<text x="${W/2}" y="${H/2}" text-anchor="middle" class="bank-eco-chart-empty">${escAttr(labels.empty || 'Not enough samples yet')}</text>`
+ `<text x="${W/2}" y="${H/2}" text-anchor="middle" class="bank-eco-chart-empty">${escAttr(empty || 'Not enough samples yet')}</text>`
+ `</svg>`;
}
const values = arr.map(s => Number(s.ecoValue || 0));
const supplies = arr.map(s => Number(s.currentSupply || 0));
const inflations = arr.map(s => Number(s.inflationFactor || 0));
const minV = Math.min(...values);
const maxV = Math.max(...values);
const rangeV = maxV - minV || 1;
const minS = Math.min(...supplies);
const maxS = Math.max(...supplies);
const rangeS = maxS - minS || 1;
const minI = Math.min(...inflations);
const maxI = Math.max(...inflations);
const rangeI = maxI - minI || 1;
const stepX = arr.length > 1 ? plotW / (arr.length - 1) : plotW;
const xy = (i, v, minR, rangeR) => {
const x = padL + i * stepX;
const y = padT + plotH - ((v - minR) / rangeR) * plotH;
return `${x.toFixed(2)},${y.toFixed(2)}`;
};
const pointsValue = values.map((v, i) => xy(i, v, minV, rangeV)).join(' ');
const pointsSupply = supplies.map((v, i) => xy(i, v, minS, rangeS)).join(' ');
const pointsInfl = inflations.map((v, i) => xy(i, v, minI, rangeI)).join(' ');
const tsStart = moment(arr[0].ts).format('YYYY-MM-DD HH:mm');
const tsEnd = moment(arr[arr.length - 1].ts).format('YYYY-MM-DD HH:mm');
const tsMid = moment(arr[Math.floor(arr.length / 2)].ts).format('YYYY-MM-DD HH:mm');
const lines = valid.map((s, idx) => {
const vals = s.points.map(v => Number(v) || 0);
const min = yDomain ? yMin : Math.min(...vals);
const max = yDomain ? yMax : Math.max(...vals);
const range = max - min || 1;
const stepX = plotW / (vals.length - 1);
const pts = vals.map((v, i) => `${(padL + i * stepX).toFixed(2)},${(padT + plotH - ((v - min) / range) * plotH).toFixed(2)}`).join(" ");
return { cls: CHART_LINE_CLASSES[idx % CHART_LINE_CLASSES.length], label: s.label || "", pts, min, max, range };
});
const yTicks = 4;
const grid = [];
for (let i = 0; i <= yTicks; i++) {
const y = padT + (plotH / yTicks) * i;
grid.push(`<line x1="${padL}" x2="${W - padR}" y1="${y.toFixed(2)}" y2="${y.toFixed(2)}" class="bank-eco-chart-grid" />`);
const val = maxV - (rangeV / yTicks) * i;
grid.push(`<text x="${padL - 6}" y="${(y + 4).toFixed(2)}" text-anchor="end" class="bank-eco-chart-axis">${val.toFixed(4)}</text>`);
const val = lines[0].max - (lines[0].range / yTicks) * i;
grid.push(`<text x="${padL - 6}" y="${(y + 4).toFixed(2)}" text-anchor="end" class="bank-eco-chart-axis">${val.toFixed(decimals)}</text>`);
}
const xLabelY = padT + plotH + 16;
const xLabels = `<text x="${padL}" y="${xLabelY}" text-anchor="start" class="bank-eco-chart-axis">${escAttr(tsStart)}</text>`
+ `<text x="${(padL + plotW/2).toFixed(2)}" y="${xLabelY}" text-anchor="middle" class="bank-eco-chart-axis">${escAttr(tsMid)}</text>`
+ `<text x="${W - padR}" y="${xLabelY}" text-anchor="end" class="bank-eco-chart-axis">${escAttr(tsEnd)}</text>`;
const legendY = padT + plotH + 44;
const legendBaseX = padL;
const legend = `<g class="bank-eco-chart-legend">`
+ `<rect x="${legendBaseX}" y="${(legendY - 7).toFixed(2)}" width="14" height="3" class="bank-eco-chart-line-value-legend" />`
+ `<text x="${(legendBaseX + 18).toFixed(2)}" y="${legendY}" class="bank-eco-chart-legend-text">${escAttr(labels.value || 'Value')}</text>`
+ `<rect x="${(legendBaseX + 170).toFixed(2)}" y="${(legendY - 7).toFixed(2)}" width="14" height="3" class="bank-eco-chart-line-supply-legend" />`
+ `<text x="${(legendBaseX + 188).toFixed(2)}" y="${legendY}" class="bank-eco-chart-legend-text">${escAttr(labels.supply || 'Supply')}</text>`
+ `<rect x="${(legendBaseX + 320).toFixed(2)}" y="${(legendY - 7).toFixed(2)}" width="14" height="3" class="bank-eco-chart-line-inflation-legend" />`
+ `<text x="${(legendBaseX + 338).toFixed(2)}" y="${legendY}" class="bank-eco-chart-legend-text">${escAttr(labels.inflation || 'Inflation')}</text>`
+ `</g>`;
const [xs = "", xm = "", xe = ""] = xLabels;
const xLabelsSvg = `<text x="${padL}" y="${xLabelY}" text-anchor="start" class="bank-eco-chart-axis">${escAttr(xs)}</text>`
+ `<text x="${(padL + plotW/2).toFixed(2)}" y="${xLabelY}" text-anchor="middle" class="bank-eco-chart-axis">${escAttr(xm)}</text>`
+ `<text x="${W - padR}" y="${xLabelY}" text-anchor="end" class="bank-eco-chart-axis">${escAttr(xe)}</text>`;
const legendY0 = padT + plotH + 44;
const legend = `<g class="bank-eco-chart-legend">` + legendRows.map((row, r) => row.map((item, i) => {
const idx = legendRows.slice(0, r).reduce((acc, rr) => acc + rr.length, 0) + i;
const l = lines[idx];
const y = legendY0 + r * 16;
return `<rect x="${item.x.toFixed(2)}" y="${(y - 7).toFixed(2)}" width="14" height="3" class="bank-eco-chart-line-${l.cls}-legend" />`
+ `<text x="${(item.x + 18).toFixed(2)}" y="${y}" class="bank-eco-chart-legend-text">${escAttr(l.label)}</text>`;
}).join("")).join("") + `</g>`;
return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" class="bank-eco-chart-svg" preserveAspectRatio="xMidYMid meet">`
+ `<rect x="0" y="0" width="${W}" height="${H}" class="bank-eco-chart-bg" />`
+ grid.join('')
+ `<polyline points="${pointsSupply}" class="bank-eco-chart-line-supply" />`
+ `<polyline points="${pointsInfl}" class="bank-eco-chart-line-inflation" />`
+ `<polyline points="${pointsValue}" class="bank-eco-chart-line-value" />`
+ xLabels
+ grid.join("")
+ lines.slice().reverse().map(l => `<polyline points="${l.pts}" class="bank-eco-chart-line-${l.cls}" />`).join("")
+ xLabelsSvg
+ legend
+ `</svg>`;
};
const renderExchange = (ex, history, taxStats) => {
const tsLabels = (arr, key) => {
if (!arr.length) return ["", "", ""];
const f = (x) => moment(x[key]).format("YYYY-MM-DD HH:mm");
return [f(arr[0]), f(arr[Math.floor(arr.length / 2)]), f(arr[arr.length - 1])];
};
const buildEcoValueChartSvg = (history, labels) => {
const arr = Array.isArray(history) ? history.slice(-120) : [];
return buildSeriesChartSvg({
series: [
{ label: labels.value || "Value", points: arr.map(s => Number(s.ecoValue || 0)) },
{ label: labels.supply || "Supply", points: arr.map(s => Number(s.currentSupply || 0)) },
{ label: labels.inflation || "Inflation", points: arr.map(s => Number(s.inflationFactor || 0)) }
],
xLabels: tsLabels(arr, "ts"),
empty: labels.empty
});
};
const renderChartBlock = (id, title, svg) =>
div({ class: "bank-eco-chart-block" },
h2({ class: "bank-eco-chart-title" }, title),
a({ href: `#${id}`, id: `${id}-src`, class: "bank-eco-chart-zoom-link", title: i18n.bankChartZoomHint },
div({ class: "bank-eco-chart-canvas", [RAW_HTML]: svg })
),
div({ id, class: "lightbox bank-eco-chart-lightbox" },
a({ href: `#${id}-src`, class: "lightbox-close" }, "\u00d7"),
div({ class: "bank-eco-chart-canvas-zoom", [RAW_HTML]: svg })
)
);
const renderRangedChart = (id, title, filterName, range, hasAnyData, svg) => {
if (!hasAnyData) return null;
return div({ class: "bank-eco-chart-block", id: `${id}-block` },
h2({ class: "bank-eco-chart-title" }, title),
renderRangeTabs(filterName, range, `${id}-block`),
svg ? a({ href: `#${id}`, id: `${id}-src`, class: "bank-eco-chart-zoom-link", title: i18n.bankChartZoomHint }, div({ class: "bank-eco-chart-canvas", [RAW_HTML]: svg })) : p({ class: "bank-chart-empty-note" }, i18n.bankChartNoDataYet),
svg ? div({ id, class: "lightbox bank-eco-chart-lightbox" }, a({ href: `#${id}-src`, class: "lightbox-close" }, "\u00d7"), div({ class: "bank-eco-chart-canvas-zoom", [RAW_HTML]: svg })) : null
);
};
const bucketLabel = (bucket) => bucket === "month" ? i18n.bankChartPerMonth : bucket === "day" ? i18n.bankChartPerDay : i18n.bankChartPerHour;
const bucketXLabels = (pts, bucket) => {
const fmt = (x) => bucket === "month" ? moment(x.ts).format("YYYY-MM") : bucket === "day" ? moment(x.ts).format("YYYY-MM-DD") : moment(x.ts).format("YYYY-MM-DD HH:mm");
return pts.length ? [fmt(pts[0]), fmt(pts[Math.floor(pts.length / 2)]), fmt(pts[pts.length - 1])] : ["", "", ""];
};
const renderBucketChart = (id, title, filterName, data, field, label, decimals = 2) => {
const pts = Array.isArray(data && data.points) ? data.points : [];
const svg = pts.length >= 2 ? buildSeriesChartSvg({
series: [{ label: `${label} ${bucketLabel(data.bucket)}`, points: pts.map(p => Number(p[field]) || 0) }],
xLabels: bucketXLabels(pts, data.bucket),
empty: i18n.bankExchangeChartEmpty, yDecimals: decimals
}) : null;
return renderRangedChart(id, title, filterName, (data && data.range) || "today", !!(data && data.hasAnyData), svg);
};
const renderSupplyChart = (history, range, hasAnyData, totalSupply = 0) => {
const arr = Array.isArray(history) ? history.slice(-240) : [];
const total = Number(totalSupply) || 0;
const svg = arr.length >= 2 ? buildSeriesChartSvg({
series: [{ label: i18n.bankCurrentSupply, points: arr.map(s => Number(s.currentSupply) || 0) }],
xLabels: tsLabels(arr, "ts"),
empty: i18n.bankExchangeChartEmpty, yDecimals: 0
}) : null;
return renderRangedChart("eco-supply-chart", i18n.bankChartSupplyTitle, "exchange", range, !!hasAnyData, svg);
};
const renderInflationChart = (history, range, hasAnyData) => {
const arr = Array.isArray(history) ? history.slice(-240) : [];
const svg = arr.length >= 2 ? buildSeriesChartSvg({
series: [{ label: i18n.bankChartInflationSeries, points: arr.map(s => Number(s.inflationFactor) || 0) }],
xLabels: tsLabels(arr, "ts"),
empty: i18n.bankExchangeChartEmpty, yDecimals: 2
}) : null;
return renderRangedChart("eco-inflation-chart", i18n.bankChartInflationTitle, "exchange", range, !!hasAnyData, svg);
};
const renderUbiCharts = (charts) => {
const pool = Array.isArray(charts && charts.pool) ? charts.pool : [];
const empty = i18n.bankExchangeChartEmpty;
const poolSvg = pool.length >= 2 ? buildSeriesChartSvg({
series: [{ label: i18n.bankChartPool, points: pool.map(x => x.balance) }],
xLabels: tsLabels(pool, "ts"),
empty, yDecimals: 2
}) : null;
const out = [
renderBucketChart("ubi-chart-network", i18n.bankChartNetworkUbiTitle, "ubi", charts && charts.payments, "distributed", i18n.bankChartDistributed, 6),
renderRangedChart("ubi-chart-pool", i18n.bankChartPoolTitle, "ubi", (charts && charts.poolRange) || "today", !!(charts && charts.hasPool), poolSvg),
renderBucketChart("ubi-chart-rebalance", i18n.bankChartRebalanceTitle, "ubi", charts && charts.rebalances, "rebalanced", i18n.bankChartRebalanced, 6)
].filter(Boolean);
return out.length ? div(...out) : null;
};
const RANGES = ["today", "week", "month", "year", "5y", "all"];
const rangeLabel = (r) => ({ today: i18n.bankRangeToday, week: i18n.bankRangeWeek, month: i18n.bankRangeMonth, year: i18n.bankRangeYear, "5y": i18n.bankRangeFiveYears, all: i18n.bankRangeAll })[r] || r;
const renderRangeTabs = (filter, current, anchor) =>
div({ class: "bank-range-tabs" },
...RANGES.map(r => a({ href: `/banking?filter=${filter}&range=${r}#${anchor}`, class: r === current ? "activity-chip active" : "activity-chip" }, String(rangeLabel(r)).toUpperCase()))
);
const renderValueChart = (history, range, hasAnyData) => {
const arr = Array.isArray(history) ? history.slice(-240) : [];
if (!hasAnyData) return null;
const hasTime = arr.some(s => Number(s.ecoTimeMs) > 0);
const hasInflation = arr.some(s => Number(s.inflationFactor) > 0);
const svg = arr.length >= 2 ? buildSeriesChartSvg({
series: [
{ label: i18n.bankExchangeChartValue || "Value (ECO/h)", points: arr.map(s => Number(s.ecoValue || 0)) },
...(hasTime ? [{ label: i18n.bankChartValueHours, points: arr.map(s => Number(s.ecoTimeMs || 0) / 3600000) }] : []),
...(hasInflation ? [{ label: i18n.bankExchangeChartInflation || "Inflation %", points: arr.map(s => Number(s.inflationFactor || 0)) }] : [])
],
xLabels: tsLabels(arr, "ts"),
empty: i18n.bankExchangeChartEmpty
}) : null;
return renderRangedChart("eco-value-chart", i18n.bankChartValueTitle, "exchange", range, !!hasAnyData, svg);
};
const renderWealthChart = (wealth, range) => {
const pts = Array.isArray(wealth && wealth.points) ? wealth.points : [];
if (!(wealth && wealth.hasAnyData)) return null;
if (pts.length < 2) return div({ class: "bank-eco-chart-block", id: "wealth-chart-block" },
h2({ class: "bank-eco-chart-title" }, i18n.bankChartWealthTitle),
renderRangeTabs("exchange", range, "wealth-chart-block"),
p({ class: "bank-chart-empty-note" }, i18n.bankChartNoDataYet)
);
const fmtBucket = (x) => wealth.bucket === "month" ? moment(x.ts).format("YYYY-MM") : wealth.bucket === "day" ? moment(x.ts).format("YYYY-MM-DD") : moment(x.ts).format("YYYY-MM-DD HH:mm");
const per = wealth.bucket === "month" ? i18n.bankChartPerMonth : wealth.bucket === "day" ? i18n.bankChartPerDay : i18n.bankChartPerHour;
const svg = buildSeriesChartSvg({
series: [
{ label: `${i18n.bankChartDistributed} ${per}`, points: pts.map(p => p.distributed) },
{ label: `${i18n.bankChartTaxes} ${per}`, points: pts.map(p => p.taxes) }
],
xLabels: [fmtBucket(pts[0]), fmtBucket(pts[Math.floor(pts.length / 2)]), fmtBucket(pts[pts.length - 1])],
empty: i18n.bankExchangeChartEmpty, yDecimals: 2
});
const enough = pts.length >= 2;
return div({ class: "bank-eco-chart-block", id: "wealth-chart-block" },
h2({ class: "bank-eco-chart-title" }, i18n.bankChartWealthTitle),
renderRangeTabs("exchange", range, "wealth-chart-block"),
enough ? a({ href: "#wealth-chart", id: "wealth-chart-src", class: "bank-eco-chart-zoom-link", title: i18n.bankChartZoomHint }, div({ class: "bank-eco-chart-canvas", [RAW_HTML]: svg })) : p({ class: "bank-chart-empty-note" }, i18n.bankChartNoDataRange),
enough ? div({ id: "wealth-chart", class: "lightbox bank-eco-chart-lightbox" }, a({ href: "#wealth-chart-src", class: "lightbox-close" }, "\u00d7"), div({ class: "bank-eco-chart-canvas-zoom", [RAW_HTML]: svg })) : null
);
};
const renderFundsChart = (history) => {
const arr = Array.isArray(history && history.points) ? history.points : [];
const svg = arr.length >= 2 ? buildSeriesChartSvg({
series: [{ label: "ECO", points: arr.map(x => x.balance) }],
xLabels: tsLabels(arr, "ts"),
empty: i18n.bankExchangeChartEmpty, yDecimals: 2
}) : null;
return renderRangedChart("funds-chart", i18n.bankChartFundsTitle, "overview", (history && history.range) || "today", !!(history && history.hasAny), svg);
};
const renderKarmaChart = (history) => {
const arr = Array.isArray(history && history.points) ? history.points : [];
const svg = arr.length >= 2 ? buildSeriesChartSvg({
series: [{ label: i18n.bankChartKarma, points: arr.map(x => x.score) }],
xLabels: tsLabels(arr, "ts"),
empty: i18n.bankExchangeChartEmpty, yDecimals: 0
}) : null;
return renderRangedChart("karma-chart", i18n.bankChartKarmaTitle, "overview", (history && history.range) || "today", !!(history && history.hasAny), svg);
};
const renderExchange = (ex, history, taxStats, wealth, range, valueRange = range, valueHasAnyData = false) => {
if (!ex) return div(p(i18n.bankExchangeNoData));
const syncStatus = ex.isSynced ? i18n.bankingSyncStatusSynced : i18n.bankingSyncStatusOutdated;
const syncStatusClass = ex.isSynced ? 'synced' : 'outdated';
const ecoTimeLabel = ex.isSynced ? fmtEcoTime(ex.ecoTimeMs) : fmtEcoTime(0);
const chartLabels = {
value: i18n.bankExchangeChartValue || 'Value (ECO/h)',
@ -154,24 +331,30 @@ const renderExchange = (ex, history, taxStats) => {
table({ class: "bank-info-table" },
tbody(
kvRow(i18n.bankingSyncStatus,
span({ class: syncStatusClass }, syncStatus)
span({ class: ex.isSynced ? "ubi-tick-ok" : "ubi-tick-bad", title: syncStatus }, ex.isSynced ? "✓" : "✗")
),
kvRow(i18n.bankExchangeCurrentValue, `${fmtIndex(ex.ecoValue)} ECO`),
kvRow(i18n.bankCurrentSupply, `${Number(ex.currentSupply || 0).toFixed(6)} ECO`),
kvRow(i18n.bankTotalSupply, `${Number(ex.totalSupply || 0).toFixed(6)} ECO`),
kvRow(i18n.bankEcoinHours, ecoTimeLabel),
kvRow(i18n.bankInflation, `${ex.inflationFactor.toFixed(2)}%`),
kvRow(i18n.bankInflationMonthly, `${Number(ex.inflationMonthly || 0).toFixed(2)}%`),
...taxRows
kvRow(strong(i18n.bankTotalSupply), strong(`${Number(ex.totalSupply || 0).toFixed(6)} ECO`)),
...(ex.isSynced ? [kvRow(strong(i18n.bankCurrentSupply), strong(`${Number(ex.currentSupply || 0).toFixed(6)} ECO`))] : []),
...(ex.holdingSupply != null ? [kvRow(strong(i18n.bankHoldingSupply), strong(`${Number(ex.holdingSupply).toFixed(6)} ECO`))] : []),
kvRow(strong(i18n.bankPubsSupply), strong(`${Number(ex.pubsSupply || 0).toFixed(6)} ECO`)),
...(ex.isSynced ? [
kvRow(strong(i18n.bankInflation), strong(`${Number(ex.inflationFactor || 0).toFixed(2)}%`)),
kvRow(i18n.bankInflationMonthly, `${Number(ex.inflationMonthly || 0).toFixed(2)}%`)
] : []),
kvRow(strong(i18n.bankTotalUbiDistributed), strong(`${Number((wealth && wealth.totals && wealth.totals.distributed) || 0).toFixed(6)} ECO`)),
kvRow(strong(i18n.bankTotalTaxesWithheld), strong(`${Number((wealth && wealth.totals && wealth.totals.taxes) || 0).toFixed(6)} ECO`)),
...taxRows,
...(ex.isSynced ? [
kvRow(strong(i18n.bankExchangeCurrentValue), strong(`${fmtIndex(ex.ecoValue)} ECO`)),
kvRow(strong(i18n.bankEcoinHours), strong(ecoTimeLabel))
] : [])
)
)
),
hasEnoughSamples
? div({ class: "bank-eco-chart-block" },
h2({ class: "bank-eco-chart-title" }, i18n.bankExchangeChartTitle || 'ECOin value over time'),
div({ class: "bank-eco-chart-canvas", innerHTML: buildEcoValueChartSvg(history, chartLabels) })
)
: null
ex.isSynced ? renderValueChart(history, valueRange, !!valueHasAnyData) : null,
ex.isSynced ? renderWealthChart(wealth, (wealth && wealth.range) || range) : null,
renderSupplyChart(history, range, hasEnoughSamples, ex.totalSupply),
renderInflationChart(history, range, hasEnoughSamples)
);
};
@ -248,7 +431,7 @@ const renderTaxes = (data, lookup) => {
(i18n.bankTaxesUserNoteOutro || ', and their ongoing contribution may be required by networking consensus.')
),
p({ class: "bank-taxes-user-note" }, i18n.bankTaxesUserNoteChips || 'The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.'),
p({ class: "bank-taxes-user-note" }, i18n.bankTaxesUserNoteChipsClick || 'Click any chip to inspect its block in the blockexplorer.'),
process.env.OASIS_MOBILE === '1' ? null : p({ class: "bank-taxes-user-note" }, i18n.bankTaxesUserNoteChipsClick || 'Click any chip to inspect its block in the blockexplorer.'),
(() => {
const firstBlock = data.firstBlock || null;
const sampleSize = Number(data.firstBlockSize || 0);
@ -258,17 +441,17 @@ const renderTaxes = (data, lookup) => {
const sampleValue = sampleSize > 0 ? formatCarbon(sampleSize) : '— CO₂';
const label = i18n.ecoTaxLabel || 'ECO Tax';
return p({ class: "bank-taxes-example-chips" },
a({ href: sampleHref, class: 'eco-tax-chip eco-tax-chip-low', title: label + ' · low' },
(process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href: sampleHref }), class: 'eco-tax-chip eco-tax-chip-low', title: label + ' · low' },
span({ class: 'eco-tax-chip-label' }, label + ': '),
span({ class: 'eco-tax-chip-value' }, sampleValue)
),
' ',
a({ href: sampleHref, class: 'eco-tax-chip eco-tax-chip-mid', title: label + ' · mid' },
(process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href: sampleHref }), class: 'eco-tax-chip eco-tax-chip-mid', title: label + ' · mid' },
span({ class: 'eco-tax-chip-label' }, label + ': '),
span({ class: 'eco-tax-chip-value' }, sampleValue)
),
' ',
a({ href: sampleHref, class: 'eco-tax-chip eco-tax-chip-high', title: label + ' · high' },
(process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href: sampleHref }), class: 'eco-tax-chip eco-tax-chip-high', title: label + ' · high' },
span({ class: 'eco-tax-chip-label' }, label + ': '),
span({ class: 'eco-tax-chip-value' }, sampleValue)
)
@ -297,6 +480,33 @@ const renderTaxes = (data, lookup) => {
);
};
const renderUbiPubs = (pubs, hasValidWallet) => {
const fmt = (ts) => Number(ts) > 0 ? moment(Number(ts)).format("YYYY/MM/DD HH:mm") : "—";
if (!pubs.length) return div({ class: "bank-summary" }, p(i18n.bankUbiPubNone));
return div({ class: "addr-cards ubi-pub-cards" },
...pubs.map(p => div({ class: "addr-card ubi-pub-card" + (p.isThisNode || p.isConnected ? " own-content" : "") },
div({ class: "addr-card-head" },
userLink(p.pubId),
p.isThisNode || p.isConnected ? span({ class: "status you" }, p.isThisNode ? i18n.bankUbiPubThisNode : i18n.bankUbiPubConnected) : null,
hasValidWallet && p.address
? div({ class: "content-actions" }, form({ method: "GET", action: "/banking/fund", class: "content-action-form" }, input({ type: "hidden", name: "pub", value: p.pubId }), button({ type: "submit", class: "btn-singleview btn-donate", title: i18n.bankDonateEco }, "\u2744")))
: null
),
div({ class: "bank-summary ubi-pub-summary" },
table({ class: "bank-info-table" },
tbody(
kvRow(i18n.bankUbiAvailability, span({ class: p.available ? "ubi-tick-ok" : "ubi-tick-bad" }, p.available ? "✓" : "✗")),
kvRow(i18n.bankUbiPool, strong(`${Number(p.balance || 0).toFixed(6)} ECO`)),
kvRow(i18n.bankUbiLastSeen, fmt(p.timestamp)),
kvRow(i18n.bankUbiLastPayout, fmt(p.lastPayoutAt)),
kvRow(i18n.bankUbiPaidOut, strong(`${Number(p.paidOut || 0).toFixed(6)} ECO`))
)
)
)
))
);
};
const renderOverviewSummaryTable = (s, rules, userEcoinTax, isPub = false) => {
const ubiWired = !!s.pubId || isPub;
const score = Number(s.userEngagementScore || 0);
@ -308,39 +518,48 @@ const renderOverviewSummaryTable = (s, rules, userEcoinTax, isPub = false) => {
const gross = Math.max(floor, Math.min(pool * (w / W), cap));
const surplus = Math.max(0, gross - floor);
const tax = Number(userEcoinTax || 0);
const future = floor + Math.max(0, surplus - tax);
const availClass = s.ubiAvailability === "OK" ? "ubi-available" : "ubi-unavailable";
const availLabel = s.ubiAvailability === "OK" ? i18n.bankUbiAvailableOk : i18n.bankUbiAvailableNo;
const ubiOn = ubiWired && s.ubiAvailability === "OK";
const future = ubiOn ? floor + Math.max(0, surplus - tax) : 0;
return div({ class: "bank-summary" },
table({ class: "bank-info-table" },
tbody(
kvRow(i18n.bankUserBalance, `${Number(s.userBalance || 0).toFixed(6)} ECO`),
kvRow(i18n.bankIndustryBalance || 'Industry Production (estimated)', a({ href: '/industry' }, `${Number(s.industryNetworkTotal || 0).toFixed(6)} ECO`)),
ubiWired ? kvRow(i18n.bankUbiAvailability, span({ class: availClass }, availLabel)) : null,
s.pubId ? kvRow(i18n.pubIdLabel, userLink(s.pubId)) : null,
kvRow(strong(i18n.bankUserBalance), strong(`${Number(s.userBalance || 0).toFixed(6)} ECO`)),
Number(s.industryNetworkTotal || 0) > 0 ? kvRow(strong(i18n.bankIndustryBalance || "Industry Production"), a({ href: '/industry' }, strong(`${Number(s.industryNetworkTotal || 0).toFixed(6)} ECO`))) : null,
kvRow(i18n.bankEpoch, String(s.epochId || "-")),
kvRow(i18n.bankPool, `${pool.toFixed(6)} ECO`),
kvRow(i18n.bankWeightsSum, String(W.toFixed(6))),
kvRow(i18n.bankingUserEngagementScore, String(score)),
kvRow(strong(i18n.bankingUserEngagementScore), strong(String(score))),
ubiWired ? kvRow(i18n.bankYourUbiMonth || 'Your UBI (this month)', `${future.toFixed(6)} ECO`) : null,
kvRow(i18n.bankYourIndustryBalance || 'Your Industry Share', a({ href: '/industry?filter=MEMBER' }, `${Number(s.industryBalance || 0).toFixed(6)} ECO`)),
kvRow(i18n.bankOverviewYourTaxes || 'Your taxes', a({ href: '/banking?filter=taxes' }, `${tax.toFixed(6)} ECO`)),
kvRow(i18n.bankYourFundsMonth || 'Your Funds (this month)', `${((ubiWired ? future : 0) + Number(s.industryBalance || 0) - tax).toFixed(6)} ECO`)
Number(s.industryBalance || 0) > 0 ? kvRow(i18n.bankYourIndustryBalance || 'Your Industry Share', a({ href: '/industry?filter=MEMBER' }, `${Number(s.industryBalance || 0).toFixed(6)} ECO`)) : null,
Number(s.schoolBalance || 0) > 0 ? kvRow(i18n.bankYourSchoolBalance || 'Your School Earnings', a({ href: '/school?filter=mine' }, `${Number(s.schoolBalance || 0).toFixed(6)} ECO`)) : null,
kvRow(i18n.bankTotalUbiDistributed, `${Number((s.wealthTotals && s.wealthTotals.distributed) || 0).toFixed(6)} ECO`),
kvRow(i18n.bankTotalTaxesWithheld, `${Number((s.wealthTotals && s.wealthTotals.taxes) || 0).toFixed(6)} ECO`),
kvRow(strong(i18n.bankOverviewYourTaxes || 'Total Taxes'), a({ href: '/banking?filter=taxes' }, strong(`${tax.toFixed(6)} ECO`))),
kvRow(strong(i18n.bankYourFundsMonth || 'Total Funds'), strong(`${((ubiWired ? future : 0) + Number(s.industryBalance || 0) + Number(s.schoolBalance || 0)).toFixed(6)} ECO`))
)
)
);
};
const renderClaimUBIBlock = (pendingAllocation, isPub, alreadyClaimed, pubId, hasValidWallet, ubiAvailability) => {
const renderClaimUBIBlock = (pendingAllocation, isPub, alreadyClaimed, pubId, hasValidWallet, ubiAvailability, alreadyRefused = false, addressPublished = false, ecoinSynced = false) => {
if (!ecoinSynced) return "";
if (alreadyClaimed) return "";
if (alreadyRefused) return div({ class: "bank-claim-ubi" }, div({ class: "bank-claim-card" }, p(i18n.bankUbiRefusedThisMonth)));
if (!pubId && !isPub) return "";
if (!isPub && !hasValidWallet) return "";
if (!isPub && !(hasValidWallet && addressPublished)) return "";
if (!isPub && ubiAvailability !== "OK") return "";
if (!pendingAllocation && !isPub) {
if (!isPub) {
return div({ class: "bank-claim-ubi" },
div({ class: "bank-claim-card" },
form({ method: "POST", action: "/banking/claim-ubi" },
button({ type: "submit", class: "create-button bank-claim-btn" }, i18n.bankClaimUBI)
pendingAllocation ? p(`${i18n.bankUbiThisMonth}: `, span({ class: "accent" }, `${Number(pendingAllocation.amount || 0).toFixed(6)} ECO`)) : null,
pendingAllocation ? p(`${i18n.bankEpoch}: `, span(pendingAllocation.concept || "")) : null,
div({ class: "bank-claim-actions" },
form({ method: "POST", action: "/banking/claim-ubi" },
button({ type: "submit", class: "create-button bank-claim-btn" }, i18n.bankClaimUBI)
),
form({ method: "POST", action: "/banking/refuse-ubi" },
button({ type: "submit", class: "delete-btn bank-claim-btn" }, i18n.bankRefuseUBI)
)
)
)
);
@ -359,7 +578,7 @@ const renderClaimUBIBlock = (pendingAllocation, isPub, alreadyClaimed, pubId, ha
const filterAllocations = (allocs, filter, userId) => {
if (filter === "mine") return allocs.filter(a => a.to === userId && (a.status === "UNCLAIMED" || a.status === "UNCONFIRMED"));
if (filter === "pending") return allocs.filter(a => a.status === "UNCLAIMED" || a.status === "UNCONFIRMED");
if (filter === "pending") return allocs.filter(a => a.status !== "EXPIRED");
if (filter === "closed") return allocs.filter(a => a.status === "CLOSED");
if (filter === "claimed") return allocs.filter(a => a.status === "CLAIMED");
if (filter === "expired") return allocs.filter(a => a.status === "EXPIRED");
@ -378,27 +597,23 @@ const allocationsTable = (rows = [], userId) =>
th(i18n.bankAllocFrom),
th(i18n.bankAllocTo),
th(i18n.bankAllocAmount),
th(i18n.bankAllocStatus),
th("")
th(i18n.bankAllocStatus)
)
),
tbody(
...rows.map(r =>
tr(
td(new Date(r.createdAt).toLocaleString()),
td(moment(r.createdAt).format("YYYY/MM/DD HH:mm")),
td(r.concept || ""),
td(userLink(r.from)),
td(userLink(r.to)),
td(String(Number(r.amount || 0).toFixed(6))),
td(r.status),
td(
(r.status === "UNCLAIMED" || r.status === "UNCONFIRMED") && r.to === userId
? form({ method: "POST", action: `/banking/claim/${encodeURIComponent(r.id)}` },
button({ type: "submit", class: "filter-btn" }, i18n.bankClaimNow)
)
: r.status === "CLOSED" && r.txid
? a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(r.txid)}`, target: "_blank", class: "btn-singleview" }, i18n.bankViewTx)
: null
r.status === "CLOSED" || r.status === "CLAIMED"
? span({ class: "ubi-tick-ok", title: r.status }, "✓")
: r.status === "UNCLAIMED" || r.status === "UNCONFIRMED"
? span({ class: "ubi-tick-bad", title: r.status }, "✗")
: r.status
)
)
)
@ -410,16 +625,15 @@ const renderEpochList = (epochs = []) =>
? div(p(i18n.bankNoEpochs))
: table(
{ class: "bank-epochs" },
thead(tr(th(i18n.bankEpochId), th(i18n.bankPool), th(i18n.bankWeightsSum), th(i18n.bankRuleHash), th(""))),
thead(tr(th(i18n.bankEpochId), th(i18n.bankPool), th(i18n.bankEpochAllocations), th(""))),
tbody(
...epochs
.sort((a, b) => String(b.id).localeCompare(String(a.id)))
...paged(epochs
.sort((a, b) => String(b.id).localeCompare(String(a.id))))
.map(e =>
tr(
td(e.id),
td(String(Number(e.pool || 0).toFixed(6))),
td(String(Number(e.weightsSum || 0).toFixed(6))),
td(e.hash || "-"),
td(String(Number(e.recipients || 0))),
td(
form({ method: "GET", action: `/banking/epoch/${encodeURIComponent(e.id)}` },
button({ type: "submit", class: "filter-btn" }, i18n.bankViewEpoch)
@ -436,6 +650,9 @@ const rulesBlock = (rules, taxRules) => {
const tr = taxRules || {};
const fmt = (n, d = 4) => (Number.isFinite(Number(n)) ? Number(n).toFixed(d) : "—");
return div({ class: "bank-rules" },
h2(i18n.bankRulesEligibilityTitle),
ul({ class: "bank-rules-list" }, ...[i18n.bankRulesEligibility1, i18n.bankRulesEligibility2, i18n.bankRulesEligibility3, i18n.bankRulesEligibility4, i18n.bankRulesEligibility5, i18n.bankRulesEligibility6].map(x => li(x))),
h2(i18n.bankRulesPoolTitle || "Monthly pool"),
table({ class: "bank-info-table" },
tbody(
@ -516,118 +733,100 @@ const flashText = (key) => {
if (key === "claimed_pending") return i18n.bankClaimedPending;
if (key === "already_claimed") return i18n.bankAlreadyClaimedThisMonth;
if (key === "no_pub_configured") return i18n.bankNoPubConfigured;
if (key === "no_pub_address") return i18n.bankNoPubAddress;
if (key === "refused") return i18n.bankUbiRefusedThisMonth;
if (key === "already_refused") return i18n.bankUbiRefusedThisMonth;
if (key === "no_funds") return i18n.bankUbiAvailableNo;
if (key === "forbidden") return i18n.bankAddressForbidden;
return "";
};
const flashBanner = (msgKey) =>
!msgKey ? null : div({ class: "flash-banner" }, p(flashText(msgKey) || msgKey));
const addressesToolbar = (rows = [], search = "") =>
div({ class: "addr-toolbar" },
div({ class: "addr-counter accent-pill" },
span({ class: "acc-title accent" }, i18n.bankAddressTotal + ":"),
span({ class: "acc-badge" }, String(rows.length))
),
form({ method: "GET", action: "/banking", class: "addr-search" },
input({ type: "hidden", name: "filter", value: "addresses" }),
input({ type: "text", name: "q", placeholder: i18n.bankAddressSearch, value: search || "" }),
br(),
button({ type: "submit", class: "filter-btn" }, i18n.search)
)
);
const renderAddresses = (data, userId) => {
const rows = data.addresses || [];
const search = data.search || "";
const hasContent = rows.length > 0 || !!search;
return div(
data.flash ? flashBanner(data.flash) : null,
addressesToolbar(rows, search),
div({ class: "bank-addresses-stack" },
div({ class: "addr-form-card wide" },
h2(i18n.bankAddAddressTitle),
form({ method: "POST", action: "/banking/addresses", class: "addr-form" },
div({ class: "form-row" },
span({ class: "form-label accent" }, i18n.bankAddAddressUser + ":"),
input({
class: "form-input xl",
type: "text",
name: "userId",
required: true,
pattern: "^@[A-Za-z0-9+/]+={0,2}\\.ed25519$",
placeholder: "@...=.ed25519",
id: "addr-user-id"
})
),
div({ class: "form-row" },
span({ class: "form-label accent" }, i18n.bankAddAddressAddress + ":"),
input({
class: "form-input xl",
type: "text",
name: "address",
required: true,
pattern: "^[A-Za-z0-9]{20,64}$",
placeholder: "ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX"
})
),
div({ class: "form-actions" },
button({ type: "submit", class: "filter-btn" }, i18n.bankAddAddressSave)
div({ class: "bank-summary" },
h2(i18n.bankAddAddressTitle),
form({ method: "POST", action: "/banking/addresses", class: "bank-form" },
label({ for: "addr-label" }, i18n.bankAddAddressLabel), br(),
input({ type: "text", id: "addr-label", name: "label", maxlength: "80", placeholder: i18n.bankAddAddressLabelPlaceholder }), br(),
label({ for: "addr-user-id" }, i18n.bankAddAddressUserOptional), br(),
input({ type: "text", id: "addr-user-id", name: "userId", pattern: "^@[A-Za-z0-9+/]+={0,2}\\.ed25519$", placeholder: "@...=.ed25519" }), br(),
label({ for: "addr-address" }, i18n.bankAddAddressAddress), br(),
input({ type: "text", id: "addr-address", name: "address", required: true, pattern: "^[A-Za-z0-9]{20,64}$", placeholder: "ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX" }), br(),
button({ type: "submit", class: "create-button" }, i18n.bankAddAddressSave)
)
),
hasContent
? form({ method: "GET", action: "/banking", class: "filter-box bank-addr-search" },
input({ type: "hidden", name: "filter", value: "addresses" }),
input({ type: "text", name: "q", placeholder: i18n.bankAddressSearch, value: search || "", class: "filter-box__input" }),
div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton))
)
: null,
rows.length === 0
? div({ class: "no-content-box" }, p(i18n.bankNoAddresses))
: div({ class: "bank-summary" },
renderModuleStats(rows.length, [
{ label: i18n.bankLocal, count: rows.filter(r => r.source !== "ssb").length },
{ label: i18n.bankFromOasis, count: rows.filter(r => r.source === "ssb").length }
]),
table({ class: "bank-addresses" },
thead(tr(th(i18n.bankAddAddressLabel.replace(/\s*\(.*\)\s*$/, "")), th(i18n.bankAddAddressUser), th(i18n.bankAddress), th(i18n.bankAddressSource), th(""))),
tbody(
...paged(rows).map(r => tr(
td(r.label || (r.id ? "" : i18n.bankAddressUnnamed)),
td(r.id ? userLink(r.id) : ""),
td(strong({ class: "bank-address-code" }, r.address)),
td(r.source === "ssb" ? i18n.bankFromOasis : i18n.bankLocal),
td(r.source === "ssb" && String(r.id) !== String(userId)
? ""
: form({ method: "POST", action: "/banking/addresses/delete", class: "addr-del" },
input({ type: "hidden", name: "userId", value: r.id || "" }),
input({ type: "hidden", name: "source", value: r.source || "local" }),
input({ type: "hidden", name: "entryId", value: r.entryId || "" }),
button({ type: "submit", class: "delete-btn" }, i18n.bankAddressDelete)
))
))
)
)
)
),
div({ class: "addr-list-card" },
rows.length === 0
? div(p(i18n.bankNoAddresses))
: table(
{ class: "bank-addresses" },
thead(
tr(
th(i18n.bankUser),
th(i18n.bankAddress),
th(i18n.bankAddressSource),
th(i18n.bankAddressActions)
)
),
tbody(
...rows.map(r =>
tr(
td(userLink(r.id)),
td(r.address),
td(r.source === "local" ? i18n.bankLocal : i18n.bankFromOasis),
td(
div({ class: "row-actions" },
form({ method: "POST", action: "/banking/addresses/delete", class: "addr-del" },
input({ type: "hidden", name: "userId", value: r.id }),
input({ type: "hidden", name: "source", value: r.source || "local" }),
button({ type: "submit", class: "delete-btn", onclick: `return confirm(${JSON.stringify(i18n.bankAddressDeleteConfirm)})` }, i18n.bankAddressDelete)
)
)
)
)
)
)
)
)
)
);
};
const bankingFilters = (data, filter, userId) => {
const allocs = data.allocations || [];
const subsets = ["mine", "pending", "closed", "claimed", "expired"].filter(f => filterAllocations(allocs, f, userId).length > 0 || filter === f);
return [
"overview", "exchange", "ubi", "taxes",
...subsets,
...((data.epochs || []).length || filter === "epochs" || String(filter || "").startsWith("epoch:") ? ["epochs"] : []),
"addresses", "rules"
];
};
const renderBankingView = (data, filter, userId, isPub) =>
template(
i18n.banking,
section(
div({ class: "tags-header" }, h2(i18n.banking), p(i18n.bankingDescription)),
div({ class: "tags-header module-header-line" }, h2(i18n.banking), p(i18n.bankingDescription), renderWalletChip()),
data.flash ? div({ class: "flash-banner" }, p(flashText(data.flash) || data.flash)) : null,
generateFilterButtons(["overview","exchange","taxes","mine","pending","closed","claimed","expired","epochs","rules","addresses"], filter, "/banking"),
generateFilterButtons(bankingFilters(data, filter, userId), filter, "/banking"),
filter === "overview"
? div(
renderOverviewSummaryTable(data.summary || {}, data.rules, data.userTotalTax || data.userEcoinTax, isPub),
renderClaimUBIBlock(data.pendingUBI || null, isPub, data.alreadyClaimed, (data.summary || {}).pubId, (data.summary || {}).hasValidWallet, (data.summary || {}).ubiAvailability),
allocationsTable((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), userId)
renderKarmaChart(data.karmaHistory),
renderFundsChart(data.fundsHistory)
)
: filter === "ubi"
? div(
renderClaimUBIBlock(data.pendingUBI || null, isPub, data.alreadyClaimed, (data.summary || {}).pubId, (data.summary || {}).hasValidWallet, (data.summary || {}).ubiAvailability, (data.summary || {}).alreadyRefused, (data.summary || {}).addressPublished, !!(data.exchange && data.exchange.isSynced)),
renderUbiPubs(data.ubiPubs || [], !!(data.summary || {}).hasValidWallet),
renderUbiCharts({ ...(data.ubiCharts || {}), payments: data.ubiPayments, rebalances: data.ubiRebalances })
)
: filter === "exchange"
? renderExchange(data.exchange, data.exchangeHistory, data.taxStats)
? renderExchange(data.exchange, data.exchangeHistory, data.taxStats, data.wealth, data.range, data.valueRange || data.range, !!data.valueHasAnyData)
: filter === "taxes"
? renderTaxes(data, data.lookup || null)
: filter === "epochs"
@ -637,7 +836,7 @@ const renderBankingView = (data, filter, userId, isPub) =>
: filter === "addresses"
? renderAddresses(data, userId)
: allocationsTable(
filterAllocations((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), filter, userId),
paged(filterAllocations((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), filter, userId)),
userId
)
)
@ -659,7 +858,6 @@ const renderSingleAllocationView = (alloc, userId) => {
kvRow(i18n.bankAllocAmount, `${Number(alloc.amount || 0).toFixed(6)} ECO`),
kvRow(i18n.bankAllocStatus, alloc.status || "-"),
kvRow(i18n.bankAllocDate, alloc.createdAt ? fmtDate(alloc.createdAt) : "-"),
alloc.txid ? kvRow("TxID", a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(alloc.txid)}`, target: "_blank" }, alloc.txid)) : null
)
)
),
@ -673,26 +871,36 @@ const renderSingleAllocationView = (alloc, userId) => {
);
};
const renderEpochView = (epoch, allocations) => {
if (!epoch) return template(i18n.banking, section(div(p(i18n.bankNoEpochs))));
const bankingHeader = () =>
div({ class: "tags-header module-header-line" }, h2(i18n.banking), p(i18n.bankingDescription), renderWalletChip());
const renderEpochView = (epoch, allocations, userId = "", data = {}) => {
const rows = allocations || [];
return template(
i18n.banking,
section(
div({ class: "tags-header" }, h2(`${i18n.bankEpoch}: ${epoch.id}`)),
div({ class: "bank-summary" },
table({ class: "bank-info-table" },
tbody(
kvRow(i18n.bankEpochId, epoch.id || "-"),
kvRow(i18n.bankPool, `${Number(epoch.pool || 0).toFixed(6)} ECO`),
kvRow(i18n.bankWeightsSum, String(Number(epoch.weightsSum || 0).toFixed(6))),
kvRow(i18n.bankRuleHash, epoch.hash || "-")
bankingHeader(),
generateFilterButtons(bankingFilters({ ...data, allocations: rows, epochs: data.epochs || [{}] }, "epochs", userId), "epochs", "/banking"),
!epoch
? div({ class: "no-content-box" }, p(i18n.bankNoEpochs))
: div(
h2(`${i18n.bankEpoch}: ${epoch.id}`),
div({ class: "bank-summary" },
table({ class: "bank-info-table" },
tbody(
kvRow(i18n.bankEpochId, epoch.id || "-"),
kvRow(i18n.bankPool, `${Number(epoch.pool || 0).toFixed(6)} ECO`),
Number.isFinite(Number(epoch.weightsSum)) && Number(epoch.weightsSum) > 0 ? kvRow(i18n.bankWeightsSum, String(Number(epoch.weightsSum).toFixed(6))) : null,
kvRow(i18n.bankEpochAllocations, String(rows.length)),
epoch.hash && epoch.hash !== "-" ? kvRow(i18n.bankRuleHash, epoch.hash) : null
)
)
),
h2(i18n.bankEpochAllocations),
allocationsTable(paged(rows), userId)
)
)
),
h2(i18n.bankEpochAllocations),
allocationsTable(allocations || [], "")
)
);
};
module.exports = { renderBankingView, renderSingleAllocationView, renderEpochView };
module.exports = { renderBankingView, renderSingleAllocationView, renderEpochView, bankingFlashText: flashText };