Sprint 2 UI and brand refresh

Workbench: ramp-adjusted headline (TCO, net run-rate, NPV, IRR, payback),
financial model panel with internal-only investment, year-by-year cash flow,
scenario panel (presets + custom, presented scenario, report comparison),
overlap heat map, shelfware and expansion panel, editable implementation
timeline (drag, resize, row editor, dependencies, reset), version history
with compare and restore, deal status, customer logo.

Deliverables: report, Word and new PowerPoint deck present the chosen
scenario with net figures only; investment and dossier entitlements never
itemized (covered by the leak suite). Exports and printing snapshot
automatically.

New pages: Portfolio dashboard, Settings (company name, logo, accent).
Knowledge base: duplicate queue with merge, confidence and sourcing,
TCO defaults, needs-review filter.

Brand: VMware by Broadcom palette per Brand Guidelines.pdf; Darkmode navy
dark theme; AA-safe accent ink; business units remapped to palette.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 15:41:15 -04:00
parent 4496312845
commit 93007a1053
30 changed files with 2005 additions and 312 deletions

View File

@@ -111,9 +111,12 @@ console.log('Shelfware');
console.log('Knowledge base curation');
{
const kb = { version: 1, updatedAt: '', entries: [newEntry('F5 BIG-IP', 'manual'), newEntry('F5 BIG-IP LTM', 'manual'), newEntry('Microsoft Entra ID', 'manual')] };
kb.entries.forEach((e) => (e.domain = 'adc'));
kb.entries.forEach((e) => { e.domain = 'adc'; e.productIds = ['ans-avi']; });
const dups = findDuplicates(kb);
t('finds F5 BIG-IP / F5 BIG-IP LTM', dups.some((d) => [d.a.label, d.b.label].sort().join('|') === 'F5 BIG-IP|F5 BIG-IP LTM'));
const fam = { entries: [newEntry('Datadog', 'manual'), newEntry('Datadog Logs', 'manual')] };
fam.entries[0].productIds = ['aod-dx-oi']; fam.entries[1].productIds = ['vcf-ops-logs'];
t('product family with different replacements is not a duplicate', findDuplicates(fam).length === 0);
const a = base([line('f5', { incumbentVendor: 'F5', incumbentProduct: 'BIG-IP', domain: 'adc', productIds: ['ans-avi'] })]);
const learned = learnFromAssessment(kb, a);
const merged = mergeEntries(learned, 'f5-big-ip-ltm', 'f5-big-ip');

View File

@@ -71,6 +71,33 @@ try {
t('Word still carries a mapped product', /Avi Load Balancer|VCF Networking|Layer7/.test(docx));
execFileSync('rm', ['-f', tmp.pathname]);
/* ---- our pricing and dossier entitlements stay internal (sprint 2) ---- */
const PRICE = 7_654_321, SERVICES = 1_234_567;
const fmt = (n) => '$' + n.toLocaleString('en-US');
const priced = await json(`/api/assessments/${a.id}`);
priced.finance = { discountRate: 10, fteCost: 150000, investment: { annual: PRICE, services: SERVICES } };
priced.candidates[0].alreadyEntitled = 'VMware Avi Load Balancer';
await send(`/api/assessments/${a.id}`, 'PUT', priced);
const deckBuf = Buffer.from(await (await fetch(`${B}/api/assessments/${a.id}/deck`)).arrayBuffer());
const deckTmp = new URL('./.leak-test.pptx', import.meta.url);
writeFileSync(deckTmp, deckBuf);
const deck = execFileSync('sh', ['-c', `unzip -p '${deckTmp.pathname}' 'ppt/slides/*.xml'`], { maxBuffer: 64e6 }).toString('utf8');
t('deck is a readable pptx with slides', deck.includes('<p:sld') && deck.length > 5000);
const docx2Buf = Buffer.from(await (await fetch(`${B}/api/assessments/${a.id}/docx`)).arrayBuffer());
writeFileSync(tmp, docx2Buf);
const docx2 = execFileSync('unzip', ['-p', tmp.pathname, 'word/document.xml'], { maxBuffer: 32e6 }).toString('utf8');
const safe2 = JSON.stringify(await json(`/api/assessments/${a.id}/report`));
for (const [name, text] of [['deck', deck], ['Word', docx2]]) {
t(`${name} omits our annual price`, !text.includes(fmt(PRICE)) && !text.includes('7.7M'));
t(`${name} omits our services figure`, !text.includes(fmt(SERVICES)));
t(`${name} never says "subscription"`, !/subscription/i.test(text));
t(`${name} never names an entitlement`, !/entitle/i.test(text));
}
t('projection drops entitlement flags', !safe2.includes('alreadyEntitled'));
execFileSync('rm', ['-f', tmp.pathname, deckTmp.pathname]);
} finally {
await fetch(`${B}/api/assessments/${a.id}`, { method: 'DELETE' });
}