mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
docs: weekly status + PPT for Apr 6-11 + Ramaiah slots seed script
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
612
docs/generate-pptx-apr06-11.cjs
Normal file
612
docs/generate-pptx-apr06-11.cjs
Normal file
@@ -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); });
|
||||||
162
docs/weekly-status-apr06-11.md
Normal file
162
docs/weekly-status-apr06-11.md
Normal file
@@ -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)
|
||||||
BIN
docs/weekly-update-apr06-11.pptx
Normal file
BIN
docs/weekly-update-apr06-11.pptx
Normal file
Binary file not shown.
114
scripts/seed-ramaiah-slots.ts
Normal file
114
scripts/seed-ramaiah-slots.ts
Normal file
@@ -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<string, string> = { '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<string, { days: string[]; start: string; end: string }> = {
|
||||||
|
// 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); });
|
||||||
Reference in New Issue
Block a user