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)
Changes
@@@ -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) : [] }),});});@@@ -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>` : ''}@@@ -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');});
@@ -50,6 +50,11 @@ function createPages(ctx) {
@@ -50,6 +50,11 @@ function createPages(ctx) {
const pick = ['price', 'close', 'rate'].map((m) => rows.find((o) => o.metric === m)).find(Boolean) || rows[0];
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];
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 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}`);
const canonical = urls.abs(pageNo === 1 ? '/' : `/?page=${pageNo}`);
common.page(req, res, {
common.page(req, res, {
@@ -59,7 +64,7 @@ function createPages(ctx) {
@@ -59,7 +64,7 @@ function createPages(ctx) {
styles: pageNo === 1 ? [showcase.STYLESHEET] : [],
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)' }],
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' })],
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) : [] }),
});
});
});
});
@@ -83,15 +83,18 @@ function clip(text, n) {
@@ -83,15 +83,18 @@ function clip(text, n) {
return `${cut.slice(0, Math.max(cut.lastIndexOf(' '), n - 20))}…`;
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).
// 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 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 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>
const rows = page.instruments.map((i) => html`<tr>
<td><a href="${urls.path.instrument(i)}"><strong>${i.symbol}</strong></a></td>
<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>`);
</tr>`);
const docs = recent.map((r) => html`<li><a href="${urls.path.instrument(r.instrument)}#${r.document.id}">${r.instrument.symbol}</a>
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'}
${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) {
@@ -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">
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>` : '';
<button type="submit">${b ? 'Check for a newer brief' : 'Write an AI brief'}</button></form>` : '';
if (!b) {
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>
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>
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>` : ''}
${b.summary ? html`<p>${b.summary}</p>` : ''}
@@ -152,8 +152,10 @@ const WORDS = (n) => Array.from({ length: n }, (_, i) => `word${i}`).join(' ');
@@ -152,8 +152,10 @@ const WORDS = (n) => Array.from({ length: n }, (_, i) => `word${i}`).join(' ');
const home = (await t.get('/')).text;
const home = (await t.get('/')).text;
assert.ok(home.includes('<meta name="ai-summary"'), 'the home carries the ai-summary meta');
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
// 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.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.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');
assert.ok(home.includes('"@type":"WebPage"'), 'the home carries WebPage JSON-LD');
});
});