feat: add structured versioned dialplan and asterisk admin

- packages/database: DialplanEntry + DialplanVersion (schema/config
  gerado, nunca dialplan cru vindo do usuario)
- apps/api/src/dialplan: DialplanService gera extensions a partir de
  entradas estruturadas (campos com allowlist estrita de caracteres),
  escreve em arquivo compartilhado via volume Docker 'dialplan-generated'
  entre api e asterisk, recarrega via AMI ('dialplan reload') e valida
  checando 'dialplan show <contexto>' por marcadores de erro. Falha na
  validacao dispara rollback automatico para a versao anterior; rollback
  manual tambem disponivel para qualquer versao no historico
- infrastructure/asterisk: extensions.conf agora inclui o arquivo gerado
  pela aplicacao; entrypoint garante que ele existe (vazio) no primeiro
  boot antes da primeira publicacao
- apps/api/src/asterisk-admin: status (AMI + heartbeat), module show, e
  diagnostico com ALLOWLIST ESTRITA de comandos exatos (nunca shell
  arbitraria) — agente.md secao 22

Testado ponta a ponta contra o Asterisk real: dialplan publicado fica
ativo imediatamente sem restart (confirmado via 'dialplan show'),
comando de diagnostico fora da allowlist rejeitado com 400.
This commit is contained in:
2026-08-27 13:14:50 -03:00
parent 8a41b5d2b2
commit cf3fc4b3ef
18 changed files with 789 additions and 10 deletions

View File

@@ -0,0 +1,33 @@
-- CreateEnum
CREATE TYPE "DialplanVersionStatus" AS ENUM ('APPLIED', 'FAILED', 'ROLLED_BACK');
-- CreateTable
CREATE TABLE "dialplan_entries" (
"id" TEXT NOT NULL,
"context" TEXT NOT NULL,
"exten" TEXT NOT NULL,
"priority" INTEGER NOT NULL,
"application" TEXT NOT NULL,
"argument" TEXT,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"order" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "dialplan_entries_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "dialplan_versions" (
"id" TEXT NOT NULL,
"generated_config" TEXT NOT NULL,
"status" "DialplanVersionStatus" NOT NULL,
"reload_result" TEXT,
"created_by_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "dialplan_versions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "dialplan_entries_context_idx" ON "dialplan_entries"("context");

View File

@@ -212,3 +212,42 @@ model ExtensionState {
@@map("extension_states")
}
// ===========================================================================
// Dialplan estruturado e versionado (agente.md seção 21). O modo "Advanced"
// citado na spec é apenas uma restrição de permissão no frontend sobre os
// mesmos dados — não um formato de armazenamento diferente.
// ===========================================================================
model DialplanEntry {
id String @id @default(uuid())
context String
exten String
priority Int
application String
argument String?
enabled Boolean @default(true)
order Int @default(0)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([context])
@@map("dialplan_entries")
}
enum DialplanVersionStatus {
APPLIED
FAILED
ROLLED_BACK
}
model DialplanVersion {
id String @id @default(uuid())
generatedConfig String @map("generated_config")
status DialplanVersionStatus
reloadResult String? @map("reload_result")
createdById String? @map("created_by_id")
createdAt DateTime @default(now()) @map("created_at")
@@map("dialplan_versions")
}