diff --git a/docs/generate-pptx-apr06-11.cjs b/docs/generate-pptx-apr06-11.cjs new file mode 100644 index 0000000..cdf8e72 --- /dev/null +++ b/docs/generate-pptx-apr06-11.cjs @@ -0,0 +1,612 @@ +/** + * Helix Engage — Weekly Update (Apr 6–11, 2026) + * "Clinical Precision" design — dark/light alternating, geometric, executive healthcare + */ +const PptxGenJS = require("pptxgenjs"); + +// ── Design System ─────────────────────────────────────────────── +const P = { + // Dark palette (hero slides) + navyDeep: "0F172A", // slate-900 + navyMid: "1E293B", // slate-800 + navyLight: "334155", // slate-700 + + // Light palette (content slides) + white: "FFFFFF", + snow: "F8FAFC", // slate-50 + mist: "F1F5F9", // slate-100 + silver: "E2E8F0", // slate-200 + + // Text + inkDark: "0F172A", + inkMid: "475569", // slate-600 + inkLight: "94A3B8", // slate-400 + inkOnDark: "F1F5F9", + inkMuted: "64748B", // slate-500 + + // Accents — healthcare-inspired + teal: "0D9488", // primary brand + tealLight: "14B8A6", + tealPale: "CCFBF1", // teal-100 + blue: "0284C7", // sky-600 + blueLight: "38BDF8", + indigo: "4F46E5", + amber: "D97706", + rose: "E11D48", + emerald: "059669", + violet: "7C3AED", +}; + +const F = "Calibri"; // Clean, universally available +const FB = "Calibri Light"; + +// ── Helpers ───────────────────────────────────────────────────── +function sn(s, n) { + s.addText(`${n}`, { + x: 9.3, y: 5.15, w: 0.5, h: 0.3, + fontSize: 8, color: P.inkLight, fontFace: FB, align: "right", + }); +} + +function darkSlide(pptx) { + const s = pptx.addSlide(); + s.background = { color: P.navyDeep }; + return s; +} + +function lightSlide(pptx) { + const s = pptx.addSlide(); + s.background = { color: P.white }; + return s; +} + +// Thin teal accent line at top +function topLine(s, color) { + s.addShape("rect", { x: 0, y: 0, w: 10, h: 0.04, fill: { color: color || P.teal } }); +} + +// Section label pill +function pill(s, text, color, x, y) { + const w = text.length * 0.075 + 0.5; + s.addShape("roundRect", { + x, y, w, h: 0.26, + fill: { color, transparency: 85 }, + rectRadius: 0.13, + }); + s.addText(text.toUpperCase(), { + x, y, w, h: 0.26, + fontSize: 7, fontFace: F, bold: true, color, + align: "center", valign: "middle", + }); +} + +// Metric block (for dark slides) +function metric(s, { x, y, value, label, color, w = 2.0 }) { + // Subtle card + s.addShape("roundRect", { + x, y, w, h: 1.4, + fill: { color: P.navyMid }, + line: { color: P.navyLight, width: 0.5 }, + rectRadius: 0.08, + }); + // Accent top bar + s.addShape("rect", { x: x + 0.15, y: y + 0.06, w: w - 0.3, h: 0.025, fill: { color } }); + // Value + s.addText(value, { + x, y: y + 0.15, w, h: 0.75, + fontSize: 38, fontFace: F, bold: true, color, + align: "center", valign: "middle", + }); + // Label + s.addText(label, { + x, y: y + 0.9, w, h: 0.35, + fontSize: 9, fontFace: FB, color: P.inkLight, + align: "center", valign: "top", + }); +} + +// Content card (for light slides) +function card(s, { x, y, w, h, title, accent, items }) { + // Card with left accent border + s.addShape("roundRect", { + x, y, w, h, + fill: { color: P.snow }, + line: { color: P.silver, width: 0.5 }, + rectRadius: 0.06, + }); + // Left accent bar + s.addShape("rect", { x, y: y + 0.1, w: 0.035, h: h - 0.2, fill: { color: accent } }); + // Title + s.addText(title, { + x: x + 0.25, y: y + 0.08, w: w - 0.4, h: 0.32, + fontSize: 10.5, fontFace: F, bold: true, color: accent, + }); + // Items + if (items?.length) { + s.addText( + items.map(t => ({ + text: t, + options: { + fontSize: 8.5, fontFace: FB, color: P.inkMid, + bullet: { code: "2022" }, // bullet dot + paraSpaceAfter: 3, breakLine: true, + }, + })), + { x: x + 0.25, y: y + 0.4, w: w - 0.5, h: h - 0.5, valign: "top", lineSpacingMultiple: 1.15 } + ); + } +} + +// Section heading for light slides +function sectionHead(s, title, subtitle) { + s.addText(title, { + x: 0.6, y: 0.35, w: 8, h: 0.45, + fontSize: 22, fontFace: F, bold: true, color: P.inkDark, + }); + if (subtitle) { + s.addText(subtitle, { + x: 0.6, y: 0.78, w: 8, h: 0.3, + fontSize: 10, fontFace: FB, color: P.inkMuted, + }); + } +} + +// ═════════════════════════════════════════════════════════════════ +async function build() { + const pptx = new PptxGenJS(); + pptx.layout = "LAYOUT_16x9"; + pptx.author = "Satya Suman Sari"; + pptx.company = "FortyTwo Platform"; + pptx.title = "Helix Engage — Weekly Update (Apr 6–11, 2026)"; + + // ─── SLIDE 1: Title (Dark) ──────────────────────────────────── + { + const s = darkSlide(pptx); + topLine(s, P.teal); + + // Geometric accent — vertical teal line + s.addShape("rect", { x: 0.6, y: 1.2, w: 0.035, h: 2.8, fill: { color: P.teal } }); + + pill(s, "Weekly Status", P.tealLight, 0.85, 1.3); + + s.addText("Helix Engage", { + x: 0.85, y: 1.7, w: 7, h: 0.9, + fontSize: 42, fontFace: F, bold: true, color: P.white, + }); + + s.addText("Engineering Progress Report", { + x: 0.85, y: 2.5, w: 7, h: 0.4, + fontSize: 16, fontFace: FB, color: P.inkLight, + }); + + // Date block + s.addShape("rect", { x: 0.85, y: 3.2, w: 2.2, h: 0.04, fill: { color: P.teal, transparency: 50 } }); + s.addText("April 6 – 11, 2026", { + x: 0.85, y: 3.35, w: 3, h: 0.3, + fontSize: 11, fontFace: F, bold: true, color: P.tealLight, + }); + + s.addText("Satya Suman Sari | FortyTwo Platform", { + x: 0.85, y: 4.8, w: 5, h: 0.25, + fontSize: 8, fontFace: FB, color: P.inkLight, + }); + sn(s, 1); + } + + // ─── SLIDE 2: At a Glance (Dark) ───────────────────────────── + { + const s = darkSlide(pptx); + topLine(s, P.teal); + + pill(s, "Overview", P.tealLight, 0.5, 0.3); + s.addText("Week at a Glance", { + x: 0.5, y: 0.6, w: 5, h: 0.45, + fontSize: 22, fontFace: F, bold: true, color: P.white, + }); + + metric(s, { x: 0.5, y: 1.25, value: "57", label: "Commits Shipped", color: P.blueLight, w: 2.05 }); + metric(s, { x: 2.7, y: 1.25, value: "9", label: "Defects Resolved", color: P.rose, w: 2.05 }); + metric(s, { x: 4.9, y: 1.25, value: "40", label: "E2E Tests Passing", color: P.emerald, w: 2.05 }); + metric(s, { x: 7.1, y: 1.25, value: "17", label: "Docker Containers", color: P.violet, w: 2.05 }); + + // Key highlights + const highlights = [ + "Multi-tenant EC2 architecture deployed — Ramaiah + Global on single instance", + "Woodpecker CI/CD pipeline operational with Teams notifications", + "Cross-tenant security vulnerability identified and patched", + "Complete documentation: architecture, runbook, CI/CD guide", + ]; + s.addText( + highlights.map(h => ({ + text: h, + options: { + fontSize: 10, fontFace: FB, color: P.inkOnDark, + bullet: { code: "25B8" }, paraSpaceAfter: 6, breakLine: true, + }, + })), + { x: 0.6, y: 2.9, w: 8.5, h: 2.0, valign: "top", lineSpacingMultiple: 1.2 } + ); + + sn(s, 2); + } + + // ─── SLIDE 3: Defect Fixes (Light) ──────────────────────────── + { + const s = lightSlide(pptx); + topLine(s, P.rose); + sectionHead(s, "Defect Resolution", "9 of 17 triaged bugs fixed and deployed this week"); + + const bugs = [ + ["#527", "Appointment creation overwrites patient details"], + ["#529", "Break/Training status doesn't block outbound calls"], + ["#531", "Agent can log out during an active call"], + ["#533", "Redundant Call History page header"], + ["#534", "Redundant Patients page header"], + ["#536", "My Performance displays wrong agent data"], + ["#538", "Supervisor dashboard metrics incorrect"], + ["#540", "Ghost calls visible for logged-out agents"], + ["#547", "SLA priority rules not reflected in worklist"], + ]; + + const rows = [ + [ + { text: "ID", options: { bold: true, color: P.white, fill: { color: P.navyMid }, fontSize: 8.5, fontFace: F } }, + { text: "Description", options: { bold: true, color: P.white, fill: { color: P.navyMid }, fontSize: 8.5, fontFace: F } }, + { text: "Status", options: { bold: true, color: P.white, fill: { color: P.navyMid }, fontSize: 8.5, fontFace: F } }, + ], + ...bugs.map(([id, desc], i) => [ + { text: id, options: { fontSize: 8.5, fontFace: F, color: P.inkMid, fill: { color: i % 2 === 0 ? P.snow : P.white } } }, + { text: desc, options: { fontSize: 8.5, fontFace: FB, color: P.inkMid, fill: { color: i % 2 === 0 ? P.snow : P.white } } }, + { text: "Resolved", options: { fontSize: 8.5, fontFace: F, bold: true, color: P.emerald, fill: { color: i % 2 === 0 ? P.snow : P.white } } }, + ]), + ]; + + s.addTable(rows, { + x: 0.5, y: 1.2, w: 9.0, + border: { type: "solid", pt: 0.3, color: P.silver }, + colW: [0.7, 6.6, 1.7], rowH: 0.36, + }); + + s.addText("Deferred by product: #516 recordings | #517 AI transcription | #519 supervisor calling | #539 real-time missed calls | #541 whisper/barge", { + x: 0.5, y: 4.9, w: 9, h: 0.3, + fontSize: 7.5, fontFace: FB, color: P.inkLight, italic: true, + }); + sn(s, 3); + } + + // ─── SLIDE 4: Security Fix (Dark) ──────────────────────────── + { + const s = darkSlide(pptx); + topLine(s, P.rose); + + pill(s, "Security", P.rose, 0.5, 0.3); + s.addText("Cross-Tenant Isolation Vulnerability", { + x: 0.5, y: 0.6, w: 9, h: 0.45, + fontSize: 22, fontFace: F, bold: true, color: P.white, + }); + s.addText("Discovered and patched within the same sprint", { + x: 0.5, y: 1.0, w: 9, h: 0.3, + fontSize: 10, fontFace: FB, color: P.inkLight, + }); + + // Problem + s.addShape("roundRect", { + x: 0.4, y: 1.5, w: 4.4, h: 2.6, + fill: { color: P.navyMid }, line: { color: P.navyLight, width: 0.5 }, rectRadius: 0.06, + }); + s.addShape("rect", { x: 0.4, y: 1.5, w: 0.035, h: 2.6, fill: { color: P.rose } }); + s.addText("Impact", { + x: 0.65, y: 1.55, w: 3, h: 0.3, + fontSize: 11, fontFace: F, bold: true, color: P.rose, + }); + s.addText( + [ + "Shared OZONETEL_AGENT_ID env var across sidecars", + "6 endpoints used silent fallback to wrong agent", + "Ramaiah operations could modify Global's session", + "Agent state, disposition, dial, metrics all affected", + "No error or warning — completely silent", + ].map(t => ({ + text: t, options: { fontSize: 8.5, fontFace: FB, color: P.inkOnDark, bullet: { code: "25B8" }, paraSpaceAfter: 4, breakLine: true }, + })), + { x: 0.65, y: 1.9, w: 3.9, h: 2.0, valign: "top" } + ); + + // Resolution + s.addShape("roundRect", { + x: 5.1, y: 1.5, w: 4.5, h: 2.6, + fill: { color: P.navyMid }, line: { color: P.navyLight, width: 0.5 }, rectRadius: 0.06, + }); + s.addShape("rect", { x: 5.1, y: 1.5, w: 0.035, h: 2.6, fill: { color: P.emerald } }); + s.addText("Resolution", { + x: 5.35, y: 1.55, w: 3, h: 0.3, + fontSize: 11, fontFace: F, bold: true, color: P.emerald, + }); + s.addText( + [ + "Removed all defaultAgentId fallbacks", + "All 6 endpoints now require agentId (400 if absent)", + "Frontend sends agentId from localStorage", + "OZONETEL_AGENT_ID removed from config entirely", + "Verified with 40 E2E tests — zero regressions", + ].map(t => ({ + text: t, options: { fontSize: 8.5, fontFace: FB, color: P.inkOnDark, bullet: { code: "25B8" }, paraSpaceAfter: 4, breakLine: true }, + })), + { x: 5.35, y: 1.9, w: 4.0, h: 2.0, valign: "top" } + ); + + // Clean layers footer + s.addText("Unaffected layers: Login (DB lookup) | Telephony dispatcher (event payload) | Sidecar registration (GraphQL) | Supervisor (webhook events)", { + x: 0.5, y: 4.4, w: 9, h: 0.3, + fontSize: 7.5, fontFace: FB, color: P.inkLight, + }); + sn(s, 4); + } + + // ─── SLIDE 5: EC2 Architecture (Light) ──────────────────────── + { + const s = lightSlide(pptx); + topLine(s, P.blue); + sectionHead(s, "AWS EC2 Multi-Tenant Architecture", "Single instance, strict tenant isolation, host-routed Caddy"); + + card(s, { + x: 0.4, y: 1.2, w: 4.4, h: 2.0, + title: "Shared Platform Layer", accent: P.blue, + items: [ + "NestJS server — multi-tenant by Origin header", + "PostgreSQL 16 with workspace-per-schema", + "BullMQ worker, ClickHouse analytics, Redpanda events", + "MinIO S3-compatible object storage", + ], + }); + + card(s, { + x: 5.1, y: 1.2, w: 4.5, h: 2.0, + title: "Isolated Sidecar Layer", accent: P.amber, + items: [ + "Per-hospital: sidecar + Redis + data volume", + "Caddy host-routes — no catchall, no cross-tenant", + "ramaiah.engage.healix360.net \u2192 sidecar-ramaiah", + "global.engage.healix360.net \u2192 sidecar-global", + ], + }); + + card(s, { + x: 0.4, y: 3.4, w: 4.4, h: 1.7, + title: "Telephony Dispatcher", accent: P.teal, + items: [ + "Routes Ozonetel events by agentId via Redis lookup", + "Sidecars self-register on boot with heartbeat", + "Zero config when onboarding new hospitals", + ], + }); + + card(s, { + x: 5.1, y: 3.4, w: 4.5, h: 1.7, + title: "Live Endpoints", accent: P.indigo, + items: [ + "ramaiah.engage / global.engage — Hospital UIs", + "telephony.engage — Event dispatcher", + "operations — CI/CD dashboard", + "git — Gitea forge (mirrors Azure DevOps)", + ], + }); + sn(s, 5); + } + + // ─── SLIDE 6: E2E Tests (Dark) ──────────────────────────────── + { + const s = darkSlide(pptx); + topLine(s, P.emerald); + + pill(s, "Quality Assurance", P.emerald, 0.5, 0.3); + s.addText("40 Automated E2E Tests", { + x: 0.5, y: 0.6, w: 9, h: 0.45, + fontSize: 22, fontFace: F, bold: true, color: P.white, + }); + s.addText("Playwright smoke tests covering every page across both hospitals", { + x: 0.5, y: 1.0, w: 9, h: 0.3, + fontSize: 10, fontFace: FB, color: P.inkLight, + }); + + // Ramaiah + s.addShape("roundRect", { + x: 0.4, y: 1.5, w: 4.4, h: 2.4, + fill: { color: P.navyMid }, line: { color: P.navyLight, width: 0.5 }, rectRadius: 0.06, + }); + s.addShape("rect", { x: 0.4, y: 1.5, w: 0.035, h: 2.4, fill: { color: P.amber } }); + s.addText("Ramaiah Hospitals — 27 tests", { + x: 0.65, y: 1.55, w: 4, h: 0.3, + fontSize: 10.5, fontFace: F, bold: true, color: P.amber, + }); + s.addText( + [ + "Login flow: branding, credentials, auth guard (4)", + "CC Agent: call desk, history, patients, appointments, performance, sidebar, sign-out (10)", + "Supervisor: dashboard, team perf, live monitor, all data pages, settings (12)", + "Auth setup with auto session unlock (1)", + ].map(t => ({ text: t, options: { fontSize: 8.5, fontFace: FB, color: P.inkOnDark, bullet: { code: "25B8" }, paraSpaceAfter: 5, breakLine: true } })), + { x: 0.65, y: 1.9, w: 3.9, h: 1.8, valign: "top" } + ); + + // Global + s.addShape("roundRect", { + x: 5.1, y: 1.5, w: 4.5, h: 2.4, + fill: { color: P.navyMid }, line: { color: P.navyLight, width: 0.5 }, rectRadius: 0.06, + }); + s.addShape("rect", { x: 5.1, y: 1.5, w: 0.035, h: 2.4, fill: { color: P.blueLight } }); + s.addText("Global Hospital — 13 tests", { + x: 5.35, y: 1.55, w: 4, h: 0.3, + fontSize: 10.5, fontFace: F, bold: true, color: P.blueLight, + }); + s.addText( + [ + "CC Agent: landing, history, patients, appointments, performance, sidebar, sign-out (7)", + "Supervisor: landing, patients, appointments, campaigns, settings (5)", + "Auth setup with auto session unlock (1)", + ].map(t => ({ text: t, options: { fontSize: 8.5, fontFace: FB, color: P.inkOnDark, bullet: { code: "25B8" }, paraSpaceAfter: 5, breakLine: true } })), + { x: 5.35, y: 1.9, w: 4.0, h: 1.8, valign: "top" } + ); + + // Self-healing footer + s.addShape("roundRect", { + x: 0.4, y: 4.15, w: 9.2, h: 0.85, + fill: { color: P.navyMid }, line: { color: P.navyLight, width: 0.5 }, rectRadius: 0.06, + }); + s.addText("Self-Healing", { + x: 0.65, y: 4.2, w: 2, h: 0.25, + fontSize: 9, fontFace: F, bold: true, color: P.emerald, + }); + s.addText("Auto-clears session locks before login | Completes sign-out after tests | Runs against live EC2, not mocked | ~6 min on Woodpecker CI", { + x: 0.65, y: 4.5, w: 8.5, h: 0.3, + fontSize: 8, fontFace: FB, color: P.inkLight, + }); + sn(s, 6); + } + + // ─── SLIDE 7: CI/CD (Light) ─────────────────────────────────── + { + const s = lightSlide(pptx); + topLine(s, P.indigo); + sectionHead(s, "CI/CD Pipeline", "Automated testing, report publishing, and team notifications"); + + // Flow bar + s.addShape("roundRect", { + x: 0.5, y: 1.15, w: 9.0, h: 0.4, + fill: { color: P.mist }, line: { color: P.silver, width: 0.5 }, rectRadius: 0.06, + }); + s.addText("Azure DevOps \u2192 Gitea Mirror \u2192 Woodpecker Pipeline \u2192 MinIO Reports \u2192 Teams Alert", { + x: 0.5, y: 1.15, w: 9.0, h: 0.4, + fontSize: 9.5, fontFace: F, bold: true, color: P.indigo, align: "center", valign: "middle", + }); + + card(s, { + x: 0.4, y: 1.75, w: 4.4, h: 1.7, + title: "Frontend Pipeline", accent: P.blue, + items: [ + "TypeScript typecheck (yarn tsc --noEmit)", + "40 Playwright E2E tests against live EC2", + "HTML report uploaded to MinIO (S3 plugin)", + "Teams Adaptive Card with report link", + ], + }); + + card(s, { + x: 5.1, y: 1.75, w: 4.5, h: 1.7, + title: "Sidecar Pipeline", accent: P.violet, + items: [ + "Jest unit tests (npm ci + jest --ci)", + "Teams notification on pass or fail", + "Triggered on push or manual run", + ], + }); + + card(s, { + x: 0.4, y: 3.65, w: 9.2, h: 1.4, + title: "Operations Dashboard", accent: P.teal, + items: [ + "operations.healix360.net — Woodpecker CI with full build history and logs", + "operations.healix360.net/reports/{run}/ — Playwright HTML reports with screenshots (basic auth protected)", + "git.healix360.net — Gitea forge mirroring Azure DevOps every 15 minutes", + "Teams 'Deployment updates' channel receives Adaptive Cards with pass/fail count and report link", + ], + }); + sn(s, 7); + } + + // ─── SLIDE 8: Timeline (Light) ──────────────────────────────── + { + const s = lightSlide(pptx); + topLine(s, P.teal); + sectionHead(s, "Development Timeline"); + + const timeline = [ + { date: "Apr 6 Sun", title: "Onboarding Wizard", desc: "6-phase setup wizard, widget config, telephony/AI CRUD, team invite, clinic/doctor management", color: P.blue }, + { date: "Apr 7 Mon", title: "SIP & ACW Fixes", desc: "3-layer ACW protection, SIP disconnect guard, dispose agentId, setup wizard polish", color: P.teal }, + { date: "Apr 8 Tue", title: "Master Data", desc: "Dynamic clinic/doctor fetching, appointment form overhaul, Ramaiah 195 doctor seed", color: P.amber }, + { date: "Apr 9 Wed", title: "EC2 Deployment", desc: "Multi-tenant architecture, telephony dispatcher, Caddy host routing, 14 containers", color: P.indigo }, + { date: "Apr 10 Thu", title: "Defect Sprint", desc: "9 bugs fixed, 40 E2E tests, architecture docs, runbook, cross-tenant discovery", color: P.rose }, + { date: "Apr 11 Fri", title: "CI/CD Pipeline", desc: "Woodpecker + Gitea + MinIO, Teams notifications, defaultAgentId security patch", color: P.emerald }, + ]; + + // Vertical line + s.addShape("rect", { x: 1.25, y: 1.2, w: 0.02, h: 3.9, fill: { color: P.silver } }); + + timeline.forEach((e, i) => { + const y = 1.2 + i * 0.65; + // Dot + s.addShape("ellipse", { + x: 1.18, y: y + 0.06, w: 0.16, h: 0.16, + fill: { color: e.color }, line: { color: P.white, width: 2 }, + }); + // Date + s.addText(e.date, { + x: 1.55, y, w: 1.2, h: 0.22, + fontSize: 7.5, fontFace: F, bold: true, color: e.color, + }); + // Title + s.addText(e.title, { + x: 2.8, y, w: 1.8, h: 0.22, + fontSize: 9.5, fontFace: F, bold: true, color: P.inkDark, + }); + // Desc + s.addText(e.desc, { + x: 4.7, y, w: 4.8, h: 0.55, + fontSize: 8, fontFace: FB, color: P.inkMid, valign: "top", + }); + }); + sn(s, 8); + } + + // ─── SLIDE 9: Closing (Dark) ────────────────────────────────── + { + const s = darkSlide(pptx); + topLine(s, P.teal); + + s.addShape("rect", { x: 0.6, y: 1.6, w: 0.035, h: 1.8, fill: { color: P.teal } }); + + s.addText("57 commits across 3 repositories", { + x: 0.85, y: 1.6, w: 8, h: 0.6, + fontSize: 28, fontFace: F, bold: true, color: P.white, + }); + + s.addText("From single-tenant VPS to multi-tenant EC2 with automated CI/CD,\n40 end-to-end tests, and a fully integrated operations dashboard.", { + x: 0.85, y: 2.3, w: 7, h: 0.7, + fontSize: 12, fontFace: FB, color: P.inkLight, lineSpacingMultiple: 1.4, + }); + + // Achievement pills + const items = [ + { text: "Multi-Tenant EC2", color: P.blue }, + { text: "40 E2E Tests", color: P.emerald }, + { text: "CI/CD Pipeline", color: P.indigo }, + { text: "9 Bugs Fixed", color: P.rose }, + { text: "Teams Alerts", color: P.violet }, + ]; + items.forEach((a, i) => { + const x = 0.85 + i * 1.7; + s.addShape("roundRect", { + x, y: 3.4, w: 1.5, h: 0.32, + fill: { color: P.navyMid }, + line: { color: a.color, width: 1 }, + rectRadius: 0.16, + }); + s.addText(a.text, { + x, y: 3.4, w: 1.5, h: 0.32, + fontSize: 8, fontFace: F, bold: true, color: a.color, + align: "center", valign: "middle", + }); + }); + + s.addText("Satya Suman Sari | FortyTwo Platform", { + x: 0.85, y: 4.8, w: 5, h: 0.25, + fontSize: 8, fontFace: FB, color: P.inkLight, + }); + sn(s, 9); + } + + await pptx.writeFile({ fileName: "docs/weekly-update-apr06-11.pptx" }); + console.log("Generated: docs/weekly-update-apr06-11.pptx"); +} + +build().catch(err => { console.error(err); process.exit(1); }); diff --git a/docs/weekly-status-apr06-11.md b/docs/weekly-status-apr06-11.md new file mode 100644 index 0000000..3a85e14 --- /dev/null +++ b/docs/weekly-status-apr06-11.md @@ -0,0 +1,162 @@ +# Helix Engage — Weekly Status Update + +**Period:** April 6 – April 11, 2026 +**Team:** Engineering + +--- + +## Executive Summary + +Major infrastructure milestone — Helix Engage is now running on AWS EC2 with multi-tenant architecture supporting both Ramaiah Hospitals and Global Hospital on a single instance. A full CI/CD pipeline with automated E2E testing and Teams notifications is operational. 17 defects from QA were triaged, 8 fixed and deployed, and a cross-tenant security vulnerability in the telephony layer was discovered and patched. + +--- + +## 1. AWS EC2 Deployment (Multi-Tenant) + +**Status: Live** + +Migrated from single-tenant VPS to multi-tenant EC2 architecture: + +- **Instance:** m6i.xlarge, Mumbai (ap-south-1), 15GB RAM +- **14 Docker containers** running: platform, 2 sidecars, telephony dispatcher, 4 Redis instances, Caddy, PostgreSQL, ClickHouse, Redpanda, MinIO +- **Strict tenant isolation:** each hospital has its own sidecar container, Redis instance, and data volume +- **Host-routed Caddy:** cross-tenant webhook routing is physically impossible + +**URLs deployed:** +- ramaiah.engage.healix360.net (Ramaiah Hospitals) +- global.engage.healix360.net (Global Hospital) +- ramaiah.app.healix360.net / global.app.healix360.net (Platform) +- telephony.engage.healix360.net (Event dispatcher) +- operations.healix360.net (CI/CD dashboard) +- git.healix360.net (Git forge) + +--- + +## 2. Telephony Event Dispatcher + +**Status: Live** + +Built a NestJS service that routes Ozonetel agent/call events to the correct hospital's sidecar: + +- Ozonetel event subscriptions are **account-level** (not per-campaign) — one URL for all agents +- Dispatcher receives all events, looks up `agentId` in Redis, forwards to the correct sidecar +- Sidecars self-register on boot with their agent list; heartbeat every 30s, TTL 90s +- No manual configuration needed when adding new hospitals + +--- + +## 3. Cross-Tenant Security Fix (defaultAgentId) + +**Status: Fixed and deployed** + +Discovered that 6 sidecar endpoints used a hardcoded `OZONETEL_AGENT_ID` env var as a fallback when `agentId` wasn't provided by the frontend. In a multi-tenant setup, this caused Ramaiah sidecar operations to silently affect Global Hospital's agent. + +**Impact:** Agent state changes, call disposition, outbound dialing, performance metrics, and maintenance commands could operate on the wrong hospital's agent with no error or warning. + +**Fix:** +- Removed `defaultAgentId` getter and all hardcoded fallbacks (`agent3`, `Test123$`, `521814`) +- All 6 endpoints now require `agentId` from the caller (400 if missing) +- Frontend updated to send `agentId` from `localStorage.helix_agent_config` in all calls +- `OZONETEL_AGENT_ID` removed from env config entirely + +--- + +## 4. Defect Fixes (8 of 17) + +| Bug | Title | Status | +|-----|-------|--------| +| #527 | Appointment creation updates existing patient incorrectly | Fixed | +| #529 | Break/Training status doesn't block outbound calls | Fixed | +| #531 | Agent can log out during active call | Fixed | +| #533 | Redundant "Call History" header | Fixed | +| #534 | Redundant "Patients" header | Fixed | +| #536 | My Performance shows wrong agent's data | Fixed | +| #538 | Supervisor dashboard metrics incorrect | Fixed | +| #540 | Ghost calls visible for logged-out agents | Fixed | +| #547 | SLA rules not reflected in Call Desk | Fixed (config seeded) | + +**Deferred (by product):** #516 (recordings real-time), #517/#548 (AI transcription), #519 (supervisor call — needs SIP seat), #539 (missed calls real-time), #541 (whisper/barge/listen) + +--- + +## 5. E2E Test Suite (Playwright) + +**Status: 40 tests, all passing** + +Automated smoke tests covering every page for both hospitals: + +- **Login (4):** branding, invalid creds, supervisor login, auth guard +- **Ramaiah CC Agent (10):** call desk, call history, patients, appointments, my performance, sidebar, sign-out +- **Ramaiah Supervisor (12):** dashboard, team performance, live monitor, leads, patients, appointments, call log, recordings, missed calls, campaigns, settings, sidebar +- **Global CC Agent (7):** all pages + sign-out +- **Global Supervisor (5):** all pages + +Self-healing: auto-clears agent session locks before login, completes sign-out after tests. + +--- + +## 6. CI/CD Pipeline (Woodpecker + Gitea) + +**Status: Operational** + +End-to-end CI/CD on EC2: + +- **Gitea** mirrors Azure DevOps repos every 15 minutes +- **Woodpecker CI** triggers pipelines on push or manual run +- **Frontend pipeline:** TypeScript typecheck → 40 E2E tests → HTML report published to MinIO → Teams notification +- **Sidecar pipeline:** Jest unit tests → Teams notification +- **Reports:** Playwright HTML reports with screenshots at `operations.healix360.net/reports/{run}/index.html` +- **Teams notifications:** Adaptive Cards to "Deployment updates" channel with pass/fail summary + report link + +--- + +## 7. Documentation + +Three docs committed to the repo: + +- **architecture.md** — Multi-tenant topology with Mermaid diagram, telephony dispatcher, failure modes +- **developer-operations-runbook.md** — SSH access, accounts, deploy steps, Redis ops, DB access, troubleshooting +- **ci-cd-operations.md** — Gitea, Woodpecker, MinIO, Teams notification setup and troubleshooting + +--- + +## 8. Data Seeding + +- **Ramaiah:** 195 real doctors scraped from msrmh.com, clinics, visit slots, campaign data +- **Global:** CC agent accounts (rekha.cc, ganesh.cc), marketing (sanjay), supervisor (dr.ramesh) created with proper roles +- **Rules engine:** 6 priority scoring rules seeded (missed call, follow-up, campaign lead, 2nd/3rd attempt, spam deprioritize) +- **Seed script:** idempotent `mkMember`, cleanup phase before seeding, runs against any workspace via env vars + +--- + +## 9. Other Improvements + +- **SIP agent tracing:** Browser console logs `agent=ramaiahadmin ext=524435` on every SIP connect/disconnect/state change for multi-agent debugging +- **ACW 3-layer protection:** beforeunload warning → sendBeacon auto-dispose → server 30s timer +- **Maint endpoints:** `force-ready` and `unlock-agent` now accept `agentId` from body (was hardcoded) +- **Security group automation:** SSH IP auto-updated via AWS CLI when ISP changes + +--- + +## Metrics + +| Metric | Value | +|--------|-------| +| Commits (frontend) | 35 | +| Commits (sidecar) | 20 | +| Commits (SDK app) | 2 | +| Bugs fixed | 9 | +| E2E tests | 40 | +| Docker containers | 17 (14 app + 3 CI) | +| DNS records | 6 | +| Uptime | EC2 live since Apr 9 | + +--- + +## Next Week Priorities + +1. Merge `feature/omnichannel-widget` → `master` (frontend) +2. Frontend Docker image (stop rsync, bake into image) +3. Appointment date validation (no past dates, auto-tomorrow after hours) +4. Pre-built CI Docker image (skip `yarn install` on every run) +5. Deferred defects: #516, #539 (real-time updates) diff --git a/docs/weekly-update-apr06-11.pptx b/docs/weekly-update-apr06-11.pptx new file mode 100644 index 0000000..404478a Binary files /dev/null and b/docs/weekly-update-apr06-11.pptx differ diff --git a/scripts/seed-ramaiah-slots.ts b/scripts/seed-ramaiah-slots.ts new file mode 100644 index 0000000..7644937 --- /dev/null +++ b/scripts/seed-ramaiah-slots.ts @@ -0,0 +1,114 @@ +/** + * Seed DoctorVisitSlots for all Ramaiah doctors. + * Assigns default visiting hours based on department patterns. + * Run after seed-ramaiah.ts has populated doctors + clinic. + * + * Run: cd helix-engage && SEED_GQL=https://ramaiah.app.healix360.net/graphql SEED_SUB=ramaiah SEED_ORIGIN=https://ramaiah.app.healix360.net npx tsx scripts/seed-ramaiah-slots.ts + */ + +const GQL = process.env.SEED_GQL ?? 'http://localhost:4000/graphql'; +const SUB = process.env.SEED_SUB ?? 'ramaiah'; +const ORIGIN = process.env.SEED_ORIGIN ?? 'http://ramaiah.localhost:5080'; + +let token = ''; + +async function gql(query: string, variables?: any) { + const h: Record = { 'Content-Type': 'application/json', 'X-Workspace-Subdomain': SUB }; + if (token) h['Authorization'] = `Bearer ${token}`; + const r = await fetch(GQL, { method: 'POST', headers: h, body: JSON.stringify({ query, variables }) }); + const d: any = await r.json(); + if (d.errors) throw new Error(d.errors[0].message); + return d.data; +} + +async function auth() { + const d1 = await gql(`mutation { getLoginTokenFromCredentials(email: "dev@fortytwo.dev", password: "tim@apple.dev", origin: "${ORIGIN}") { loginToken { token } } }`); + const lt = d1.getLoginTokenFromCredentials.loginToken.token; + const d2 = await gql(`mutation { getAuthTokensFromLoginToken(loginToken: "${lt}", origin: "${ORIGIN}") { tokens { accessOrWorkspaceAgnosticToken { token } } } }`); + token = d2.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken.token; +} + +// Default schedule patterns by department type +const schedulePatterns: Record = { + // Surgical departments: morning OPD + surgery: { days: ['MONDAY', 'WEDNESDAY', 'FRIDAY', 'SATURDAY'], start: '09:00', end: '13:00' }, + // Medical departments: afternoon OPD + medicine: { days: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY'], start: '14:00', end: '17:00' }, + // High-traffic: full day Mon-Sat + fullDay: { days: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'], start: '09:00', end: '17:00' }, + // Emergency/Critical: all week + allWeek: { days: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'], start: '08:00', end: '20:00' }, + // Specialists: limited days + specialist: { days: ['TUESDAY', 'THURSDAY', 'SATURDAY'], start: '10:00', end: '14:00' }, +}; + +function getPattern(department: string): { days: string[]; start: string; end: string } { + const d = department.toLowerCase(); + if (d.includes('emergency') || d.includes('critical care')) return schedulePatterns.allWeek; + if (d.includes('general medicine') || d.includes('paediatrics') || d.includes('obstetrics')) return schedulePatterns.fullDay; + if (d.includes('surgery') || d.includes('ortho') || d.includes('neuro')) return schedulePatterns.surgery; + if (d.includes('cardiology') || d.includes('nephrology') || d.includes('oncology')) return schedulePatterns.medicine; + if (d.includes('dermatology') || d.includes('psychiatry') || d.includes('rheumatology') || d.includes('endocrinology')) return schedulePatterns.specialist; + // Default: Mon-Fri mornings + return { days: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY'], start: '09:00', end: '13:00' }; +} + +async function main() { + console.log('🕐 Seeding visit slots for Ramaiah doctors...\n'); + await auth(); + console.log('✅ Auth OK\n'); + + // Fetch all doctors + const docData = await gql(`{ doctors(first: 500) { edges { node { id name department } } } }`); + const doctors = docData.doctors.edges.map((e: any) => e.node); + console.log(`📋 Found ${doctors.length} doctors\n`); + + // Fetch clinic + const clinicData = await gql(`{ clinics(first: 1) { edges { node { id clinicName } } } }`); + const clinicId = clinicData.clinics.edges[0]?.node.id; + const clinicName = clinicData.clinics.edges[0]?.node.clinicName ?? 'Clinic'; + if (!clinicId) { console.error('No clinic found!'); process.exit(1); } + console.log(`🏥 Clinic: ${clinicName} (${clinicId})\n`); + + let created = 0; + let failed = 0; + + for (let i = 0; i < doctors.length; i++) { + if (i > 0 && i % 40 === 0) { + await auth(); + console.log(` (re-authed at ${i})`); + } + + const doc = doctors[i]; + const pattern = getPattern(doc.department ?? ''); + + for (const day of pattern.days) { + try { + await gql( + `mutation($data: DoctorVisitSlotCreateInput!) { createDoctorVisitSlot(data: $data) { id } }`, + { + data: { + name: `${doc.name} — ${day} ${pattern.start}–${pattern.end}`, + doctorId: doc.id, + clinicId, + dayOfWeek: day, + startTime: pattern.start, + endTime: pattern.end, + }, + }, + ); + created++; + } catch (err: any) { + failed++; + if (failed <= 5) console.error(` ✗ ${doc.name} ${day}: ${err.message?.slice(0, 60)}`); + } + } + + if ((i + 1) % 30 === 0) console.log(` ${i + 1}/${doctors.length} doctors processed (${created} slots)...`); + } + + console.log(`\n✅ ${created} visit slots created, ${failed} failed`); + console.log(` ${doctors.length} doctors × avg ${Math.round(created / doctors.length)} days each`); +} + +main().catch(e => { console.error('💥', e.message); process.exit(1); });