From d199a6bcae959b60a6888d5bb6dabdbe1e057063 Mon Sep 17 00:00:00 2001 From: Oxicode Date: Fri, 8 May 2026 13:07:57 -0500 Subject: [PATCH 1/3] feat: implement call service with realtime updates and frontend dashboard --- package-lock.json | 81 +++++- packages/call-service/src/routes/calls.ts | 1 + packages/call-service/src/routes/events.ts | 2 + .../call-service/src/services/CallService.ts | 260 +++++++++++++++++- packages/frontend/package.json | 1 + packages/frontend/src/app/api/calls/route.ts | 23 ++ packages/frontend/src/app/page.tsx | 11 +- packages/frontend/src/hooks/useCallEvents.ts | 43 ++- packages/frontend/src/hooks/useCalls.ts | 50 +++- .../realtime-service/src/bus/subscriber.ts | 21 +- .../realtime-service/src/socket/server.ts | 12 +- 11 files changed, 462 insertions(+), 43 deletions(-) create mode 100644 packages/frontend/src/app/api/calls/route.ts diff --git a/package-lock.json b/package-lock.json index b4d30f3..a7f1287 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,9 @@ "workspaces": [ "packages/*" ], + "dependencies": { + "swr": "^2.4.1" + }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", @@ -2852,7 +2855,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/autoprefixer": { @@ -2918,6 +2920,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3320,7 +3333,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -3640,7 +3652,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3664,6 +3675,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -4014,7 +4034,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5055,6 +5074,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -5092,7 +5131,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5526,7 +5564,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7704,6 +7741,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -9069,6 +9115,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -9622,6 +9681,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10188,6 +10256,7 @@ "version": "1.0.0", "dependencies": { "@voycelink/contracts": "*", + "axios": "^1.16.0", "next": "^14.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/packages/call-service/src/routes/calls.ts b/packages/call-service/src/routes/calls.ts index 762face..9753934 100644 --- a/packages/call-service/src/routes/calls.ts +++ b/packages/call-service/src/routes/calls.ts @@ -21,6 +21,7 @@ router.get('/', async (req: Request, res: Response) => { const calls = await callService.getCalls(filters); res.json(calls); } catch (_error) { + console.log('Error fetching calls:', _error); res.status(500).json({ message: 'Internal server error' }); } }); diff --git a/packages/call-service/src/routes/events.ts b/packages/call-service/src/routes/events.ts index 2bb57af..36d0bb2 100644 --- a/packages/call-service/src/routes/events.ts +++ b/packages/call-service/src/routes/events.ts @@ -21,6 +21,8 @@ router.post('/', apiKeyAuth, async (req: Request, res: Response) => { return; } + console.log('Error processing event:', error); + res.status(500).json({ message: 'Internal server error' }); } }); diff --git a/packages/call-service/src/services/CallService.ts b/packages/call-service/src/services/CallService.ts index 3c9b082..9d5c2f1 100644 --- a/packages/call-service/src/services/CallService.ts +++ b/packages/call-service/src/services/CallService.ts @@ -1,21 +1,271 @@ +import type { PoolClient } from 'pg'; +import { randomUUID } from 'crypto'; +import { db } from '../db/client'; +import { publishStatusUpdate } from '../bus/publisher'; import { Call, + CallStatus, CallEvent, CallFilters, CallServiceContract, EventPayload, } from '../domain/call'; +import { + CallAnsweredPayload, + CallEndedPayload, + CallHoldPayload, + CallInitiatedPayload, + CallRoutedPayload, +} from '@voycelink/contracts'; + +type ProcessEventResult = { + callEvent: CallEvent; + status: CallStatus; +}; export class CallService implements CallServiceContract { - async processEvent(_payload: EventPayload): Promise { - throw new Error('CallService.processEvent not implemented'); + private async insertEvent( + client: PoolClient, + callId: string, + type: EventPayload['event'], + timestamp: Date, + metadata: Record, + ): Promise { + const event = await client.query( + 'INSERT INTO call_events (id, call_id, type, timestamp, metadata) VALUES ($1, $2, $3, $4, $5) RETURNING *', + [randomUUID(), callId, type, timestamp, metadata], + ); + + return new CallEvent( + event.rows[0].id, + event.rows[0].call_id, + event.rows[0].type, + new Date(event.rows[0].timestamp), + event.rows[0].metadata, + ); + } + + private async callInitiated( + client: PoolClient, + now: Date, + payload: CallInitiatedPayload): Promise { + + await client.query( + 'INSERT INTO calls (id, type, status, queue_id, start_time) VALUES ($1, $2, $3, $4, $5) RETURNING *', + [payload.callId, payload.type, 'waiting', payload.queueId, now], + ); + + const callEvent = await this.insertEvent(client, payload.callId, payload.event, now, { + ...payload, + slaMax: 30, + }); + + return { + callEvent, + status: 'waiting', + }; + } + + private async callRouted( + client: PoolClient, + now: Date, + payload: CallRoutedPayload): Promise { + + const query = 'SELECT status FROM calls WHERE id = $1'; + const routedCallResult = await client.query(query, [payload.callId]); + + if (routedCallResult.rowCount === 0) { + throw new Error(`Call with id ${payload.callId} not found`); + } + + await client.query( + 'UPDATE calls SET status = $1 WHERE id = $2', + ['waiting', payload.callId], + ); + + const callEvent = await this.insertEvent(client, payload.callId, payload.event, now, { + agentId: payload.agentId, + routingTime: payload.routingTime, + rerouteAfterSeconds: 15, + rerouteRequired: payload.routingTime > 15, + }); + + return { + callEvent, + status: 'waiting', + }; + } + + private async callAnswered( + client: PoolClient, + now: Date, + payload: CallAnsweredPayload, + ): Promise { + const query = 'SELECT status FROM calls WHERE id = $1'; + const answeredCallResult = await client.query(query, [payload.callId]); + + if (answeredCallResult.rowCount === 0) { + throw new Error(`Call with id ${payload.callId} not found`); + } + + await client.query('UPDATE calls SET status = $1 WHERE id = $2', ['active', payload.callId]); + + const callEvent = await this.insertEvent(client, payload.callId, payload.event, now, { + waitTime: payload.waitTime, + slaMax: 30, + }); + + return { + callEvent, + status: 'active', + }; + } + + private async callHold( + client: PoolClient, + now: Date, + payload: CallHoldPayload, + ): Promise { + const query = 'SELECT status FROM calls WHERE id = $1'; + const holdCallResult = await client.query(query, [payload.callId]); + if (holdCallResult.rowCount === 0) { + throw new Error(`Call with id ${payload.callId} not found`); + } + + await client.query('UPDATE calls SET status = $1 WHERE id = $2', ['on_hold', payload.callId]); + + const callEvent = await this.insertEvent(client, payload.callId, payload.event, now, { + holdDuration: payload.holdDuration, + maxHoldSeconds: 60, + }); + + return { + callEvent, + status: 'on_hold', + }; } - async getCalls(_filters: CallFilters): Promise { - throw new Error('CallService.getCalls not implemented'); + private async callEnded( + client: PoolClient, + now: Date, + payload: CallEndedPayload, + ): Promise { + const query = 'SELECT status FROM calls WHERE id = $1'; + const endedCallResult = await client.query(query, [payload.callId]); + if (endedCallResult.rowCount === 0) { + throw new Error(`Call with id ${payload.callId} not found`); + } + + await client.query( + 'UPDATE calls SET status = $1, end_time = $2 WHERE id = $3', + ['ended', now, payload.callId], + ); + + const callEvent = await this.insertEvent(client, payload.callId, payload.event, now, { + endReason: payload.endReason, + duration: payload.duration, + }); + + return { + callEvent, + status: 'ended', + }; + } + + async processEvent(payload: EventPayload): Promise { + const client = await db.connect(); + + try { + const now = new Date(); + await client.query('BEGIN'); + + let result: ProcessEventResult; + + if (payload.event === 'call_initiated') { + result = await this.callInitiated(client, now, payload); + } else if (payload.event === 'call_routed') { + result = await this.callRouted(client, now, payload); + } else if (payload.event === 'call_answered') { + result = await this.callAnswered(client, now, payload); + } else if (payload.event === 'call_hold') { + result = await this.callHold(client, now, payload); + } else if (payload.event === 'call_ended') { + result = await this.callEnded(client, now, payload); + } else { + throw new Error('Unsupported event type'); + } + + await client.query('COMMIT'); + + try { + // TODO: Publicar en redis + // await publishStatusUpdate({..}); + } catch (publishError) { + console.error({ publishError }); + } + + return result.callEvent; + } catch (error) { + await client.query('ROLLBACK'); + console.log('Error in processEvent:', error); + throw error; + } finally { + client.release(); + } + } + + async getCalls(filters: CallFilters): Promise { + const queryParts: string[] = ['SELECT * FROM calls']; + const whereClauses: string[] = []; + const values: Array = []; + + if (filters.status) { + values.push(filters.status); + whereClauses.push(`status = $${values.length}`); + } + + if (filters.queueId) { + values.push(filters.queueId); + whereClauses.push(`queue_id = $${values.length}`); + } + + if (whereClauses.length > 0) { + // Seria más facil usar un ORM o query builder para esto + queryParts.push(`WHERE ${whereClauses.join(' AND ')}`); + } + + queryParts.push('ORDER BY start_time DESC'); + + const query = queryParts.join(' '); + + const calls = await db.query(query, values); + return calls.rows.map( + (row) => + new Call( + row.id, + row.type, + row.status, + row.queue_id, + new Date(row.start_time), + row.end_time ? new Date(row.end_time) : undefined, + ), + ); } async getCallEvents(_callId: string): Promise { - throw new Error('CallService.getCallEvents not implemented'); + const query = 'SELECT * FROM call_events where call_id = $1 ORDER BY timestamp DESC'; + + const events = await db.query(query, [_callId]); + return events.rows.map( + (row) => + new CallEvent( + row.id, + row.call_id, + row.type, + new Date(row.timestamp), + row.metadata, + ), + ); + // throw new Error('CallService.getCallEvents not implemented'); } } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 7e82132..93b3193 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@voycelink/contracts": "*", + "axios": "^1.16.0", "next": "^14.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/packages/frontend/src/app/api/calls/route.ts b/packages/frontend/src/app/api/calls/route.ts new file mode 100644 index 0000000..c35803b --- /dev/null +++ b/packages/frontend/src/app/api/calls/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from 'next/server'; + +const CALL_SERVICE_URL = + process.env.NEXT_PUBLIC_CALL_SERVICE_URL ?? 'http://localhost:3001'; + +export async function GET(_request: Request) { + // Hago este step para que no se exponga el CALL_SERVICE_URL directamente al cliente + const response = await fetch(`${CALL_SERVICE_URL}/api/calls`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + cache: 'no-store', + }); + + const body = await response.text(); + return new NextResponse(body, { + status: response.status, + headers: { + 'Content-Type': response.headers.get('content-type') ?? 'application/json', + }, + }); +} diff --git a/packages/frontend/src/app/page.tsx b/packages/frontend/src/app/page.tsx index 0ddb140..6f35929 100644 --- a/packages/frontend/src/app/page.tsx +++ b/packages/frontend/src/app/page.tsx @@ -13,9 +13,11 @@ export default function DashboardPage() { const [filters, setFilters] = useState({ status: 'all' }); const [selectedCallId, setSelectedCallId] = useState(null); - const { calls, loading } = useCalls(filters); + const { calls, loading, isConnected } = useCalls(filters); const { events, loading: eventsLoading } = useCallEvents(selectedCallId); + // console.log({ events, eventsLoading }) + return (
{/* ── Header ─────────────────────────────────────────── */} @@ -26,10 +28,9 @@ export default function DashboardPage() {

Call Center Dashboard

- {/* TODO: show green dot + "Live" text when Socket.io is connected */} - - - Not connected + + + {isConnected ? 'Live' : 'Not connected'} diff --git a/packages/frontend/src/hooks/useCallEvents.ts b/packages/frontend/src/hooks/useCallEvents.ts index c428f4e..8bb144e 100644 --- a/packages/frontend/src/hooks/useCallEvents.ts +++ b/packages/frontend/src/hooks/useCallEvents.ts @@ -2,7 +2,8 @@ import { useState, useEffect } from 'react'; import { CallEvent } from '../types'; -import { MOCK_EVENTS } from '../mocks/data'; +import { fetchCallEvents } from '@/lib/api'; +import { getSocket, subscribeToCall, unsubscribeFromCall } from '@/lib/socket'; /** * Returns the event history for a specific call. @@ -19,13 +20,41 @@ export function useCallEvents(callId: string | null) { } setLoading(true); - // TODO: replace with fetchCallEvents(callId) - const t = setTimeout(() => { - setEvents(MOCK_EVENTS[callId] ?? []); - setLoading(false); - }, 200); - return () => clearTimeout(t); + async function loadCallEvents() { + setLoading(true); + try { + const data = await fetchCallEvents(callId as string); + setEvents(data); + } catch (error) { + console.error('Error fetching call events:', error); + // Optionally set an error state here + } finally { + setLoading(false); + } + } + + loadCallEvents(); + }, [callId]); + + useEffect(() => { + if (!callId) { + return; + } + + const socket = getSocket(); + + socket.on('call_status_update', console.log); + subscribeToCall(callId); + + if (!socket.connected) { + socket.connect(); + } + + return () => { + socket.off('call_status_update', console.log); + unsubscribeFromCall(callId); + }; }, [callId]); return { events, loading }; diff --git a/packages/frontend/src/hooks/useCalls.ts b/packages/frontend/src/hooks/useCalls.ts index a35ab33..cf6f60e 100644 --- a/packages/frontend/src/hooks/useCalls.ts +++ b/packages/frontend/src/hooks/useCalls.ts @@ -2,25 +2,55 @@ import { useState, useEffect } from 'react'; import { Call, CallFilters } from '../types'; -import { MOCK_CALLS } from '../mocks/data'; +import { fetchCalls } from '../lib/api'; +import { getSocket, subscribeToCall, unsubscribeFromCall } from '../lib/socket'; /** * Returns the live call list and a loading indicator. * TODO: replace mock data with real API + Socket.io updates. */ -export function useCalls(_filters: CallFilters) { +export function useCalls(filters: CallFilters) { const [calls, setCalls] = useState([]); const [loading, setLoading] = useState(true); + const [isConnected, setIsConnected] = useState(false); useEffect(() => { - // TODO: replace with fetchCalls(_filters) + socket subscription - const t = setTimeout(() => { - setCalls(MOCK_CALLS); - setLoading(false); - }, 300); + async function loadCalls() { + setLoading(true); + try { + const data = await fetchCalls(filters); + setCalls(data); + } catch (error) { + console.error('Error fetching calls:', error); + // Optionally set an error state here + } finally { + setLoading(false); + } + } - return () => clearTimeout(t); - }, []); + loadCalls(); + }, [filters]); - return { calls, loading, setCalls }; + useEffect(() => { + const socket = getSocket(); + + const onConnect = () => setIsConnected(true); + const onDisconnect = () => setIsConnected(false); + + socket.on('connect', onConnect); + socket.on('disconnect', onDisconnect); + + if (!socket.connected) { + socket.connect(); + } else { + setIsConnected(true); + } + + return () => { + socket.off('connect', onConnect); + socket.off('disconnect', onDisconnect); + }; + }, [filters]); + + return { calls, loading, isConnected }; } diff --git a/packages/realtime-service/src/bus/subscriber.ts b/packages/realtime-service/src/bus/subscriber.ts index 723b278..f97f585 100644 --- a/packages/realtime-service/src/bus/subscriber.ts +++ b/packages/realtime-service/src/bus/subscriber.ts @@ -6,8 +6,21 @@ const CHANNEL = 'call-status-updates'; export function subscribeToCallUpdates( onUpdate: (update: CallStatusUpdate) => void, ): void { - // TODO: subscribe to CHANNEL on Redis and call onUpdate for each message - void Redis; - void CHANNEL; - void onUpdate; + const redisUrl = process.env.REDIS_URL; + const subscriber = new Redis(redisUrl as string); + + subscriber.on('message', (channel, message) => { + if (channel !== CHANNEL) return; + + try { + const payload = JSON.parse(message) as CallStatusUpdate; + onUpdate(payload); + } catch (error) { + console.error('[redis] failed to parse call update:', error); + } + }); + + subscriber.subscribe(CHANNEL).catch((error) => { + console.error(`[redis] failed to subscribe to ${CHANNEL}:`, error); + }); } diff --git a/packages/realtime-service/src/socket/server.ts b/packages/realtime-service/src/socket/server.ts index d344552..4dd7f51 100644 --- a/packages/realtime-service/src/socket/server.ts +++ b/packages/realtime-service/src/socket/server.ts @@ -14,12 +14,12 @@ export function createSocketServer(httpServer: HttpServer): IoServer { io.on('connection', (socket) => { console.log(`[ws] client connected ${socket.id}`); - socket.on('subscribe_call', (_callId) => { - // TODO: socket.join(_callId); + socket.on('subscribe_call', (callId) => { + socket.join(callId); }); - socket.on('unsubscribe_call', (_callId) => { - // TODO: socket.leave(_callId); + socket.on('unsubscribe_call', (callId) => { + socket.leave(callId); }); socket.on('disconnect', () => { @@ -32,6 +32,6 @@ export function createSocketServer(httpServer: HttpServer): IoServer { export function broadcastStatusUpdate(update: CallStatusUpdate): void { if (!io) return; - // TODO: io.to(update.callId).emit('call_status_update', update); - io.emit('call_status_update', update); // naive – replace with room-based + io.to(update.callId).emit('call_status_update', update); + io.to('dashboard').emit('call_status_update', update); } From 6e158cd1cb32cf09e975e9124c633dd91ebea71e Mon Sep 17 00:00:00 2001 From: Oxicode Date: Fri, 8 May 2026 13:10:39 -0500 Subject: [PATCH 2/3] removiendo librerias que no no debi instalar --- package-lock.json | 81 +++------------------------------- packages/frontend/package.json | 1 - 2 files changed, 6 insertions(+), 76 deletions(-) diff --git a/package-lock.json b/package-lock.json index a7f1287..b4d30f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,6 @@ "workspaces": [ "packages/*" ], - "dependencies": { - "swr": "^2.4.1" - }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/parser": "^8.32.1", @@ -2855,6 +2852,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, "license": "MIT" }, "node_modules/autoprefixer": { @@ -2920,17 +2918,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" - } - }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3333,6 +3320,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -3652,6 +3640,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3675,15 +3664,6 @@ "node": ">= 0.8" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -4034,6 +4014,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5074,26 +5055,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -5131,6 +5092,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5564,6 +5526,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7741,15 +7704,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -9115,19 +9069,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swr": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", - "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -9681,15 +9622,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10256,7 +10188,6 @@ "version": "1.0.0", "dependencies": { "@voycelink/contracts": "*", - "axios": "^1.16.0", "next": "^14.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 93b3193..7e82132 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@voycelink/contracts": "*", - "axios": "^1.16.0", "next": "^14.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", From 82b22ea8d118f9bccf54eb254dfab242481b8f79 Mon Sep 17 00:00:00 2001 From: Oxicode Date: Fri, 8 May 2026 13:11:55 -0500 Subject: [PATCH 3/3] Ejecutando lint --- packages/call-service/src/services/CallService.ts | 2 +- packages/frontend/src/hooks/useCalls.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/call-service/src/services/CallService.ts b/packages/call-service/src/services/CallService.ts index 9d5c2f1..ed0a4cb 100644 --- a/packages/call-service/src/services/CallService.ts +++ b/packages/call-service/src/services/CallService.ts @@ -1,7 +1,7 @@ import type { PoolClient } from 'pg'; import { randomUUID } from 'crypto'; import { db } from '../db/client'; -import { publishStatusUpdate } from '../bus/publisher'; +// import { publishStatusUpdate } from '../bus/publisher'; import { Call, CallStatus, diff --git a/packages/frontend/src/hooks/useCalls.ts b/packages/frontend/src/hooks/useCalls.ts index cf6f60e..c5f5c83 100644 --- a/packages/frontend/src/hooks/useCalls.ts +++ b/packages/frontend/src/hooks/useCalls.ts @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import { Call, CallFilters } from '../types'; import { fetchCalls } from '../lib/api'; -import { getSocket, subscribeToCall, unsubscribeFromCall } from '../lib/socket'; +import { getSocket } from '../lib/socket'; /** * Returns the live call list and a loading indicator.