Pedido do usuário: "antes de ir para IVR no monitoramento tem que mostrar quantos ramais estão online e o status de cada ramal criado". apps/api roda no host e não alcança o ESL do FreeSWITCH diretamente (freeswitch:8021 só existe na rede interna do Docker) — mesmo padrão já documentado em platform-freeswitch.controller.ts. Por isso, seguido o mesmo caminho já usado por Trunk.status: apps/freeswitch-events (conexão ESL permanente) já consumia sofia::register/unregister/expire pra outros fins, então virou a fonte de verdade — grava `Extension.registeredAt` a cada evento, mais uma reconciliação completa (`show registrations`) ao conectar/reconectar no ESL pra não ficar com dado desatualizado se o serviço caiu no meio de uma sessão de registro. GET /extensions agora devolve `registeredAt`; a página /app/monitoramento ganhou um painel "Ramais" com status ao vivo (online/offline, desde quando, grupo de captura) e um instrumento "Ramais online: X de Y" — a mesma conexão SSE que já existia só precisou aprender a atualizar esse novo estado a partir dos eventos EXTENSION_REGISTERED/UNREGISTERED, que já estavam sendo publicados e só apareciam no log de eventos. Testado: reconciliação confirmada rodando no container real (2 ramais já registrados antes do restart do fs-events foram marcados online corretamente); GET /extensions confirmado devolvendo registeredAt (null num ramal recém-criado, sem registro). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
88 lines
3.5 KiB
TypeScript
88 lines
3.5 KiB
TypeScript
import { getPrismaClient, withTenantContext } from "@b2bcall/database";
|
|
import { createLogger } from "@b2bcall/shared";
|
|
import type { FreeSwitchTelephonyProvider } from "@b2bcall/telephony";
|
|
|
|
const logger = createLogger("b2bcall-fs-events");
|
|
|
|
/**
|
|
* sofia::register/unregister/expire não carregam `b2bcall_tenant_id` (só
|
|
* existe como channel variable de CHAMADA, nunca de REGISTER) — resolve
|
|
* por domínio (from-host), único por tenant desde a PHASE 52/53. Mesmo
|
|
* padrão de fan-out do `updateTrunkStatusFromGatewayEvent`, mas aqui o
|
|
* domínio já identifica o tenant direto, sem precisar tentar cada um.
|
|
*/
|
|
export async function updateExtensionRegistrationStatus(
|
|
user: string | undefined,
|
|
host: string | undefined,
|
|
registered: boolean,
|
|
): Promise<void> {
|
|
if (!user || !host) return;
|
|
const prisma = getPrismaClient();
|
|
|
|
const tenant = await prisma.tenant.findFirst({ where: { telephonyDomain: host }, select: { id: true } });
|
|
if (!tenant) {
|
|
logger.debug("nenhum tenant encontrado pro dominio do REGISTER", { host, user });
|
|
return;
|
|
}
|
|
|
|
const result = await withTenantContext(prisma, tenant.id, (tx) =>
|
|
tx.extension.updateMany({
|
|
where: { number: user, tenantId: tenant.id, deletedAt: null },
|
|
data: { registeredAt: registered ? new Date() : null },
|
|
}),
|
|
);
|
|
if (result.count > 0) {
|
|
logger.info("status de registro do ramal atualizado", { user, host, registered });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reconciliação ao conectar/reconectar no ESL (secao 55): sem isto, um
|
|
* ramal que já estava registrado ANTES do fs-events subir (ou durante uma
|
|
* queda do serviço) ficaria com `registered_at` desatualizado até o
|
|
* próximo REGISTER natural do aparelho — minutos de atraso, dependendo do
|
|
* `registration-expires` configurado no softphone. `show registrations`
|
|
* é a fonte de verdade ao vivo: zera todo mundo do tenant primeiro,
|
|
* depois marca só quem está de fato na lista agora.
|
|
*/
|
|
export async function reconcileExtensionRegistrations(provider: FreeSwitchTelephonyProvider): Promise<void> {
|
|
const prisma = getPrismaClient();
|
|
let raw: unknown;
|
|
try {
|
|
raw = await provider.getRegistrations();
|
|
} catch (err) {
|
|
logger.error("falha ao consultar registrations pra reconciliacao", { error: String(err) });
|
|
return;
|
|
}
|
|
|
|
const rows = (raw as { rows?: Array<{ reg_user?: string; realm?: string }> } | undefined)?.rows ?? [];
|
|
const registeredByDomain = new Map<string, Set<string>>();
|
|
for (const row of rows) {
|
|
if (!row.reg_user || !row.realm) continue;
|
|
if (!registeredByDomain.has(row.realm)) registeredByDomain.set(row.realm, new Set());
|
|
registeredByDomain.get(row.realm)!.add(row.reg_user);
|
|
}
|
|
|
|
const tenants = await prisma.tenant.findMany({
|
|
where: { status: "ACTIVE", telephonyDomain: { not: null } },
|
|
select: { id: true, telephonyDomain: true },
|
|
});
|
|
|
|
for (const tenant of tenants) {
|
|
const registeredNumbers = Array.from(registeredByDomain.get(tenant.telephonyDomain!) ?? []);
|
|
await withTenantContext(prisma, tenant.id, async (tx) => {
|
|
await tx.extension.updateMany({
|
|
where: { tenantId: tenant.id, deletedAt: null, number: { notIn: registeredNumbers } },
|
|
data: { registeredAt: null },
|
|
});
|
|
if (registeredNumbers.length > 0) {
|
|
await tx.extension.updateMany({
|
|
where: { tenantId: tenant.id, deletedAt: null, number: { in: registeredNumbers } },
|
|
data: { registeredAt: new Date() },
|
|
});
|
|
}
|
|
});
|
|
}
|
|
logger.info("reconciliacao de registrations de ramais concluida", { tenantsChecked: tenants.length });
|
|
}
|