# Phase 2: Missed Call Queue + Login Redesign + Button Fix — 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:** Implement missed call queue with FIFO auto-assignment, dedup, and callback SLA tracking; redesign the login page; fix button widths in call desk forms. **Architecture:** Sidecar polls Ozonetel abandon calls every 30s, deduplicates by phone number, writes/updates Call records on the platform via GraphQL. Auto-assignment triggered on agent Ready state (via dispose or manual toggle). Frontend shows status-tabbed missed call view in the worklist panel. **Tech Stack:** NestJS sidecar (TypeScript), Fortytwo platform GraphQL API, React + Tailwind frontend **Spec:** `docs/superpowers/specs/2026-03-22-phase2-missed-call-queue-login-redesign.md` --- ## File Map ### Sidecar (`helix-engage-server/src/`) | File | Action | Responsibility | |------|--------|----------------| | `worklist/missed-queue.service.ts` | Create | Ingestion polling, dedup, auto-assignment, status updates | | `worklist/worklist.controller.ts` | Modify | Add missed queue endpoints (GET, PATCH, POST assign) | | `worklist/worklist.service.ts` | Modify | Add new fields to missed call query, change sort order to FIFO | | `worklist/worklist.module.ts` | Modify | Register MissedQueueService, forwardRef OzonetelAgentModule | | `ozonetel/ozonetel-agent.controller.ts` | Modify | Hook auto-assignment into dispose + agent-state endpoints, add `missedCallId` to dispose | | `ozonetel/ozonetel-agent.module.ts` | Modify | forwardRef WorklistModule for MissedQueueService access | | `platform/platform-graphql.service.ts` | Modify | Make `query()` method public (currently private) | | `config/configuration.ts` | Modify | Add `MISSED_QUEUE_POLL_INTERVAL_MS` env var | ### Frontend (`helix-engage/src/`) | File | Action | Responsibility | |------|--------|----------------| | `hooks/use-worklist.ts` | Modify | Add callback fields to MissedCall type, transform data | | `components/call-desk/worklist-panel.tsx` | Modify | Add status sub-tabs under Missed tab | | `components/call-desk/active-call-card.tsx` | Modify | Pass `missedCallId` to dispose endpoint when callback | | `components/call-desk/disposition-form.tsx` | Modify | Fix button width | | `pages/login.tsx` | Rewrite | Centered white card on blue background | | `pages/call-desk.tsx` | Modify | Track active missed call ID for disposition flow | --- ## Task 1: MissedQueueService — Phone Normalization + Core Service Skeleton **Files:** - Create: `helix-engage-server/src/worklist/missed-queue.service.ts` - [ ] **Step 1: Create the service with phone normalization utility** ```typescript // helix-engage-server/src/worklist/missed-queue.service.ts 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'; // Normalize phone to +91XXXXXXXXXX format export function normalizePhone(raw: string): string { let digits = raw.replace(/[^0-9]/g, ''); // Strip leading country code variations: 0091, 91, 0 if (digits.startsWith('0091')) digits = digits.slice(4); else if (digits.startsWith('91') && digits.length > 10) digits = digits.slice(2); else if (digits.startsWith('0') && digits.length > 10) digits = digits.slice(1); return `+91${digits.slice(-10)}`; } @Injectable() export class MissedQueueService implements OnModuleInit { private readonly logger = new Logger(MissedQueueService.name); private readonly pollIntervalMs: number; private readonly processedUcids = new Set(); private assignmentMutex = false; constructor( private readonly config: ConfigService, private readonly platform: PlatformGraphqlService, private readonly ozonetel: OzonetelAgentService, ) { this.pollIntervalMs = this.config.get('missedQueue.pollIntervalMs', 30000); } onModuleInit() { this.logger.log(`Starting missed call ingestion polling every ${this.pollIntervalMs}ms`); setInterval(() => this.ingest().catch(err => this.logger.error('Ingestion failed', err)), this.pollIntervalMs); } async ingest(): Promise<{ created: number; updated: number }> { // Implemented in Task 2 return { created: 0, updated: 0 }; } async assignNext(agentName: string): Promise { // Implemented in Task 3 return null; } async updateStatus(callId: string, status: string, authHeader: string): Promise { // Implemented in Task 4 return {}; } async getMissedQueue(agentName: string, authHeader: string): Promise { // Implemented in Task 4 return { pending: [], attempted: [], completed: [], invalid: [] }; } } ``` - [ ] **Step 2: Make `PlatformGraphqlService.query()` public** The `query()` method at `platform/platform-graphql.service.ts:17` is `private`. The `MissedQueueService` needs to call it for server-to-server operations (ingestion, assignment). Change `private` to `public`: ```typescript // Line 17: change from private async query(query: string, variables?: Record): Promise { // to async query(query: string, variables?: Record): Promise { ``` - [ ] **Step 3: Register in WorklistModule with forwardRef** Modify `helix-engage-server/src/worklist/worklist.module.ts`: ```typescript // Add imports at top import { forwardRef } from '@nestjs/common'; import { MissedQueueService } from './missed-queue.service'; import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module'; // Update @Module decorator: @Module({ imports: [PlatformModule, forwardRef(() => OzonetelAgentModule)], controllers: [WorklistController, MissedCallWebhookController, KookooCallbackController], providers: [WorklistService, MissedQueueService], exports: [MissedQueueService], }) ``` Check that `OzonetelAgentModule` exports `OzonetelAgentService`. Read `helix-engage-server/src/ozonetel/ozonetel-agent.module.ts` to verify — if not exported, add it. - [ ] **Step 4: Add config entry** Modify `helix-engage-server/src/config/configuration.ts` — add inside the returned config object: ```typescript missedQueue: { pollIntervalMs: parseInt(process.env.MISSED_QUEUE_POLL_INTERVAL_MS ?? '30000', 10), }, ``` - [ ] **Step 5: Verify sidecar compiles** Run: `cd helix-engage-server && npm run build` Expected: Clean compilation, no errors - [ ] **Step 6: Commit** ```bash git add helix-engage-server/src/worklist/missed-queue.service.ts helix-engage-server/src/worklist/worklist.module.ts helix-engage-server/src/platform/platform-graphql.service.ts helix-engage-server/src/config/configuration.ts git commit -m "feat: add MissedQueueService skeleton with phone normalization" ``` --- ## Task 2: MissedQueueService — Ingestion + Dedup **Files:** - Modify: `helix-engage-server/src/worklist/missed-queue.service.ts` - [ ] **Step 1: Implement the `ingest()` method** Replace the placeholder `ingest()` method in `missed-queue.service.ts`: ```typescript async ingest(): Promise<{ created: number; updated: number }> { let created = 0; let updated = 0; // Query last 5 minutes of abandon calls const now = new Date(); const fiveMinAgo = new Date(now.getTime() - 5 * 60 * 1000); const format = (d: Date) => d.toISOString().replace('T', ' ').slice(0, 19); let abandonCalls: any[]; try { abandonCalls = await this.ozonetel.getAbandonCalls({ fromTime: format(fiveMinAgo), toTime: format(now) }); } catch (err) { this.logger.warn(`Failed to fetch abandon calls: ${err}`); return { created: 0, updated: 0 }; } if (!abandonCalls?.length) return { created: 0, updated: 0 }; for (const call of abandonCalls) { const ucid = call.monitorUCID; if (!ucid || this.processedUcids.has(ucid)) continue; this.processedUcids.add(ucid); const phone = normalizePhone(call.callerID || ''); if (!phone || phone.length < 13) continue; // +91 + 10 digits = 13 const did = call.did || ''; const callTime = call.callTime || new Date().toISOString(); try { // Check for existing PENDING_CALLBACK record with same phone const existing = await this.platform.query( `{ calls(first: 1, filter: { callbackstatus: { eq: PENDING_CALLBACK }, callerNumber: { primaryPhoneNumber: { eq: "${phone}" } } }) { edges { node { id missedcallcount } } } }`, ); const existingNode = existing?.calls?.edges?.[0]?.node; if (existingNode) { // Dedup: increment count, update timestamp const newCount = (existingNode.missedcallcount || 1) + 1; await this.platform.query( `mutation { updateCall(id: "${existingNode.id}", data: { missedcallcount: ${newCount}, startedAt: "${callTime}", callsourcenumber: "${did}" }) { id } }`, ); updated++; this.logger.log(`Dedup missed call ${phone}: count now ${newCount}`); } else { // New missed call record await this.platform.query( `mutation { createCall(data: { callStatus: MISSED, direction: INBOUND, callerNumber: { primaryPhoneNumber: "${phone}", primaryPhoneCallingCode: "+91" }, callsourcenumber: "${did}", callbackstatus: PENDING_CALLBACK, missedcallcount: 1, startedAt: "${callTime}" }) { id } }`, ); created++; this.logger.log(`Created missed call record for ${phone}`); } } catch (err) { this.logger.warn(`Failed to process abandon call ${ucid}: ${err}`); } } // Trim processedUcids set to prevent unbounded growth (keep last 500) if (this.processedUcids.size > 500) { const arr = Array.from(this.processedUcids); this.processedUcids.clear(); arr.slice(-200).forEach(u => this.processedUcids.add(u)); } if (created || updated) this.logger.log(`Ingestion: ${created} created, ${updated} updated`); return { created, updated }; } ``` **Note**: `this.platform.query()` uses the server API key (not user JWT). The `getAbandonCalls()` method accepts an options object: `{ fromTime?, toTime?, campaignName? }` (see `ozonetel-agent.service.ts:263`). - [ ] **Step 2: Verify sidecar compiles** Run: `cd helix-engage-server && npm run build` Expected: Clean compilation - [ ] **Step 3: Commit** ```bash git add helix-engage-server/src/worklist/missed-queue.service.ts git commit -m "feat: implement missed call ingestion with dedup and phone normalization" ``` --- ## Task 3: MissedQueueService — Auto-Assignment **Files:** - Modify: `helix-engage-server/src/worklist/missed-queue.service.ts` - Modify: `helix-engage-server/src/ozonetel/ozonetel-agent.controller.ts` - [ ] **Step 1: Implement `assignNext()` in the service** Replace the placeholder `assignNext()` method: ```typescript async assignNext(agentName: string): Promise { // Simple mutex to prevent race conditions if (this.assignmentMutex) return null; this.assignmentMutex = true; try { // Find oldest unassigned PENDING_CALLBACK call const result = await this.platform.query( `{ calls(first: 1, filter: { callbackstatus: { eq: PENDING_CALLBACK }, agentName: { eq: "" } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { id callerNumber { primaryPhoneNumber } startedAt callsourcenumber missedcallcount } } } }`, ); const call = result?.calls?.edges?.[0]?.node; if (!call) { // Also check for null agentName (not just empty string) const result2 = await this.platform.query( `{ calls(first: 1, filter: { callbackstatus: { eq: PENDING_CALLBACK }, agentName: { is: NULL } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { id callerNumber { primaryPhoneNumber } startedAt callsourcenumber missedcallcount } } } }`, ); const call2 = result2?.calls?.edges?.[0]?.node; if (!call2) return null; await this.platform.query( `mutation { updateCall(id: "${call2.id}", data: { agentName: "${agentName}" }) { id } }`, ); this.logger.log(`Assigned missed call ${call2.id} to ${agentName}`); return call2; } await this.platform.query( `mutation { updateCall(id: "${call.id}", data: { agentName: "${agentName}" }) { id } }`, ); this.logger.log(`Assigned missed call ${call.id} to ${agentName}`); return call; } catch (err) { this.logger.warn(`Assignment failed: ${err}`); return null; } finally { this.assignmentMutex = false; } } ``` - [ ] **Step 2: Wire OzonetelAgentModule ↔ WorklistModule with forwardRef** **This must be done BEFORE adding injections to the controller**, otherwise NestJS cannot resolve dependencies. In `ozonetel-agent.module.ts`: ```typescript import { forwardRef } from '@nestjs/common'; import { WorklistModule } from '../worklist/worklist.module'; @Module({ imports: [forwardRef(() => WorklistModule)], // ... keep existing controllers, providers, exports }) ``` Verify `worklist.module.ts` already has `forwardRef(() => OzonetelAgentModule)` from Task 1 Step 3. - [ ] **Step 3: Hook auto-assignment into the dispose endpoint** Modify `helix-engage-server/src/ozonetel/ozonetel-agent.controller.ts`. Add imports at top: ```typescript import { MissedQueueService } from '../worklist/missed-queue.service'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; ``` Add to constructor (after `private readonly config: ConfigService`): ```typescript private readonly missedQueue: MissedQueueService, private readonly platform: PlatformGraphqlService, ``` Then at the END of the dispose handler (after line ~133, after the try-catch for `setDisposition`), add: ```typescript // Handle missed call callback status update const missedCallId = body.missedCallId; if (missedCallId) { const statusMap: Record = { APPOINTMENT_BOOKED: 'CALLBACK_COMPLETED', INFO_PROVIDED: 'CALLBACK_COMPLETED', FOLLOW_UP_SCHEDULED: 'CALLBACK_COMPLETED', CALLBACK_REQUESTED: 'CALLBACK_COMPLETED', WRONG_NUMBER: 'WRONG_NUMBER', // NO_ANSWER stays CALLBACK_ATTEMPTED — agent can retry }; const newStatus = statusMap[body.disposition]; if (newStatus) { try { await this.platform.query( `mutation { updateCall(id: "${missedCallId}", data: { callbackstatus: ${newStatus} }) { id } }`, ); } catch (err) { this.logger.warn(`Failed to update missed call status: ${err}`); } } } // Auto-assign next missed call to this agent try { await this.missedQueue.assignNext(this.defaultAgentId); } catch (err) { this.logger.warn(`Auto-assignment after dispose failed: ${err}`); } ``` - [ ] **Step 4: Hook auto-assignment into agent-state endpoint** In the same controller, at the end of the agent-state handler (after line ~76), add: ```typescript // Auto-assign missed call when agent goes Ready if (body.state === 'Ready') { try { const assigned = await this.missedQueue.assignNext(this.agentId); if (assigned) { return { status: 'ok', message: `State changed to Ready. Assigned missed call ${assigned.id}`, assignedCall: assigned }; } } catch (err) { this.logger.warn(`Auto-assignment on Ready failed: ${err}`); } } ``` - [ ] **Step 5: Verify sidecar compiles** Run: `cd helix-engage-server && npm run build` Expected: Clean compilation - [ ] **Step 6: Commit** ```bash git add helix-engage-server/src/worklist/missed-queue.service.ts helix-engage-server/src/ozonetel/ozonetel-agent.controller.ts helix-engage-server/src/ozonetel/ozonetel-agent.module.ts helix-engage-server/src/worklist/worklist.module.ts git commit -m "feat: auto-assign missed calls on agent Ready state" ``` --- ## Task 4: Missed Queue Endpoints + Worklist Update **Files:** - Modify: `helix-engage-server/src/worklist/missed-queue.service.ts` - Modify: `helix-engage-server/src/worklist/worklist.controller.ts` - Modify: `helix-engage-server/src/worklist/worklist.service.ts` - [ ] **Step 1: Implement `getMissedQueue()` and `updateStatus()` in the service** Replace the placeholder methods in `missed-queue.service.ts`: ```typescript async getMissedQueue(agentName: string, authHeader: string): Promise<{ pending: any[]; attempted: any[]; completed: any[]; invalid: any[]; }> { const fields = `id name createdAt direction callStatus agentName callerNumber { primaryPhoneNumber } startedAt endedAt durationSec disposition leadId callbackstatus callsourcenumber missedcallcount callbackattemptedat`; const query = (status: string) => `{ calls(first: 50, filter: { agentName: { eq: "${agentName}" }, callStatus: { eq: MISSED }, callbackstatus: { eq: ${status} } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { ${fields} } } } }`; try { const [pending, attempted, completed, invalid, wrongNumber] = await Promise.all([ this.platform.queryWithAuth(query('PENDING_CALLBACK'), undefined, authHeader), this.platform.queryWithAuth(query('CALLBACK_ATTEMPTED'), undefined, authHeader), this.platform.queryWithAuth(query('CALLBACK_COMPLETED'), undefined, authHeader), this.platform.queryWithAuth(query('INVALID'), undefined, authHeader), this.platform.queryWithAuth(query('WRONG_NUMBER'), undefined, authHeader), ]); const extract = (r: any) => r?.calls?.edges?.map((e: any) => e.node) ?? []; return { pending: extract(pending), attempted: extract(attempted), completed: [...extract(completed), ...extract(wrongNumber)], invalid: extract(invalid), }; } catch (err) { this.logger.warn(`Failed to fetch missed queue: ${err}`); return { pending: [], attempted: [], completed: [], invalid: [] }; } } async updateStatus(callId: string, status: string, authHeader: string): Promise { const validStatuses = ['PENDING_CALLBACK', 'CALLBACK_ATTEMPTED', 'CALLBACK_COMPLETED', 'INVALID', 'WRONG_NUMBER']; if (!validStatuses.includes(status)) { throw new Error(`Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}`); } const data: Record = { callbackstatus: status }; if (status === 'CALLBACK_ATTEMPTED') { data.callbackattemptedat = new Date().toISOString(); } const dataStr = Object.entries(data) .map(([k, v]) => typeof v === 'string' && !validStatuses.includes(v) ? `${k}: "${v}"` : `${k}: ${v}`) .join(', '); return this.platform.queryWithAuth( `mutation { updateCall(id: "${callId}", data: { ${dataStr} }) { id callbackstatus callbackattemptedat } }`, undefined, authHeader, ); } ``` - [ ] **Step 2: Add endpoints to the controller** Modify `helix-engage-server/src/worklist/worklist.controller.ts`. Add imports and inject `MissedQueueService`: ```typescript import { Controller, Get, Patch, Post, Headers, Param, Body, HttpException, Logger } from '@nestjs/common'; import { MissedQueueService } from './missed-queue.service'; ``` Add `MissedQueueService` to constructor: ```typescript constructor( private readonly worklist: WorklistService, private readonly missedQueue: MissedQueueService, private readonly platform: PlatformGraphqlService, ) {} ``` Add these endpoints after the existing `@Get()`: ```typescript @Get('missed-queue') async getMissedQueue(@Headers('authorization') authHeader: string) { if (!authHeader) throw new HttpException('Authorization header required', 401); const agentName = await this.resolveAgentName(authHeader); return this.missedQueue.getMissedQueue(agentName, authHeader); } @Patch('missed-queue/:id/status') async updateMissedCallStatus( @Param('id') id: string, @Headers('authorization') authHeader: string, @Body() body: { status: string }, ) { if (!authHeader) throw new HttpException('Authorization header required', 401); if (!body.status) throw new HttpException('status is required', 400); return this.missedQueue.updateStatus(id, body.status, authHeader); } ``` - [ ] **Step 3: Update worklist service to include new fields** Modify `helix-engage-server/src/worklist/worklist.service.ts` — update the `getMissedCalls` method (lines 77-95). Change the query to include new fields and FIFO ordering: ```typescript private async getMissedCalls(agentName: string, authHeader: string): Promise { try { const data = await this.platform.queryWithAuth( `{ calls(first: 20, filter: { agentName: { eq: "${agentName}" }, callStatus: { eq: MISSED }, callbackstatus: { in: [PENDING_CALLBACK, CALLBACK_ATTEMPTED] } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { id name createdAt direction callStatus agentName callerNumber { primaryPhoneNumber } startedAt endedAt durationSec disposition leadId callbackstatus callsourcenumber missedcallcount callbackattemptedat } } } }`, undefined, authHeader, ); return data.calls.edges.map((e: any) => e.node); } catch (err) { this.logger.warn(`Failed to fetch missed calls: ${err}`); return []; } } ``` **Note**: Sort changed from `DescNullsLast` to `AscNullsLast` for FIFO ordering. Added `callbackstatus` filter to only show active missed calls in the worklist (not completed/invalid). - [ ] **Step 4: Verify sidecar compiles** Run: `cd helix-engage-server && npm run build` Expected: Clean compilation - [ ] **Step 5: Commit** ```bash git add helix-engage-server/src/worklist/missed-queue.service.ts helix-engage-server/src/worklist/worklist.controller.ts helix-engage-server/src/worklist/worklist.service.ts git commit -m "feat: missed queue endpoints and worklist FIFO ordering with callback fields" ``` --- ## Task 5: Frontend — Worklist Types + Hook Update **Files:** - Modify: `helix-engage/src/hooks/use-worklist.ts` - [ ] **Step 1: Update MissedCall type and transform** In `helix-engage/src/hooks/use-worklist.ts`, update the `MissedCall` type (lines 5-18) to add the new fields: ```typescript type MissedCall = { id: string; createdAt: string; callDirection: string | null; callStatus: string | null; callerNumber: { number: string; callingCode: string }[] | null; agentName: string | null; startedAt: string | null; endedAt: string | null; durationSeconds: number | null; disposition: string | null; callNotes: string | null; leadId: string | null; // New callback tracking fields callbackstatus: string | null; callsourcenumber: string | null; missedcallcount: number | null; callbackattemptedat: string | null; }; ``` The transform in `missedCalls` mapping (lines 103-110) already spreads all fields — the new lowercase fields will pass through automatically since they match the GraphQL names. - [ ] **Step 2: Verify frontend compiles** Run: `cd helix-engage && npm run build` Expected: Clean compilation (may have warnings, no errors) - [ ] **Step 3: Commit** ```bash git add helix-engage/src/hooks/use-worklist.ts git commit -m "feat: add callback tracking fields to MissedCall type" ``` --- ## Task 6: Frontend — Missed Call Status Sub-Tabs **Files:** - Modify: `helix-engage/src/components/call-desk/worklist-panel.tsx` This is the largest frontend change. The worklist panel currently has tabs: All | Missed | Callbacks | Follow-ups. We add status sub-tabs under the Missed tab. - [ ] **Step 1: Add sub-tab state and filtering logic** At the top of the `WorklistPanel` component, add a sub-tab state: ```typescript type MissedSubTab = 'pending' | 'attempted' | 'completed' | 'invalid'; const [missedSubTab, setMissedSubTab] = useState('pending'); ``` Add a filtering function that splits missed calls by `callbackstatus`: ```typescript const missedByStatus = useMemo(() => { const pending = missedCalls.filter(c => c.callbackstatus === 'PENDING_CALLBACK' || !c.callbackstatus); const attempted = missedCalls.filter(c => c.callbackstatus === 'CALLBACK_ATTEMPTED'); const completed = missedCalls.filter(c => c.callbackstatus === 'CALLBACK_COMPLETED' || c.callbackstatus === 'WRONG_NUMBER'); const invalid = missedCalls.filter(c => c.callbackstatus === 'INVALID'); return { pending, attempted, completed, invalid }; }, [missedCalls]); ``` - [ ] **Step 2: Render sub-tabs when Missed tab is active** Inside the Missed tab content section, add sub-tab buttons before the table: ```tsx {activeTab === 'missed' && (
{(['pending', 'attempted', 'completed', 'invalid'] as MissedSubTab[]).map(sub => ( ))}
)} ``` - [ ] **Step 3: Use filtered missed calls in the table rows** Where the component currently maps `missedCalls` to table rows for the Missed tab, replace it to use `missedByStatus[missedSubTab]` instead. The active sub-tab determines which missed calls to display. Also add the `missedcallcount` badge and `callsourcenumber` to the row rendering: ```tsx {mc.missedcallcount && mc.missedcallcount > 1 && ( {mc.missedcallcount}x )} {mc.callsourcenumber && ( {mc.callsourcenumber} )} ``` - [ ] **Step 4: Verify frontend compiles** Run: `cd helix-engage && npm run build` Expected: Clean compilation - [ ] **Step 5: Commit** ```bash git add helix-engage/src/components/call-desk/worklist-panel.tsx git commit -m "feat: add status sub-tabs to missed call worklist panel" ``` --- ## Task 7: Frontend — Pass missedCallId Through Disposition Flow **Files:** - Modify: `helix-engage/src/pages/call-desk.tsx` - Modify: `helix-engage/src/components/call-desk/active-call-card.tsx` - [ ] **Step 1: Track the active missed call ID in call-desk page** Read `src/pages/call-desk.tsx` to understand how it passes props to `ActiveCallCard`. Add state to track when the current call is a missed call callback: ```typescript const [activeMissedCallId, setActiveMissedCallId] = useState(null); ``` When the agent clicks call-back on a missed call from the worklist, set this ID. Pass it to `ActiveCallCard`: ```tsx ``` The worklist panel's click-to-call handler should: 1. Call `PATCH /api/worklist/missed-queue/:id/status` with `{ status: 'CALLBACK_ATTEMPTED' }` 2. Set `activeMissedCallId` to the missed call's ID 3. Initiate the outbound call via SIP - [ ] **Step 2: Update ActiveCallCard to pass missedCallId in disposition** Modify `helix-engage/src/components/call-desk/active-call-card.tsx`: Add `missedCallId` to the props interface: ```typescript interface ActiveCallCardProps { lead: Lead | null; callerPhone: string; missedCallId?: string | null; } ``` Update the destructured props: ```typescript export const ActiveCallCard = ({ lead, callerPhone, missedCallId }: ActiveCallCardProps) => { ``` In `handleDisposition` (line 62-72), add `missedCallId` to the dispose payload: ```typescript apiClient.post('/api/ozonetel/dispose', { ucid: callUcid, disposition, callerPhone, direction: callDirectionRef.current, durationSec: callDuration, leadId: lead?.id ?? null, notes, missedCallId: missedCallId ?? undefined, }).catch((err) => console.warn('Disposition failed:', err)); ``` - [ ] **Step 3: Clear activeMissedCallId after disposition completes** In `call-desk.tsx`, when the call ends and disposition is done (when `ActiveCallCard` signals completion), reset: ```typescript setActiveMissedCallId(null); ``` - [ ] **Step 4: Verify frontend compiles** Run: `cd helix-engage && npm run build` Expected: Clean compilation - [ ] **Step 5: Commit** ```bash git add helix-engage/src/pages/call-desk.tsx helix-engage/src/components/call-desk/active-call-card.tsx git commit -m "feat: pass missedCallId through disposition flow for callback status tracking" ``` --- ## Task 8: Login Page Redesign **Files:** - Modify: `helix-engage/src/pages/login.tsx` - [ ] **Step 1: Rewrite login page layout** Replace the entire JSX return in `login.tsx` (from line ~89 onward). Keep ALL the logic (handleSubmit, state, useEffect for remember-me, error handling) — only change the layout: ```tsx return (
{/* Login Card */}
{/* Logo */}
Helix Engage

Sign in to Helix Engage

Global Hospital

{/* Google Sign In */} {/* Divider */}
or continue with
{/* Form */}
{error && (
{error}
)}
setEmail(e.target.value)} placeholder="you@company.com" className="w-full px-3 py-2 border border-primary rounded-lg text-sm text-primary placeholder:text-placeholder focus:outline-none focus:ring-2 focus:ring-brand-300 focus:border-brand" required />
setPassword(e.target.value)} placeholder="••••••••" className="w-full px-3 py-2 border border-primary rounded-lg text-sm text-primary placeholder:text-placeholder focus:outline-none focus:ring-2 focus:ring-brand-300 focus:border-brand pr-10" required />
Forgot password?
{/* Footer */}

Powered by FortyTwo

); ``` **Notes**: - Check which imports are used — remove unused imports from the old layout (feature card icons, etc.). Keep `FontAwesomeIcon`, `faEye`, `faEyeSlash`. - Add `showPassword` state if not already present: `const [showPassword, setShowPassword] = useState(false);` - The existing login page uses `Input`, `Button`, `Checkbox` components from the Untitled UI library. **Prefer reusing these components** over raw HTML elements for consistency. Read the existing login page to see which components are already imported and adapt the template above to use them (e.g., `` instead of ``, `` instead of `
``` - [ ] **Step 2: Check other forms** Read `appointment-form.tsx`, `enquiry-form.tsx`, `transfer-dialog.tsx` — check if their buttons also use `w-full`. Only fix the ones that do. The spec review found that these may already be content-width. - [ ] **Step 3: Verify frontend compiles** Run: `cd helix-engage && npm run build` Expected: Clean compilation - [ ] **Step 4: Commit** ```bash git add helix-engage/src/components/call-desk/disposition-form.tsx git commit -m "fix: use content-width buttons in call desk forms" ``` --- ## Task 10: Deploy + Verify **Files:** None (deploy only) - [ ] **Step 1: Build and deploy sidecar** ```bash cd helix-engage-server && npm run build && \ tar czf /tmp/sidecar-src.tar.gz --exclude=node_modules --exclude=.git --exclude=dist . && \ tar czf /tmp/sidecar-dist.tar.gz -C dist . && \ scp -i ~/Downloads/fortytwoai_hostinger /tmp/sidecar-src.tar.gz /tmp/sidecar-dist.tar.gz root@148.230.67.184:/tmp/ && \ ssh -i ~/Downloads/fortytwoai_hostinger root@148.230.67.184 "docker cp /tmp/sidecar-src.tar.gz fortytwo-staging-sidecar-1:/tmp/ && docker cp /tmp/sidecar-dist.tar.gz fortytwo-staging-sidecar-1:/tmp/ && docker exec fortytwo-staging-sidecar-1 sh -c 'cd /app && tar xzf /tmp/sidecar-src.tar.gz && tar xzf /tmp/sidecar-dist.tar.gz -C dist' && cd /opt/fortytwo && docker compose restart sidecar" ``` - [ ] **Step 2: Build and deploy frontend** ```bash cd helix-engage && \ VITE_API_URL=https://engage-api.srv1477139.hstgr.cloud \ VITE_SIDECAR_URL=https://engage-api.srv1477139.hstgr.cloud \ VITE_SIP_URI=sip:523590@blr-pub-rtc4.ozonetel.com \ VITE_SIP_PASSWORD=523590 \ VITE_SIP_WS_SERVER=wss://blr-pub-rtc4.ozonetel.com:444 \ npm run build && \ tar czf /tmp/frontend-deploy.tar.gz -C dist . && \ scp -i ~/Downloads/fortytwoai_hostinger /tmp/frontend-deploy.tar.gz root@148.230.67.184:/tmp/ && \ ssh -i ~/Downloads/fortytwoai_hostinger root@148.230.67.184 "cd /opt/fortytwo/helix-engage-frontend && rm -rf * && tar xzf /tmp/frontend-deploy.tar.gz" ``` - [ ] **Step 3: Verify sidecar ingestion is running** ```bash ssh -i ~/Downloads/fortytwoai_hostinger root@148.230.67.184 "docker logs fortytwo-staging-sidecar-1 --tail 20 2>&1 | grep -i 'missed\|ingest\|queue'" ``` Expected: Log lines showing ingestion polling started - [ ] **Step 4: Verify login page** Open the deployed frontend URL. Login page should show centered white card on blue background. - [ ] **Step 5: Verify missed call sub-tabs** Log in as a CC agent, navigate to Call Desk. The Missed tab should show sub-tabs: Pending | Attempted | Completed | Invalid. - [ ] **Step 6: Commit any deployment fixes** If any issues found during verification, fix and recommit.