Skip to content

Commit 086213a

Browse files
markets/: standalone-host affordances — root prefix, account-mode auth, leaderboard rail
Three additive changes for graduating the plugin to a standalone site host (the ripple → solidpay pattern): prefix '' now means "mount at site root" (?? instead of ||); config.accountsUi swaps the pod-bearer paste for a username/password register/login form that mints a bearer at {prefix}/api/{register,login} (host-provided routes) and then runs the normal session exchange — JSS deployments unchanged; and the rail gains a Top-predictors card wired to the EXISTING signed-in, pseudonymized /api/leaderboard (a first attempt added a public duplicate route — boot loudly refused the collision, and the incumbent's privacy design was better anyway; ugrep's binary heuristic had hidden it from the survey). 75 tests green.
1 parent b0b7d39 commit 086213a

2 files changed

Lines changed: 78 additions & 12 deletions

File tree

markets/plugin.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,13 @@ export function isAgentId(s) {
132132
}
133133

134134
export async function activate(api) {
135-
const prefix = api.prefix || '/markets';
135+
const prefix = api.prefix ?? '/markets'; // '' = site root (standalone host)
136136
const cfg = api.config || {};
137137

138138
// ------------------------------------------------------------ config
139139
const num = (v, d) => (v === undefined ? d : v);
140140
const grantMicro = Math.round(num(cfg.grantCredits, 1000) * MICRO);
141+
const accountsUi = !!cfg.accountsUi; // standalone hosts: register/login form instead of pod-bearer paste
141142
const feeBps = num(cfg.feeBps, 100);
142143
const houseFeeShareBps = num(cfg.houseFeeShareBps, 5000);
143144
const disputeWindowMs = num(cfg.disputeWindowMs, 60 * 60 * 1000);
@@ -1307,7 +1308,7 @@ export async function activate(api) {
13071308
// ---------------------------------------------------------------- UI
13081309
const uiHandler = async (request, reply) => {
13091310
for (const [k, v] of Object.entries(UI_HEADERS)) reply.header(k, v);
1310-
return reply.header('content-type', 'text/html; charset=utf-8').send(renderUi(prefix));
1311+
return reply.header('content-type', 'text/html; charset=utf-8').send(renderUi(prefix, { accounts: accountsUi }));
13111312
};
13121313
api.fastify.get(prefix, uiHandler);
13131314
api.fastify.get(`${prefix}/`, uiHandler);

markets/ui.js

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
// - --up/--down never colour a button, only a number;
2323
// - one left edge: the gutter lives on `main`, children sit flush.
2424

25-
export function renderUi(prefix) {
25+
export function renderUi(prefix, opts = {}) {
26+
const accounts = !!opts.accounts; // standalone host: register/login form
2627
const P = JSON.stringify(prefix);
2728
return `<!doctype html>
2829
<html lang="en">
@@ -296,6 +297,13 @@ export function renderUi(prefix) {
296297
.brand span{display:none}
297298
.stats{grid-template-columns:repeat(2,1fr)}
298299
}
300+
.board{list-style:none;margin:0;padding:0}
301+
.board li{display:flex;align-items:baseline;gap:var(--s3);padding:var(--s2) 0;border-bottom:1px solid var(--line);font-size:var(--f-sm)}
302+
.board li:last-child{border-bottom:0}
303+
.board li.you .board-name{font-weight:700;color:var(--accent,inherit)}
304+
.board-rank{color:var(--ink-3);font-variant-numeric:tabular-nums;min-width:1.4em;text-align:right}
305+
.board-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
306+
.board-pl{font-variant-numeric:tabular-nums;font-weight:600}
299307
</style>
300308
</head>
301309
<body>
@@ -327,6 +335,17 @@ export function renderUi(prefix) {
327335
<main id="main">
328336
<div class="card hidden" id="auth-card">
329337
<h2>Start with 1,000 free paper credits</h2>
338+
${accounts ? `
339+
<p class="hint">Pick a username, get 1,000 paper credits, and start calling the news. Your
340+
session is an HttpOnly cookie scoped to this app; nothing is stored in the browser.</p>
341+
<div class="row">
342+
<label class="sr" for="acct-user">Username</label>
343+
<input id="acct-user" autocomplete="username" placeholder="Username" style="flex:1;min-width:130px">
344+
<label class="sr" for="acct-pass">Password</label>
345+
<input id="acct-pass" type="password" autocomplete="current-password" placeholder="Password" style="flex:1;min-width:130px">
346+
<button type="button" class="primary" id="do-acct-signin">Sign in</button>
347+
<button type="button" id="do-acct-register">Create account</button>
348+
</div>` : `
330349
<p class="hint">New accounts are granted 1,000 paper credits to bet with. Sign in with a pod
331350
bearer token (from <code>POST /idp/credentials</code>) — it is exchanged for a session cookie
332351
scoped to this app and <b>never stored in the browser</b>, so this page cannot leak access to
@@ -335,7 +354,7 @@ export function renderUi(prefix) {
335354
<label class="sr" for="token">Pod bearer token</label>
336355
<input id="token" type="password" placeholder="Pod bearer token" style="flex:1;min-width:200px">
337356
<button type="button" class="primary" id="do-signin">Start session</button>
338-
</div>
357+
</div>`}
339358
<div class="msg" id="auth-msg" role="alert"></div>
340359
</div>
341360
@@ -359,6 +378,10 @@ export function renderUi(prefix) {
359378
</div>
360379
361380
<aside class="rail">
381+
<div class="card" id="board-card">
382+
<h2>Top predictors</h2>
383+
<ol id="board" class="board"><li class="empty">No predictions yet.</li></ol>
384+
</div>
362385
<div class="card hidden" id="me-card">
363386
<h2>My positions</h2>
364387
<div id="positions"></div>
@@ -1323,7 +1346,26 @@ export function renderUi(prefix) {
13231346
} catch (e) { $('t-msg').textContent = e.message; $('t-msg').className = 'msg err'; }
13241347
}
13251348
1349+
async function renderBoard() {
1350+
// The endpoint is signed-in-only and pseudonymized by design (a public
1351+
// wealth ranking of identities is recon); signed out, the card is the
1352+
// invitation instead.
1353+
const el = $('board');
1354+
try {
1355+
const { leaderboard } = await api('/leaderboard');
1356+
if (!leaderboard.length) { el.innerHTML = '<li class="empty">No predictions yet.</li>'; return; }
1357+
el.innerHTML = leaderboard.slice(0, 10).map((r) =>
1358+
'<li' + (r.you ? ' class="you"' : '') + '><span class="board-rank">' + r.rank + '</span>'
1359+
+ '<span class="board-name">' + (r.you ? 'you' : esc(agentName(r.agent))) + '</span>'
1360+
+ '<span class="board-pl">' + r.balance.toFixed(2) + '</span></li>').join('');
1361+
} catch {
1362+
el.innerHTML = '<li class="empty">Sign in to see the top predictors.</li>';
1363+
}
1364+
}
1365+
setInterval(renderBoard, 30000);
1366+
13261367
async function route() {
1368+
renderBoard();
13271369
const h = location.hash;
13281370
if (h.startsWith('#m/')) return renderDetail(h.slice(3), true);
13291371
$('detail-view').classList.add('hidden');
@@ -1358,17 +1400,40 @@ export function renderUi(prefix) {
13581400
if (me) { await api('/session', { method: 'DELETE' }); me = null; await refreshMe(true); await route(); return; }
13591401
$('auth-card').classList.remove('hidden');
13601402
$('auth-card').scrollIntoView({ behavior: 'smooth', block: 'center' });
1361-
$('token').focus();
1403+
($('token') || $('acct-user')).focus();
13621404
};
1363-
$('do-signin').onclick = async () => {
1364-
$('auth-msg').textContent = 'Starting session…'; $('auth-msg').className = 'msg';
1405+
async function startSession(bearer) {
1406+
await api('/session', { method: 'POST', headers: { authorization: 'Bearer ' + bearer } });
1407+
await refreshMe(); await route();
1408+
toast('Signed in');
1409+
}
1410+
if ($('do-signin')) {
1411+
$('do-signin').onclick = async () => {
1412+
$('auth-msg').textContent = 'Starting session…'; $('auth-msg').className = 'msg';
1413+
try {
1414+
await startSession($('token').value.trim());
1415+
$('token').value = '';
1416+
} catch (e) { $('auth-msg').textContent = e.message; $('auth-msg').className = 'msg err'; }
1417+
};
1418+
}
1419+
// Account mode (standalone host): register/login mint a bearer at
1420+
// {prefix}/api/{register,login}, then the normal session exchange runs.
1421+
async function acctAuth(pathName) {
1422+
const username = $('acct-user').value.trim();
1423+
const password = $('acct-pass').value;
1424+
$('auth-msg').textContent = pathName === '/register' ? 'Creating account…' : 'Signing in…';
1425+
$('auth-msg').className = 'msg';
13651426
try {
1366-
await api('/session', { method: 'POST', headers: { authorization: 'Bearer ' + $('token').value.trim() } });
1367-
$('token').value = '';
1368-
await refreshMe(); await route();
1369-
toast('Signed in');
1427+
const r = await api(pathName, { method: 'POST', body: JSON.stringify({ username, password }) });
1428+
await startSession(r.token);
1429+
$('acct-pass').value = '';
13701430
} catch (e) { $('auth-msg').textContent = e.message; $('auth-msg').className = 'msg err'; }
1371-
};
1431+
}
1432+
if ($('do-acct-signin')) {
1433+
$('do-acct-signin').onclick = () => acctAuth('/login');
1434+
$('do-acct-register').onclick = () => acctAuth('/register');
1435+
$('acct-pass').addEventListener('keydown', (e) => { if (e.key === 'Enter') acctAuth('/login'); });
1436+
}
13721437
$('back').onclick = (e) => { e.preventDefault(); location.hash = ''; };
13731438
$('t-buy').onclick = () => {
13741439
if (!me) {

0 commit comments

Comments
 (0)