17 KiB
Supervisor Module Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the supervisor module with team performance dashboard (PP-5), live call monitor (PP-6), master data pages, and admin sidebar restructure.
Architecture: Frontend pages query platform GraphQL directly for entity data (calls, appointments, leads, agents). Sidecar provides Ozonetel-specific data (agent time breakdown, active calls via event subscription). No hardcoded/mock data anywhere.
Tech Stack: React + Tailwind + ECharts (frontend), NestJS sidecar (Ozonetel integration), Fortytwo platform GraphQL
Spec: docs/superpowers/specs/2026-03-24-supervisor-module.md
File Map
Frontend (helix-engage/src/)
| File | Action | Responsibility |
|---|---|---|
pages/team-performance.tsx |
Create | PP-5 full dashboard |
pages/live-monitor.tsx |
Create | PP-6 active call table |
pages/call-recordings.tsx |
Create | Calls with recordings master |
pages/missed-calls.tsx |
Create | Missed calls master (supervisor view) |
components/layout/sidebar.tsx |
Modify | Admin nav restructure |
main.tsx |
Modify | Add new routes |
Sidecar (helix-engage-server/src/)
| File | Action | Responsibility |
|---|---|---|
supervisor/supervisor.service.ts |
Create | Team perf aggregation + active call tracking |
supervisor/supervisor.controller.ts |
Create | REST endpoints |
supervisor/supervisor.module.ts |
Create | Module registration |
app.module.ts |
Modify | Import SupervisorModule |
Task 1: Admin Sidebar Nav + Routes
Files:
-
Modify:
helix-engage/src/components/layout/sidebar.tsx -
Modify:
helix-engage/src/main.tsx -
Step 1: Add new icon imports to sidebar
In sidebar.tsx, add to the FontAwesome imports:
import {
// existing imports...
faRadio,
faFileAudio,
faPhoneMissed,
faChartLine,
} from '@fortawesome/pro-duotone-svg-icons';
Add icon wrappers:
const IconRadio = faIcon(faRadio);
const IconFileAudio = faIcon(faFileAudio);
const IconPhoneMissed = faIcon(faPhoneMissed);
const IconChartLine = faIcon(faChartLine);
- Step 2: Restructure admin nav
Replace the admin nav section (currently has Overview + Management + Admin groups) with:
if (role === 'admin') {
return [
{ label: 'Supervisor', items: [
{ label: 'Dashboard', href: '/', icon: IconGrid2 },
{ label: 'Team Performance', href: '/team-performance', icon: IconChartLine },
{ label: 'Live Call Monitor', href: '/live-monitor', icon: IconRadio },
]},
{ label: 'Data & Reports', items: [
{ label: 'Lead Master', href: '/leads', icon: IconUsers },
{ label: 'Patient Master', href: '/patients', icon: IconHospitalUser },
{ label: 'Appointment Master', href: '/appointments', icon: IconCalendarCheck },
{ label: 'Call Log Master', href: '/call-history', icon: IconClockRewind },
{ label: 'Call Recordings', href: '/call-recordings', icon: IconFileAudio },
{ label: 'Missed Calls', href: '/missed-calls', icon: IconPhoneMissed },
]},
{ label: 'Admin', items: [
{ label: 'Settings', href: '/settings', icon: IconGear },
]},
];
}
- Step 3: Add routes in main.tsx
Import new page components (they'll be created in later tasks — use placeholder components for now):
import { TeamPerformancePage } from "@/pages/team-performance";
import { LiveMonitorPage } from "@/pages/live-monitor";
import { CallRecordingsPage } from "@/pages/call-recordings";
import { MissedCallsPage } from "@/pages/missed-calls";
Add routes:
<Route path="/team-performance" element={<TeamPerformancePage />} />
<Route path="/live-monitor" element={<LiveMonitorPage />} />
<Route path="/call-recordings" element={<CallRecordingsPage />} />
<Route path="/missed-calls" element={<MissedCallsPage />} />
- Step 4: Create placeholder pages
Create minimal placeholder files for each new page so the build doesn't fail:
// src/pages/team-performance.tsx
export const TeamPerformancePage = () => <div>Team Performance — coming soon</div>;
// src/pages/live-monitor.tsx
export const LiveMonitorPage = () => <div>Live Call Monitor — coming soon</div>;
// src/pages/call-recordings.tsx
export const CallRecordingsPage = () => <div>Call Recordings — coming soon</div>;
// src/pages/missed-calls.tsx
export const MissedCallsPage = () => <div>Missed Calls — coming soon</div>;
- Step 5: Verify build
cd helix-engage && npm run build
- Step 6: Commit
git add src/components/layout/sidebar.tsx src/main.tsx src/pages/team-performance.tsx src/pages/live-monitor.tsx src/pages/call-recordings.tsx src/pages/missed-calls.tsx
git commit -m "feat: admin sidebar restructure + placeholder pages for supervisor module"
Task 2: Call Recordings Page
Files:
-
Modify:
helix-engage/src/pages/call-recordings.tsx -
Step 1: Implement call recordings page
Query platform for calls with recordings. Reuse patterns from call-history.tsx.
// Query: calls where recording primaryLinkUrl is not empty
const QUERY = `{ calls(first: 100, filter: {
recording: { primaryLinkUrl: { neq: "" } }
}, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
id direction callStatus callerNumber { primaryPhoneNumber }
agentName startedAt durationSec disposition
recording { primaryLinkUrl primaryLinkLabel }
} } } }`;
Table columns: Agent, Caller (PhoneActionCell), Type (In/Out badge), Date, Duration, Disposition, Recording (play button).
Search by agent name or phone number. Date filter optional.
- Step 2: Verify build
cd helix-engage && npm run build
- Step 3: Commit
git add src/pages/call-recordings.tsx
git commit -m "feat: call recordings master page"
Task 3: Missed Calls Page (Supervisor View)
Files:
-
Modify:
helix-engage/src/pages/missed-calls.tsx -
Step 1: Implement missed calls page
Query platform for all missed calls — no agent filter (supervisor sees all).
const QUERY = `{ calls(first: 100, filter: {
callStatus: { eq: MISSED }
}, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
id callerNumber { primaryPhoneNumber } agentName
startedAt callsourcenumber callbackstatus missedcallcount callbackattemptedat
} } } }`;
Table columns: Caller (PhoneActionCell), Date/Time, Branch (callsourcenumber), Agent, Callback Status (badge), SLA (computed from startedAt).
Tabs: All | Pending (PENDING_CALLBACK) | Attempted (CALLBACK_ATTEMPTED) | Completed (CALLBACK_COMPLETED + WRONG_NUMBER).
Search by phone or agent.
- Step 2: Verify build
cd helix-engage && npm run build
- Step 3: Commit
git add src/pages/missed-calls.tsx
git commit -m "feat: missed calls master page for supervisors"
Task 4: Sidecar — Supervisor Module
Files:
-
Create:
helix-engage-server/src/supervisor/supervisor.service.ts -
Create:
helix-engage-server/src/supervisor/supervisor.controller.ts -
Create:
helix-engage-server/src/supervisor/supervisor.module.ts -
Modify:
helix-engage-server/src/app.module.ts -
Step 1: Create supervisor service
// supervisor.service.ts
// Two responsibilities:
// 1. Aggregate Ozonetel agent summary across all agents
// 2. Track active calls from Ozonetel real-time events
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service';
type ActiveCall = {
ucid: string;
agentId: string;
callerNumber: string;
callType: string;
startTime: string;
status: 'active' | 'on-hold';
};
@Injectable()
export class SupervisorService implements OnModuleInit {
private readonly logger = new Logger(SupervisorService.name);
private readonly activeCalls = new Map<string, ActiveCall>();
constructor(
private platform: PlatformGraphqlService,
private ozonetel: OzonetelAgentService,
private config: ConfigService,
) {}
async onModuleInit() {
// Subscribe to Ozonetel events (fire and forget)
// Will be implemented when webhook URL is configured
this.logger.log('Supervisor service initialized');
}
// Called by webhook when Ozonetel pushes call events
handleCallEvent(event: any) {
const { action, ucid, agent_id, caller_id, call_type, event_time } = event;
if (action === 'Answered' || action === 'Calling') {
this.activeCalls.set(ucid, {
ucid, agentId: agent_id, callerNumber: caller_id,
callType: call_type, startTime: event_time, status: 'active',
});
} else if (action === 'Disconnect') {
this.activeCalls.delete(ucid);
}
}
// Called by webhook when Ozonetel pushes agent events
handleAgentEvent(event: any) {
this.logger.log(`Agent event: ${event.agentId} → ${event.action}`);
}
getActiveCalls(): ActiveCall[] {
return Array.from(this.activeCalls.values());
}
// Aggregate time breakdown across all agents
async getTeamPerformance(date: string): Promise<any> {
// Get all agent IDs from platform
const agentData = await this.platform.query<any>(
`{ agents(first: 20) { edges { node {
id name ozonetelagentid npsscore maxidleminutes minnpsthreshold minconversionpercent
} } } }`,
);
const agents = agentData?.agents?.edges?.map((e: any) => e.node) ?? [];
// Fetch Ozonetel summary per agent
const summaries = await Promise.all(
agents.map(async (agent: any) => {
try {
const summary = await this.ozonetel.getAgentSummary(agent.ozonetelagentid, date);
return { ...agent, timeBreakdown: summary };
} catch {
return { ...agent, timeBreakdown: null };
}
}),
);
return { date, agents: summaries };
}
}
- Step 2: Create supervisor controller
// supervisor.controller.ts
import { Controller, Get, Post, Body, Query, Logger } from '@nestjs/common';
import { SupervisorService } from './supervisor.service';
@Controller('api/supervisor')
export class SupervisorController {
private readonly logger = new Logger(SupervisorController.name);
constructor(private readonly supervisor: SupervisorService) {}
@Get('active-calls')
getActiveCalls() {
return this.supervisor.getActiveCalls();
}
@Get('team-performance')
async getTeamPerformance(@Query('date') date?: string) {
const targetDate = date ?? new Date().toISOString().split('T')[0];
return this.supervisor.getTeamPerformance(targetDate);
}
@Post('call-event')
handleCallEvent(@Body() body: any) {
// Ozonetel pushes events here
const event = body.data ?? body;
this.logger.log(`Call event: ${event.action} ucid=${event.ucid} agent=${event.agent_id}`);
this.supervisor.handleCallEvent(event);
return { received: true };
}
@Post('agent-event')
handleAgentEvent(@Body() body: any) {
const event = body.data ?? body;
this.logger.log(`Agent event: ${event.action} agent=${event.agentId}`);
this.supervisor.handleAgentEvent(event);
return { received: true };
}
}
- Step 3: Create supervisor module and register
// supervisor.module.ts
import { Module } from '@nestjs/common';
import { PlatformModule } from '../platform/platform.module';
import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module';
import { SupervisorController } from './supervisor.controller';
import { SupervisorService } from './supervisor.service';
@Module({
imports: [PlatformModule, OzonetelAgentModule],
controllers: [SupervisorController],
providers: [SupervisorService],
})
export class SupervisorModule {}
Add to app.module.ts:
import { SupervisorModule } from './supervisor/supervisor.module';
// Add to imports array
- Step 4: Verify sidecar build
cd helix-engage-server && npm run build
- Step 5: Commit
git add src/supervisor/ src/app.module.ts
git commit -m "feat: supervisor module with team performance + active calls endpoints"
Task 5: Team Performance Dashboard (PP-5)
Files:
- Modify:
helix-engage/src/pages/team-performance.tsx
This is the largest task. The page queries platform directly for calls/appointments/leads and the sidecar for time breakdown.
- Step 1: Build the full page
The page has 6 sections. Use apiClient.graphql() for platform data and apiClient.get() for sidecar data.
Queries needed:
- Calls by date range:
calls(first: 500, filter: { startedAt: { gte: "...", lte: "..." } }) - Appointments by date range:
appointments(first: 200, filter: { scheduledAt: { gte: "...", lte: "..." } }) - Leads:
leads(first: 200) - Follow-ups:
followUps(first: 200) - Agents with thresholds:
agents(first: 20) { ... npsscore maxidleminutes minnpsthreshold minconversionpercent } - Sidecar:
GET /api/supervisor/team-performance?date=YYYY-MM-DD
Date range logic:
- Today: today start → now
- Week: Monday of current week → now
- Month: 1st of current month → now
- Year: Jan 1 → now
- Custom: user-selected range
Sections to implement:
- Key Metrics bar (6 cards in a row)
- Call Breakdown Trends (2 ECharts line charts side by side)
- Agent Performance table (sortable)
- Time Breakdown (team average + per-agent stacked bars)
- NPS + Conversion Metrics (donut + cards)
- Performance Alerts (threshold comparison)
Check if ECharts is already installed:
grep echarts helix-engage/package.json
If not, install: npm install echarts echarts-for-react
Follow the existing My Performance page (my-performance.tsx) for ECharts patterns.
- Step 2: Verify build
cd helix-engage && npm run build
- Step 3: Test locally
cd helix-engage && npm run dev
Navigate to /team-performance as admin user. Verify all 6 sections render with real data.
- Step 4: Commit
git add src/pages/team-performance.tsx package.json package-lock.json
git commit -m "feat: team performance dashboard (PP-5) with 6 data sections"
Task 6: Live Call Monitor (PP-6)
Files:
-
Modify:
helix-engage/src/pages/live-monitor.tsx -
Step 1: Build the live monitor page
Page polls GET /api/supervisor/active-calls every 5 seconds.
Structure:
- TopBar: "Live Call Monitor" with subtitle "Listen, whisper, or barge into active calls"
- Three KPI cards: Active Calls, On Hold, Avg Duration
- Active Calls table: Agent, Caller, Type, Department, Duration (live counter), Status, Actions
- Actions: Listen / Whisper / Barge buttons — all disabled with tooltip "Coming soon — pending Ozonetel API"
- Empty state: headphones icon + "No active calls"
Duration should be a live counter — calculated client-side from startTime in the active call data. Use setInterval to update every second.
Caller name: attempt to match callerNumber against leads from useData(). If matched, show lead name + phone. If not, show phone only.
- Step 2: Verify build
cd helix-engage && npm run build
- Step 3: Test locally
Navigate to /live-monitor. Verify empty state renders. If Ozonetel events are flowing, verify active calls appear.
- Step 4: Commit
git add src/pages/live-monitor.tsx
git commit -m "feat: live call monitor page (PP-6) with polling + KPI cards"
Task 7: Local Testing + Final Verification
- Step 1: Run both locally
Terminal 1: cd helix-engage-server && npm run start:dev
Terminal 2: cd helix-engage && npm run dev
- Step 2: Test admin login
Login as admin (sanjay.marketing@globalhospital.com). Verify:
-
Sidebar shows new nav structure (Supervisor + Data & Reports sections)
-
Dashboard loads
-
Team Performance shows data from platform
-
Live Monitor shows empty state or active calls
-
All master data pages load (Lead, Patient, Appointment, Call Log, Call Recordings, Missed Calls)
-
Step 3: Commit any fixes
-
Step 4: Push to Azure
cd helix-engage && git push origin dev
cd helix-engage-server && git push origin dev