openvibers/openvibe.report Commits
Your AI on /you: calls, tokens, the cache's share, cost and fees, who paid (#36)
Changes
@@@ -154,12 +154,16 @@ function createPageRoutes(ctx) {const user = req.user;if (!user || !isUsername(String(user.username || ''))) return send(req, res, views.youSignedOut(), 200, 'private, no-store');const days = normaliseDays(req.query.days);-- const [stats, recaps, speaker] = await Promise.all([++ // Your AI (plan L40/L41): the ledger's own rows for this person, read in-process, never by a query value.++ const subject = typeof user.subject_id === 'string' ? user.subject_id.trim() : '';++ const from = new Date((ctx.now ? ctx.now() : Date.now()) - (days - 1) * 86400000).toISOString().slice(0, 10);++ const [stats, recaps, speaker, ai] = await Promise.all([ctx.analytics && typeof ctx.analytics.channel === 'function' ? ctx.analytics.channel(user.username, days).catch(() => null) : null,Promise.resolve(live.channelRecaps(user.username)).catch(() => null),Promise.resolve(live.speaker(user.username)).catch(() => null),++ subject && ctx.aiUsage ? ctx.aiUsage.forPerson({ subject, from, group: 'day' }).catch(() => null) : null,]);-- return send(req, res, views.you({ user, days, stats, recaps, speaker }), 200, 'private, no-store');++ return send(req, res, views.you({ user, days, stats, recaps, speaker, ai }), 200, 'private, no-store');});r.get('/c/:username', async (req, res) => {@@@ -381,7 +381,33 @@ function youSentence({ name, days, stats, speaker }) {return `In the last ${days} days, ${name}, you ${list}.`;}--function you({ user, days = 30, stats = null, recaps = null, speaker = null } = {}) {++// Your AI on /you (plan L40/L41): who paid, in words a person uses.++const PAID_BY = { own_key: 'Your own keys', openvibe_key: 'The OpenVibe key', plan: 'Your plan', own_machine: 'Your own machine', free_tier: 'Free tiers', network_allowance: 'OpenVibe\u2019s allowance', org_key: 'Your organisation\u2019s key' };++const aiUsd = (v) => { const x = n(v); if (x == null) return '\u2014'; if (x === 0) return '$0'; return x < 0.01 ? 'under $0.01' : `$${x.toFixed(x < 10 ? 2 : 0)}`; };++const bigNum = (v) => { const x = n(v) || 0; return x >= 1e6 ? `${(x / 1e6).toFixed(1)}M` : x >= 1e4 ? `${Math.round(x / 1e3)}k` : exact(x); };++++function youAi(ai, days) {++ const T = ai && ai.totals;++ if (!T || !n(T.calls)) {++ return `<section aria-labelledby="rp-you-ai"><h2 id="rp-you-ai">Your AI</h2><p class="rp-fine">No AI calls in the last ${esc(days)} days. When you use AI on any OpenVibe site, it is counted here: the tokens, how much came from cache, what it cost and whose key paid. Only you see it.</p></section>`;++ }++ const tokens = (n(T.input_tokens) || 0) + (n(T.output_tokens) || 0);++ const cacheShare = (n(T.cached_tokens) || 0) / Math.max(1, (n(T.input_tokens) || 0) + (n(T.cached_tokens) || 0));++ const daily = (ai.rows || []).map((r) => ({ day: r.key, n: n(r.calls) || 0 }));++ const paid = Object.entries(ai.by_paid_by || {}).filter(([, v]) => v && n(v.calls)).sort((a, b) => (n(b[1].calls) || 0) - (n(a[1].calls) || 0));++ const chips = paid.map(([k, v]) => `<li><b>${esc(PAID_BY[k] || k)}</b> ${esc(plural(v.calls, 'call', 'calls'))} \u00b7 ${n(v.charged_usd) ? esc(aiUsd(v.charged_usd)) + (n(v.fee_usd) ? ` incl. ${esc(aiUsd(v.fee_usd))} fee` : '') : 'no charge'}</li>`).join('');++ const lines = (ai.insights || []).slice(0, 3);++ return `<section aria-labelledby="rp-you-ai"><h2 id="rp-you-ai">Your AI</h2>++${lines.length ? `<p class="rp-lede">${esc(lines[0])}</p>` : ''}++<div class="rp-tiles rp-tiles-sm">${tile('Calls', exact(T.calls))}${tile('Tokens', esc(bigNum(tokens)))}${tile('From cache', `${Math.round(cacheShare * 100)}%`)}${tile('Cost', esc(aiUsd(T.cost_usd)), n(T.saved_usd) ? `cache saved ${esc(aiUsd(T.saved_usd))}` : '')}${tile('Fees', esc(aiUsd(T.fee_usd)))}</div>++${daily.length > 1 ? bars(daily, 'AI calls a day', { h: 100 }) : ''}++${chips ? `<h3 class="rp-you-sub">Who paid</h3><ul class="rp-chips rp-paid">${chips}</ul>` : ''}++${lines.slice(1).map((l) => `<p class="rp-fine">${esc(l)}</p>`).join('')}++<p class="rp-meta"><a href="https://ai.openvibe.services/you">Your keys and how AI is paid for</a> \u00b7 <a href="/api/v1/me/ai/usage">As JSON</a></p>++</section>`;++}++++function you({ user, days = 30, stats = null, recaps = null, speaker = null, ai = null } = {}) {const name = String(user.display_name || user.username || 'you');const username = String(user.username || '');const T = (stats && stats.totals) || {};@@@ -404,8 +430,8 @@ ${(sp.episodes || []).length ? `<p class="rp-meta">${plural(sp.episodes.length,</section>` : '';const body = `<header class="rp-page-h rp-person">${kit.mark({ name, src: user.avatar_url || '', size: 64 })}<div><p class="rp-kicker">Your numbers</p><h1>${esc(name)}</h1><p class="rp-lede rp-you-say">${esc(youSentence({ name, days, stats, speaker }))}</p><nav class="rp-tabs" aria-label="Time range">${tabs}</nav></div></header>--${streams}${money}${rundown}--<section aria-labelledby="rp-you-next" class="rp-you-next"><h2 id="rp-you-next">Coming here</h2><p class="rp-fine">Your AI use across every OpenVibe site (tokens, how much came from cache, what it cost and whose key paid), your agents' work and what your computers ran. Only you see this page.</p></section>`;++${streams}${money}${youAi(ai, days)}${rundown}++<section aria-labelledby="rp-you-next" class="rp-you-next"><h2 id="rp-you-next">Coming here</h2><p class="rp-fine">Your agents' work and what your computers ran. Only you see this page.</p></section>`;return layout({ title: 'Your numbers', description: 'Your own numbers across OpenVibe: streams, money and The Rundown, in one place.', body, active: '/you', robots: 'noindex, nofollow' });}@@@ -169,6 +169,7 @@ async function boot(opts = {}) {analytics: opts.analytics,sso: opts.sso,shadow: opts.shadow,++ ...(opts.aiUsage ? { aiUsage: opts.aiUsage } : {}),releaseEnv: { ...process.env, RELEASE_COMMIT: '0000000' },});const server = await new Promise((resolve) => {@@@ -7,6 +7,8 @@*/const assert = require('assert');const crypto = require('crypto');++const fs = require('fs');++const path = require('path');const { createSsoClient } = require('openvibe-sdk/sso');const { boot, check, done } = require('./helpers/boot');@@@ -36,7 +38,21 @@ const goosely = token({ sub: '7', subject_id: 'usr_01JTESTGOOSELY0000000000000',stats: async () => null, metrics: async () => null, reportStats: () => null,};const sso = createSsoClient({ site: 'report', baseUrl: 'https://openvibe.report', clientId: 'report', clientSecret: 'x', networkUrl: 'https://openvibe.network', publicKey: publicKey.export({ type: 'spki', format: 'pem' }), log: { warn() {}, info() {}, log() {}, error() {} } });-- const t = await boot({ sso, analytics });++ // The AI ledger (plan L40/L41), answered by hand: the rows of the person the session names.++ const aiAsked = [];++ const aiUsage = {++ async forPerson({ subject, from, group }) {++ aiAsked.push({ subject, from, group });++ return {++ group, source: 'raw',++ rows: [{ key: '2026-10-30', calls: 1, cost_usd: 0.05 }, { key: '2026-10-31', calls: 2, cost_usd: 0.002 }],++ totals: { calls: 3, input_tokens: 1250, output_tokens: 590, cached_tokens: 800, cost_usd: 0.0531, fee_usd: 0.01, charged_usd: 0.06, saved_usd: 0.0017 },++ by_paid_by: { own_key: { calls: 2, charged_usd: 0, fee_usd: 0 }, openvibe_key: { calls: 1, charged_usd: 0.06, fee_usd: 0.01 } },++ insights: ['67% of your calls ran on your own keys, so no fee on those.', 'Your calls cost $0.06, including $0.01 in fees.'],++ };++ },++ };++ const t = await boot({ sso, analytics, aiUsage });try {await check('signed out: what it is, a sign-in back to /you, private and not indexed', async () => {const r = await t.get('/you');@@@ -60,7 +76,20 @@ const goosely = token({ sub: '7', subject_id: 'usr_01JTESTGOOSELY0000000000000',assert.ok(html.includes('2 payments in Vibes reached you') && html.includes('https://billing.openvibe.network/'), 'money says where the amounts are');assert.ok(html.includes('<h2 id="rp-you-rd">In The Rundown</h2>') && html.includes('href="/rundown/t/seattle"'));assert.ok(html.includes('aria-current="page">7 days'));-- assert.ok(html.includes('whose key paid'), 'what joins next (L40)');++ // Your AI (L40/L41): the person's own ledger, by the session's subject, never a query value.++ assert.deepStrictEqual(aiAsked[aiAsked.length - 1].subject, 'usr_01JTESTGOOSELY0000000000000', 'the session\'s own subject');++ assert.strictEqual(aiAsked[aiAsked.length - 1].from, new Date(Date.now() - 6 * 86400000).toISOString().slice(0, 10), 'the window asked for');++ assert.ok(html.includes('<h2 id="rp-you-ai">Your AI</h2>'));++ assert.ok(html.includes('67% of your calls ran on your own keys, so no fee on those.'), 'the first insight leads');++ assert.ok(html.includes('>From cache<') && html.includes('>39%<'), 'the share from cache: 800 of 2,050 input tokens');++ assert.ok(html.includes('<b>Your own keys</b> 2 calls'), 'who paid, in words');++ assert.ok(html.includes('<b>The OpenVibe key</b> 1 call') && html.includes('incl. $0.01 fee'));++ assert.ok(html.includes('href="https://ai.openvibe.services/you"'), 'where the keys are');++ const before = await t.get('/you?days=7&subject=usr_01JSOMEONEELSE00000000000000', { headers: { cookie: `ov_token=${goosely}` } });++ assert.strictEqual(before.status, 200);++ assert.strictEqual(aiAsked[aiAsked.length - 1].subject, 'usr_01JTESTGOOSELY0000000000000', 'a subject in the address changes nothing');++ const pagesSrc = fs.readFileSync(path.join(__dirname, '..', 'server/http/pages.js'), 'utf8');++ assert.match(pagesSrc, /ctx\.aiUsage\.forPerson\(\{ subject, from, group: 'day' \}\)/, 'the page reads the ledger in-process, by the session\'s subject');});await check('only the parts with something in them: no streams or money, just The Rundown', async () => {
@@ -154,12 +154,16 @@ function createPageRoutes(ctx) {
@@ -154,12 +154,16 @@ function createPageRoutes(ctx) {
const user = req.user;
const user = req.user;
if (!user || !isUsername(String(user.username || ''))) return send(req, res, views.youSignedOut(), 200, 'private, no-store');
if (!user || !isUsername(String(user.username || ''))) return send(req, res, views.youSignedOut(), 200, 'private, no-store');
const days = normaliseDays(req.query.days);
const days = normaliseDays(req.query.days);
- const [stats, recaps, speaker] = await Promise.all([
+ // Your AI (plan L40/L41): the ledger's own rows for this person, read in-process, never by a query value.
+ const subject = typeof user.subject_id === 'string' ? user.subject_id.trim() : '';
+ const from = new Date((ctx.now ? ctx.now() : Date.now()) - (days - 1) * 86400000).toISOString().slice(0, 10);
+ const [stats, recaps, speaker, ai] = await Promise.all([
ctx.analytics && typeof ctx.analytics.channel === 'function' ? ctx.analytics.channel(user.username, days).catch(() => null) : null,
ctx.analytics && typeof ctx.analytics.channel === 'function' ? ctx.analytics.channel(user.username, days).catch(() => null) : null,
Promise.resolve(live.channelRecaps(user.username)).catch(() => null),
Promise.resolve(live.channelRecaps(user.username)).catch(() => null),
Promise.resolve(live.speaker(user.username)).catch(() => null),
Promise.resolve(live.speaker(user.username)).catch(() => null),
+ subject && ctx.aiUsage ? ctx.aiUsage.forPerson({ subject, from, group: 'day' }).catch(() => null) : null,
]);
]);
- return send(req, res, views.you({ user, days, stats, recaps, speaker }), 200, 'private, no-store');
+ return send(req, res, views.you({ user, days, stats, recaps, speaker, ai }), 200, 'private, no-store');
});
});
r.get('/c/:username', async (req, res) => {
r.get('/c/:username', async (req, res) => {
@@ -381,7 +381,33 @@ function youSentence({ name, days, stats, speaker }) {
@@ -381,7 +381,33 @@ function youSentence({ name, days, stats, speaker }) {
return `In the last ${days} days, ${name}, you ${list}.`;
return `In the last ${days} days, ${name}, you ${list}.`;
}
}
-function you({ user, days = 30, stats = null, recaps = null, speaker = null } = {}) {
+// Your AI on /you (plan L40/L41): who paid, in words a person uses.
+const PAID_BY = { own_key: 'Your own keys', openvibe_key: 'The OpenVibe key', plan: 'Your plan', own_machine: 'Your own machine', free_tier: 'Free tiers', network_allowance: 'OpenVibe\u2019s allowance', org_key: 'Your organisation\u2019s key' };
+const aiUsd = (v) => { const x = n(v); if (x == null) return '\u2014'; if (x === 0) return '$0'; return x < 0.01 ? 'under $0.01' : `$${x.toFixed(x < 10 ? 2 : 0)}`; };
+const bigNum = (v) => { const x = n(v) || 0; return x >= 1e6 ? `${(x / 1e6).toFixed(1)}M` : x >= 1e4 ? `${Math.round(x / 1e3)}k` : exact(x); };
+
+function youAi(ai, days) {
+ const T = ai && ai.totals;
+ if (!T || !n(T.calls)) {
+ return `<section aria-labelledby="rp-you-ai"><h2 id="rp-you-ai">Your AI</h2><p class="rp-fine">No AI calls in the last ${esc(days)} days. When you use AI on any OpenVibe site, it is counted here: the tokens, how much came from cache, what it cost and whose key paid. Only you see it.</p></section>`;
+ }
+ const tokens = (n(T.input_tokens) || 0) + (n(T.output_tokens) || 0);
+ const cacheShare = (n(T.cached_tokens) || 0) / Math.max(1, (n(T.input_tokens) || 0) + (n(T.cached_tokens) || 0));
+ const daily = (ai.rows || []).map((r) => ({ day: r.key, n: n(r.calls) || 0 }));
+ const paid = Object.entries(ai.by_paid_by || {}).filter(([, v]) => v && n(v.calls)).sort((a, b) => (n(b[1].calls) || 0) - (n(a[1].calls) || 0));
+ const chips = paid.map(([k, v]) => `<li><b>${esc(PAID_BY[k] || k)}</b> ${esc(plural(v.calls, 'call', 'calls'))} \u00b7 ${n(v.charged_usd) ? esc(aiUsd(v.charged_usd)) + (n(v.fee_usd) ? ` incl. ${esc(aiUsd(v.fee_usd))} fee` : '') : 'no charge'}</li>`).join('');
+ const lines = (ai.insights || []).slice(0, 3);
+ return `<section aria-labelledby="rp-you-ai"><h2 id="rp-you-ai">Your AI</h2>
+${lines.length ? `<p class="rp-lede">${esc(lines[0])}</p>` : ''}
+<div class="rp-tiles rp-tiles-sm">${tile('Calls', exact(T.calls))}${tile('Tokens', esc(bigNum(tokens)))}${tile('From cache', `${Math.round(cacheShare * 100)}%`)}${tile('Cost', esc(aiUsd(T.cost_usd)), n(T.saved_usd) ? `cache saved ${esc(aiUsd(T.saved_usd))}` : '')}${tile('Fees', esc(aiUsd(T.fee_usd)))}</div>
+${daily.length > 1 ? bars(daily, 'AI calls a day', { h: 100 }) : ''}
+${chips ? `<h3 class="rp-you-sub">Who paid</h3><ul class="rp-chips rp-paid">${chips}</ul>` : ''}
+${lines.slice(1).map((l) => `<p class="rp-fine">${esc(l)}</p>`).join('')}
+<p class="rp-meta"><a href="https://ai.openvibe.services/you">Your keys and how AI is paid for</a> \u00b7 <a href="/api/v1/me/ai/usage">As JSON</a></p>
+</section>`;
+}
+
+function you({ user, days = 30, stats = null, recaps = null, speaker = null, ai = null } = {}) {
const name = String(user.display_name || user.username || 'you');
const name = String(user.display_name || user.username || 'you');
const username = String(user.username || '');
const username = String(user.username || '');
const T = (stats && stats.totals) || {};
const T = (stats && stats.totals) || {};
@@ -404,8 +430,8 @@ ${(sp.episodes || []).length ? `<p class="rp-meta">${plural(sp.episodes.length,
@@ -404,8 +430,8 @@ ${(sp.episodes || []).length ? `<p class="rp-meta">${plural(sp.episodes.length,
</section>` : '';
</section>` : '';
const body = `<header class="rp-page-h rp-person">${kit.mark({ name, src: user.avatar_url || '', size: 64 })}<div><p class="rp-kicker">Your numbers</p><h1>${esc(name)}</h1>
const body = `<header class="rp-page-h rp-person">${kit.mark({ name, src: user.avatar_url || '', size: 64 })}<div><p class="rp-kicker">Your numbers</p><h1>${esc(name)}</h1>
<p class="rp-lede rp-you-say">${esc(youSentence({ name, days, stats, speaker }))}</p><nav class="rp-tabs" aria-label="Time range">${tabs}</nav></div></header>
<p class="rp-lede rp-you-say">${esc(youSentence({ name, days, stats, speaker }))}</p><nav class="rp-tabs" aria-label="Time range">${tabs}</nav></div></header>
-${streams}${money}${rundown}
-<section aria-labelledby="rp-you-next" class="rp-you-next"><h2 id="rp-you-next">Coming here</h2><p class="rp-fine">Your AI use across every OpenVibe site (tokens, how much came from cache, what it cost and whose key paid), your agents' work and what your computers ran. Only you see this page.</p></section>`;
+${streams}${money}${youAi(ai, days)}${rundown}
+<section aria-labelledby="rp-you-next" class="rp-you-next"><h2 id="rp-you-next">Coming here</h2><p class="rp-fine">Your agents' work and what your computers ran. Only you see this page.</p></section>`;
return layout({ title: 'Your numbers', description: 'Your own numbers across OpenVibe: streams, money and The Rundown, in one place.', body, active: '/you', robots: 'noindex, nofollow' });
return layout({ title: 'Your numbers', description: 'Your own numbers across OpenVibe: streams, money and The Rundown, in one place.', body, active: '/you', robots: 'noindex, nofollow' });
}
}
@@ -169,6 +169,7 @@ async function boot(opts = {}) {
@@ -169,6 +169,7 @@ async function boot(opts = {}) {
analytics: opts.analytics,
analytics: opts.analytics,
sso: opts.sso,
sso: opts.sso,
shadow: opts.shadow,
shadow: opts.shadow,
+ ...(opts.aiUsage ? { aiUsage: opts.aiUsage } : {}),
releaseEnv: { ...process.env, RELEASE_COMMIT: '0000000' },
releaseEnv: { ...process.env, RELEASE_COMMIT: '0000000' },
});
});
const server = await new Promise((resolve) => {
const server = await new Promise((resolve) => {
@@ -7,6 +7,8 @@
@@ -7,6 +7,8 @@
*/
*/
const assert = require('assert');
const assert = require('assert');
const crypto = require('crypto');
const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
const { createSsoClient } = require('openvibe-sdk/sso');
const { createSsoClient } = require('openvibe-sdk/sso');
const { boot, check, done } = require('./helpers/boot');
const { boot, check, done } = require('./helpers/boot');
@@ -36,7 +38,21 @@ const goosely = token({ sub: '7', subject_id: 'usr_01JTESTGOOSELY0000000000000',
@@ -36,7 +38,21 @@ const goosely = token({ sub: '7', subject_id: 'usr_01JTESTGOOSELY0000000000000',
stats: async () => null, metrics: async () => null, reportStats: () => null,
stats: async () => null, metrics: async () => null, reportStats: () => null,
};
};
const sso = createSsoClient({ site: 'report', baseUrl: 'https://openvibe.report', clientId: 'report', clientSecret: 'x', networkUrl: 'https://openvibe.network', publicKey: publicKey.export({ type: 'spki', format: 'pem' }), log: { warn() {}, info() {}, log() {}, error() {} } });
const sso = createSsoClient({ site: 'report', baseUrl: 'https://openvibe.report', clientId: 'report', clientSecret: 'x', networkUrl: 'https://openvibe.network', publicKey: publicKey.export({ type: 'spki', format: 'pem' }), log: { warn() {}, info() {}, log() {}, error() {} } });
- const t = await boot({ sso, analytics });
+ // The AI ledger (plan L40/L41), answered by hand: the rows of the person the session names.
+ const aiAsked = [];
+ const aiUsage = {
+ async forPerson({ subject, from, group }) {
+ aiAsked.push({ subject, from, group });
+ return {
+ group, source: 'raw',
+ rows: [{ key: '2026-10-30', calls: 1, cost_usd: 0.05 }, { key: '2026-10-31', calls: 2, cost_usd: 0.002 }],
+ totals: { calls: 3, input_tokens: 1250, output_tokens: 590, cached_tokens: 800, cost_usd: 0.0531, fee_usd: 0.01, charged_usd: 0.06, saved_usd: 0.0017 },
+ by_paid_by: { own_key: { calls: 2, charged_usd: 0, fee_usd: 0 }, openvibe_key: { calls: 1, charged_usd: 0.06, fee_usd: 0.01 } },
+ insights: ['67% of your calls ran on your own keys, so no fee on those.', 'Your calls cost $0.06, including $0.01 in fees.'],
+ };
+ },
+ };
+ const t = await boot({ sso, analytics, aiUsage });
try {
try {
await check('signed out: what it is, a sign-in back to /you, private and not indexed', async () => {
await check('signed out: what it is, a sign-in back to /you, private and not indexed', async () => {
const r = await t.get('/you');
const r = await t.get('/you');
@@ -60,7 +76,20 @@ const goosely = token({ sub: '7', subject_id: 'usr_01JTESTGOOSELY0000000000000',
@@ -60,7 +76,20 @@ const goosely = token({ sub: '7', subject_id: 'usr_01JTESTGOOSELY0000000000000',
assert.ok(html.includes('2 payments in Vibes reached you') && html.includes('https://billing.openvibe.network/'), 'money says where the amounts are');
assert.ok(html.includes('2 payments in Vibes reached you') && html.includes('https://billing.openvibe.network/'), 'money says where the amounts are');
assert.ok(html.includes('<h2 id="rp-you-rd">In The Rundown</h2>') && html.includes('href="/rundown/t/seattle"'));
assert.ok(html.includes('<h2 id="rp-you-rd">In The Rundown</h2>') && html.includes('href="/rundown/t/seattle"'));
assert.ok(html.includes('aria-current="page">7 days'));
assert.ok(html.includes('aria-current="page">7 days'));
- assert.ok(html.includes('whose key paid'), 'what joins next (L40)');
+ // Your AI (L40/L41): the person's own ledger, by the session's subject, never a query value.
+ assert.deepStrictEqual(aiAsked[aiAsked.length - 1].subject, 'usr_01JTESTGOOSELY0000000000000', 'the session\'s own subject');
+ assert.strictEqual(aiAsked[aiAsked.length - 1].from, new Date(Date.now() - 6 * 86400000).toISOString().slice(0, 10), 'the window asked for');
+ assert.ok(html.includes('<h2 id="rp-you-ai">Your AI</h2>'));
+ assert.ok(html.includes('67% of your calls ran on your own keys, so no fee on those.'), 'the first insight leads');
+ assert.ok(html.includes('>From cache<') && html.includes('>39%<'), 'the share from cache: 800 of 2,050 input tokens');
+ assert.ok(html.includes('<b>Your own keys</b> 2 calls'), 'who paid, in words');
+ assert.ok(html.includes('<b>The OpenVibe key</b> 1 call') && html.includes('incl. $0.01 fee'));
+ assert.ok(html.includes('href="https://ai.openvibe.services/you"'), 'where the keys are');
+ const before = await t.get('/you?days=7&subject=usr_01JSOMEONEELSE00000000000000', { headers: { cookie: `ov_token=${goosely}` } });
+ assert.strictEqual(before.status, 200);
+ assert.strictEqual(aiAsked[aiAsked.length - 1].subject, 'usr_01JTESTGOOSELY0000000000000', 'a subject in the address changes nothing');
+ const pagesSrc = fs.readFileSync(path.join(__dirname, '..', 'server/http/pages.js'), 'utf8');
+ assert.match(pagesSrc, /ctx\.aiUsage\.forPerson\(\{ subject, from, group: 'day' \}\)/, 'the page reads the ledger in-process, by the session\'s subject');
});
});
await check('only the parts with something in them: no streams or money, just The Rundown', async () => {
await check('only the parts with something in them: no streams or money, just The Rundown', async () => {