featurecodes: DND, forwarding, pickup, echo, say-ext; /me settings

Feature codes per 03 §6.13: *78/*79 DND, *72<number>/*73 forward always,
**<ext> directed pickup, *8 group pickup (also as a dialplan line, since
pickupexten only catches a phone's own INVITE), *43 echo test, *65 say
my extension; queue login and intercom stay for M8. SpToggle DND/CFU
update user_runtime_state; a *72 target the user's calling permission
doesn't allow is removed from AstDB, audited (forward_rejected) and sent
to the user as a toast (new private live message). Forward targets are
checked the same way everywhere (services/forwards.ts: an existing
internal number, or an outside number some outbound rule allows them).

GET/PATCH /api/me: DND, cfu/cfb/cfna, voicemail email options and PIN;
PostgreSQL first, then AstDB; AstDB resync on connect now includes DND
and forwards. Web: /me/settings (forwarding, DND, voicemail PIN and
email options), a status card on Home, toasts.

e2e m6-t7 with real phones covers every code, the refused forward toast,
and the settings page reaching AstDB; it found the settings forms losing
"Saved." on remount.

Task: M6-T7
This commit is contained in:
iamdoubz committed 2026-10-18 11:19:21 -05:00
1 parent d746772b97
commit 57e0e3dac2
31 files changed
+1270 -33

No files matched your search

+1
View File
@@ -16,3 +16,4 @@ export * from './ringgroups.ts';
export * from './ivrs.ts';
export * from './timeconditions.ts';
export * from './announcements.ts';
export * from './me.ts';
+2
View File
@@ -62,6 +62,8 @@ export type LiveMessage =
| { type: 'emergency.call'; data: EmergencyCall }
/** Only to the mailbox owner. */
| { type: 'voicemail.new'; data: VoicemailDto & { userId: string } }
/** A short message for one user (e.g. "forwarding refused"). */
| { type: 'toast'; data: { userId: string; message: string } }
| { type: 'ami.status'; data: { connected: boolean } }
| {
type: 'system.configApplied';
+40
View File
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// GET/PATCH /api/me (04 §7.3): the signed-in user's own phone settings.
import { z } from 'zod';
/** What a person types for a forward: "101", "(312) 555-1234", "+44 20 …"; null = off. */
const ForwardInput = z
.string()
.trim()
.max(40)
.regex(/^[0-9*#+()\s.-]*[0-9][0-9*#+()\s.-]*$/, 'A number like 101 or (312) 555-1234')
.nullable();
export const MeUpdateSchema = z.object({
dnd: z.boolean().optional(),
/** Forward always / when busy / when not answered. */
cfu: ForwardInput.optional(),
cfb: ForwardInput.optional(),
cfna: ForwardInput.optional(),
voicemailEmail: z.boolean().optional(),
voicemailAttachAudio: z.boolean().optional(),
voicemailDeleteAfterEmail: z.boolean().optional(),
voicemailPin: z
.string()
.regex(/^\d{4,10}$/, 'Use 4 to 10 digits')
.optional(),
});
export type MeUpdate = z.input<typeof MeUpdateSchema>;
export type MeDto = {
extension: { number: string; name: string };
dnd: boolean;
/** Stored normalized: an internal number, or an outside number like 13125551234. */
cfu: string | null;
cfb: string | null;
cfna: string | null;
voicemailEnabled: boolean;
voicemailEmail: boolean;
voicemailAttachAudio: boolean;
voicemailDeleteAfterEmail: boolean;
};