From 459041753632470c22040ad048514323d608147a Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Wed, 15 Apr 2026 06:49:41 +0530 Subject: [PATCH] docs: weekly status + PPT for Apr 6-11 + Ramaiah slots seed script Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/generate-pptx-apr06-11.cjs | 612 +++++++++++++++++++++++++++++++ docs/weekly-status-apr06-11.md | 162 ++++++++ docs/weekly-update-apr06-11.pptx | Bin 0 -> 45320 bytes scripts/seed-ramaiah-slots.ts | 114 ++++++ 4 files changed, 888 insertions(+) create mode 100644 docs/generate-pptx-apr06-11.cjs create mode 100644 docs/weekly-status-apr06-11.md create mode 100644 docs/weekly-update-apr06-11.pptx create mode 100644 scripts/seed-ramaiah-slots.ts 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 0000000000000000000000000000000000000000..404478a8f6fa2d9b60d7b9bf70a1312ba8c9c187 GIT binary patch literal 45320 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 literal 0 HcmV?d00001 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); });