From a75113b6acaa493c57e6f96cfb6fa16c8f33b01d Mon Sep 17 00:00:00 2001
From: iamdoubz
Date: Wed, 28 Oct 2026 15:07:05 -0500
Subject: [PATCH] web: report spam from my calls, reports on Blocked numbers
Users report an outside call that rang them: the spam check's line when it
said medium or high, else a reason they type. Admins block or dismiss each
report on Blocked numbers, and the Dashboard counts them. Migration 0015 adds
spam_reports.
---
.../src/db/migrations/0015_spam_reports.sql | 11 +
.../src/db/migrations/meta/0015_snapshot.json | 4865 +++++++++++++++++
apps/api/src/db/migrations/meta/_journal.json | 7 +
apps/api/src/db/schema.ts | 13 +
apps/api/src/routes/blocked.ts | 30 +-
apps/api/src/routes/calls.ts | 56 +-
apps/api/test/blocked.test.ts | 73 +-
apps/api/test/schema.test.ts | 4 +-
apps/web/src/api/client.ts | 5 +
apps/web/src/pages/admin/BlockedPage.tsx | 50 +-
apps/web/src/pages/admin/DashboardPage.tsx | 17 +-
apps/web/src/pages/user/MyCallsPage.tsx | 70 +-
docs/spec/02-data-model.md | 8 +
docs/spec/04-backend-api.md | 2 +
docs/spec/05-web-ui.md | 4 +-
packages/shared/src/blocked.ts | 21 +-
16 files changed, 5212 insertions(+), 24 deletions(-)
create mode 100644 apps/api/src/db/migrations/0015_spam_reports.sql
create mode 100644 apps/api/src/db/migrations/meta/0015_snapshot.json
diff --git a/apps/api/src/db/migrations/0015_spam_reports.sql b/apps/api/src/db/migrations/0015_spam_reports.sql
new file mode 100644
index 0000000..79f58a5
--- /dev/null
+++ b/apps/api/src/db/migrations/0015_spam_reports.sql
@@ -0,0 +1,11 @@
+CREATE TABLE "spam_reports" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "number" text NOT NULL,
+ "reason" text NOT NULL,
+ "reported_by" uuid,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT "spam_reports_number_reportedBy_unique" UNIQUE("number","reported_by")
+);
+--> statement-breakpoint
+ALTER TABLE "spam_reports" ADD CONSTRAINT "spam_reports_reported_by_users_id_fk" FOREIGN KEY ("reported_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
\ No newline at end of file
diff --git a/apps/api/src/db/migrations/meta/0015_snapshot.json b/apps/api/src/db/migrations/meta/0015_snapshot.json
new file mode 100644
index 0000000..0ac4275
--- /dev/null
+++ b/apps/api/src/db/migrations/meta/0015_snapshot.json
@@ -0,0 +1,4865 @@
+{
+ "id": "3c27e38f-8601-4da6-b1eb-a7c82f9add78",
+ "prevId": "b1b1e555-676f-492e-b64e-da5244f74e5c",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.announcements": {
+ "name": "announcements",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sound_id": {
+ "name": "sound_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination": {
+ "name": "destination",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "announcements_sound_id_sound_files_id_fk": {
+ "name": "announcements_sound_id_sound_files_id_fk",
+ "tableFrom": "announcements",
+ "tableTo": "sound_files",
+ "columnsFrom": [
+ "sound_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.api_tokens": {
+ "name": "api_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_prefix": {
+ "name": "token_prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "api_tokens_created_by_users_id_fk": {
+ "name": "api_tokens_created_by_users_id_fk",
+ "tableFrom": "api_tokens",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "api_tokens_tokenHash_unique": {
+ "name": "api_tokens_tokenHash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token_hash"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audit_log": {
+ "name": "audit_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigserial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_ip": {
+ "name": "actor_ip",
+ "type": "inet",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "diff": {
+ "name": "diff",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "audit_log_actor_user_id_users_id_fk": {
+ "name": "audit_log_actor_user_id_users_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "users",
+ "columnsFrom": [
+ "actor_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.blocked_numbers": {
+ "name": "blocked_numbers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "blocked_numbers_number_unique": {
+ "name": "blocked_numbers_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.call_extensions": {
+ "name": "call_extensions",
+ "schema": "",
+ "columns": {
+ "call_id": {
+ "name": "call_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "extension_id": {
+ "name": "extension_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "call_extensions_call_id_calls_id_fk": {
+ "name": "call_extensions_call_id_calls_id_fk",
+ "tableFrom": "call_extensions",
+ "tableTo": "calls",
+ "columnsFrom": [
+ "call_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "call_extensions_extension_id_extensions_id_fk": {
+ "name": "call_extensions_extension_id_extensions_id_fk",
+ "tableFrom": "call_extensions",
+ "tableTo": "extensions",
+ "columnsFrom": [
+ "extension_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "call_extensions_call_id_extension_id_role_pk": {
+ "name": "call_extensions_call_id_extension_id_role_pk",
+ "columns": [
+ "call_id",
+ "extension_id",
+ "role"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "call_extensions_role_check": {
+ "name": "call_extensions_role_check",
+ "value": "\"call_extensions\".\"role\" in ('caller', 'rang', 'answered')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.calls": {
+ "name": "calls",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "linkedid": {
+ "name": "linkedid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "direction": {
+ "name": "direction",
+ "type": "call_direction",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "call_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "answered_at": {
+ "name": "answered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "from_number": {
+ "name": "from_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "from_name": {
+ "name": "from_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "to_number": {
+ "name": "to_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "did_id": {
+ "name": "did_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "answered_by_extension_id": {
+ "name": "answered_by_extension_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "talk_seconds": {
+ "name": "talk_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "recording_id": {
+ "name": "recording_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "spam_risk": {
+ "name": "spam_risk",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "spam_info": {
+ "name": "spam_info",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "calls_started_at_index": {
+ "name": "calls_started_at_index",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "calls_did_id_dids_id_fk": {
+ "name": "calls_did_id_dids_id_fk",
+ "tableFrom": "calls",
+ "tableTo": "dids",
+ "columnsFrom": [
+ "did_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "calls_answered_by_extension_id_extensions_id_fk": {
+ "name": "calls_answered_by_extension_id_extensions_id_fk",
+ "tableFrom": "calls",
+ "tableTo": "extensions",
+ "columnsFrom": [
+ "answered_by_extension_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "calls_recording_id_recordings_id_fk": {
+ "name": "calls_recording_id_recordings_id_fk",
+ "tableFrom": "calls",
+ "tableTo": "recordings",
+ "columnsFrom": [
+ "recording_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "calls_linkedid_unique": {
+ "name": "calls_linkedid_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "linkedid"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.cdr": {
+ "name": "cdr",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigserial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "uniqueid": {
+ "name": "uniqueid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "linkedid": {
+ "name": "linkedid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "start_at": {
+ "name": "start_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "answer_at": {
+ "name": "answer_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "end_at": {
+ "name": "end_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration": {
+ "name": "duration",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billsec": {
+ "name": "billsec",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "src": {
+ "name": "src",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dst": {
+ "name": "dst",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dcontext": {
+ "name": "dcontext",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "clid": {
+ "name": "clid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "channel": {
+ "name": "channel",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dstchannel": {
+ "name": "dstchannel",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lastapp": {
+ "name": "lastapp",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lastdata": {
+ "name": "lastdata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disposition": {
+ "name": "disposition",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "accountcode": {
+ "name": "accountcode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "userfield": {
+ "name": "userfield",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sp_did": {
+ "name": "sp_did",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sp_direction": {
+ "name": "sp_direction",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sp_recording": {
+ "name": "sp_recording",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "raw": {
+ "name": "raw",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "cdr_linkedid_index": {
+ "name": "cdr_linkedid_index",
+ "columns": [
+ {
+ "expression": "linkedid",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.conferences": {
+ "name": "conferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pin_enc": {
+ "name": "pin_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "admin_pin_enc": {
+ "name": "admin_pin_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "max_members": {
+ "name": "max_members",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 20
+ },
+ "record": {
+ "name": "record",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "conferences_number_unique": {
+ "name": "conferences_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.contact_shares": {
+ "name": "contact_shares",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "from_user_id": {
+ "name": "from_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "to_user_id": {
+ "name": "to_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "contact": {
+ "name": "contact",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "decided_at": {
+ "name": "decided_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "contact_shares_to_user_id_index": {
+ "name": "contact_shares_to_user_id_index",
+ "columns": [
+ {
+ "expression": "to_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"contact_shares\".\"status\" = 'pending'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "contact_shares_from_user_id_users_id_fk": {
+ "name": "contact_shares_from_user_id_users_id_fk",
+ "tableFrom": "contact_shares",
+ "tableTo": "users",
+ "columnsFrom": [
+ "from_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "contact_shares_to_user_id_users_id_fk": {
+ "name": "contact_shares_to_user_id_users_id_fk",
+ "tableFrom": "contact_shares",
+ "tableTo": "users",
+ "columnsFrom": [
+ "to_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "contact_shares_status_check": {
+ "name": "contact_shares_status_check",
+ "value": "\"contact_shares\".\"status\" in ('pending', 'accepted', 'denied')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.contacts": {
+ "name": "contacts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "owner_user_id": {
+ "name": "owner_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company": {
+ "name": "company",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "numbers": {
+ "name": "numbers",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "contacts_owner_user_id_users_id_fk": {
+ "name": "contacts_owner_user_id_users_id_fk",
+ "tableFrom": "contacts",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.contrack_call_log": {
+ "name": "contrack_call_log",
+ "schema": "",
+ "columns": {
+ "call_id": {
+ "name": "call_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "interaction_id": {
+ "name": "interaction_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "logged_at": {
+ "name": "logged_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "contrack_call_log_call_id_calls_id_fk": {
+ "name": "contrack_call_log_call_id_calls_id_fk",
+ "tableFrom": "contrack_call_log",
+ "tableTo": "calls",
+ "columnsFrom": [
+ "call_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "contrack_call_log_user_id_users_id_fk": {
+ "name": "contrack_call_log_user_id_users_id_fk",
+ "tableFrom": "contrack_call_log",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "contrack_call_log_call_id_user_id_pk": {
+ "name": "contrack_call_log_call_id_user_id_pk",
+ "columns": [
+ "call_id",
+ "user_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.devices": {
+ "name": "devices",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "extension_id": {
+ "name": "extension_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mac": {
+ "name": "mac",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor": {
+ "name": "vendor",
+ "type": "phone_vendor",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provisioning_password_enc": {
+ "name": "provisioning_password_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_provisioned_at": {
+ "name": "last_provisioned_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_provisioned_ip": {
+ "name": "last_provisioned_ip",
+ "type": "inet",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "devices_extension_id_extensions_id_fk": {
+ "name": "devices_extension_id_extensions_id_fk",
+ "tableFrom": "devices",
+ "tableTo": "extensions",
+ "columnsFrom": [
+ "extension_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "devices_mac_unique": {
+ "name": "devices_mac_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "mac"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.did_sms_users": {
+ "name": "did_sms_users",
+ "schema": "",
+ "columns": {
+ "did_id": {
+ "name": "did_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "did_sms_users_did_id_dids_id_fk": {
+ "name": "did_sms_users_did_id_dids_id_fk",
+ "tableFrom": "did_sms_users",
+ "tableTo": "dids",
+ "columnsFrom": [
+ "did_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "did_sms_users_user_id_users_id_fk": {
+ "name": "did_sms_users_user_id_users_id_fk",
+ "tableFrom": "did_sms_users",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "did_sms_users_did_id_user_id_pk": {
+ "name": "did_sms_users_did_id_user_id_pk",
+ "columns": [
+ "did_id",
+ "user_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.dids": {
+ "name": "dids",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "trunk_id": {
+ "name": "trunk_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "destination": {
+ "name": "destination",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "caller_id_name_prefix": {
+ "name": "caller_id_name_prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "record_inbound": {
+ "name": "record_inbound",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sms_enabled": {
+ "name": "sms_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "e911_address": {
+ "name": "e911_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "dids_trunk_id_trunks_id_fk": {
+ "name": "dids_trunk_id_trunks_id_fk",
+ "tableFrom": "dids",
+ "tableTo": "trunks",
+ "columnsFrom": [
+ "trunk_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "dids_number_unique": {
+ "name": "dids_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.emergency_events": {
+ "name": "emergency_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "extension_number": {
+ "name": "extension_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dialed": {
+ "name": "dialed",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "caller_id_used": {
+ "name": "caller_id_used",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "location": {
+ "name": "location",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notified": {
+ "name": "notified",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.extensions": {
+ "name": "extensions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sip_password_enc": {
+ "name": "sip_password_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "webrtc_enabled": {
+ "name": "webrtc_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "max_contacts": {
+ "name": "max_contacts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 5
+ },
+ "ring_seconds": {
+ "name": "ring_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 25
+ },
+ "voicemail_enabled": {
+ "name": "voicemail_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "voicemail_pin_enc": {
+ "name": "voicemail_pin_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "voicemail_email": {
+ "name": "voicemail_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "voicemail_attach_audio": {
+ "name": "voicemail_attach_audio",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "voicemail_delete_after_email": {
+ "name": "voicemail_delete_after_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "outbound_class": {
+ "name": "outbound_class",
+ "type": "outbound_class",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'national'"
+ },
+ "outbound_caller_id_did_id": {
+ "name": "outbound_caller_id_did_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "emergency_caller_id_did_id": {
+ "name": "emergency_caller_id_did_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "emergency_location": {
+ "name": "emergency_location",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "record_mode": {
+ "name": "record_mode",
+ "type": "record_mode",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'never'"
+ },
+ "pickup_group": {
+ "name": "pickup_group",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "language": {
+ "name": "language",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'en'"
+ },
+ "moh_class": {
+ "name": "moh_class",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "blf_numbers": {
+ "name": "blf_numbers",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "call_report": {
+ "name": "call_report",
+ "type": "call_report",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'off'"
+ },
+ "call_report_time": {
+ "name": "call_report_time",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'08:00'"
+ },
+ "call_report_weekday": {
+ "name": "call_report_weekday",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "call_report_summaries": {
+ "name": "call_report_summaries",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "call_report_sent_at": {
+ "name": "call_report_sent_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "extensions_user_id_users_id_fk": {
+ "name": "extensions_user_id_users_id_fk",
+ "tableFrom": "extensions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "extensions_outbound_caller_id_did_id_dids_id_fk": {
+ "name": "extensions_outbound_caller_id_did_id_dids_id_fk",
+ "tableFrom": "extensions",
+ "tableTo": "dids",
+ "columnsFrom": [
+ "outbound_caller_id_did_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "extensions_emergency_caller_id_did_id_dids_id_fk": {
+ "name": "extensions_emergency_caller_id_did_id_dids_id_fk",
+ "tableFrom": "extensions",
+ "tableTo": "dids",
+ "columnsFrom": [
+ "emergency_caller_id_did_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "extensions_number_unique": {
+ "name": "extensions_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "number"
+ ]
+ },
+ "extensions_userId_unique": {
+ "name": "extensions_userId_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ftc_complaints": {
+ "name": "ftc_complaints",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reported_at": {
+ "name": "reported_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "robocall": {
+ "name": "robocall",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "ftc_complaints_number_index": {
+ "name": "ftc_complaints_number_index",
+ "columns": [
+ {
+ "expression": "number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ftc_complaints_reported_at_index": {
+ "name": "ftc_complaints_reported_at_index",
+ "columns": [
+ {
+ "expression": "reported_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.generator_runs": {
+ "name": "generator_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigserial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "files_changed": {
+ "name": "files_changed",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "commands": {
+ "name": "commands",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ok": {
+ "name": "ok",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.holidays": {
+ "name": "holidays",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "start_date": {
+ "name": "start_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_date": {
+ "name": "end_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "recurring_yearly": {
+ "name": "recurring_yearly",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ivr_options": {
+ "name": "ivr_options",
+ "schema": "",
+ "columns": {
+ "ivr_id": {
+ "name": "ivr_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "digit": {
+ "name": "digit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination": {
+ "name": "destination",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ivr_options_ivr_id_ivrs_id_fk": {
+ "name": "ivr_options_ivr_id_ivrs_id_fk",
+ "tableFrom": "ivr_options",
+ "tableTo": "ivrs",
+ "columnsFrom": [
+ "ivr_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "ivr_options_ivr_id_digit_pk": {
+ "name": "ivr_options_ivr_id_digit_pk",
+ "columns": [
+ "ivr_id",
+ "digit"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "ivr_options_digit_check": {
+ "name": "ivr_options_digit_check",
+ "value": "\"ivr_options\".\"digit\" ~ '^[0-9*#]$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.ivrs": {
+ "name": "ivrs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "greeting_sound_id": {
+ "name": "greeting_sound_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "invalid_sound_id": {
+ "name": "invalid_sound_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "timeout_seconds": {
+ "name": "timeout_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 5
+ },
+ "max_retries": {
+ "name": "max_retries",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 3
+ },
+ "allow_direct_dial": {
+ "name": "allow_direct_dial",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "timeout_destination": {
+ "name": "timeout_destination",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "invalid_destination": {
+ "name": "invalid_destination",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ivrs_greeting_sound_id_sound_files_id_fk": {
+ "name": "ivrs_greeting_sound_id_sound_files_id_fk",
+ "tableFrom": "ivrs",
+ "tableTo": "sound_files",
+ "columnsFrom": [
+ "greeting_sound_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "ivrs_invalid_sound_id_sound_files_id_fk": {
+ "name": "ivrs_invalid_sound_id_sound_files_id_fk",
+ "tableFrom": "ivrs",
+ "tableTo": "sound_files",
+ "columnsFrom": [
+ "invalid_sound_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ivrs_number_unique": {
+ "name": "ivrs_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mobile_devices": {
+ "name": "mobile_devices",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "platform": {
+ "name": "platform",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_prefix": {
+ "name": "token_prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pair_code_hash": {
+ "name": "pair_code_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pair_expires_at": {
+ "name": "pair_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "mobile_devices_user_id_index": {
+ "name": "mobile_devices_user_id_index",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mobile_devices_user_id_users_id_fk": {
+ "name": "mobile_devices_user_id_users_id_fk",
+ "tableFrom": "mobile_devices",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mobile_devices_tokenHash_unique": {
+ "name": "mobile_devices_tokenHash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token_hash"
+ ]
+ },
+ "mobile_devices_pairCodeHash_unique": {
+ "name": "mobile_devices_pairCodeHash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "pair_code_hash"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "mobile_devices_platform_check": {
+ "name": "mobile_devices_platform_check",
+ "value": "\"mobile_devices\".\"platform\" in ('android', 'ios')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.moh_classes": {
+ "name": "moh_classes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "shuffle": {
+ "name": "shuffle",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "moh_classes_name_unique": {
+ "name": "moh_classes_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.outbound_routes": {
+ "name": "outbound_routes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "patterns": {
+ "name": "patterns",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "required_class": {
+ "name": "required_class",
+ "type": "outbound_class",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "trunk_ids": {
+ "name": "trunk_ids",
+ "type": "uuid[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "caller_id_did_id": {
+ "name": "caller_id_did_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "outbound_routes_caller_id_did_id_dids_id_fk": {
+ "name": "outbound_routes_caller_id_did_id_dids_id_fk",
+ "tableFrom": "outbound_routes",
+ "tableTo": "dids",
+ "columnsFrom": [
+ "caller_id_did_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.paging_group_members": {
+ "name": "paging_group_members",
+ "schema": "",
+ "columns": {
+ "paging_group_id": {
+ "name": "paging_group_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "extension_id": {
+ "name": "extension_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "paging_group_members_paging_group_id_paging_groups_id_fk": {
+ "name": "paging_group_members_paging_group_id_paging_groups_id_fk",
+ "tableFrom": "paging_group_members",
+ "tableTo": "paging_groups",
+ "columnsFrom": [
+ "paging_group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "paging_group_members_extension_id_extensions_id_fk": {
+ "name": "paging_group_members_extension_id_extensions_id_fk",
+ "tableFrom": "paging_group_members",
+ "tableTo": "extensions",
+ "columnsFrom": [
+ "extension_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "paging_group_members_paging_group_id_extension_id_pk": {
+ "name": "paging_group_members_paging_group_id_extension_id_pk",
+ "columns": [
+ "paging_group_id",
+ "extension_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.paging_groups": {
+ "name": "paging_groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duplex": {
+ "name": "duplex",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "paging_groups_number_unique": {
+ "name": "paging_groups_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.queue_calls": {
+ "name": "queue_calls",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigserial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "queue_id": {
+ "name": "queue_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uniqueid": {
+ "name": "uniqueid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "linkedid": {
+ "name": "linkedid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "joined_at": {
+ "name": "joined_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "outcome": {
+ "name": "outcome",
+ "type": "queue_outcome",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "wait_seconds": {
+ "name": "wait_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_extension_id": {
+ "name": "agent_extension_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "queue_calls_queue_id_joined_at_index": {
+ "name": "queue_calls_queue_id_joined_at_index",
+ "columns": [
+ {
+ "expression": "queue_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "joined_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "queue_calls_uniqueid_index": {
+ "name": "queue_calls_uniqueid_index",
+ "columns": [
+ {
+ "expression": "uniqueid",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "queue_calls_queue_id_queues_id_fk": {
+ "name": "queue_calls_queue_id_queues_id_fk",
+ "tableFrom": "queue_calls",
+ "tableTo": "queues",
+ "columnsFrom": [
+ "queue_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "queue_calls_agent_extension_id_extensions_id_fk": {
+ "name": "queue_calls_agent_extension_id_extensions_id_fk",
+ "tableFrom": "queue_calls",
+ "tableTo": "extensions",
+ "columnsFrom": [
+ "agent_extension_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.queue_member_state": {
+ "name": "queue_member_state",
+ "schema": "",
+ "columns": {
+ "queue_id": {
+ "name": "queue_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "extension_id": {
+ "name": "extension_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "paused": {
+ "name": "paused",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "queue_member_state_queue_id_queues_id_fk": {
+ "name": "queue_member_state_queue_id_queues_id_fk",
+ "tableFrom": "queue_member_state",
+ "tableTo": "queues",
+ "columnsFrom": [
+ "queue_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "queue_member_state_extension_id_extensions_id_fk": {
+ "name": "queue_member_state_extension_id_extensions_id_fk",
+ "tableFrom": "queue_member_state",
+ "tableTo": "extensions",
+ "columnsFrom": [
+ "extension_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "queue_member_state_queue_id_extension_id_pk": {
+ "name": "queue_member_state_queue_id_extension_id_pk",
+ "columns": [
+ "queue_id",
+ "extension_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.queue_members": {
+ "name": "queue_members",
+ "schema": "",
+ "columns": {
+ "queue_id": {
+ "name": "queue_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "extension_id": {
+ "name": "extension_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "penalty": {
+ "name": "penalty",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "queue_members_queue_id_queues_id_fk": {
+ "name": "queue_members_queue_id_queues_id_fk",
+ "tableFrom": "queue_members",
+ "tableTo": "queues",
+ "columnsFrom": [
+ "queue_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "queue_members_extension_id_extensions_id_fk": {
+ "name": "queue_members_extension_id_extensions_id_fk",
+ "tableFrom": "queue_members",
+ "tableTo": "extensions",
+ "columnsFrom": [
+ "extension_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "queue_members_queue_id_extension_id_pk": {
+ "name": "queue_members_queue_id_extension_id_pk",
+ "columns": [
+ "queue_id",
+ "extension_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.queues": {
+ "name": "queues",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "strategy": {
+ "name": "strategy",
+ "type": "queue_strategy",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'rrmemory'"
+ },
+ "member_ring_seconds": {
+ "name": "member_ring_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 15
+ },
+ "retry_seconds": {
+ "name": "retry_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 5
+ },
+ "wrapup_seconds": {
+ "name": "wrapup_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 10
+ },
+ "max_wait_seconds": {
+ "name": "max_wait_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 300
+ },
+ "max_callers": {
+ "name": "max_callers",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "moh_class": {
+ "name": "moh_class",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "announce_position": {
+ "name": "announce_position",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "announce_every_seconds": {
+ "name": "announce_every_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 60
+ },
+ "join_when_no_agents": {
+ "name": "join_when_no_agents",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "record": {
+ "name": "record",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sla_seconds": {
+ "name": "sla_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 20
+ },
+ "caller_id_name_prefix": {
+ "name": "caller_id_name_prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "timeout_destination": {
+ "name": "timeout_destination",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "queues_number_unique": {
+ "name": "queues_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.recordings": {
+ "name": "recordings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "linkedid": {
+ "name": "linkedid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "size_bytes": {
+ "name": "size_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "transcript": {
+ "name": "transcript",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ai_done_at": {
+ "name": "ai_done_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ai_error": {
+ "name": "ai_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "recordings_filePath_unique": {
+ "name": "recordings_filePath_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "file_path"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ring_group_members": {
+ "name": "ring_group_members",
+ "schema": "",
+ "columns": {
+ "ring_group_id": {
+ "name": "ring_group_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "extension_id": {
+ "name": "extension_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ring_group_members_ring_group_id_ring_groups_id_fk": {
+ "name": "ring_group_members_ring_group_id_ring_groups_id_fk",
+ "tableFrom": "ring_group_members",
+ "tableTo": "ring_groups",
+ "columnsFrom": [
+ "ring_group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "ring_group_members_extension_id_extensions_id_fk": {
+ "name": "ring_group_members_extension_id_extensions_id_fk",
+ "tableFrom": "ring_group_members",
+ "tableTo": "extensions",
+ "columnsFrom": [
+ "extension_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "ring_group_members_ring_group_id_extension_id_pk": {
+ "name": "ring_group_members_ring_group_id_extension_id_pk",
+ "columns": [
+ "ring_group_id",
+ "extension_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ring_groups": {
+ "name": "ring_groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "strategy": {
+ "name": "strategy",
+ "type": "ring_strategy",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ringall'"
+ },
+ "ring_seconds": {
+ "name": "ring_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 25
+ },
+ "caller_id_name_prefix": {
+ "name": "caller_id_name_prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "music_instead_of_ring": {
+ "name": "music_instead_of_ring",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "no_answer_destination": {
+ "name": "no_answer_destination",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ring_groups_number_unique": {
+ "name": "ring_groups_number_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "number"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sessions": {
+ "name": "sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip": {
+ "name": "ip",
+ "type": "inet",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "method": {
+ "name": "method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'password'"
+ },
+ "idp_refresh_enc": {
+ "name": "idp_refresh_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "idp_id_token_enc": {
+ "name": "idp_id_token_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "idp_checked_at": {
+ "name": "idp_checked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "sessions_user_id_index": {
+ "name": "sessions_user_id_index",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sessions_user_id_users_id_fk": {
+ "name": "sessions_user_id_users_id_fk",
+ "tableFrom": "sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "sessions_method_check": {
+ "name": "sessions_method_check",
+ "value": "\"sessions\".\"method\" in ('password', 'ldap', 'oidc')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.settings": {
+ "name": "settings",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sms_messages": {
+ "name": "sms_messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "did_id": {
+ "name": "did_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "direction": {
+ "name": "direction",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "remote_number": {
+ "name": "remote_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "media": {
+ "name": "media",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sent_by_user_id": {
+ "name": "sent_by_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "read_at": {
+ "name": "read_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "sms_messages_did_id_remote_number_created_at_index": {
+ "name": "sms_messages_did_id_remote_number_created_at_index",
+ "columns": [
+ {
+ "expression": "did_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "remote_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sms_messages_did_id_dids_id_fk": {
+ "name": "sms_messages_did_id_dids_id_fk",
+ "tableFrom": "sms_messages",
+ "tableTo": "dids",
+ "columnsFrom": [
+ "did_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "sms_messages_sent_by_user_id_users_id_fk": {
+ "name": "sms_messages_sent_by_user_id_users_id_fk",
+ "tableFrom": "sms_messages",
+ "tableTo": "users",
+ "columnsFrom": [
+ "sent_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "sms_messages_direction_check": {
+ "name": "sms_messages_direction_check",
+ "value": "\"sms_messages\".\"direction\" in ('in', 'out')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.sound_files": {
+ "name": "sound_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "moh_class_id": {
+ "name": "moh_class_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tts_text": {
+ "name": "tts_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "original_filename": {
+ "name": "original_filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "asterisk_name": {
+ "name": "asterisk_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sound_files_moh_class_id_moh_classes_id_fk": {
+ "name": "sound_files_moh_class_id_moh_classes_id_fk",
+ "tableFrom": "sound_files",
+ "tableTo": "moh_classes",
+ "columnsFrom": [
+ "moh_class_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "sound_files_asteriskName_unique": {
+ "name": "sound_files_asteriskName_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "asterisk_name"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "sound_files_kind_check": {
+ "name": "sound_files_kind_check",
+ "value": "\"sound_files\".\"kind\" in ('prompt', 'moh')"
+ },
+ "sound_files_source_check": {
+ "name": "sound_files_source_check",
+ "value": "\"sound_files\".\"source\" in ('upload', 'tts', 'phone')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.spam_lookups": {
+ "name": "spam_lookups",
+ "schema": "",
+ "columns": {
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "risk": {
+ "name": "risk",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "info": {
+ "name": "info",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "spam_lookups_expires_at_index": {
+ "name": "spam_lookups_expires_at_index",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "spam_lookups_risk_check": {
+ "name": "spam_lookups_risk_check",
+ "value": "\"spam_lookups\".\"risk\" in ('low', 'medium', 'high')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.spam_reports": {
+ "name": "spam_reports",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "number": {
+ "name": "number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reported_by": {
+ "name": "reported_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "spam_reports_reported_by_users_id_fk": {
+ "name": "spam_reports_reported_by_users_id_fk",
+ "tableFrom": "spam_reports",
+ "tableTo": "users",
+ "columnsFrom": [
+ "reported_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "spam_reports_number_reportedBy_unique": {
+ "name": "spam_reports_number_reportedBy_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "number",
+ "reported_by"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.system_events": {
+ "name": "system_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigserial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "severity": {
+ "name": "severity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "system_events_created_at_index": {
+ "name": "system_events_created_at_index",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "system_events_severity_check": {
+ "name": "system_events_severity_check",
+ "value": "\"system_events\".\"severity\" in ('info', 'warning', 'error')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.time_conditions": {
+ "name": "time_conditions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "open_ranges": {
+ "name": "open_ranges",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "use_holidays": {
+ "name": "use_holidays",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "override_digit": {
+ "name": "override_digit",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "override_state": {
+ "name": "override_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "open_destination": {
+ "name": "open_destination",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "closed_destination": {
+ "name": "closed_destination",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "holiday_destination": {
+ "name": "holiday_destination",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "time_conditions_overrideDigit_unique": {
+ "name": "time_conditions_overrideDigit_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "override_digit"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "time_conditions_override_digit_check": {
+ "name": "time_conditions_override_digit_check",
+ "value": "\"time_conditions\".\"override_digit\" between 1 and 9"
+ },
+ "time_conditions_override_state_check": {
+ "name": "time_conditions_override_state_check",
+ "value": "\"time_conditions\".\"override_state\" in ('open', 'closed')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.trunks": {
+ "name": "trunks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "trunk_provider",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'voipms'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "server_host": {
+ "name": "server_host",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "server_port": {
+ "name": "server_port",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 5060
+ },
+ "transport": {
+ "name": "transport",
+ "type": "sip_transport",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'udp'"
+ },
+ "auth_mode": {
+ "name": "auth_mode",
+ "type": "trunk_auth_mode",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'registration'"
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password_enc": {
+ "name": "password_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "from_user": {
+ "name": "from_user",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "from_domain": {
+ "name": "from_domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "codecs": {
+ "name": "codecs",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{ulaw}'"
+ },
+ "srtp": {
+ "name": "srtp",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "max_channels": {
+ "name": "max_channels",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "voipms_api_username": {
+ "name": "voipms_api_username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "voipms_api_password_enc": {
+ "name": "voipms_api_password_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "trunks_name_unique": {
+ "name": "trunks_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_identities": {
+ "name": "user_identities",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "identity_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "groups": {
+ "name": "groups",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_identities_user_id_users_id_fk": {
+ "name": "user_identities_user_id_users_id_fk",
+ "tableFrom": "user_identities",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_identities_kind_subject_unique": {
+ "name": "user_identities_kind_subject_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "kind",
+ "subject"
+ ]
+ },
+ "user_identities_userId_kind_unique": {
+ "name": "user_identities_userId_kind_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id",
+ "kind"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_runtime_state": {
+ "name": "user_runtime_state",
+ "schema": "",
+ "columns": {
+ "extension_id": {
+ "name": "extension_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "dnd": {
+ "name": "dnd",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cfu": {
+ "name": "cfu",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cfb": {
+ "name": "cfb",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cfna": {
+ "name": "cfna",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_runtime_state_extension_id_extensions_id_fk": {
+ "name": "user_runtime_state_extension_id_extensions_id_fk",
+ "tableFrom": "user_runtime_state",
+ "tableTo": "extensions",
+ "columnsFrom": [
+ "extension_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "email": {
+ "name": "email",
+ "type": "citext",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "user_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "totp_secret_enc": {
+ "name": "totp_secret_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "totp_recovery_hashes": {
+ "name": "totp_recovery_hashes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "totp_last_step": {
+ "name": "totp_last_step",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "must_change_password": {
+ "name": "must_change_password",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "last_login_at": {
+ "name": "last_login_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contrack_token_enc": {
+ "name": "contrack_token_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contrack_log_calls": {
+ "name": "contrack_log_calls",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "contrack_log_summaries": {
+ "name": "contrack_log_summaries",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "contrack_log_since": {
+ "name": "contrack_log_since",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.voicemail_messages": {
+ "name": "voicemail_messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "extension_id": {
+ "name": "extension_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "msg_id": {
+ "name": "msg_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "folder": {
+ "name": "folder",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "caller_number": {
+ "name": "caller_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "caller_name": {
+ "name": "caller_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_s": {
+ "name": "duration_s",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "received_at": {
+ "name": "received_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_path": {
+ "name": "file_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "emailed_at": {
+ "name": "emailed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "transcript": {
+ "name": "transcript",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "voicemail_messages_extension_id_extensions_id_fk": {
+ "name": "voicemail_messages_extension_id_extensions_id_fk",
+ "tableFrom": "voicemail_messages",
+ "tableTo": "extensions",
+ "columnsFrom": [
+ "extension_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "voicemail_messages_msgId_unique": {
+ "name": "voicemail_messages_msgId_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "msg_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webhook_deliveries": {
+ "name": "webhook_deliveries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigserial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "webhook_id": {
+ "name": "webhook_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event": {
+ "name": "event",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attempt": {
+ "name": "attempt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "response_code": {
+ "name": "response_code",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "webhook_deliveries_webhook_id_webhooks_id_fk": {
+ "name": "webhook_deliveries_webhook_id_webhooks_id_fk",
+ "tableFrom": "webhook_deliveries",
+ "tableTo": "webhooks",
+ "columnsFrom": [
+ "webhook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webhooks": {
+ "name": "webhooks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret_enc": {
+ "name": "secret_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "events": {
+ "name": "events",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.call_direction": {
+ "name": "call_direction",
+ "schema": "public",
+ "values": [
+ "inbound",
+ "outbound",
+ "internal"
+ ]
+ },
+ "public.call_report": {
+ "name": "call_report",
+ "schema": "public",
+ "values": [
+ "off",
+ "hourly",
+ "daily",
+ "weekly"
+ ]
+ },
+ "public.call_status": {
+ "name": "call_status",
+ "schema": "public",
+ "values": [
+ "answered",
+ "missed",
+ "voicemail",
+ "abandoned",
+ "busy",
+ "failed"
+ ]
+ },
+ "public.identity_kind": {
+ "name": "identity_kind",
+ "schema": "public",
+ "values": [
+ "oidc",
+ "ldap"
+ ]
+ },
+ "public.outbound_class": {
+ "name": "outbound_class",
+ "schema": "public",
+ "values": [
+ "internal",
+ "local",
+ "national",
+ "international"
+ ]
+ },
+ "public.phone_vendor": {
+ "name": "phone_vendor",
+ "schema": "public",
+ "values": [
+ "yealink",
+ "grandstream",
+ "polycom",
+ "generic"
+ ]
+ },
+ "public.queue_outcome": {
+ "name": "queue_outcome",
+ "schema": "public",
+ "values": [
+ "answered",
+ "abandoned",
+ "timed_out"
+ ]
+ },
+ "public.queue_strategy": {
+ "name": "queue_strategy",
+ "schema": "public",
+ "values": [
+ "ringall",
+ "leastrecent",
+ "fewestcalls",
+ "random",
+ "rrmemory",
+ "linear"
+ ]
+ },
+ "public.record_mode": {
+ "name": "record_mode",
+ "schema": "public",
+ "values": [
+ "never",
+ "inbound",
+ "outbound",
+ "all"
+ ]
+ },
+ "public.ring_strategy": {
+ "name": "ring_strategy",
+ "schema": "public",
+ "values": [
+ "ringall",
+ "hunt"
+ ]
+ },
+ "public.sip_transport": {
+ "name": "sip_transport",
+ "schema": "public",
+ "values": [
+ "udp",
+ "tcp",
+ "tls"
+ ]
+ },
+ "public.trunk_auth_mode": {
+ "name": "trunk_auth_mode",
+ "schema": "public",
+ "values": [
+ "registration",
+ "ip"
+ ]
+ },
+ "public.trunk_provider": {
+ "name": "trunk_provider",
+ "schema": "public",
+ "values": [
+ "voipms",
+ "generic"
+ ]
+ },
+ "public.user_role": {
+ "name": "user_role",
+ "schema": "public",
+ "values": [
+ "admin",
+ "user"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/api/src/db/migrations/meta/_journal.json b/apps/api/src/db/migrations/meta/_journal.json
index f17f781..7839cc3 100644
--- a/apps/api/src/db/migrations/meta/_journal.json
+++ b/apps/api/src/db/migrations/meta/_journal.json
@@ -106,6 +106,13 @@
"when": 1793127210386,
"tag": "0014_spam_check",
"breakpoints": true
+ },
+ {
+ "idx": 15,
+ "version": "7",
+ "when": 1793212840276,
+ "tag": "0015_spam_reports",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts
index 9bb0f6a..fd549ac 100644
--- a/apps/api/src/db/schema.ts
+++ b/apps/api/src/db/schema.ts
@@ -670,6 +670,19 @@ export const ftcComplaints = pgTable(
(t) => [index().on(t.number), index().on(t.reportedAt)],
);
+/** "Report spam" from a user's call history, waiting for an admin (05 §2 Blocked numbers). */
+export const spamReports = pgTable(
+ 'spam_reports',
+ {
+ id: id(),
+ number: text().notNull(),
+ reason: text().notNull(),
+ reportedBy: uuid().references(() => users.id, { onDelete: 'set null' }),
+ ...timestamps,
+ },
+ (t) => [unique().on(t.number, t.reportedBy)],
+);
+
export const callExtensions = pgTable(
'call_extensions',
{
diff --git a/apps/api/src/routes/blocked.ts b/apps/api/src/routes/blocked.ts
index 5a97ca2..afcd181 100644
--- a/apps/api/src/routes/blocked.ts
+++ b/apps/api/src/routes/blocked.ts
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// /api/blocked-numbers (04 §7.3): PostgreSQL, then AstDB SP/BLOCK/, which every trunk
-// entry checks (03 §6.4). Admin only.
+// entry checks (03 §6.4). /api/spam-reports: users' Report spam, to block or dismiss. Admin only.
import { desc, eq } from 'drizzle-orm';
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
@@ -9,9 +9,10 @@ import {
formatNumber,
type BlockedDto,
type ListResponse,
+ type SpamReportDto,
} from '@selfphoneng/shared';
import { requireAdmin } from '../auth/guards.ts';
-import { blockedNumbers } from '../db/schema.ts';
+import { blockedNumbers, spamReports, users } from '../db/schema.ts';
import { conflict, notFound } from '../errors.ts';
import { setBlock } from '../services/astdb.ts';
import { audit } from '../services/audit.ts';
@@ -52,6 +53,7 @@ export async function blockedRoutes(app: FastifyInstance) {
throw e;
}
await mirror(b.number, true);
+ await db.delete(spamReports).where(eq(spamReports.number, b.number));
await audit(db, {
actorUserId: req.user!.id,
actorIp: req.ip,
@@ -78,4 +80,28 @@ export async function blockedRoutes(app: FastifyInstance) {
});
return { ok: true };
});
+
+ app.get('/api/spam-reports', async (): Promise> => ({
+ items: (
+ await db
+ .select({ r: spamReports, name: users.displayName })
+ .from(spamReports)
+ .leftJoin(users, eq(users.id, spamReports.reportedBy))
+ .orderBy(desc(spamReports.createdAt))
+ ).map(({ r, name }) => ({
+ id: r.id,
+ number: r.number,
+ reason: r.reason,
+ reportedBy: name,
+ createdAt: r.createdAt.toISOString(),
+ })),
+ nextCursor: null,
+ }));
+
+ app.delete('/api/spam-reports/:id', async (req) => {
+ const { id } = IdParams.parse(req.params);
+ const [r] = await db.delete(spamReports).where(eq(spamReports.id, id)).returning();
+ if (!r) throw notFound('Already handled');
+ return { ok: true };
+ });
}
diff --git a/apps/api/src/routes/calls.ts b/apps/api/src/routes/calls.ts
index bbfb10c..89b23ab 100644
--- a/apps/api/src/routes/calls.ts
+++ b/apps/api/src/routes/calls.ts
@@ -1,11 +1,15 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// /api/calls (admin) and /api/me/calls (04 §7.3): call history, newest first, keyset paging.
-import { and, asc, desc, eq, ilike, inArray, or, sql, type SQL } from 'drizzle-orm';
+// /api/me/spam-reports: Report spam on a call that rang me (05 §4).
+import { and, asc, desc, eq, ilike, inArray, ne, or, sql, type SQL } from 'drizzle-orm';
import type { FastifyInstance } from 'fastify';
import { alias } from 'drizzle-orm/pg-core';
import { z } from 'zod';
import {
CallFilterSchema,
+ SpamReportCreateSchema,
+ formatNumber,
+ normalizeNumber,
type CallDetailDto,
type CallDto,
type ListResponse,
@@ -14,9 +18,17 @@ import {
} from '@selfphoneng/shared';
import { requireAdmin, requireAuth } from '../auth/guards.ts';
import type { Db } from '../db/client.ts';
-import { callExtensions, calls, cdr, dids, extensions } from '../db/schema.ts';
+import {
+ blockedNumbers,
+ callExtensions,
+ calls,
+ cdr,
+ dids,
+ extensions,
+ spamReports,
+} from '../db/schema.ts';
import { contactNames } from '../contacts/book.ts';
-import { badRequest, notFound } from '../errors.ts';
+import { badRequest, conflict, notFound } from '../errors.ts';
import { getSetting } from '../services/settings.ts';
const answerer = alias(extensions, 'answerer');
@@ -224,4 +236,42 @@ export async function callRoutes(app: FastifyInstance) {
};
},
);
+
+ /** The admins see it on Blocked numbers and block or dismiss it. */
+ app.post('/api/me/spam-reports', { preHandler: requireAuth }, async (req, reply) => {
+ const body = SpamReportCreateSchema.parse(req.body);
+ const [c] = await db
+ .select({ number: calls.fromNumber })
+ .from(calls)
+ .innerJoin(callExtensions, eq(callExtensions.callId, calls.id))
+ .innerJoin(extensions, eq(extensions.id, callExtensions.extensionId))
+ .where(
+ and(
+ eq(calls.id, body.callId),
+ eq(calls.direction, 'inbound'),
+ eq(extensions.userId, req.user!.id),
+ ne(callExtensions.role, 'caller'),
+ ),
+ )
+ .limit(1);
+ if (!c) throw notFound('Not a call from outside that rang you');
+ let number: string;
+ try {
+ number = normalizeNumber(c.number ?? '');
+ } catch {
+ throw badRequest("This caller's number was hidden, so it can't be blocked");
+ }
+ const [blocked] = await db
+ .select({ id: blockedNumbers.id })
+ .from(blockedNumbers)
+ .where(eq(blockedNumbers.number, number));
+ if (blocked) throw conflict(`${formatNumber(number)} is already blocked`);
+ const [r] = await db
+ .insert(spamReports)
+ .values({ number, reason: body.reason, reportedBy: req.user!.id })
+ .onConflictDoNothing()
+ .returning();
+ if (!r) throw conflict(`You already reported ${formatNumber(number)}`);
+ return reply.code(201).send({ ok: true });
+ });
}
diff --git a/apps/api/test/blocked.test.ts b/apps/api/test/blocked.test.ts
index 7d7abb4..76cb8ad 100644
--- a/apps/api/test/blocked.test.ts
+++ b/apps/api/test/blocked.test.ts
@@ -1,25 +1,32 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
-// M6-T8: /api/blocked-numbers and the AstDB mirror (04 §5.5).
+// M6-T8: /api/blocked-numbers and the AstDB mirror (04 §5.5); users' Report spam (05 §4).
import type { LightMyRequestResponse } from 'fastify';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { callExtensions, calls, extensions } from '../src/db/schema.ts';
+import { encrypt } from '../src/crypto/secrets.ts';
import { syncAstDb } from '../src/services/astdb.ts';
import { hasTestDb } from './db.ts';
import { ADMIN, H, createTestApp } from './app.ts';
describe.skipIf(!hasTestDb)('/api/blocked-numbers', () => {
let t: Awaited>;
- let req: (method: string, url: string, payload?: unknown) => Promise;
+ let req: (
+ method: string,
+ url: string,
+ payload?: unknown,
+ as?: string,
+ ) => Promise;
beforeAll(async () => {
t = await createTestApp();
await t.clearMustChange(ADMIN.email);
const cookie = await t.login(ADMIN.email, ADMIN.password);
- req = (method, url, payload) =>
+ req = (method, url, payload, as = cookie) =>
t.app.inject({
method: method as 'GET',
url,
payload: payload as object,
- headers: { ...H, cookie },
+ headers: { ...H, cookie: as },
});
});
afterAll(() => t?.close());
@@ -59,4 +66,62 @@ describe.skipIf(!hasTestDb)('/api/blocked-numbers', () => {
});
expect((await req('GET', '/api/blocked-numbers')).json().items).toEqual([]);
});
+
+ it('Report spam: only on an outside call that rang me; Block clears it, Dismiss drops it', async () => {
+ const sam = await t.addUser('sam@example.com', 'sam-password-123');
+ await t.addUser('pat@example.com', 'pat-password-123');
+ const [ext] = await t.db
+ .insert(extensions)
+ .values({
+ number: '103',
+ name: 'Sam',
+ sipPasswordEnc: encrypt('x'),
+ voicemailPinEnc: encrypt('4826'),
+ userId: sam.id,
+ })
+ .returning();
+ const call = async (linkedid: string, from: string | null, role: string) => {
+ const [c] = await t.db
+ .insert(calls)
+ .values({
+ linkedid,
+ direction: 'inbound',
+ status: 'answered',
+ fromNumber: from,
+ startedAt: new Date(),
+ endedAt: new Date(),
+ })
+ .returning();
+ await t.db.insert(callExtensions).values({ callId: c!.id, extensionId: ext!.id, role });
+ return c!.id;
+ };
+ const robo = await call('r1', '13125550142', 'rang');
+ const hidden = await call('r2', null, 'answered');
+ const samC = await t.login('sam@example.com', 'sam-password-123');
+ const patC = await t.login('pat@example.com', 'pat-password-123');
+ const report = (callId: string, as: string, reason = 'Car warranty robocall') =>
+ req('POST', '/api/me/spam-reports', { callId, reason }, as);
+
+ expect((await report(robo, patC)).statusCode).toBe(404); // not a call that rang Pat
+ expect((await report(robo, samC, ' ')).statusCode).toBe(400); // no reason
+ expect((await report(hidden, samC)).statusCode).toBe(400); // no number to block
+ expect((await report(robo, samC)).statusCode).toBe(201);
+ expect((await report(robo, samC)).statusCode).toBe(409); // once each
+ expect((await req('GET', '/api/spam-reports', undefined, samC)).statusCode).toBe(403);
+ const [r] = (await req('GET', '/api/spam-reports')).json().items;
+ expect(r).toMatchObject({
+ number: '13125550142',
+ reason: 'Car warranty robocall',
+ reportedBy: 'sam@example.com',
+ });
+
+ expect((await req('DELETE', `/api/spam-reports/${r.id}`)).statusCode).toBe(200);
+ expect((await req('GET', '/api/spam-reports')).json().items).toEqual([]);
+
+ expect((await report(robo, samC)).statusCode).toBe(201); // dismissed: may report again
+ const b = await req('POST', '/api/blocked-numbers', { number: r.number, reason: r.reason });
+ expect(b.statusCode).toBe(201);
+ expect((await req('GET', '/api/spam-reports')).json().items).toEqual([]);
+ expect((await report(robo, samC)).statusCode).toBe(409); // already blocked
+ });
});
diff --git a/apps/api/test/schema.test.ts b/apps/api/test/schema.test.ts
index 3ce55af..5d2ab1d 100644
--- a/apps/api/test/schema.test.ts
+++ b/apps/api/test/schema.test.ts
@@ -117,11 +117,11 @@ describe.skipIf(!hasTestDb)('schema matches spec 02', () => {
beforeAll(async () => ({ db, drop } = await createTestDb()));
afterAll(() => drop?.());
- it('creates all 47 tables', async () => {
+ it('creates all 48 tables', async () => {
const r = await db.execute(
sql`select count(*)::int as n from information_schema.tables where table_schema = 'public'`,
);
- expect(r.rows[0]).toEqual({ n: 47 });
+ expect(r.rows[0]).toEqual({ n: 48 });
});
it.each(Object.keys(EXPECTED))('%s columns', async (table) => {
diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts
index 546d57e..6f00373 100644
--- a/apps/web/src/api/client.ts
+++ b/apps/web/src/api/client.ts
@@ -26,6 +26,8 @@ import type {
AnnouncementUpdate,
BlockedCreate,
BlockedDto,
+ SpamReportCreate,
+ SpamReportDto,
CallDetailDto,
CallDto,
CallFilter,
@@ -306,6 +308,7 @@ export const systemApi = {
export const meApi = {
calls: (cursor?: string) =>
api>('GET', `/api/me/calls${cursor ? `?cursor=${cursor}` : ''}`),
+ reportSpam: (r: SpamReportCreate) => api<{ ok: true }>('POST', '/api/me/spam-reports', r),
/** The summary of the calls made to me. */
report: (r: ReportRange) => api('GET', `/api/me/reports/summary?${callQuery(r)}`),
get: () => api('GET', '/api/me'),
@@ -507,6 +510,8 @@ export const blockedApi = {
list: () => api>('GET', '/api/blocked-numbers').then((r) => r.items),
create: (b: BlockedCreate) => api('POST', '/api/blocked-numbers', b),
remove: (id: string) => api<{ ok: true }>('DELETE', `/api/blocked-numbers/${id}`),
+ reports: () => api>('GET', '/api/spam-reports').then((r) => r.items),
+ dismiss: (id: string) => api<{ ok: true }>('DELETE', `/api/spam-reports/${id}`),
};
/** Filters as a query string (empty values left out). */
diff --git a/apps/web/src/pages/admin/BlockedPage.tsx b/apps/web/src/pages/admin/BlockedPage.tsx
index dc7cb3f..8cfd292 100644
--- a/apps/web/src/pages/admin/BlockedPage.tsx
+++ b/apps/web/src/pages/admin/BlockedPage.tsx
@@ -1,14 +1,16 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
-// Blocked numbers (05 §2): calls from these numbers are rejected before anything rings.
+// Blocked numbers (05 §2): calls from these numbers are rejected before anything rings. Users'
+// Report spam waits at the top for Block or Dismiss.
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { formatNumber } from '@selfphoneng/shared';
import { blockedApi } from '../../api/client.ts';
-import { Alert, Button, Field, inputClass } from '../../components/ui.tsx';
+import { Alert, Button, Field, Panel, inputClass } from '../../components/ui.tsx';
export function BlockedPage() {
const qc = useQueryClient();
const list = useQuery({ queryKey: ['blocked'], queryFn: blockedApi.list });
+ const reports = useQuery({ queryKey: ['blocked', 'reports'], queryFn: blockedApi.reports });
const refresh = () => qc.invalidateQueries({ queryKey: ['blocked'] });
const [number, setNumber] = useState('');
const [reason, setReason] = useState('');
@@ -24,6 +26,16 @@ export function BlockedPage() {
mutationFn: (id: string) => blockedApi.remove(id),
onSuccess: refresh,
});
+ // Blocking a number clears every report of it (api).
+ const block = useMutation({
+ mutationFn: (r: { number: string; reason: string }) => blockedApi.create(r),
+ onSuccess: refresh,
+ });
+ const dismiss = useMutation({
+ mutationFn: (id: string) => blockedApi.dismiss(id),
+ onSuccess: refresh,
+ });
+ const error = add.error ?? remove.error ?? block.error ?? dismiss.error ?? reports.error;
return (
@@ -32,6 +44,38 @@ export function BlockedPage() {
Calls from these numbers are rejected before any phone rings. Emergency calls you make are
never affected.
+ {!!reports.data?.length && (
+
+
+ {reports.data.map((r) => (
+
+ {formatNumber(r.number)}
+
+ {r.reason}
+
+ {r.reportedBy ?? 'A former user'}, {new Date(r.createdAt).toLocaleString()}
+
+
+ block.mutate({ number: r.number, reason: r.reason })}
+ >
+ Block
+
+ dismiss.mutate(r.id)}
+ >
+ Dismiss
+
+
+ ))}
+
+
+ )}
- {(add.error ?? remove.error) && {(add.error ?? remove.error)!.message} }
+ {error && {error.message} }
{list.data?.length === 0 && Nobody is blocked.
}
{list.data && list.data.length > 0 && (
diff --git a/apps/web/src/pages/admin/DashboardPage.tsx b/apps/web/src/pages/admin/DashboardPage.tsx
index 2a52d51..19293e8 100644
--- a/apps/web/src/pages/admin/DashboardPage.tsx
+++ b/apps/web/src/pages/admin/DashboardPage.tsx
@@ -1,11 +1,11 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Dashboard (05 §2). Warnings, telephony link, trunks, calls in progress (hang up / transfer)
-// queue tiles, parked calls and extensions online.
+// queue tiles, parked calls and extensions online, spam reports waiting.
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect, useState, type ReactNode } from 'react';
import type { ActiveCall, QueueLive, Warning } from '@selfphoneng/shared';
import { Link } from 'react-router';
-import { liveApi, settingsApi, type SpyMode } from '../../api/client.ts';
+import { blockedApi, liveApi, settingsApi, type SpyMode } from '../../api/client.ts';
import { ConfirmDialog } from '../../components/Dialog.tsx';
import { Alert, Button, inputClass } from '../../components/ui.tsx';
import { fmtNumber } from './CallsPage.tsx';
@@ -31,6 +31,11 @@ export function DashboardPage() {
const onCall = list.filter((e) =>
['inuse', 'busy', 'onhold', 'ringinuse'].includes(e.state),
).length;
+ const reports = useQuery({
+ queryKey: ['blocked', 'reports'],
+ queryFn: blockedApi.reports,
+ refetchInterval: 30_000,
+ });
return (
@@ -40,7 +45,7 @@ export function DashboardPage() {
(t) => t.registration === 'rejected' || (!t.reachable && t.registration !== 'registered'),
)}
/>
-
+
{status === 'connecting' ? '…' : `${online} / ${list.length}`}
@@ -61,6 +66,12 @@ export function DashboardPage() {
{status === 'connecting' ? '…' : amiConnected ? 'Connected' : 'Not connected'}
+
+
+ {reports.data?.length ?? '…'}
+ waiting on Blocked numbers
+
+
diff --git a/apps/web/src/pages/user/MyCallsPage.tsx b/apps/web/src/pages/user/MyCallsPage.tsx
index 91eac1e..b645cdf 100644
--- a/apps/web/src/pages/user/MyCallsPage.tsx
+++ b/apps/web/src/pages/user/MyCallsPage.tsx
@@ -1,15 +1,16 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// My calls (05 §4): my call history; click the other number to call back (the softphone when it's
-// on, else my desk phone rings first). Names from my contacts (11 §1), "Add to contacts".
-import { useInfiniteQuery } from '@tanstack/react-query';
+// on, else my desk phone rings first). Names from my contacts (11 §1), "Add to contacts", "Report
+// spam".
+import { useInfiniteQuery, useMutation } from '@tanstack/react-query';
import { useState } from 'react';
import type { MyCallDto } from '@selfphoneng/shared';
import { meApi, recordingsApi } from '../../api/client.ts';
-import { Alert, Button } from '../../components/ui.tsx';
+import { Alert, Button, Field, inputClass } from '../../components/ui.tsx';
import { softphone } from '../../softphone/SoftphoneEngine.ts';
import { DIRECTION_TEXT, StatusBadge, fmtDuration, fmtNumber } from '../admin/CallsPage.tsx';
import { AddToContacts, useContactName } from './ContactsPage.tsx';
-import { SpamNote } from '../../components/SpamNote.tsx';
+import { SpamNote, spamLine } from '../../components/SpamNote.tsx';
/** The other side of the call, from where I sit. */
export const otherParty = (c: MyCallDto) =>
@@ -30,6 +31,64 @@ export function useCallBack() {
return { call, note };
}
+/** To the admins' Blocked numbers: the spam check's line when it flagged the call, else my words. */
+function ReportSpam({ c }: { c: MyCallDto }) {
+ const flagged = c.spam && c.spam.risk !== 'low' ? spamLine(c.spam) : null;
+ const [open, setOpen] = useState(false);
+ const [reason, setReason] = useState('');
+ const send = useMutation({
+ mutationFn: (r: string) => meApi.reportSpam({ callId: c.id, reason: r }),
+ });
+ if (send.isSuccess)
+ return (
+
+ Reported to your admin
+
+ );
+ return (
+ <>
+ {open ? (
+
+ ) : (
+
(flagged ? send.mutate(flagged) : setOpen(true))}
+ >
+ Report spam
+
+ )}
+ {send.error &&
{send.error.message} }
+ >
+ );
+}
+
export function MyCallsPage() {
const list = useInfiniteQuery({
queryKey: ['me', 'calls'],
@@ -98,6 +157,9 @@ export function MyCallsPage() {
)}
{fmtDuration(c.talkSeconds)}
+ {c.myRole === 'in' && c.direction === 'inbound' && other.number && !known && (
+
+ )}
);
})}
diff --git a/docs/spec/02-data-model.md b/docs/spec/02-data-model.md
index 58d9778..e526296 100644
--- a/docs/spec/02-data-model.md
+++ b/docs/spec/02-data-model.md
@@ -480,6 +480,14 @@ create table ftc_complaints ( -- the FTC copy (14 §2.2), kept windowDa
create index on ftc_complaints (number);
create index on ftc_complaints (reported_at);
+create table spam_reports ( -- a user's Report spam (05 §4), until an admin blocks or dismisses it
+ id uuid primary key default gen_random_uuid(),
+ number text not null, -- normalized, from the call
+ reason text not null, -- the spam check's line, or the user's words
+ reported_by uuid references users(id) on delete set null,
+ unique (number, reported_by)
+);
+
create table call_extensions ( -- which extensions were involved (for per-user history)
call_id uuid not null references calls(id) on delete cascade,
extension_id uuid not null references extensions(id) on delete cascade,
diff --git a/docs/spec/04-backend-api.md b/docs/spec/04-backend-api.md
index e5817da..a256593 100644
--- a/docs/spec/04-backend-api.md
+++ b/docs/spec/04-backend-api.md
@@ -384,6 +384,7 @@ Auth & self:
| GET | `/api/me/webrtc` | user | `{ wsUrl: "wss://
/ws", sipUri: "sip:101-web@", authUser: "101-web", password, displayName, iceServers: [...] }` |
| GET | `/api/me/calls` | user | own call history (via `call_extensions`) |
| GET | `/api/me/reports/summary?from&to` | user | `/api/reports/summary` of the calls that rang or were answered by me; `answered` and talk time are the ones I picked up |
+| POST | `/api/me/spam-reports` {callId, reason} | user | Report spam on an outside call that rang me; the number comes from the call. 404 not my call, 400 hidden number, 409 already blocked or already reported by me |
| GET | `/api/me/voicemail` | user | list |
| GET | `/api/me/voicemail/:id/audio` | user | streams the wav (`Content-Type: audio/wav`, supports Range) |
| PATCH | `/api/me/voicemail/:id` | user | `{folder: 'Old'|'INBOX'}` |
@@ -417,6 +418,7 @@ Admin resources (all `requireAdmin`; standard CRUD = `GET /x`, `POST /x`, `GET /
| `/api/moh-classes` | `POST {name}` (slug, not `default`), `DELETE /:id` (409 with `usedBy` while an extension or queue uses it), `POST /:id/files?name=&filename=` (raw file body, like sounds), `GET /:id/files/:fileId/audio`, `DELETE /:id/files/:fileId`. No rename: the name is the folder and is stored on extensions and queues. Extensions and queues may only pick `default` or an existing class (400) |
| `/api/devices` | `GET /:id/config-preview` (rendered provisioning file, passwords masked). As built (M10-T1): body `{mac (any format), vendor, model, label, extensionId}`; the DTO carries `provisioningUrl` and the `provisioningPassword` (admins type it into the phone); MAC taken → 409; preview of a generic phone → 400 |
| `/api/blocked-numbers` | |
+| `/api/spam-reports` | GET (with the reporter's name) and DELETE (Dismiss). Blocking a number deletes its reports |
| `/api/contacts` | company phonebook; `POST /import` CSV (`name,company,email,label1,number1,...`) or vCard (11 §3.1) |
| `/api/webhooks` | `POST /:id/test`, `GET /:id/deliveries` |
| `/api/api-tokens` | POST returns the token once |
diff --git a/docs/spec/05-web-ui.md b/docs/spec/05-web-ui.md
index 9b05e4b..d0332bb 100644
--- a/docs/spec/05-web-ui.md
+++ b/docs/spec/05-web-ui.md
@@ -46,7 +46,7 @@ Left sidebar navigation, in this order:
| Recordings | `/admin/recordings` | Searchable list, play, download, delete |
| Reports | `/admin/reports` | Summary charts (calls per day stacked by direction, missed per day, busiest hours heatmap), queue report table. As built (M11-T6): From/To dates (the last 30 days by default, a year at most); totals (calls, missed, average talk time); the charts are plain bars, no chart library, each bar labeled for screen readers; the heatmap is a weekday × hour table in the company time zone; the queue table has the same numbers as the queue page. Owner follow-up (2026-10-21, "total minutes, calltime, missed calls"): totals also have answered, talk time and outside minutes (in and out: inbound from arrival, since a receptionist or voicemail answers too; outbound from the answer; roughly what the provider bills), an outside-minutes-per-day chart, and one tile per voip.ms trunk with its account balance (`GET /api/reports/balance`, `getBalance`; a trunk without the API login says what to add) |
| SMS | `/admin/sms` | Same UI as the user one, with all DIDs |
-| Blocked numbers | `/admin/blocked` | |
+| Blocked numbers | `/admin/blocked` | Users' Report spam on top (number, reason, who, when; Block or Dismiss); their count is a Dashboard tile |
| Settings | `/admin/settings` | Tabs: General, Network (public IP detect, LAN IP, local nets, STUN/TURN), SIP (ports, TCP/TLS), Numbering ranges, Email (SMTP + test), Voicemail, Recording (retention, announcement sound + **legal notice text, 08 §5**), Emergency (default E911 caller ID, notify emails/extensions + a **test notification** button that does NOT dial 911), Backups (schedule, keep, include recordings, list + download + "Back up now"), Webhooks, API tokens. Security: desk phone networks, **outside calls at once** (per extension, whole system; 0 = no limit), phone setup files, SIP TLS. Spam check (14 §7.1) |
| System | `/admin/system` | Status JSON pretty-printed, Asterisk version, generator runs list, "Apply config now", "Rollback", "Restart telephony", generated-files viewer (read-only, masked), allowlisted CLI dropdown (04 §7.3) with output panel, audit log. As built (owner follow-ups to M11-T7, 2026-10-20), laid out after 3CX's admin dashboard (not copied): a **System information** panel (version, host name, company, public IP static/dynamic, LAN IPs, ports SIP/SIPS/media/HTTPS, telephony running, restart needed; copy buttons) with how to restart (`docker compose restart asterisk` on the server; no restart button),, a **Diagnostics** panel (08 §2.6: the server's own checks, then a Quick or Complete network test through the probe, each line with how to fix it), and an **Event log** panel (severity, event code, time, source, details; Load more; Download = `/api/system/events.csv`). Events (`system_events`, kept 90 days): `trunk.down`/`trunk.up` (registration changes), `telephony.down`/`telephony.up` (AMI), `telephony.restartNeeded`, `config.failed`, `emergency.call`/`emergency.test`, `network.publicIpChanged`, `backup.done`/`backup.failed`, `disk.low`, `spam.quotaExhausted`/`spam.quotaRestored` (14 §3). Admin edits stay in the audit log (not shown yet). The dashboard's `restart_required` warning links here. Generator runs, generated files and CLI views aren't built; the admin guide's troubleshooting page covers them from the shell |
@@ -102,7 +102,7 @@ Implementation notes (M3):
| Page | Path | Contents |
|---|---|---|
| Home | `/me` | Status card (DND toggle, forwarding summary, queue login toggles), missed calls, new voicemails, colleagues presence list. M4 ships only new voicemails; the rest arrives with DND/forwarding (M6), call history (M7) and presence (M5-T3) |
-| Calls | `/me/calls` | Own call history, click a number to call back, add to contacts |
+| Calls | `/me/calls` | Own call history, click a number to call back, add to contacts, Report spam on an outside call that rang me (the spam check's line when it said medium or high, else a reason I type) |
| Voicemail | `/me/voicemail` | List (new bold), play (marks read), download, delete, search (transcript, caller). Greetings section: record in the browser (MediaRecorder → upload), upload, or delete for unavail/busy/name |
| Contacts | `/me/contacts` | Personal + company (read-only) with search, click to call |
| Reports | `/me/reports` | The admin Reports' summary of the calls made to me (rang or answered me, not the ones I made): calls, answered by me, missed, talk time, calls and missed per day, busiest hours. No outside minutes, balance or queues. Shown only with an extension |
diff --git a/packages/shared/src/blocked.ts b/packages/shared/src/blocked.ts
index aa8b355..f2ac597 100644
--- a/packages/shared/src/blocked.ts
+++ b/packages/shared/src/blocked.ts
@@ -17,8 +17,27 @@ export const BlockedCreateSchema = z.object({
return z.NEVER;
}
}),
- reason: z.string().trim().max(100).nullable().optional(),
+ reason: z.string().trim().max(200).nullable().optional(),
});
export type BlockedCreate = z.input;
export type BlockedDto = { id: string; number: string; reason: string | null; createdAt: string };
+
+/**
+ * A user's "Report spam" on a call that rang them (05 §4). The reason is the spam check's line
+ * when it flagged the call, else what the user typed. An admin blocks or dismisses it.
+ */
+export const SpamReportCreateSchema = z.object({
+ callId: z.uuid(),
+ reason: z.string().trim().min(1, 'Say why').max(200),
+});
+export type SpamReportCreate = z.input;
+
+export type SpamReportDto = {
+ id: string;
+ number: string;
+ reason: string;
+ /** The reporter's display name; null once their account is gone. */
+ reportedBy: string | null;
+ createdAt: string;
+};