openvibers/openvibe.trade Commits

Trade: the front page shows each instrument's latest number, and the exchange column only when one is recorded (#53)

OP OpenVibers committed 29 days ago · 59e6a761f1 · 3 files, +22 −5

**The Trade front page shows each instrument's latest number.**

**Found:** while reviewing every home page. The front page listed 28 currencies with Symbol, Name, Kind and "Exchange: not recorded" on every row. A currency has no exchange, so the column said nothing, while each instrument's page had the number people come for (AUD: 1.6031 per euro, observed today, source current).

**Change:**
- **A "Latest" column:** each listed instrument's headline observation (its price, close or rate, else its first metric), as the source stated it, with its unit, the metric and when it was observed. A stale source shows the stale badge, and an instrument with nothing observed says "no observation yet".
- **The Exchange column** appears only when an instrument on the page has an exchange recorded.
- The handler reads the latest observations for the page's instruments (50 at most) through the same `observations.latest` and `dto` the instrument page uses, so the freshness rule is the same one.

**Test:** `pages.test.js` checks that the home table has Exchange (ACME is on NYSE) and Latest, and that ACME's row shows `10.50` with its metric and time.

Changes

ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 7138704..3285a8f 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -24,7 +24,7 @@ const { SITE_SUMMARY } = require('../render/layout');
const { define } = require('./routes');
function createPages(ctx) {
-- const { config, instruments, documents, reading, freshness, urls, watchlists, context, common, sync } = ctx;
++ const { config, instruments, documents, reading, freshness, urls, watchlists, context, common, sync, observations } = ctx;
const router = express.Router();
const listing = (path) => seo.evaluate({ state: 'published', visibility: 'public', canonicalUrl: urls.abs(path), wordCount: 0 }, { policy: { minWords: 0 } });
@@@ -40,6 +40,14 @@ function createPages(ctx) {
const pager = paginate({ page: pageNo, perPage: per, total: first.total, href: (p) => (p === 1 ? '/' : `/?page=${p}`) });
if (pager.outOfRange && first.total) return common.notFound(req, res);
const page = await instruments.page({ limit: pager.limit, offset: pager.offset });
++ // Each listed instrument's headline number on the list itself: its latest price, close or rate (else its first
++ // metric), with when it was observed and whether its source is current. Fifty at most per page.
++ const now = Date.now();
++ const latest = new Map(await Promise.all(page.instruments.map(async (i) => {
++ const rows = await observations.latest(i);
++ const pick = ['price', 'close', 'rate'].map((m) => rows.find((o) => o.metric === m)).find(Boolean) || rows[0];
++ return [i.id, pick ? await observations.dto(pick, { now }) : null];
++ })));
const recent = (await Promise.all((await documents.recent(10)).map(async (d) => ({ document: documents.dto(d), instrument: await instruments.get(d.instrument_id) }))));
const canonical = urls.abs(pageNo === 1 ? '/' : `/?page=${pageNo}`);
common.page(req, res, {
@@@ -49,7 +57,7 @@ function createPages(ctx) {
styles: pageNo === 1 ? [showcase.STYLESHEET] : [],
feeds: [{ type: 'rss', href: '/feed.xml', title: 'Recent documents (RSS)' }, { type: 'atom', href: '/atom.xml', title: 'Recent documents (Atom)' }, { type: 'json', href: '/feed.json', title: 'Recent documents (JSON Feed)' }],
jsonLd: [seo.compact({ '@context': 'https://schema.org', '@type': 'WebSite', url: urls.abs('/'), name: 'OpenVibe.Trade', inLanguage: 'en' })],
-- body: views.home({ page, recent, urls, sync: await sync.state(), pager }),
++ body: views.home({ page, recent, urls, sync: await sync.state(), pager, latest }),
});
});
ddiff --git a/server/render/views.js b/server/render/views.js
iindex 645a343..51f03d0 100644
---- a/server/render/views.js
++++ b/server/render/views.js
@@@ -60,10 +60,15 @@ function tradeShowcase() {
});
}
--function home({ page, recent, urls, sync, pager }) {
++function home({ page, recent, urls, sync, pager, latest = new Map() }) {
++ // The exchange column only when an instrument on the page has one (a currency has none to record).
++ const exchanges = page.instruments.some((i) => i.exchange);
++ const latestCell = (o) => (o
++ ? html`<td class="num"><data value="${o.value}">${o.value}</data> ${o.unit} <span class="muted">${o.metric}, ${when(o.observed_at)}</span>${o.freshness && o.freshness.stale ? html` ${staleBadge(o.freshness)}` : ''}</td>`
++ : html`<td><span class="muted">no observation yet</span></td>`);
const rows = page.instruments.map((i) => html`<tr>
<td><a href="${urls.path.instrument(i)}"><strong>${i.symbol}</strong></a></td>
-- <td>${i.name}</td><td>${i.kind}</td><td>${i.exchange || html`<span class="muted">not recorded</span>`}</td>
++ <td>${i.name}</td><td>${i.kind}</td>${exchanges ? html`<td>${i.exchange || html`<span class="muted">not recorded</span>`}</td>` : ''}${latestCell(latest.get(i.id))}
</tr>`);
const docs = recent.map((r) => html`<li><a href="${urls.path.instrument(r.instrument)}#${r.document.id}">${r.instrument.symbol}</a>
${r.document.form_type ? html`<span class="badge">${r.document.form_type}</span>` : ''} ${r.document.title || r.document.filer_name || 'Untitled document'}
@@@ -78,7 +83,7 @@ ${first ? raw(tradeShowcase()) : html`<h1>Sourced market information</h1>`}
<button type="submit">Find</button>
</form>
<h2>Instruments</h2>
--${page.instruments.length ? html`<table class="data"><thead><tr><th scope="col">Symbol</th><th scope="col">Name</th><th scope="col">Kind</th><th scope="col">Exchange</th></tr></thead><tbody>${rows}</tbody></table>
++${page.instruments.length ? html`<table class="data"><thead><tr><th scope="col">Symbol</th><th scope="col">Name</th><th scope="col">Kind</th>${exchanges ? html`<th scope="col">Exchange</th>` : ''}<th scope="col">Latest</th></tr></thead><tbody>${rows}</tbody></table>
${raw(paginationHtml(pager))}`
: html`<p class="empty">No instruments have been added yet.</p>`}
<h2>Recent documents</h2>
ddiff --git a/test/pages.test.js b/test/pages.test.js
iindex f62c318..8a3473a 100644
---- a/test/pages.test.js
++++ b/test/pages.test.js
@@@ -151,6 +151,10 @@ const WORDS = (n) => Array.from({ length: n }, (_, i) => `word${i}`).join(' ');
const home = (await t.get('/')).text;
assert.ok(home.includes('<meta name="ai-summary"'), 'the home carries the ai-summary meta');
++ // The list shows each instrument's latest number with its metric and time; the exchange column stays while one
++ // is recorded (ACME's NYSE), and an instrument with nothing observed says so.
++ assert.match(home, /<th scope="col">Exchange<\/th><th scope="col">Latest<\/th>/);
++ assert.match(home, /<td class="num"><data value="10\.50">10\.50<\/data> [^<]*<span class="muted">price\.close, /);
assert.ok(home.includes('"@type":"WebPage"'), 'the home carries WebPage JSON-LD');
});