paging: paging groups, *80 intercom, auto-answer headers
ci / check (push) Successful in 1m7s
ci / images (push) Successful in 3m20s

/api/paging-groups CRUD (members as memberIds). The generator renders
sp-page-{id} per 03 §6.11 (desk phones only, auto-answer headers via
sp-sub-autoanswer) plus the number in sp-local-numbers. Added the `s`
option so phones already on a call aren't paged, which also keeps a
pager who is a member from being paged themselves. A group with no
members just hangs up.

*80<ext> intercom checks the target first: digits only and an endpoint
in sp-from-ext, else ss-noservice (the spec's one-liner passed
${EXTEN:3} straight into Page()). Both written back into 03.

Web: Paging page (list, drawer form with member checkboxes). Golden
case paging. e2e m8-t4 with baresip phones that honor the headers
(sip_autoanswer): an ordinary call rings, a page is answered by itself
and muted, the busy pager is skipped, *80 is two-way, *80999 refused.
The check on a real Yealink/Grandstream stays open (09).

Task: M8-T4
This commit is contained in:
iamdoubz committed 2026-10-19 09:40:22 -05:00
1 parent a6c423de97
commit e95271e694
47 files changed
+1466 -22

No files matched your search

+24
View File
@@ -10,6 +10,8 @@ import {
extensions,
holidays,
ivrOptions,
pagingGroupMembers,
pagingGroups,
ivrs,
outboundRoutes,
queueMembers,
@@ -126,6 +128,15 @@ export type SnapConference = {
record: boolean;
};
export type SnapPagingGroup = {
id: string;
number: string;
name: string;
duplex: boolean;
/** Extension numbers. */
members: string[];
};
export type SnapIvr = {
id: string;
number: string;
@@ -179,6 +190,7 @@ export type Snapshot = {
ringGroups: SnapRingGroup[];
queues: SnapQueue[];
conferences: SnapConference[];
pagingGroups: SnapPagingGroup[];
ivrs: SnapIvr[];
timeConditions: SnapTimeCondition[];
holidays: SnapHoliday[];
@@ -233,6 +245,11 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
.from(queueMembers)
.innerJoin(extensions, eq(extensions.id, queueMembers.extensionId));
const confRows = await db.select().from(conferences);
const pageRows = await db.select().from(pagingGroups);
const pagedRows = await db
.select({ groupId: pagingGroupMembers.pagingGroupId, number: extensions.number })
.from(pagingGroupMembers)
.innerJoin(extensions, eq(extensions.id, pagingGroupMembers.extensionId));
const ivrRows = await db.select().from(ivrs);
const tcRows = await db.select().from(timeConditions);
const holidayRows = await db.select().from(holidays);
@@ -335,6 +352,13 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
maxMembers: c.maxMembers,
record: c.record,
})),
pagingGroups: pageRows.map((g) => ({
id: g.id,
number: g.number,
name: g.name,
duplex: g.duplex,
members: pagedRows.filter((m) => m.groupId === g.id).map((m) => m.number),
})),
routes: routeRows.map((r) => ({
id: r.id,
name: r.name,