conferences: rooms with PIN and admin PIN, per-room limits, recording
ci / images (push) Canceled after 0s
ci / check (push) Canceled after 3s

/api/conferences CRUD: PINs stored encrypted, shown to admins (they hand
them out), never in the audit log; an admin PIN needs a different room
PIN (checked against the stored values on PATCH). The generator renders
sp-conf-{id} per 03 §6.11 (PIN prompt, 3 tries, admin PIN joins with
sp_admin_user), the number in sp-local-numbers and destApp conference.

Two changes from the spec's snippet, both found with real calls and
written back into 03:
- Set(CONFBRIDGE(bridge,max_members)) is ignored once ConfBridge()
  names a bridge profile: a 4th caller got into a room of 3. Each room
  now has its own [sp_room_<n>](default_bridge) profile in
  confbridge_selfphoneng.conf.
- A recorded room runs sp-sub-record on each caller's leg (just before
  ConfBridge) instead of record_conference, so the files are ordinary
  call recordings: call history, Recordings page, retention.

Web: Conference rooms page (list, drawer form), "Conference room" in
the DestinationPicker. Golden case conferences. e2e m8-t3: room built
in the UI, wrong PIN refused and asked again, admin PIN joins with
flags AM, a 4th caller turned away, recordings listed, and a no-PIN
room with two desk phones.

Task: M8-T3
This commit is contained in:
iamdoubz committed 2026-10-19 04:56:41 -05:00
1 parent 964a3c0b25
commit a6c423de97
45 files changed
+1456 -14

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Conference rooms (02 conferences, 03 §6.11): a number people dial to talk together.
import { z } from 'zod';
import { InternalNumber } from './extensions.ts';
const Pin = z.string().regex(/^[0-9]{4,10}$/, '4 to 10 digits');
const ConferenceFields = z.object({
number: InternalNumber,
name: z.string().trim().min(1, 'Required').max(40),
/** null = anyone who dials the number gets in. */
pin: Pin.nullable(),
/** Callers who enter it join as the room's admin (marked user: can kick, mute others). */
adminPin: Pin.nullable(),
maxMembers: z.number().int().min(2).max(100),
/** Every caller's leg is recorded, like a recorded call (call history, Recordings). */
record: z.boolean(),
});
const pinsMakeSense = (c: { pin?: string | null; adminPin?: string | null }) =>
!c.adminPin || (!!c.pin && c.pin !== c.adminPin);
const PINS_MESSAGE = {
message: 'An admin PIN needs a (different) room PIN',
path: ['adminPin'],
};
export const ConferenceCreateSchema = ConferenceFields.partial()
.required({ number: true, name: true })
.refine(pinsMakeSense, PINS_MESSAGE);
/** The route checks the PINs again against what is stored. */
export const ConferenceUpdateSchema = ConferenceFields.partial();
export type ConferenceCreate = z.input<typeof ConferenceCreateSchema>;
export type ConferenceUpdate = z.input<typeof ConferenceUpdateSchema>;
/** Admins see the PINs: they hand them out. */
export type ConferenceDto = z.output<typeof ConferenceFields> & { id: string; createdAt: string };
export { pinsMakeSense as conferencePinsMakeSense };
+1
View File
@@ -14,6 +14,7 @@ export * from './voicemail.ts';
export * from './sounds.ts';
export * from './ringgroups.ts';
export * from './queues.ts';
export * from './conferences.ts';
export * from './ivrs.ts';
export * from './timeconditions.ts';
export * from './announcements.ts';