Skip to content

Commit 116cf78

Browse files
markets/: label the right table, keep Cash out focusable, disarm the slip
Final gate, three blockers. The <thead> I added last round went on the SETTLED table, so a realised payout on a market you cannot sell was labelled 'Sell now for' -- and it emitted three <th> against two <td>. The live table, which is the one with the unlabelled mark-to-market the review actually raised, had no header at all. Settled now reads Bet / Result; live reads Bet / Sell now for / Actions. #d-position was still rebuilt on every websocket tick, so Cash out -- the one money-moving control left in that container -- was unusable by keyboard on any market with trade flow. It now updates the value and P&L cells in place when the position's shape is unchanged, the same treatment the outcome buttons already had. Verified by focusing the button, pushing a real trade through the API, and asserting the focused node is identical afterwards. An armed confirmation slip followed the user between markets: opening a review on one market and changing the hash left it open, silently re-priced onto the new market with a live Confirm button and the old clock still running. It is cancelled on market change and on route. Also: the in-place reuse guard compared only the first outcome name, so a reorder preserving that name would have written prices into the wrong rows. 75 plugin tests green.
1 parent 076fff6 commit 116cf78

1 file changed

Lines changed: 42 additions & 10 deletions

File tree

markets/ui.js

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -904,7 +904,11 @@ export function renderUi(prefix) {
904904
return;
905905
}
906906
if (seq !== detailSeq) return; // a newer render already landed
907-
if (!current || current.id !== m.id) { pick = 0; newIntent(); $('t-fill').textContent = ''; }
907+
if (!current || current.id !== m.id) {
908+
// A confirmation opened for one market must not follow the user to
909+
// another still armed.
910+
pick = 0; newIntent(); cancelSlip(); $('t-fill').textContent = '';
911+
}
908912
current = m;
909913
$('list-view').classList.add('hidden');
910914
$('detail-view').classList.remove('hidden');
@@ -944,7 +948,7 @@ export function renderUi(prefix) {
944948
// to BODY every few seconds on a live market.
945949
const existing = $('d-outcomes').querySelectorAll('.out-btn');
946950
const reuse = !settledView(m) && existing.length === m.outcomes.length
947-
&& existing[0].dataset.o === m.outcomes[0];
951+
&& m.outcomes.every((o, i) => existing[i].dataset.o === o);
948952
if (reuse) {
949953
existing.forEach((b, i) => {
950954
b.querySelector('.pc').textContent = pct(m.prices[i]);
@@ -1011,12 +1015,13 @@ export function renderUi(prefix) {
10111015
// A decided market has no "current price". Show what happened, not
10121016
// a mark-to-market on prices that no longer mean anything.
10131017
pd.classList.remove('hidden');
1018+
pd.dataset.shape = ''; // settled card is a different shape entirely
10141019
const won = m.resolvedOutcome;
10151020
const receipt = (me && (me.settlements || []).find((x) => x.market === m.id)) || null;
10161021
const returned = receipt ? receipt.payout : null;
10171022
const net = receipt ? (receipt.net === undefined ? receipt.payout : receipt.net) : null;
10181023
pd.innerHTML = '<h2>Your bet</h2>'
1019-
+ '<table><thead><tr><th>Bet</th><th class="num">Sell now for</th><th class="num"></th></tr></thead>'
1024+
+ '<table><thead><tr><th>Bet</th><th class="num">Result</th></tr></thead>'
10201025
+ '<tbody>' + pos.shares.map((s, i) => (s > 0
10211026
? '<tr><td class="' + (m.status === 'resolved' && i !== won ? 'lost' : '') + '">'
10221027
+ '<i style="display:inline-block;width:8px;height:8px;border-radius:2px;background:'
@@ -1034,25 +1039,51 @@ export function renderUi(prefix) {
10341039
} else if (pos && pos.shares.some((s) => s > 0)) {
10351040
pd.classList.remove('hidden');
10361041
const cls = pos.unrealizedPnl >= 0 ? 'up' : 'down';
1042+
// Update the numbers in place when the position's shape is
1043+
// unchanged; rebuilding threw focus off the Cash out buttons every
1044+
// time anyone else traded.
1045+
const legs = pos.shares.map((s, i) => (s > 0 ? i : -1)).filter((i) => i >= 0);
1046+
const shape = m.id + ':' + legs.join(',');
1047+
if (pd.dataset.shape === shape) {
1048+
legs.forEach((i) => {
1049+
const row = pd.querySelector('tr[data-i="' + i + '"]');
1050+
if (!row) return;
1051+
row.querySelector('.v-now').textContent = cr(pos.value[i]);
1052+
const d = row.querySelector('.v-pnl');
1053+
d.textContent = (pos.value[i] >= pos.cost[i] ? '+' : '') + cr(pos.value[i] - pos.cost[i]);
1054+
d.className = 'pnl ' + (pos.value[i] >= pos.cost[i] ? 'up' : 'down');
1055+
});
1056+
const tot = pd.querySelector('.v-total');
1057+
if (tot) {
1058+
tot.textContent = (pos.unrealizedPnl >= 0 ? '+' : '') + cr(pos.unrealizedPnl);
1059+
tot.className = 'pnl ' + (pos.unrealizedPnl >= 0 ? 'up' : 'down');
1060+
}
1061+
const cost = pd.querySelector('.v-cost');
1062+
if (cost) cost.textContent = 'cost ' + cr(pos.totalCost) + ' · value ' + cr(pos.totalValue);
1063+
return;
1064+
}
1065+
pd.dataset.shape = shape;
10371066
pd.innerHTML = '<h2>Your position</h2>'
1038-
+ '<table><tbody>' + pos.shares.map((s, i) => (s > 0
1039-
? '<tr><td><i style="display:inline-block;width:8px;height:8px;border-radius:2px;background:'
1067+
+ '<table><thead><tr><th>Bet</th><th class="num">Sell now for</th>'
1068+
+ '<th class="num"><span class="sr">Actions</span></th></tr></thead>'
1069+
+ '<tbody>' + pos.shares.map((s, i) => (s > 0
1070+
? '<tr data-i="' + i + '"><td><i style="display:inline-block;width:8px;height:8px;border-radius:2px;background:'
10401071
+ col(i) + '"></i> <b>' + esc(m.outcomes[i]) + '</b>'
10411072
+ '<div class="meta">risked ' + cr(pos.cost[i]) + '<span class="dot">→</span>returns '
10421073
+ cr(s) + ' if right</div></td>'
1043-
+ '<td class="num">' + cr(pos.value[i])
1044-
+ '<div class="pnl ' + (pos.value[i] >= pos.cost[i] ? 'up' : 'down') + '">'
1074+
+ '<td class="num"><span class="v-now">' + cr(pos.value[i]) + '</span>'
1075+
+ '<div class="v-pnl pnl ' + (pos.value[i] >= pos.cost[i] ? 'up' : 'down') + '">'
10451076
+ (pos.value[i] >= pos.cost[i] ? '+' : '') + cr(pos.value[i] - pos.cost[i]) + '</div></td>'
10461077
+ '<td class="num">' + (m.tradable
10471078
? '<button type="button" class="small cash sell-one" data-i="' + i + '">Cash out</button>' : '') + '</td></tr>'
10481079
: '')).join('') + '</tbody></table>'
1049-
+ '<div class="row"><span class="hint">cost ' + cr(pos.totalCost) + ' · value ' + cr(pos.totalValue)
1050-
+ '</span><span class="spacer"></span><span class="pnl ' + cls + '">'
1080+
+ '<div class="row"><span class="hint v-cost">cost ' + cr(pos.totalCost) + ' · value '
1081+
+ cr(pos.totalValue) + '</span><span class="spacer"></span><span class="v-total pnl ' + cls + '">'
10511082
+ (pos.unrealizedPnl >= 0 ? '+' : '') + cr(pos.unrealizedPnl) + '</span></div>';
10521083
pd.querySelectorAll('.sell-one').forEach((b) => {
10531084
b.onclick = () => cashOut(m.id, Number(b.dataset.i));
10541085
});
1055-
} else pd.classList.add('hidden');
1086+
} else { pd.classList.add('hidden'); pd.dataset.shape = ''; }
10561087
10571088
$('oracle-card').classList.toggle('hidden', !(isYou && m.canResolve));
10581089
$('o-outcome').innerHTML = m.outcomes.map((o, i) => '<option value="' + i + '">' + esc(o) + '</option>').join('');
@@ -1276,6 +1307,7 @@ export function renderUi(prefix) {
12761307
$('list-view').classList.remove('hidden');
12771308
document.title = 'Markets — prediction markets on your pod';
12781309
current = null; cursor = null; paged = false;
1310+
cancelSlip();
12791311
$('t-msg').textContent = ''; $('t-msg').className = 'msg';
12801312
$('t-fill').textContent = '';
12811313
renderTabs();

0 commit comments

Comments
 (0)