openvibers/openvibe.trade Commits

Trade home: without a headline number, a company's newest filing fills the Latest column (no "no observation yet"); the AI brief section offers a brief instead of saying there is none (#71)

OP OpenVibers committed 28 days ago · bf407eb857 · 3 files, +19 −7

Changes

ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 88f9364..1499a86 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -50,6 +50,11 @@ function createPages(ctx) {
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];
})));
++ // Without a headline number, the newest filing stands in on the list.
++ const latestDocs = new Map((await Promise.all(page.instruments.filter((i) => !latest.get(i.id)).map(async (i) => {
++ const [d] = await documents.forInstrument(i, { limit: 1 });
++ return d ? [i.id, documents.dto(d)] : null;
++ }))).filter(Boolean));
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, {
@@@ -59,7 +64,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, latest, briefs: pageNo === 1 ? await ctx.briefs.recent(6) : [] }),
++ body: views.home({ page, recent, urls, sync: await sync.state(), pager, latest, latestDocs, briefs: pageNo === 1 ? await ctx.briefs.recent(6) : [] }),
});
});
ddiff --git a/server/render/views.js b/server/render/views.js
iindex 0891fbb..3857a01 100644
---- a/server/render/views.js
++++ b/server/render/views.js
@@@ -83,15 +83,18 @@ function clip(text, n) {
return `${cut.slice(0, Math.max(cut.lastIndexOf(' '), n - 20))}…`;
}
--function home({ page, recent, urls, sync, pager, latest = new Map(), briefs = [] }) {
++function home({ page, recent, urls, sync, pager, latest = new Map(), latestDocs = new Map(), briefs = [] }) {
// 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
++ // The headline number when there is one; else the newest filing (most companies on a given day only filed); else
++ // nothing to show yet beyond the SEC's company list.
++ const latestCell = (i, o, d) => (o
? html`<td class="num"><data value="${o.value}">${grouped(o.value)}</data> ${o.unit} <span class="muted">${labelOf(o.metric)}${o.period ? `, ${o.period}` : `, ${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>`);
++ : d ? html`<td><a href="${urls.path.instrument(i)}#${d.id}">${d.form_type ? html`<span class="badge">${d.form_type}</span> ` : ''}filed ${when(d.published_at || d.first_seen_at)}</a></td>`
++ : html`<td><span class="muted">on the SEC's company list</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>${exchanges ? html`<td>${i.exchange || html`<span class="muted">not recorded</span>`}</td>` : ''}${latestCell(latest.get(i.id))}
++ <td>${i.name}</td><td>${i.kind}</td>${exchanges ? html`<td>${i.exchange || html`<span class="muted">not recorded</span>`}</td>` : ''}${latestCell(i, latest.get(i.id), latestDocs.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'}
@@@ -249,8 +252,10 @@ function briefSection(instrument, d) {
const ask = d.briefs && d.briefs.canAsk ? html`<form method="post" action="/i/${encodeURIComponent(instrument.symbol)}/brief" class="inline-form">
<button type="submit">${b ? 'Check for a newer brief' : 'Write an AI brief'}</button></form>` : '';
if (!b) {
++ // Briefs are written when a company files, or when a reader asks: with nothing to write from, there is no section.
++ if (!ask) return '';
return html`<section aria-labelledby="ai-brief-h" id="ai-brief"><h2 id="ai-brief-h">AI brief</h2>
--<p class="empty">No AI brief for ${instrument.symbol} yet.${d.briefs && d.briefs.canAsk ? ' One can be written from the SEC numbers and filings Trade has for it.' : ''}</p>${ask}</section>`;
++<p>OpenVibe.AI can write a short brief on ${instrument.symbol} from the SEC numbers and filings on this page, every point linked to the figure it rests on. It takes a few seconds.</p>${ask}</section>`;
}
return html`<section aria-labelledby="ai-brief-h" id="ai-brief" class="ai-brief"><h2 id="ai-brief-h">AI brief <span class="badge">AI-written, not reviewed by a person</span></h2>
${b.summary ? html`<p>${b.summary}</p>` : ''}
ddiff --git a/test/pages.test.js b/test/pages.test.js
iindex 8a3473a..ebb64fa 100644
---- a/test/pages.test.js
++++ b/test/pages.test.js
@@@ -152,8 +152,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.
++ // is recorded (ACME's NYSE); an instrument with nothing observed shows its newest filing, or that it is on the SEC's list.
assert.match(home, /<th scope="col">Exchange<\/th><th scope="col">Latest<\/th>/);
++ assert.ok(!home.includes('no observation yet'));
++ assert.match(home, /filed \d{4}-\d{2}-\d{2}|on the SEC's company list/);
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');
});