recordings: record calls, link them to history, Recordings page

sp-sub-record now also sets CDR(SP_REC); phone numbers can record every
call (record_inbound), with the notice from Settings → Recording played
first (SP_REC_ANNOUNCE). Aggregation links the MixMonitor file as a
recordings row (size, ffprobe length). /api/recordings lists, plays
(Range; admins and the people on the call) and deletes; recordings past
recordings.retentionDays are deleted daily.

Web: Recordings page (play, download, delete), a player in Call history
and My calls, a Recording settings tab with the consent notice (08 §5),
and "Record every call" on phone numbers.

e2e m7-t3: the notice plays before a recorded inbound call and the
recording plays from Call history (the M7 acceptance). The AMI
Originate helper moved to scripts/e2e/ami.mjs.

Task: M7-T3
This commit is contained in:
iamdoubz committed 2026-10-18 14:57:52 -05:00
1 parent 8abcea9533
commit 4b6b95090a
46 files changed
+810 -54

No files matched your search

+7
View File
@@ -49,6 +49,7 @@ export type SnapDid = {
label: string | null;
destination: Destination;
callerIdNamePrefix: string | null;
recordInbound: boolean;
};
export type SnapRoute = {
@@ -147,6 +148,8 @@ export type Snapshot = {
timeConditions: SnapTimeCondition[];
holidays: SnapHoliday[];
announcements: SnapAnnouncement[];
/** Played before recording a call that a phone number records (Settings → Recording). */
recordingAnnouncement: string | null;
};
export function todayIn(timezone: string, now = new Date()): string {
@@ -178,6 +181,7 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
label: dids.label,
destination: dids.destination,
callerIdNamePrefix: dids.callerIdNamePrefix,
recordInbound: dids.recordInbound,
})
.from(dids);
const trunkRows = await db.select().from(trunks);
@@ -214,6 +218,9 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
closedDestination: t.closedDestination as Destination,
holidayDestination: t.holidayDestination as Destination | null,
})),
recordingAnnouncement: settings['recordings.announcementSoundId']
? (soundName.get(settings['recordings.announcementSoundId']) ?? null)
: null,
announcements: annRows.map((a) => ({
id: a.id,
name: a.name,