Lands per-site UNS subtree files (Warsaw West/North, Shannon, Galway, TMT, Ponce) seeded from OpenText facility docs — Warsaw split confirmed as numbered = legacy Zimmer = West, lettered = legacy Biomet = North. Renames project framing from "Shopfloor IT/OT" to "SCADA IT/OT" for accuracy. Extracts a ZB-branded PowerPoint template from example_powerpoint.pptx and wires it into the outputs pipeline. Trims deck from 18 to 16 slides (BOBJ->Power BI transferred to another team, Non-Goals and Asks dropped); goal-state BOBJ analysis pruned to a stub.
156 lines
6.6 KiB
JavaScript
156 lines
6.6 KiB
JavaScript
const pptxgen = require('pptxgenjs');
|
||
const html2pptx = require('/Users/dohertj2/.claude/plugins/cache/anthropic-agent-skills/document-skills/unknown/skills/pptx/scripts/html2pptx.js');
|
||
const path = require('path');
|
||
|
||
const SLIDES_DIR = path.join(__dirname, 'slides');
|
||
const OUTPUT = '/Users/dohertj2/Desktop/plan/outputs/generated/plan-presentation.pptx';
|
||
|
||
// ZB template assets and design constants (see /Users/dohertj2/Desktop/plan/templates/README.md)
|
||
const TEMPLATE_ASSETS = '/Users/dohertj2/Desktop/plan/templates/assets';
|
||
const COVER_BG = path.join(TEMPLATE_ASSETS, 'cover-background.jpeg');
|
||
const BOTTOM_BORDER = path.join(TEMPLATE_ASSETS, 'bottom-border.jpg');
|
||
|
||
const ZB_BLUE = '0066B3';
|
||
const ZB_NAVY = '2B2A80';
|
||
const ZB_TITLE_WHITE = 'F4F4F4';
|
||
const CONFIDENTIAL_GRAY = '404040';
|
||
const CONFIDENTIAL_TEXT = 'CONFIDENTIAL – FOR ZIMMER BIOMET INTERNAL USE ONLY';
|
||
|
||
// Roadmap-table colors (slide 13)
|
||
const NAVY = '1C2833';
|
||
const SLATE = '2E4053';
|
||
const SILVER = 'AAB7B8';
|
||
const WHITE = 'FFFFFF';
|
||
const ROW_BG = 'FFFFFF';
|
||
const ROW_BG_ALT = 'EEF1F3';
|
||
|
||
async function main() {
|
||
const pres = new pptxgen();
|
||
pres.layout = 'LAYOUT_16x9'; // 10.0 × 5.625 in — matches the HTML slide CSS
|
||
pres.author = 'IT/OT Transformation Team';
|
||
pres.subject = 'SCADA IT/OT Transformation — 3-Year Plan';
|
||
pres.title = 'SCADA IT/OT Transformation';
|
||
|
||
// Slide 1 — ZB cover (replaces slide01.html; cover background + title overlay)
|
||
console.log('Processing slide 1 (ZB cover)...');
|
||
addCoverSlide(pres);
|
||
|
||
// Slides 2–16 — html2pptx-rendered content + ZB brand chrome (bottom border, confidentiality, page number)
|
||
for (let i = 2; i <= 16; i++) {
|
||
const num = String(i).padStart(2, '0');
|
||
const file = path.join(SLIDES_DIR, `slide${num}.html`);
|
||
console.log(`Processing slide ${i}...`);
|
||
|
||
const { slide } = await html2pptx(file, pres);
|
||
addBrandChrome(slide, i);
|
||
if (i === 13) {
|
||
addRoadmapTable(pres, slide);
|
||
}
|
||
}
|
||
|
||
await pres.writeFile({ fileName: OUTPUT });
|
||
console.log(`Saved: ${OUTPUT}`);
|
||
}
|
||
|
||
// Coordinates below are scaled 0.75x from the 13.33×7.5 ZB template (templates/README.md)
|
||
// to fit pptxgen's LAYOUT_16x9 (10×5.625 in). The HTML CSS uses 720×405pt = 10×5.625 in.
|
||
|
||
function addCoverSlide(pres) {
|
||
const slide = pres.addSlide();
|
||
slide.background = { color: 'FFFFFF' };
|
||
slide.addImage({ path: COVER_BG, x: 0, y: 0, w: 10.0, h: 5.625 });
|
||
slide.addText('SCADA IT/OT Transformation', {
|
||
x: 0.69, y: 1.04, w: 4.5, h: 1.13,
|
||
fontFace: 'Arial', fontSize: 24, bold: true,
|
||
color: ZB_TITLE_WHITE, valign: 'top',
|
||
});
|
||
slide.addText('3-Year Plan', {
|
||
x: 0.69, y: 2.21, w: 4.5, h: 0.38,
|
||
fontFace: 'Arial', fontSize: 14,
|
||
color: ZB_TITLE_WHITE,
|
||
});
|
||
slide.addText('AS OF 2026-04-30', {
|
||
x: 0.69, y: 2.59, w: 4.5, h: 0.30,
|
||
fontFace: 'Arial', fontSize: 9,
|
||
color: ZB_TITLE_WHITE, charSpacing: 1,
|
||
});
|
||
return slide;
|
||
}
|
||
|
||
function addBrandChrome(slide, pageNumber) {
|
||
slide.addImage({
|
||
path: BOTTOM_BORDER,
|
||
x: 0.01, y: 5.07, w: 9.99, h: 0.555,
|
||
});
|
||
slide.addText(CONFIDENTIAL_TEXT, {
|
||
x: 0.22, y: 4.91, w: 6.0, h: 0.15,
|
||
fontFace: 'Arial', fontSize: 6, color: CONFIDENTIAL_GRAY,
|
||
});
|
||
slide.addText(String(pageNumber), {
|
||
x: 9.55, y: 4.91, w: 0.35, h: 0.15,
|
||
fontFace: 'Arial', fontSize: 8, color: ZB_NAVY, align: 'right',
|
||
});
|
||
}
|
||
|
||
function addRoadmapTable(pres, slide) {
|
||
const headerOpts = { bold: true, color: WHITE, fill: { color: NAVY }, fontSize: 10, fontFace: 'Arial', align: 'center', valign: 'middle' };
|
||
const wsOpts = { bold: true, fontSize: 9, fontFace: 'Arial', color: NAVY, fill: { color: ROW_BG_ALT }, valign: 'middle' };
|
||
const cellOpts = { fontSize: 8, fontFace: 'Arial', color: SLATE, fill: { color: ROW_BG }, valign: 'middle' };
|
||
|
||
const rows = [
|
||
[
|
||
{ text: 'Workstream', options: headerOpts },
|
||
{ text: 'Year 1 — Foundation', options: headerOpts },
|
||
{ text: 'Year 2 — Scale', options: headerOpts },
|
||
{ text: 'Year 3 — Completion', options: headerOpts },
|
||
],
|
||
[
|
||
{ text: 'OtOpcUa', options: wsOpts },
|
||
{ text: 'Evolve LmxOpcUa; deploy to every site; tier-1 cutover begins; UNS hierarchy walk', options: cellOpts },
|
||
{ text: 'Complete tier 1; begin tier 2 (Ignition); long-tail drivers on demand', options: cellOpts },
|
||
{ text: 'Complete tier 2; execute tier 3 (System Platform IO) with compliance validation', options: cellOpts },
|
||
],
|
||
[
|
||
{ text: 'Redpanda EventHub', options: wsOpts },
|
||
{ text: 'Central cluster in SB; schema registry; canonical model v1 published (FANUC CNC pilot)', options: cellOpts },
|
||
{ text: 'Expand topic coverage; WAN-outage replay drill; iterate canonical model', options: cellOpts },
|
||
{ text: 'Steady state; canonical model mature; alerting + runbooks hardened', options: cellOpts },
|
||
],
|
||
[
|
||
{ text: 'SnowBridge', options: wsOpts },
|
||
{ text: 'Custom build in .NET; first adapter (Historian SQL); first curated tables aligned to canonical model', options: cellOpts },
|
||
{ text: 'Add Redpanda adapter; operator UI + approval workflow; canonical-state OEE model; first "not possible before" use case in dev', options: cellOpts },
|
||
{ text: 'All source adapters live; hardening. "Not possible before" use case in production — pillar 2 passes', options: cellOpts },
|
||
],
|
||
[
|
||
{ text: 'ScadaBridge Extensions', options: wsOpts },
|
||
{ text: 'Deadband / exception-based publishing; EventHub producer with store-and-forward', options: cellOpts },
|
||
{ text: 'Roll to all integrated sites; tune deadband from observed Snowflake cost', options: cellOpts },
|
||
{ text: 'Steady state; residual cleanup for onboarding / retirement tails', options: cellOpts },
|
||
],
|
||
[
|
||
{ text: 'Site Onboarding', options: wsOpts },
|
||
{ text: 'No new sites; define lightweight onboarding pattern for smaller sites', options: cellOpts },
|
||
{ text: 'Pilot one smaller site; scale to additional smaller sites', options: cellOpts },
|
||
{ text: 'Complete onboarding of all remaining smaller sites — pillar 1 passes', options: cellOpts },
|
||
],
|
||
[
|
||
{ text: 'Legacy Retirement', options: wsOpts },
|
||
{ text: 'Inventory populated (3 rows); retire first integration as pattern-proving exercise', options: cellOpts },
|
||
{ text: 'Bulk migration against inventory; quarterly burn-down tracking', options: cellOpts },
|
||
{ text: 'Inventory reaches zero — pillar 3 passes', options: cellOpts },
|
||
],
|
||
];
|
||
|
||
slide.addTable(rows, {
|
||
x: 0.42,
|
||
y: 1.2,
|
||
w: 9.2,
|
||
colW: [1.5, 2.566, 2.567, 2.567],
|
||
rowH: 0.55,
|
||
border: { pt: 0.5, color: SILVER },
|
||
});
|
||
}
|
||
|
||
main().catch((err) => { console.error(err); process.exit(1); });
|